diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7c5a44f..2581424 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -511,9 +511,9 @@ rule, and the required-certification gate (checked against the normal `member_certifications` data; an `equipment.manage` holder gets the override and may book on a member's behalf). No anonymous path. -### Door / access control (migrations 034-035; epic in progress) +### Door / access control (migrations 034-036; epic in progress) -Phased; P1-P2 built. `member_cards` (034) associates RFID/NFC UIDs to +Phased; P1-P3 built (member self-entry deferred to a separately reviewed step). `member_cards` (034) associates RFID/NFC UIDs to members; the UID is a credential (`door.manage`-only RLS, no member SELECT; masked count+last4 self-view via a service-client action). `door.manage` / `door.operate` permissions (group Access). `door_connections` (035) is a @@ -529,9 +529,23 @@ body, and redacts secrets before any audit write. `door_access_log` is an append-only, secrets-redacted audit with no client write policy (validated service-client executor only). `/door/manage` (door.manage) configures connections, picks a vault secret, and runs a safe `status`-only test. -Phase 3 adds live grant/revoke/open and the per-connection member -self-entry (opt-in, elevated risk, off by default); phases 4-5 add inbound -log ingest and the universal API-call UI builder. No anonymous path. +Phase 3 (036) adds the live actions, all `door.operate`, rate-limited, each +writing one redacted `door_access_log` row, reading through the service +client after the permission check (operators have no RLS read on +`door_connections`/`member_cards`). `door_card_slots` is the per-connection +integer-slot allocation map for controllers that key cards by slot +(HeatSync 0-200); the lowest-free policy is pure unit-tested logic +(`lib/door-slots-logic.ts`), `UNIQUE (connection_id, slot)` lets the DB +arbitrate concurrent grants, `UNIQUE (connection_id, card_id)` makes +re-grant idempotent. `grantCard` reserves the slot in the DB first, calls +the controller, and rolls the reservation back if the call fails; +`revokeCard` is idempotent and frees the slot only on confirmed controller +success so the app map never diverges from the device; `doorControl` +(open/unlock/lock) touches no slot. `/door/manage` surfaces these to +`door.operate` holders. The per-connection member self-entry (opt-in, +elevated risk, off by default) is the next, separately reviewed step; +phases 4-5 add inbound log ingest and the universal API-call UI builder. +No anonymous path. --- diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 793bc4b..b22a583 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,6 +4,26 @@ Append-only. Newest entries on top. Keep each entry to one screen. --- +## 2026-05-17 (pass 27) — Door P3 code-complete LOCAL (slot allocator + live actions); self-entry CHECKPOINT pending + +Branch `main`. Start state was the pass-26 clean checkpoint. Reaffirmed framing: this is a generic multi-space hackerspace platform, HeatSync is one tenant/adapter (memory `platform-not-heatsync-only`). Slot-allocator design presented and APPROVED before building; user decisions locked: slot range **0-200 no reservation**, grant ordering **reserve -> call -> roll back on failure**, **checkpoint before member self-entry: YES**. + +### Door P3 — CODE-COMPLETE, LOCAL, 5 commits, NOT deployed +Phase-by-phase, each gated (vitest + `pnpm build` read in-run) + own commit: +- **A** `036_door_card_slots.sql` + schema.sql Section 23 + types + DATABASE_SCHEMA/DB_SCHEMA_MAP. Per-connection integer-slot map. `UNIQUE(connection_id,slot)` (DB arbitrates concurrent grants) + `UNIQUE(connection_id,card_id)` (idempotent re-grant). RLS additive/default-deny: SELECT = door.manage/operate; NO client write policy (service-client executor only). +- **B** pure `lib/door-slots-logic.ts` (`pickLowestFreeSlot` lowest-free, range a param so adapter-generic; `slotCapacity`) + 8 tests. Suite 409 -> **417**. +- **C** `lib/actions/door.ts`: `grantCard`/`revokeCard`/`doorControl` (door.operate, rate-limited via `checkRateLimit`, each writes one redacted `door_access_log` row; reads via service client after the perm check since operators lack RLS read on door_connections/member_cards). Grant: reserve slot in DB first, call controller, roll back reservation on failure; idempotent; `slot_exhausted` message. Revoke: idempotent (no slot = already revoked); frees slot only on confirmed controller success. Control open/unlock/lock via the existing hardened `lib/door/executor.ts`. Zod schemas + API_REFERENCE. +- **D** `listDoorCards` (door.operate, service client, masked last4 only) + `/door/manage` passes `canOperate` (door.operate rpc) and renders per-connection Open/Unlock/Lock + expandable card list Grant/Revoke, every action behind a confirm. +- **E** ARCHITECTURE + this entry. + +### Open / next +- LOCAL & undeployed: Door P3 (5 commits: `3985419`, `a5e1ed9`, `e5cedb6`, `c787766`, + this docs commit). Plus the still-unpushed pass-26 HANDOFF docs commit `7ea3dea`. **Awaiting deploy approval** (migration 036). After push: confirm 036 applied; with a reachable controller, exercise grant (slot assigned) -> revoke (slot freed) -> open/lock/unlock; verify audit rows redacted. NOT browser-verified (no live controller/browser here). +- **CHECKPOINT (locked, do next): present the member self-entry design for review BEFORE building it.** Scope: a member self "buzz me in" action allowed only when `door_connections.allow_member_self_entry` is true AND the caller has an active authorized `member_card`; strict per-member rate limit + full audit + confirm + never anonymous; surfaced on `/me` or a member door page. This is the single highest-risk piece in the epic; build only after explicit approval. +- Then **P4** inbound access-log ingest (poll `?z`/webhook -> resolve card_uid->member into door_access_log); **P5** universal API-call UI builder (`api_buttons`, same SSRF executor, door template). +- Residual hardening (unchanged): SSRF pin is host-string equality; hostname pin -> theoretical DNS-rebinding (mitigated by no-redirect + typically-IP pin); consider resolve-and-check. Systemic test gap: no Supabase mock harness, so action orchestration (slot reserve/rollback, idempotency, RLS) is not unit-tested; pure slot logic is. + +--- + ## 2026-05-17 (pass 26) — CLEAN CHECKPOINT: modules 1-3 + Door P1/P2 all DEPLOYED Clean state: `origin/main == main`, tree clean, latest deploy (`feat(door): flexible transport…`, run 25980463354) green. Migrations applied through **035**. 409 unit tests, `pnpm build` clean. Nothing local/undeployed except this HANDOFF commit (docs-only, unpushed; deploy or carry forward next pass).