Files
2026-05-27 23:47:53 +01:00

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 ±60 steps (= ±30 minutes at the default period=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 — both delta and suggestedClockOffsetMs are null when 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.