Door epic phases 4-5: inbound access-log ingest + universal API-call buttons
Phase 4 (migration 053): pull entry/denied events into door_access_log and match each card to a member, via two transports sharing one ingest core. Poll: a CRON_SECRET-guarded /api/cron/door-ingest reads each opted-in native-HeatSync connection's ?z log through the existing hardened executor and parses it (firmware-characterized, best-effort ring buffer). Webhook: /api/door/inbound/[connection] accepts pushed event JSON authenticated by a per-connection bearer secret from the vault. Both dedupe on (connection_id, dedupe_key) so re-polls and retries are no-ops. Adds an inbound config panel to /door/manage. Phase 5 (migration 054): admins define api_buttons that fire a configured HTTP call (full verbs + headers + body) when an authorized member presses them, through the same hardened egress (now shared between callDoor and the new callApi). Each button carries a required_permission (default the new apicall.invoke code); members see only buttons they may press. Builder at /door/buttons with a door-template preset; members press from /doors. Every press is recorded in api_call_log with secrets redacted. Both subsystems are inert until an admin enables inbound on a connection or creates a button. 624 unit + 50 integration tests; migrations applied top-to-bottom in schema.sql; types and docs updated.
This commit is contained in:
@@ -733,6 +733,8 @@ migration is mirrored as a section in it. Tables/columns added:
|
||||
| 051 | `notifications.read_at timestamptz` (+ partial index `idx_notifications_unread` on `(space_id, member_id) WHERE read_at IS NULL`): the outbox doubles as the member's in-app inbox on `/me`; `read_at IS NULL` = unread. The mark-read action is a validated service-client write scoped to the caller's own `member_id` (no RLS change; notifications stays no-client-write). The in-app inbox is the always-on channel: a notification whose email was muted (`skipped`) still appears here. No new permission code |
|
||||
| 052 | HOTFIX (production outage): drop `notification_preferences_space_id_fkey`. The table (048) had FKs to BOTH `space_members` and `spaces`, which PostgREST reads as a junction, making the auth layout's `space_members.select('*, spaces(*)')` embed ambiguous (PGRST201) -- membership resolved to null and every logged-in user was bounced to `/signup`. Dropping the redundant `space_id -> spaces` FK leaves one FK so the table is no longer a junction; space is still reachable via `member_id -> space_members`, and space deletion still cascades through it. **Never add a `spaces` FK to a `space_members`-referencing table** (reintroduces the junction). Guarded by `integration/auth-embed.test.ts`. No new table/column |
|
||||
| 050 | `dues_payment_methods.url` gains a CHECK constraint (`dues_payment_methods_url_https`, `url ~* '^https://'`). The url is rendered to members as a clickable href; RLS lets an admin/board write the row directly via PostgREST, bypassing the app Zod check, so the DB enforces https as the source of truth (a `javascript:`/`http:` value can never be stored). The member-read action also re-validates with `isSafeDuesUrl` (defense-in-depth). Idempotent DO-block add-if-missing. No new table/column/policy |
|
||||
| 054 | Door epic Phase 5 (universal API-call UI builder). New permission code `apicall.invoke` (group Access), seeded into the `seed_default_role_permissions` trigger fn (CREATE OR REPLACE) + backfilled for `board` on existing spaces (admin holds it implicitly), additive exactly like the door.* codes in 034. `api_buttons` (per-space button defs: label, button_group, sort_order, method GET/POST/PUT/PATCH/DELETE, base_url https?, pinned_host SSRF pin, url_template, headers jsonb, body_template, auth_mode/auth_param, secret_ref -> vault, required_permission default `apicall.invoke`, confirm, is_enabled). All CRUD = `door.manage` (the catalog already scopes door.manage to "buttons"); each button's `required_permission` gates pressing it (enforced by the service-client invoke action, not RLS, so members never need RLS read on the defs). `api_call_log` (append-only press audit, secrets redacted; SELECT = `door.manage`; NO client write policy, service-client invoker only, immutable). secret_ref is the only `secrets` FK so no junction shape. `touch_updated_at` on api_buttons |
|
||||
| 053 | Door epic Phase 4 (inbound access-log ingest). Additive columns only. `door_access_log.dedupe_key text` + partial-unique `idx_door_access_log_dedupe (connection_id, dedupe_key) WHERE dedupe_key IS NOT NULL`: a per-event idempotency token so a re-poll of the same `?z` ring-buffer slot or a webhook retry cannot double-insert (insert path uses ON CONFLICT DO NOTHING); existing action rows leave it NULL and are unaffected. `door_connections.inbound_enabled boolean default false` (opt-in, gates the per-connection webhook endpoint) + `door_connections.inbound_secret_ref uuid -> secrets` (INBOUND bearer secret in the AES vault, distinct from `secret_ref` the outbound door password; loaded + constant-time-compared server-side, never returned). PGRST: door_connections now has two FKs to `secrets`, but its surrogate-`id` PK means it is NOT a junction shape (the 048 outage class), nothing embeds `secrets` via PostgREST, and the auth embed is untouched. No new permission code; cron + webhook writers are the validated service client (door_access_log stays no-client-write/immutable) |
|
||||
|
||||
New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user