Pivot from the original U13 spec. Original spec ("router.refresh rework") was already shipped by Slice J (2026-05-18, -2966ms / -67%). This U13 attacks the remaining architectural floor: the PIN verify itself. Despite the Slice J memo's "bcrypt 300ms" label, the PIN hash is actually PBKDF2-SHA-256 at 100,000 iterations via Web Crypto — bcrypt is only used for web user passwords. Dropping iterations 100k → 25k is ~4× faster on CF Workers (~225ms shaved) and still exceeds the NIST 800-63B minimum (10k) by 2.5×. PIN hash cost is largely security theater for low-entropy PINs anyway — real defense is online rate-limiting + DB access control.
lib/auth/pin.ts — ITERATIONS constant flipped 100_000 → 25_000. New exported needsRehash(stored): boolean helper returns true when stored hash's iter count differs from the current default. hashPin + verifyPin unchanged in shape (already iter-count-aware via the $pbkdf2$<iters>$<salt>$<hash> storage format).lib/db/pin_identities.ts — new rehashPinIdentity + maybeRehashPinIdentity helpers. Both verifyPinForIdentity (single-row path used by unlockRegisterAction) and verifyPinIdentity (linear-scan path) await the maybeRehash call on successful verify. In a Next request scope, after() queues the rehash off the response critical path; outside a request (tsx probes), falls back to inline-await so migration still happens deterministically.lib/db/tenant_users.ts — same shape for the manager-override POS PIN. verifyPosPinForUser awaits maybeRehashPosPin on success.| ✓ | ITERATIONS constant = 25,000 + U13 rationale comment present |
| ✓ | needsRehash exported; returns false for fresh hashes, true for legacy 100k / malformed / wrong-algo, false for empty |
| ✓ | hashPin produces a hash with iters=25000 in the storage format |
| ✓ | verifyPin accepts BOTH a manually-built legacy 100k hash AND a new 25k hash; rejects wrong PINs on both |
| ✓ | pin_identities.ts: maybeRehashPinIdentity helper exists; both verifyPinForIdentity + verifyPinIdentity (linear scan) await it on success |
| ✓ | tenant_users.ts: maybeRehashPosPin helper exists; verifyPosPinForUser awaits it on success |
| ✓ | DAO round-trip on local lumiere: seed pin_identity with hand-crafted 100k legacy hash → verifyPinForIdentity returns row → poll DB → pinHash flipped to iters=25000 (rehash worked via the inline-fallback path in tsx) |
| ✓ | Local timing: 25k verify is ≥1.5× faster than 100k on the test runner's CPU (typical ratio ≈ 3-4× — soft floor at 1.5× to keep harness stable) |
Typecheck: npx tsc --noEmit on nix-cafe → 0 errors.
karouna-dev → CF auto-deploy.test-u13-prod.mjs on lumiere: drive the full PIN unlock flow on lumiere, time PIN-Enter → workspace top-bar visible. Assert ≤1300ms (target floor from 1437ms baseline). Reset a test cashier's PIN to verify the new hash lands at 25k. Verify a legacy 100k cashier (if any exist on prod) unlocks successfully + the post-verify row update flips it to 25k.