2.1 KiB
drift/detect
(no submodules)
Purpose
Search outside the regular verify window for clock drift. Callers use this
to render UI hints like "your device clock is 47 seconds fast" — the
verify path with window=1 would simply reject such a code, leaving the
user with no idea why. detectDrift sweeps a wider range and returns both
the signed step delta and the inferred clock offset in milliseconds.
Features
- Default
±60steps (= ±30 minutes at the defaultperiod=30). - Returns
{ delta, suggestedClockOffsetMs }discriminated union. - Constant-time across the sweep range — same defence as
verify. - Honours the same TOTP option bag as
verify(algorithm / digits / period / t0 / clock).
Usage: Basic Example
import { detectDrift } from "@suaveplan/otp";
const r = detectDrift(code, secret);
if (r.delta !== null && Math.abs(r.delta) > 1) {
show("Your device clock is " + r.suggestedClockOffsetMs + " ms off.");
}
Usage: Advanced Example
import { detectDrift } from "@suaveplan/otp";
const r = detectDrift(code, secret, {
maxSteps: 120, // sweep ±60 minutes at period=30
clock: () => Date.now(),
});
API Reference
detectDrift(code, secret, opts?): DriftResult— bothdeltaandsuggestedClockOffsetMsarenullwhen no candidate matched.
Implementation Notes
The negative-step guard is critical near time origin (t < period * maxSteps)
because BigInt arithmetic does not raise on underflow; the guard lets the
sweep skip unreachable negative steps without affecting wall-clock timing
of the loop. The inferred suggestedClockOffsetMs is delta * period * 1000
and is signed so the UI can render direction.
This module is intentionally separate from verify because the two have
different threat models: verify is on the hot path of every login and
must be tightly bounded, while detectDrift runs only when verify
already failed, sweeps a wider range, and exists to render a helpful
error message rather than to authenticate. Callers should never accept a
code on the basis of detectDrift alone — its purpose is diagnostic, not
gating.