2026-05-12 on prod (get-coffee). Two features bundled. View existing PIN — owner/manager reveals an existing cashier or member POS PIN without resetting it, via new AES-GCM encrypted column populated on every create/reset, gated by new permission nix_cafe.team.view_pin. Admin set/reset/view member POS PIN — new admin-side flow on /cafe/team Members tab; the existing self-serve flow (POS lock screen + password 2FA) is unchanged. PBKDF2 hash stays authoritative for verification; pin_enc is reversible storage purely for display. Every reveal audit-logged.
| test-pin-view-prod.mjs | 7/7 |
| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-phase2-cafe-multishop-prod.mjs | 6/6 |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |
Run with NODE_TLS_REJECT_UNAUTHORIZED=0 to work around today's Windows schannel cert-chain issue against api.nixtech.app + cafe/api routes from Node's built-in fetch() — same environment issue that needed --strict-ssl=false on npm install and -c http.sslVerify=false on git push. Browser-driven Playwright tests (own TLS stack) never failed. Not a code regression.
backend (1 migration + 2 migrate.js entries):
migrations/20260512100000_pin_enc_columns.ts
migrate.js:
20260512100000 — ADD COLUMN commerce.pin_identities.pin_enc + tenant_users.pos_pin_enc
20260512110000 — INSERT nix_permissions row + grant to owner+manager
across all tenants (8/8 prod grants applied)
cafe (4 commits — 1 main + 3 follow-up fixes surfaced by Gate 2):
7208b40 feat(cafe-team): view existing PIN + admin set/reset/view member POS PIN
(main bundle: schema + DAOs + actions + UI on Cashiers tab + EditMemberModal)
f2953c4 fix(cafe-team): EditMemberModal scrolls when tall
(POS PIN section was pushing footer buttons below viewport
on default 900px height — added max-h-[90vh] overflow-y-auto)
8065bd8 fix(cafe-team): router.refresh after create cashier so the
new row appears immediately (newly-created cashier wasn't
visible until next page navigation)
4af72e0 fix(cafe-team): wrap getPinForIdentity + getMemberPosPin in
db.transaction to skip Hyperdrive cache (view-immediately-
after-reset was decrypting the previous pin_enc — same
pattern as listPinIdentities + telegram getters)
(1) Modal sizing — when adding sections to an existing modal, check the
total height fits a 900px viewport. EditMemberModal grew from ~3
sections to 4 with POS PIN; the footer Save/Remove/Cancel buttons
were getting pushed below the viewport on the standard test
resolution. max-h-[90vh] + overflow-y-auto on the card fixes it.
(2) revalidatePath ≠ refresh — server-side revalidatePath marks /team
stale for subsequent navigations but does NOT trigger the current
client tree to refetch. The create-cashier flow needed an explicit
router.refresh() to make the new row visible immediately.
Pre-existing latent bug; surfaced because the new test exercises
the row-just-after-create path more aggressively than before.
(3) Hyperdrive caching strikes again — getPinForIdentity +
getMemberPosPin were NOT wrapped in db.transaction, so a view
immediately after a reset would decrypt the previous pin_enc.
Symptom on prod: reset returns 5535 then view returns 5411 (the
pre-reset value). Fix: wrap both DAOs in db.transaction — same
pattern documented in project_hyperdrive_cache_stale_reads.md.
Local DAO probe missed this because docker postgres has no
Hyperdrive layer; only surfaces on prod cf-workers + cf-pages.