feat(door): P3-C live grant/revoke/open-lock-unlock actions

door.operate, rate-limited, every attempt writes one redacted
door_access_log row. Operators have no RLS read on door_connections/
member_cards so reads use the service client after the permission check,
space-scoped. grantCard reserves the slot in the DB first (unique
constraints arbitrate concurrency), calls the controller, rolls the
reservation back on failure; idempotent re-grant; slot_exhausted when
full. revokeCard idempotent; frees the slot only on confirmed controller
success so the app map never diverges from the device. doorControl
open/unlock/lock via the single hardened executor. API_REFERENCE.
This commit is contained in:
Moheeb Zara
2026-05-16 21:09:59 -07:00
parent a5e1ed985c
commit e5cedb62e5
3 changed files with 361 additions and 2 deletions
+19
View File
@@ -946,6 +946,25 @@ export const doorConnectionIdSchema = z.object({
connectionId: z.string().uuid('Invalid connection ID'),
})
// ─── Door live actions (door.operate) ────────────────────────────────────────
export const doorGrantSchema = z.object({
connectionId: z.string().uuid('Invalid connection ID'),
cardId: z.string().uuid('Invalid card ID'),
// HeatSync permission mask; defaults to 1 (basic access).
permissionMask: z.number().int().min(0).max(255).optional().default(1),
})
export const doorRevokeSchema = z.object({
connectionId: z.string().uuid('Invalid connection ID'),
cardId: z.string().uuid('Invalid card ID'),
})
export const doorControlSchema = z.object({
connectionId: z.string().uuid('Invalid connection ID'),
verb: z.enum(['open', 'unlock', 'lock']),
})
// ─── Generic ID schemas ──────────────────────────────────────────────────────
export const uuidSchema = z.string().uuid('Invalid ID format')