v0.1.3 — accept the immediately-previous bound cookie within its lifetime
The bound cookie rotates on every /dbsc/refresh. The refresh round-trip is a propagation window during which a normal, already-dispatched request still carries the pre-rotation value; an enforcement gate built on boundCookieMatches() saw that as a mismatch and tore the session down, even though the request was legitimate. The window equals the refresh round-trip, so the failure is latency-proportional — frequent behind a CDN/WAN, near-invisible on loopback.
boundCookieMatches() now also accepts the single immediately-previous cookie value, but only until the instant that value would itself have expired in the browser (cookieIssuedAt + cookieMaxAgeSeconds). No arbitrary grace constant.
- Single-depth history — two-rotations-ago never matches.
- A stolen cookie still cannot complete a refresh without the device-bound key, so it still hard-fails at the next refresh.
- Required
Bindingfields stay strictly type-checked: the v0.1.2 corrupt-state fail-closed contract is unchanged. The new fields are optional with safe defaults — records written by v0.1.0–v0.1.2 decode without throwing and simply have no previous-value overlap until their next refresh. Never a lockout, never fail-open.
No API removals; Binding::withRotatedCookieAndChallenge() gains $now and $cookieMaxAgeSeconds parameters.