first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
# `totp/verify-many`
## Purpose
Backup-aware TOTP verify. Used during rotation windows where an old and a
new TOTP secret coexist — the user's authenticator may still be holding the
old secret while the server has moved to the new one. Returns the matching
secret index and signed step delta so the caller can mark the rotation
complete after the user has successfully authenticated against the new
secret.
## Features
- Multi-secret loop with constant-time evaluation across the entire list.
- Same `±window` semantics as `verify`.
- Returns `{ valid, delta, secretIndex }` so the caller can record which
secret was used.
- Window cap and Zod-boundary semantics shared with `verify`.
## Usage: Basic Example
```ts
import { verifyMany } from "@suaveplan/otp";
const r = verifyMany(code, [oldSecret, newSecret]);
if (r.valid) markUsed(r.secretIndex);
```
## Usage: Advanced Example
```ts
import { verifyMany } from "@suaveplan/otp";
// During rotation grace period — accept either secret but log when the old
// one is used so the rollout dashboard tracks lag.
const r = verifyMany(code, [oldSecret, newSecret], {
window: 1,
clock: () => Date.now(),
});
if (r.valid && r.secretIndex === 0) metric.inc("totp.rotation.legacy");
```
## API Reference
- `verifyMany(code, secrets, opts?): TotpVerifyManyResult` —
`{ valid: true; delta; secretIndex }` on match, all-`null` triple on miss.
## Implementation Notes
Both loops (over secrets and over windowed steps) run to completion even
after the first match so the wall-time of `verifyMany` is independent of
which secret / which step matched. The match flag uses `matchedDelta === null`
to record the first hit without affecting subsequent iterations. This is
the same constant-time pattern as `verify`, extended to cover the secret
dimension as well.
When the secret rotation completes (every authenticated user has
re-enrolled against the new secret), the caller drops the old secret from
the list and `verifyMany` collapses back to single-secret behaviour. There
is no separate "single-secret" code path inside `verifyMany` — a
single-element list runs the same way as a multi-element one. This keeps
the constant-time guarantees uniform across rotation states.