Skip to main content
Once a browser has solved a Ribaunt challenge and your server has accepted the solution, that token should never be accepted again. Without replay protection, an attacker who intercepts or records a valid { tokens, solutions } pair could resubmit it as many times as they like until the token expires — bypassing the entire proof-of-work requirement. Ribaunt addresses this by tracking consumed token IDs (jti values) and rejecting any token that has already been seen.

Replay prevention modes

Ribaunt supports three modes for replay prevention, configured via the replayPrevention option on verifySolution().
ModeBehaviorWhen to use
local (default)Tracks used tokens in an in-memory Map within the current Node.js processSingle-process deployments
remoteDelegates to your own distributed store via the ReplayStore interfaceServerless functions, horizontally scaled services, multiple Node.js instances
disabledSkips replay checks entirelyOnly when another layer already prevents replay, or as a legacy opt-out during migration

Local mode (default)

Local mode requires no configuration. When you call verifySolution() without a replayPrevention option, Ribaunt automatically uses a process-local Map to record consumed jti values. Any attempt to submit the same token twice within the same process is rejected.
// Default behavior — local replay protection is active
const valid = await verifySolution(tokens, solutions);
This is the right choice for most traditional Node.js deployments where a single long-running process handles all requests. Expired token IDs are cleaned up automatically as new verifications occur, so memory usage stays bounded.
You do not need to configure anything extra to benefit from local replay protection — it is on by default.

Remote mode for distributed deployments

If your application runs across multiple Node.js instances, uses serverless functions, or deploys to edge workers, each process has its own memory. Local mode cannot share state between them, which means a token consumed by one instance can be replayed against any other instance. In these cases you need remote mode. With remote mode you provide a replayStore object that implements the ReplayStore interface. Ribaunt calls replayStore.consume(jti, expiresAt) for every verified token. Your implementation is responsible for the atomic “mark as used” operation — the standard pattern with Redis or Valkey is SET NX EX (set if not exists, with an expiry).
const valid = await verifySolution(tokens, solutions, {
  replayPrevention: 'remote',
  replayStore: {
    consume: async (jti, expiresAt) => {
      // Returns true if consumed (first time), false if already seen.
      // Implement with Redis/Valkey SET NX EX for atomic "set if not exists".
      return true;
    },
  },
});
The expiresAt value is a Unix timestamp in seconds — pass it directly as the TTL for your cache key so that entries expire automatically and you do not accumulate stale data.
Use the expiresAt argument to set the TTL on your Redis/Valkey key. This keeps your store clean without any manual housekeeping.

Disabling replay protection

Only disable replay protection if a separate mechanism in your stack already prevents token reuse. With replay protection disabled, a valid solution can be submitted multiple times within the token’s TTL window.
If you previously relied on the old behavior where verifySolution() had no replay checks, you can restore that behavior explicitly while you migrate:
const valid = await verifySolution(tokens, solutions, {
  replayPrevention: 'disabled',
});

ReplayStore interface

When using remote mode, your replayStore must satisfy the following interface:
interface ReplayStore {
  consume(jti: string, expiresAt: number): Promise<boolean>;
}
ParameterTypeDescription
jtistringThe unique ID of the challenge token being verified
expiresAtnumberUnix timestamp in seconds when this token expires
Return true to allow the verification to proceed (this is the first time this token has been seen). Return false to reject it as a replay. The operation must be atomic — use a single Redis SET NX EX command rather than a separate check-then-set to avoid race conditions under concurrent load.