Skip to main content
createChallenge() is imported from ribaunt and called server-side to generate one or more proof-of-work challenge tokens. Each token is a signed JWT that the browser solver decodes and works against.

Import

Signature

createChallenge() is asynchronous and returns a Promise<ChallengeToken[]>. Always await it. The async signature exists so that optional hooks like rateLimiter can be awaited before a challenge is issued.
difficulty accepts either a positive integer or the string "auto". In "auto" mode, Ribaunt picks a difficulty and amount at runtime using selectWorkload() based on an optional client calibration, a server-side riskScore, targetDurationMs, and the min/max bounds you configure. Calibration is treated as untrusted — a fast benchmark can only raise work up to your maximums, never lower the server-owned baseline.

Parameters

You can call createChallenge() in either of two styles:
  • positional arguments: createChallenge(difficulty, amount, ttlSeconds)
  • an options object: createChallenge({ difficulty, amount, ttlSeconds, context, workload })
number | "auto"
default:"5"
Number of leading zero hex digits required in the hash. Each increment roughly doubles solve time. SHA-256 accepts 164 (values above 6 may cause browsers to hang); Argon2id accepts 18. Pass "auto" to have Ribaunt select difficulty and amount adaptively — see Adaptive workload below.
'sha256' | 'argon2id'
default:"sha256"
Proof-of-work algorithm. The default sha256 hashes in microseconds and verifies cheaply. Opt in to argon2id for a memory-hard algorithm that raises the cost of GPU and ASIC solver farms. See Argon2id opt-in below.
'mobile' | 'standard'
default:"mobile"
Memory-hardness profile, only valid with algorithm: 'argon2id'. The profile abstracts the raw Argon2id parameters (m, t, p) so you never pass memory sizes yourself. Both profiles currently resolve to the same conservative tuning; pick the tier that matches your audience now, and future retuning will not break in-flight tokens because each token embeds its own parameters. Passing argonProfile with algorithm: 'sha256' throws.
number
default:"4"
Number of challenge tokens to generate. More challenges increase total proof-of-work but also increase network bandwidth.
number
default:"30"
Challenge token lifetime in seconds. Tokens submitted after expiry are rejected by verifySolution.
string
Optional scope string that is bound into the challenge token. Supply the same value to verifySolution({ context }) to require that the same context is used when verifying.
Pick<Workload, 'difficulty' | 'amount'>
Optional shorthand for setting the challenge difficulty and amount together. Use this when you want to keep the challenge configuration in a single object.

Auto-hardness options

These fields are only used when difficulty is "auto".
number
default:"750"
Desired browser solve time in milliseconds. The selector aims for this duration when it has calibration data.
number
default:"50"
Server-side risk appetite from 0–100. Higher scores bias the selector toward more work within your configured bounds, independent of the client calibration.
ClientCalibration
Untrusted client benchmark, typically forwarded from the widget when challenge-method="POST" and calibrate="true" are set. Used as a raise-only signal: fast calibration can increase work up to your maximum bounds, a slow or fake one cannot reduce it below the server baseline.
number
default:"3"
Lower bound for difficulty when using "auto". Defaults to 1 when algorithm is 'argon2id'.
number
default:"6"
Upper bound for difficulty when using "auto". Defaults to 2 when algorithm is 'argon2id'.
number
default:"1"
Lower bound for amount when using "auto".
number
default:"8"
Upper bound for amount when using "auto".

Hooks

RateLimiter
Optional bring-your-own rate limiter. createChallenge() calls rateLimiter.check(context) before issuing tokens. If the limiter resolves false, Ribaunt throws a RateLimitedError (code: 'rate-limited') and does not issue any tokens. Use this to reject abusive callers by IP, session, or user before spending JWT signing work.
(event: RibauntEvent) => void
Optional telemetry hook. createChallenge() calls it with { type: 'challenge-issued', difficulty, amount, algorithm } after tokens are issued. Errors thrown by the callback are caught and ignored so telemetry cannot break challenge issuance. See the Types reference for the full event union.

Return value

Returns Promise<ChallengeToken[]> — an array of signed JWT strings. Send this array to the browser as { challenges: tokens }.

Examples

Rate-limited challenge issuance

Key the limiter on the client IP by closing over the request, as shown above. Do not pass context: req.ip for rate limiting. context cryptographically binds every token to that value, so verifySolution() must receive the exact same context or verification fails with context-mismatch. Reserve context for binding a challenge to a specific action, such as one signup attempt.

Telemetry with onEvent

Adaptive workload

Two paths are supported for adaptive difficulty:
  1. Pass difficulty: "auto" directly to createChallenge() and let it call the selector internally.
  2. Call selectWorkload() yourself and pass the result via workload.
Both use the same engine, so the results match for equivalent inputs.
selectWorkload() respects the configured bounds and returns a Workload object with difficulty, amount, estimatedAttempts, and algorithm. It accepts the same algorithm and argonProfile options as createChallenge(); for argon2id the result also includes the resolved argon parameters.
To derive riskScore from application signals such as account age or request velocity instead of hardcoding it, use assess(). When it recommends a challenge, it returns a ready-made Workload you can pass here.

Calibration helpers

Ribaunt exposes calibration helpers for both environments so you can benchmark the runtime that will actually solve the challenge:
calibrateClient and calibrateArgonClient are cross-environment aliases — bundlers resolve the correct implementation via the package export map.
Use the calibrator that matches your algorithm. A SHA-256 calibration passed to an argon2id workload (or vice versa) will over- or under-estimate the device and skew the selected difficulty.
Any calibration value coming from a browser request is untrusted. Ribaunt only uses it to raise work above the server-owned baseline, but you should still validate its shape before passing it through.

Argon2id opt-in

By default, Ribaunt uses SHA-256, which hashes in microseconds and keeps server-side verification cheap. Opt in to argon2id when you want a memory-hard proof of work: each hash allocates a fixed amount of memory, which makes large-scale solving on GPUs and ASICs far more expensive relative to a real user’s browser.
Key differences from SHA-256:
  • Difficulty scale. Each Argon2id hash takes milliseconds instead of microseconds, so difficulty caps at 8 and "auto" bounds default to 12 (versus 64 and 36 for SHA-256).
  • Profiles instead of raw parameters. argonProfile resolves the Argon2id memory, iteration, and parallelism parameters for you. The library enforces a hard upper bound (HARD_MAX, exported from ribaunt) on those parameters, and tokens carrying values above it are rejected as invalid-token.
  • Tokens are self-describing. Each challenge token carries its algorithm, its Argon2id parameters, and a construction version (v: 1), signed into the JWT. verifySolution() detects the algorithm per token, so your verify endpoint needs no changes, and future profile retuning cannot break tokens that are already in flight.
  • Browser support is automatic. The widget and its solver worker detect the algorithm per token and load the Argon2id solver on demand. The solver-backend event reports argon2id so you can confirm it in telemetry.
  • Testing. The synchronous solveChallenge() helper supports SHA-256 only. Use solveChallengeAsync() in tests that solve argon2id tokens.
The adaptive engine works the same for both algorithms: riskScore and calibration remain raise-only signals within your configured bounds. assess() accepts algorithm and argonProfile in its workload options when you want the risk engine to produce Argon2id workloads.
Argon2id narrows the attacker’s GPU and ASIC advantage but does not eliminate it. A well-funded adversary can still solve any proof-of-work challenge, just at a higher memory cost. Treat it as one abuse-cost layer alongside rate limiting and risk signals, not as human verification or the sole gate for sensitive actions.

Validation

createChallenge() validates its inputs at runtime and throws if anything is invalid:
  • difficulty — must be a finite number and at least 1. Fractional values are rounded down with Math.floor(). The maximum is 64 for sha256 and 8 for argon2id.
  • algorithm — must be 'sha256' or 'argon2id' when provided.
  • argonProfile — must be 'mobile' or 'standard', and is only accepted when algorithm is 'argon2id'.
  • amount — must be a finite number and at least 1. Fractional values are rounded down with Math.floor().
  • ttlSeconds — must be a finite number and at least 1. Fractional values are rounded down with Math.floor().
  • workload — if you provide it, the selected values must still fit the configured bounds.
Never let user-controlled request parameters flow directly into createChallenge() without validation.
Requires RIBAUNT_SECRET to be set as an environment variable. createChallenge() throws if the secret is missing or shorter than 32 UTF-8 bytes.