> ## Documentation Index
> Fetch the complete documentation index at: https://ribaunt-e66481b6-mintlify-4e7c3afc.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# verifySolution

> verifySolution() validates JWT signatures, hash proofs, expiry, replay protection, and optional context binding. Returns a structured result object.

`verifySolution()` is called server-side in your verify endpoint to check that the browser correctly solved all challenge tokens. It validates the JWT signature, token expiry, the hash proof, replay state, and optional context binding. Each token carries its own algorithm, so `verifySolution()` verifies SHA-256 and [Argon2id](/api/create-challenge#argon2id-opt-in) proofs automatically with no extra options.

## Import

```ts theme={null}
import { verifySolution } from 'ribaunt';
```

## Signature

```ts theme={null}
function verifySolution(
  token: ChallengeToken | ChallengeToken[],
  nonce: number | string | Array<number | string> | ChallengeSolution | ChallengeSolution[],
  options?: VerifySolutionOptions
): Promise<VerifySolutionResult>
```

## Parameters

<ParamField path="token" type="ChallengeToken | ChallengeToken[]" required>
  The original JWT token(s) returned by `createChallenge()`. Pass the same tokens your challenge endpoint issued.
</ParamField>

<ParamField path="nonce" type="number | string | Array<number | string> | ChallengeSolution | ChallengeSolution[]" required>
  The solution(s) submitted by the browser. Can be:

  * A single nonce string
  * An array of nonce strings
  * A `ChallengeSolution` object `{ nonce: string; hash: string }`
  * An array of `ChallengeSolution` objects (what the widget sends as `solutions`)
</ParamField>

<ParamField path="options" type="VerifySolutionOptions">
  Optional configuration object. See the options table below.
</ParamField>

## Options

| Option             | Type                                | Default                    | Description                                                                                                                                   |
| ------------------ | ----------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `replayPrevention` | `'local' \| 'remote' \| 'disabled'` | `'local'`                  | Controls how token reuse is prevented                                                                                                         |
| `replayStore`      | `ReplayStore`                       | `undefined`                | Required when `replayPrevention` is `'remote'`                                                                                                |
| `context`          | `string`                            | `undefined`                | Requires the same context value that was used when the challenge was issued                                                                   |
| `debug`            | `boolean`                           | auto (true in development) | Logs verification warnings to the console                                                                                                     |
| `onWarning`        | `(warning: VerifyWarning) => void`  | `undefined`                | Callback for structured warning events                                                                                                        |
| `rateLimiter`      | `RateLimiter`                       | `undefined`                | Bring-your-own rate limiter. When `check()` resolves `false`, `verifySolution()` throws a `RateLimitedError` instead of running verification. |
| `onEvent`          | `(event: RibauntEvent) => void`     | `undefined`                | Telemetry hook. Emits `verify-success` on pass and `verify-failure` (with the same `reason` as `VerifyFailureReason`) on failure.             |

## Return value

`Promise<VerifySolutionResult>`. The function returns either:

```ts theme={null}
{ valid: true }
```

or

```ts theme={null}
{ valid: false, reason, message }
```

## Examples

Basic usage:

```ts theme={null}
const { tokens, solutions } = req.body;
const result = await verifySolution(tokens, solutions);

if (!result.valid) {
  return res.status(400).json({ success: false, error: result.reason });
}
```

With structured warning telemetry:

```ts theme={null}
const result = await verifySolution(tokens, solutions, {
  context: 'signup',
  onWarning: (warning) => {
    // warning.reason: 'invalid-token' | 'expired-token' | 'invalid-solution' | 'context-mismatch'
    //                 | 'replay-detected' | 'replay-store-unavailable' | 'configuration-error'
    console.log('captcha warning', warning.reason, warning.message);
  },
});
```

With a rate limiter and telemetry:

```ts theme={null}
import { verifySolution, RateLimitedError } from 'ribaunt';

try {
  const result = await verifySolution(tokens, solutions, {
    rateLimiter: {
      check: async () => allowRequest(req.ip),
    },
    onEvent: (event) => {
      if (event.type === 'verify-success') metrics.increment('captcha.pass');
      if (event.type === 'verify-failure') {
        metrics.increment('captcha.fail', { reason: event.reason });
      }
    },
  });

  if (!result.valid) {
    return res.status(400).json({ error: result.reason });
  }
} catch (error) {
  if (error instanceof RateLimitedError) {
    return res.status(429).json({ error: error.message });
  }
  throw error;
}
```

<Note>
  `rateLimiter` runs before token validation, so it protects the verify endpoint even when callers submit malformed input. `onEvent` fires only after verification completes; if `rateLimiter` rejects a request, no event is emitted.
</Note>

<Warning>
  Do not pass `context: req.ip` just to key the rate limiter. Close over the request instead, as shown above. When you pass `context`, the tokens must have been created with the identical `context` value or verification fails with `context-mismatch`.
</Warning>

With a remote replay store:

```ts theme={null}
const result = await verifySolution(tokens, solutions, {
  replayPrevention: 'remote',
  replayStore: {
    consume: async (jti, expiresAt) => {
      // Atomic set-if-not-exists with TTL (e.g. Redis SET NX EXAT)
      return await redis.set(jti, '1', 'NX', 'EXAT', expiresAt) !== null;
    },
  },
});
```

## Warning reasons

| Reason                     | Description                                                                                                    |
| -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `invalid-token`            | JWT signature is invalid, the payload is malformed, or the token carries an unknown construction version (`v`) |
| `expired-token`            | Challenge TTL has passed                                                                                       |
| `invalid-solution`         | The nonce does not produce a valid hash                                                                        |
| `context-mismatch`         | The supplied context does not match the token's bound context                                                  |
| `replay-detected`          | Token was already consumed                                                                                     |
| `replay-store-unavailable` | The replay store threw during consumption (for example, Redis is unreachable). Verification fails closed.      |
| `configuration-error`      | `replayPrevention` is `'remote'` but no `replayStore` was provided                                             |
