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.
56 lines
3.0 KiB
SQL
56 lines
3.0 KiB
SQL
-- =============================================================================
|
|
-- 053: Door inbound access-log ingest (Door epic, phase 4)
|
|
-- =============================================================================
|
|
-- Phase 4 pulls real entry/denied events FROM a controller INTO door_access_log,
|
|
-- via two transports that share one ingest core:
|
|
-- * poll: a CRON_SECRET-guarded route reads each enabled connection's log
|
|
-- verb (HeatSync ?z) through the existing hardened executor and parses it;
|
|
-- * webhook: a per-connection public endpoint accepts normalized event JSON
|
|
-- pushed by the controller or a relay, authenticated by a per-connection
|
|
-- bearer secret kept in the AES-256-GCM vault.
|
|
--
|
|
-- This migration is ADDITIVE ONLY (no table re-create, no policy weakening):
|
|
--
|
|
-- door_access_log.dedupe_key
|
|
-- A per-event idempotency token so a re-poll of the same ring-buffer slot
|
|
-- or a webhook retry cannot double-insert. Partial-unique on
|
|
-- (connection_id, dedupe_key) WHERE dedupe_key IS NOT NULL: existing
|
|
-- action rows (grant/revoke/open/self_entry) leave it NULL and are
|
|
-- unaffected; only ingested rows set it. The insert path uses ON CONFLICT
|
|
-- DO NOTHING against this index.
|
|
--
|
|
-- door_connections.inbound_enabled (default false; opt-in, like
|
|
-- allow_member_self_entry) gates the webhook endpoint for a connection.
|
|
-- door_connections.inbound_secret_ref references the SAME secrets vault as
|
|
-- secret_ref but holds the INBOUND webhook secret (a distinct credential
|
|
-- from the outbound door password). It is loaded + compared server-side
|
|
-- only and never returned to the browser.
|
|
--
|
|
-- PGRST embed note (the migration-048 outage class): door_connections now has
|
|
-- two FKs to public.secrets (secret_ref + inbound_secret_ref). This does NOT
|
|
-- create the ambiguous-junction shape that broke the auth embed -- that only
|
|
-- happens when a table's PRIMARY KEY covers both FK columns; door_connections
|
|
-- has a surrogate `id` PK. Nothing embeds secrets through PostgREST anyway
|
|
-- (secrets are read service-side by id), and the auth path
|
|
-- (space_members <-> spaces) is untouched. Safe.
|
|
--
|
|
-- No new permission codes: door.manage configures inbound; the cron + webhook
|
|
-- writers are the validated service client (door_access_log already has no
|
|
-- client INSERT policy and stays immutable from the client).
|
|
--
|
|
-- Idempotent: ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS.
|
|
-- =============================================================================
|
|
|
|
ALTER TABLE public.door_access_log
|
|
ADD COLUMN IF NOT EXISTS dedupe_key text;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_door_access_log_dedupe
|
|
ON public.door_access_log (connection_id, dedupe_key)
|
|
WHERE dedupe_key IS NOT NULL;
|
|
|
|
ALTER TABLE public.door_connections
|
|
ADD COLUMN IF NOT EXISTS inbound_enabled boolean NOT NULL DEFAULT false;
|
|
|
|
ALTER TABLE public.door_connections
|
|
ADD COLUMN IF NOT EXISTS inbound_secret_ref uuid REFERENCES public.secrets(id) ON DELETE SET NULL;
|