A proof-of-work challenge system for rate limiting and bot protection. This SDK provides a server-side implementation for creating cryptographic challenges that clients must solve to prove computational work.
npm install @usewayn/server # NodeJS
bun install @usewayn/server # Bun
import Wayn from '@usewayn/server';
// Create a new Wayn instance
const wayn = new Wayn({
stateless: true, // Optional: enables JWT authentication (default: true)
jwtSecret: 'ReallySecretKey29!', // Optional: defaults to random bytes
});
// Create a challenge for the client
const challenge = wayn.createChallenge({
challengeCount: 5, // Number of challenges
challengeSize: 32, // Size of each challenge
challengeDifficulty: 5, // Difficulty level
expiresMs: 300000, // Expiration time (5 minutes)
});
// Validate a solution from the client
const result = await wayn.redeemChallenge({
token: challenge.token,
solutions: clientSolutions, // Array of [salt, target, nonce] tuples
});
if (result.success) {
// Challenge solved successfully
const validationResult = await wayn.validateToken(result.token);
console.log('Token is valid:', validationResult.success);
}
new Wayn(config?: Partial<WaynConfig>)
Creates a new Wayn instance with optional configuration.
Parameters:
-
config.jwtSecret
(string): Secret key for JWT token signing. Defaults to random bytes. -
config.stateless
(boolean): Whether to use JWT tokens (true) or in-memory tokens (false). Default: true. -
config.state
(ChallengeState): Initial state for challenges and tokens. Only used when stateless is false.
Creates a new proof-of-work challenge for clients to solve.
Parameters:
-
config.challengeCount
(number): Number of individual challenges. Default: 50 -
config.challengeSize
(number): Size of each challenge string in characters. Default: 32 -
config.challengeDifficulty
(number): Number of leading zeros required in hash. Default: 4 -
config.expiresMs
(number): Challenge expiration time in milliseconds. Default: 600000 (10 minutes) -
config.store
(boolean): Whether to store the challenge server-side. Default: true
Returns:
{
challenge: Array<[string, string]>, // Array of [salt, target] tuples
token?: string, // Challenge token (if stored)
expires: number // Expiration timestamp
}
Example:
const challenge = wayn.createChallenge({
challengeCount: 5,
challengeDifficulty: 3,
expiresMs: 300000, // 5 minutes
});
// Send challenge.challenge to client
// Store challenge.token for validation
Validates a client's solution to a previously created challenge.
Parameters:
{
token: string, // Challenge token
solutions: Array<[string, string, number]> // Array of [salt, target, nonce] tuples
}
Returns:
{
success: boolean,
message?: string, // Error message if success is false
token?: string, // Verification token if success is true
expires?: number // Token expiration timestamp
}
Example:
const result = await wayn.redeemChallenge({
token: challengeToken,
solutions: [
['salt1', 'target1', 12345],
['salt2', 'target2', 67890],
// ... more solutions
]
});
if (result.success) {
// Store result.token for future validations
console.log('Challenge solved! Token expires at:', new Date(result.expires));
} else {
console.error('Challenge failed:', result.message);
}
Validates a previously issued verification token.
Parameters:
-
token
(string): The verification token to validate -
config.keepToken
(boolean): Whether to keep the token valid after validation (only for non-stateless mode). Default: false
Returns:
{
success: boolean,
payload?: JWTPayload // JWT payload (only in stateless mode)
}
Example:
const validation = await wayn.validateToken(userToken);
if (validation.success) {
if (validation.payload) {
// Stateless mode - JWT token
console.log('User ID:', validation.payload.id);
console.log('Challenge solved at:', new Date(validation.payload.solvedAt * 1000));
console.log('Average solution time:', validation.payload.avgSolutionTime, 'ms');
}
// Allow user to proceed
} else {
// Token is invalid or expired
}
Option | Type | Default | Description |
---|---|---|---|
challengeCount |
number | 50 | Number of proof-of-work challenges to generate |
challengeSize |
number | 32 | Length of each challenge salt in characters |
challengeDifficulty |
number | 4 | Number of leading zeros required in SHA-256 hash |
expiresMs |
number | 600000 | Challenge expiration time in milliseconds (10 min) |
store |
boolean | true | Whether to store challenge server-side for validation |
Option | Type | Default | Description |
---|---|---|---|
keepToken |
boolean | false | Keep token valid after validation (non-stateless only) |
const wayn = new Wayn();
// Create challenge with higher difficulty for rate limiting
const challenge = wayn.createChallenge({
challengeCount: 20,
challengeDifficulty: 5, // Requires more computation
expiresMs: 300000, // 5 minutes
});
// Client must solve challenge before making API calls
const wayn = new Wayn();
// Lighter challenge for bot protection
const challenge = wayn.createChallenge({
challengeCount: 3,
challengeDifficulty: 3,
expiresMs: 120000, // 2 minutes
});
// Validate before allowing form submissions
Stateless Mode (Recommended):
- Uses JWT tokens
- No server-side storage required
- Scales horizontally
- Includes metadata about solution performance
const wayn = new Wayn({ stateless: true });
Stateful Mode:
- Stores tokens in memory
- Requires server-side state management
- Better for single-instance deployments
const wayn = new Wayn({ stateless: false });
- JWT Secret: Use a strong, randomly generated secret for JWT signing in production
- Challenge Difficulty: Higher difficulty provides better protection but increases client computation time
- Expiration Times: Set appropriate expiration times based on your use case
- Token Validation: Always validate tokens before allowing protected operations
The SDK is written in TypeScript and includes full type definitions:
import Wayn, {
WaynConfig,
ChallengeConfig,
Solution,
JWTPayload
} from '@usewayn/server';
This project was created using bun init
in bun v1.2.0. Bun is a fast all-in-one JavaScript runtime.