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 callcreateChallenge() 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
1–64 (values above 6 may cause browsers to hang); Argon2id accepts 1–8. 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 whendifficulty 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
ReturnsPromise<ChallengeToken[]> — an array of signed JWT strings. Send this array to the browser as { challenges: tokens }.
Examples
Rate-limited challenge issuance
Telemetry with onEvent
Adaptive workload
Two paths are supported for adaptive difficulty:- Pass
difficulty: "auto"directly tocreateChallenge()and let it call the selector internally. - Call
selectWorkload()yourself and pass the result viaworkload.
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.
Argon2id opt-in
By default, Ribaunt uses SHA-256, which hashes in microseconds and keeps server-side verification cheap. Opt in toargon2id 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.
- Difficulty scale. Each Argon2id hash takes milliseconds instead of microseconds, so difficulty caps at
8and"auto"bounds default to1–2(versus64and3–6for SHA-256). - Profiles instead of raw parameters.
argonProfileresolves the Argon2id memory, iteration, and parallelism parameters for you. The library enforces a hard upper bound (HARD_MAX, exported fromribaunt) on those parameters, and tokens carrying values above it are rejected asinvalid-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-backendevent reportsargon2idso you can confirm it in telemetry. - Testing. The synchronous
solveChallenge()helper supports SHA-256 only. UsesolveChallengeAsync()in tests that solveargon2idtokens.
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.
Validation
createChallenge() validates its inputs at runtime and throws if anything is invalid:
difficulty— must be a finite number and at least1. Fractional values are rounded down withMath.floor(). The maximum is64forsha256and8forargon2id.algorithm— must be'sha256'or'argon2id'when provided.argonProfile— must be'mobile'or'standard', and is only accepted whenalgorithmis'argon2id'.amount— must be a finite number and at least1. Fractional values are rounded down withMath.floor().ttlSeconds— must be a finite number and at least1. Fractional values are rounded down withMath.floor().workload— if you provide it, the selected values must still fit the configured bounds.
Requires
RIBAUNT_SECRET to be set as an environment variable. createChallenge() throws if the secret is missing or shorter than 32 UTF-8 bytes.