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:
@@ -939,6 +939,16 @@ Phase 3 live actions (all `door.operate`, rate-limited, every attempt writes one
|
||||
|
||||
Member self-entry (no permission code; any active member): `selfEntry({connectionId})` triggers a momentary OPEN only, allowed only when the connection is `is_enabled` AND `allow_member_self_entry` AND the caller has at least one active `member_card` on file (the locked eligibility rule: a `door_card_slots` row is NOT required). Membership and cards are resolved server-side, so a member can only ever open a door for themselves; never unlock/lock/grant/revoke, never anonymous. Strict per-member rate limit (5/min), one redacted `door_access_log` row `action='self_entry'` (`target_member_id` = self), through the same hardened executor. `listSelfEntryDoors()` returns the enabled self-entry connections for the caller's space, but only when the caller has an active card (otherwise empty), so the surface is hidden for ineligible members. `listMyDoorActivity()` returns the caller's own `door_access_log` rows (actor or target; service client after `requireMember`; `detail` is already secret-redacted at write time). Surfaced as a "Door access" panel on the dashboard and on the member `/doors` page (self-entry + masked own cards via `getMyCards` + recent personal activity).
|
||||
|
||||
Phase 4 inbound access-log ingest (migration 053): real entry/denied events are pulled INTO `door_access_log` and matched to a member, via two transports sharing one ingest core (`lib/door/ingest.ts`, service-client writer; the audit log stays no-client-write). Config is folded into `updateDoorConnection`/`createDoorConnection`, which gained `inbound_enabled` (gates both transports, off by default) and `inbound_secret_ref` (a distinct vault secret from the outbound `secret_ref`; the create/update path verifies every referenced secret belongs to the caller's space). Both transports resolve a presented card to a member by numeric hex/decimal equality (`cardUidsEquivalent`) and dedupe-insert: `door_access_log.dedupe_key` + the partial-unique `(connection_id, dedupe_key)` index make a re-poll / webhook retry a no-op; action rows leave it NULL.
|
||||
- `POST /api/cron/door-ingest` (nodejs, force-dynamic; constant-time `CRON_SECRET` bearer, 503 if unset / 401 on mismatch; `proxy.ts`-whitelisted). Polls each `is_enabled` + `inbound_enabled` + `adapter='native_heatsync'` connection's `?z` log through the hardened executor (`fullBody`, 32KB cap), parses it with the pure firmware-characterized `lib/door-log-logic.ts` (`parseHeatSyncLog`: `<pre>` ring-buffer dump, G+g/D+d card-number reconstruction via the 32767 divisor, best-effort H:M:E, raw R/r reads intentionally skipped), and ingests. Bounded to 50 connections/run. Returns `{ ok, polled, inserted, resolved, failed }`. Best-effort by nature (the HeatSync ring buffer has no per-entry sequence id); idempotent across overlapping/missed minutes.
|
||||
- `POST /api/door/inbound/[connection]` (nodejs, force-dynamic; session-exempt, `proxy.ts`-whitelists the `/api/door/inbound` prefix). Authenticated by the connection's per-connection bearer secret (`inbound_secret_ref` from the vault, constant-time compared; the same generic 401 for not-found / inbound-disabled / wrong-secret so it is not an existence oracle). Body-size guarded (413 over 64KB), per-connection rate-limited (120/min), Zod-validated (`doorWebhookPayloadSchema`: `{ events: [{ id, card_uid?|card_number?, result?, occurred_at? }] }`, ≤100 events; `id` is the dedupe token). `normalizeWebhookEvents` → ingest core. A supplied `occurred_at` is clamped to not-in-the-future (60s skew) so a relay cannot reorder the operator's log. Returns `{ received, inserted, resolved, skipped }`. This is the reliable inbound transport for any (incl. generic/non-HeatSync) controller.
|
||||
|
||||
### Universal API-call buttons: `api-buttons.ts` (migration 054, door epic P5)
|
||||
Admin-defined buttons that fire a configured HTTP call through the same hardened egress as door control. Managing buttons reuses `door.manage` (the catalog scopes it to "buttons"); pressing a button is gated by its own `required_permission` (default the new generic `apicall.invoke`; a door-flavored button can require `door.operate`). One new permission code `apicall.invoke` (group Access), additive + backfilled for board like the door.* codes.
|
||||
- Manage (all `door.manage`, RLS-gated): `createApiButton`/`updateApiButton`/`deleteApiButton`/`listApiButtons`. A button stores label, button_group, sort_order, method (GET/POST/PUT/PATCH/DELETE), base_url (http(s)), pinned_host (SSRF pin), url_template, headers (jsonb), body_template, auth_mode (none/query/header/bearer) + auth_param, secret_ref (vault; the create/update path verifies it belongs to the space), required_permission (validated against the catalog), confirm, is_enabled. `listApiCallLog()` is the press audit (`door.manage`).
|
||||
- Member-facing (any member, gated per button): `listInvokableButtons()` is a service-client read (members have no RLS read on the defs) returning ONLY presentational fields (id/label/group/confirm) for enabled buttons whose `required_permission` the caller holds. `invokeApiButton({ buttonId })` is rate-limited first (30/min), loads the def service-side scoped to the member's space, checks the per-row permission (denials audited), decrypts the secret server-side and fires via `callApi` (full verbs + headers + body; the secret is injected per auth_mode and never returned), writing one redacted `api_call_log` row. Returns a generic error on failure (the redacted detail goes only to the log).
|
||||
- Egress: `lib/door/executor.ts` now shares one internal `egress` (SSRF host pin, resolve-once-connect-by-IP, no redirects, time/body caps, secret redaction). `callDoor` (GET) is unchanged; `callApi` adds method/headers/body with the host header forced to the pin. Pure request assembly + secret placement is unit-tested in `lib/api-call-logic.ts` (`buildApiRequest`). The metadata/link-local block is absolute even for an admin-configured button. Builder UI at `/door/buttons` (door-template preset); members press from `/doors`.
|
||||
|
||||
### Presence & attendance: `presence.ts` (migration 038)
|
||||
No permission code; any active member. All writes are self-only — the member is resolved server-side, never taken from input — and go through validated service-client actions (`space_visits` has no client write policy). Pure logic in `lib/presence-logic.ts` (`presenceStatus`, `hostEligibility`, `summarizePresence`) is unit-tested.
|
||||
- `checkIn({asHost?, note?})` — opens a visit. Enforces one open visit per member (partial unique index); if an existing open visit is stale (`presenceStatus` past `PRESENCE_MAX_OPEN_HOURS`, 18h) it is auto-closed first (handles forgotten check-outs without a cron), and a fresh existing open visit is rejected ("already checked in"). `asHost` runs `hostEligibility`: if `spaces.host_requires_card` (default true) the caller must have an active `member_card` on file (count checked via the service client); a space may flip the toggle off to let anyone self-mark host. Writes an `activity_log` row.
|
||||
|
||||
+44
-2
@@ -616,8 +616,50 @@ row; surfaced as a "Door access" panel on the dashboard and on the
|
||||
member `/doors` page (self-entry + masked own cards via `getMyCards` +
|
||||
the member's own recent activity via `listMyDoorActivity`, a service-client
|
||||
read after `requireMember` filtered to that member), hidden entirely for
|
||||
ineligible members. Phases 4-5 add inbound log ingest and the
|
||||
universal API-call UI builder. No anonymous path. The executor is the
|
||||
ineligible members. Phase 4 (053) adds inbound access-log ingest: real
|
||||
entry/denied events are pulled INTO `door_access_log` and matched to a
|
||||
member, via two transports that share one ingest core
|
||||
(`lib/door/ingest.ts`). Poll: `POST /api/cron/door-ingest` (constant-time
|
||||
`CRON_SECRET`, `proxy.ts`-whitelisted, once-a-minute crontab) reads each
|
||||
`inbound_enabled` native-HeatSync connection's `?z` log through the same
|
||||
hardened executor (`fullBody`, capped) and parses it with the pure,
|
||||
firmware-characterized `lib/door-log-logic.ts` (`<pre>` ring-buffer dump,
|
||||
G+g/D+d card-number reconstruction with the 32767 divisor, best-effort
|
||||
H:M:E). Webhook: `POST /api/door/inbound/[connection]` (also
|
||||
`proxy.ts`-whitelisted, session-exempt) accepts pushed event JSON from any
|
||||
controller/relay, authenticated by a per-connection bearer secret
|
||||
(`inbound_secret_ref`, a distinct vault secret from the outbound password,
|
||||
constant-time compared); body-size guarded, rate-limited, Zod-validated
|
||||
(≤100 events). Both resolve a presented card to a member via numeric
|
||||
hex/decimal equality (`cardUidsEquivalent`) and dedupe-insert through the
|
||||
service client: `door_access_log.dedupe_key` + the partial-unique
|
||||
`(connection_id, dedupe_key)` index make a re-poll or webhook retry a
|
||||
no-op, while NULL-keyed action rows stay unconstrained. The poll is
|
||||
inherently best-effort (the HeatSync ring buffer has no per-entry sequence
|
||||
id); the webhook (explicit event ids) is the reliable transport. The audit
|
||||
log stays no-client-write/immutable; a webhook-supplied `occurred_at` is
|
||||
clamped to not-in-the-future so a relay cannot reorder the operator's log.
|
||||
Phase 5 (054) is the universal API-call UI builder: admins (door.manage,
|
||||
which the catalog already scopes to "buttons") define `api_buttons` (label,
|
||||
group, method GET/POST/PUT/PATCH/DELETE, base_url, pinned_host, url_template,
|
||||
headers, body_template, auth_mode/auth_param, vault `secret_ref`, confirm,
|
||||
and a per-button `required_permission`). A member presses only buttons whose
|
||||
`required_permission` they hold (one new generic code `apicall.invoke`, the
|
||||
default; a door-flavored button can require `door.operate`). `listInvokable
|
||||
Buttons` is a service-client read returning only presentational fields (never
|
||||
url/headers/secret); `invokeApiButton` is rate-limited-first, loads the
|
||||
definition service-side scoped to the member's space, checks the per-row
|
||||
permission (denials audited), decrypts the secret server-side and fires
|
||||
through `callApi`. The egress is now shared: the SSRF+resolve+connect-by-IP+
|
||||
no-redirect+caps+redact core is factored into one internal `egress`, with
|
||||
`callDoor` (GET, query secret) and `callApi` (full verbs + headers + body +
|
||||
secret injected per auth_mode, host header forced to the pin) on top; pure
|
||||
request assembly + secret placement is unit-tested in `lib/api-call-logic.ts`
|
||||
(`buildApiRequest`). The metadata/link-local block is absolute even for an
|
||||
admin-configured button. `api_call_log` is the append-only, redacted,
|
||||
service-client-only press audit (SELECT = door.manage). The builder lives at
|
||||
`/door/buttons` (with a door-template preset); members press from `/doors`.
|
||||
No anonymous path. The executor is the
|
||||
single hardened egress: it resolves the controller host once via
|
||||
`dns.lookup`, rejects if any resolved IP is loopback / unspecified /
|
||||
link-local / metadata (IPv4-mapped IPv6 normalized; RFC1918 / LAN / ULA
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -64,6 +64,16 @@ Transactional email is an outbox drained by `POST /api/cron/notifications`. Add
|
||||
|
||||
It hits the app locally (bypassing Caddy), self-throttles under Resend's rate limit, and is idempotent, so a missed or overlapping minute is harmless. Migration `041_notifications.sql` is applied automatically by `deploy.sh` like every other `scripts/0*.sql`; no manual database step.
|
||||
|
||||
### Door inbound-log poll cron (optional, door epic Phase 4)
|
||||
|
||||
If a space runs a native-HeatSync controller and turns on inbound ingest (per connection, on `/door/manage`), the once-a-minute poll pulls its `?z` log into the access log and matches each card to a member. It reuses the same `CRON_SECRET`; add a second crontab entry:
|
||||
|
||||
```
|
||||
* * * * * curl -fsS -m 30 -X POST http://127.0.0.1:3000/api/cron/door-ingest -H "Authorization: Bearer $CRON_SECRET" >/dev/null 2>&1
|
||||
```
|
||||
|
||||
No new env var. The route returns 503 if `CRON_SECRET` is unset and 401 on mismatch, polls only connections with `inbound_enabled` + `adapter='native_heatsync'`, and is idempotent (overlapping or missed minutes are harmless; events dedupe on `(connection_id, dedupe_key)`). Generic (non-HeatSync) controllers do NOT use this poll; they push to the per-connection webhook instead (`POST /api/door/inbound/[connection]`, authenticated by the connection's bearer secret from the vault — no env var, no crontab; the webhook URL is shown on `/door/manage` when inbound is enabled). Migration `053_door_inbound_ingest.sql` applies automatically with the rest.
|
||||
|
||||
### Member email change (Supabase Auth config — required)
|
||||
|
||||
The self-serve "change my login email" feature (`/me` Profile tab) needs Supabase project configuration that is NOT in code. Until this is set, requesting an email change will send a link that does not land correctly.
|
||||
|
||||
@@ -28,6 +28,43 @@ Append-only. Newest entries on top. Keep each entry to one screen.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-20 (pass 64): Door epic Phase 5 (universal API-call UI builder) (LOCAL, NOT deployed)
|
||||
|
||||
Branch `main`. Completes the door epic. Design-first via AskUserQuestion; the user chose **full verbs** (GET/POST/PUT/PATCH/DELETE) + **per-button required permission**. Suite 624 unit (was 615; +9 buildApiRequest tests) / 50 integration (was 44; +6 api-buttons) / build green / lint clean.
|
||||
|
||||
**Shipped (local):**
|
||||
- **Migration 054**: new permission code `apicall.invoke` (group Access) seeded into `seed_default_role_permissions` (CREATE OR REPLACE) + backfilled for board on existing spaces (additive, like door.* in 034). `api_buttons` (label, button_group, sort_order, method, base_url, pinned_host, url_template, headers jsonb, body_template, auth_mode/auth_param, secret_ref -> vault, required_permission default apicall.invoke, confirm, is_enabled) + `api_call_log` (append-only press audit, redacted, service-client-only). All button CRUD = `door.manage` (the catalog already scopes door.manage to "buttons"); per-button required_permission gates pressing (enforced by the invoke action, members have no RLS read on defs). schema.sql + types + DATABASE_SCHEMA + DB_SCHEMA_MAP + catalog same-change.
|
||||
- **Shared egress**: refactored `lib/door/executor.ts` so the SSRF+resolve+connect-by-IP+no-redirect+caps+redact core is one internal `egress`; `callDoor` (GET) is behavior-identical (guarded by the existing door tests), `callApi` adds full verbs + headers + body + per-auth_mode secret injection, host header forced to the pin. Pure request assembly in `lib/api-call-logic.ts` (`buildApiRequest`) + 9 unit tests.
|
||||
- **Actions** `lib/actions/api-buttons.ts`: manage CRUD + `listApiCallLog` (door.manage); `listInvokableButtons` (curated, presentational-only) + `invokeApiButton` (rate-limit-first, per-row permission check, denials audited, secret server-side, redacted audit). Zod schemas (method enum, https? url, header count/length caps, required_permission refined against the catalog).
|
||||
- **UI**: builder `/door/buttons` (grouped list, create form, door-template preset, secret + required_permission pickers, enable/delete, the call log) + sidebar "API buttons" link (door.manage). Member invoke: an "Actions" section on `/doors` (grouped buttons, confirm dialog).
|
||||
- **Audit**: SSRF egress reused (host pin + metadata block absolute even for admin config; member never supplies url/headers/body, only a buttonId); tenant-isolated; secret space-scoped + never returned + redacted; all rendering escaped JSX; single secrets FK (PGRST safe, auth-embed green). **Fix:** moved the invoke rate-limit before any DB work so a member can't flood denial-audit rows.
|
||||
|
||||
**Deploy state.** NOT deployed; **batched with the pass-63 (P4) work** awaiting one deploy authorization. Migrations 053 + 054 applied to the LOCAL stack only. No new env var or cron for P5 (all in-app). Inert until an admin creates a button.
|
||||
|
||||
**Open / next.** Door epic is feature-complete (P1-P5). v1 limits (documented, not bugs): the builder supports create/enable/delete (full field edit = delete+recreate); buttons use static url/headers/body (no per-press placeholder substitution). Browser click-through of `/door/buttons` + the `/doors` Actions section still pending (auth-gated). Back to the main backlog after this: owner-gated spine validation, observability, etc.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-20 (pass 63): Door epic Phase 4 (inbound access-log ingest) (LOCAL, NOT deployed)
|
||||
|
||||
Branch `main`. Took backlog item 6. Design-first via AskUserQuestion; the user chose BOTH transports. Built poll + webhook sharing one ingest core. Suite 612 unit (was 594; +18 parser tests) / 44 integration (was 40; +4 door-ingest) / build green / lint clean.
|
||||
|
||||
**Web-verified first.** Refetched `zyphlar/Open_Access_Control_Ethernet.ino` and characterized the `?z` wire format (recorded in memory `integration-api-facts`): `<pre>`-wrapped `K: N` stream from a fixed 40-slot ring (no per-entry id), card number split across G+g/D+d with divisor 32767, emitted decimal vs hex-stored uid, H:M:E only at verbosity <2. Conclusion: poll is best-effort, webhook (explicit ids) is the reliable path.
|
||||
|
||||
**Shipped (local):**
|
||||
- **Migration 053** (additive only): `door_access_log.dedupe_key` + partial-unique `(connection_id, dedupe_key)` (re-poll / webhook-retry no-op; action rows stay NULL/unconstrained); `door_connections.inbound_enabled` (opt-in, off) + `inbound_secret_ref -> secrets` (inbound bearer secret, distinct from the outbound password). schema.sql + types + DATABASE_SCHEMA + DB_SCHEMA_MAP same-change. PGRST: 2 secrets FKs but surrogate PK = not a junction (safe re 052); auth-embed test still green.
|
||||
- **Pure parser** `lib/door-log-logic.ts` (`parseHeatSyncLog`, `normalizeWebhookEvents`, `cardUidsEquivalent` hex/decimal match) + 18 unit tests.
|
||||
- **Ingest core** `lib/door/ingest.ts` (resolve card->member, dedupe-insert via service client). Extracted the shared `resolveDoorSecret` to `lib/door/secret.ts` (door.ts now imports it). Executor gained an additive `fullBody` option (32KB cap; existing callers unchanged at 4096/500).
|
||||
- **Poll** `POST /api/cron/door-ingest` (CRON_SECRET, proxy-whitelisted, native_heatsync + inbound_enabled only, ≤50 conns/run). **Webhook** `POST /api/door/inbound/[connection]` (proxy-whitelisted prefix, per-connection bearer constant-time, generic 401 non-oracle, 64KB body guard, 120/min, Zod ≤100 events). Both feed the ingest core.
|
||||
- **Admin UI**: per-connection "Inbound" panel on `/door/manage` (secret picker, on/off toggle that requires a secret first, copyable webhook URL + payload hint, "inbound on" badge).
|
||||
- **Audit hardening** (3 passes): body-size guard before JSON parse; `occurred_at` clamped not-in-future so a relay can't reorder the log. **Fresh-pass real bug fixed:** the `?z` body is untrusted plaintext HTTP, and `Number('9'*400)===Infinity` then `BigInt(Infinity)` THROWS, which would 500 the whole poll and starve other connections; capped the parser value at 15 digits (firmware is int32) + bounded the G/g pairing window + wrapped each connection in the cron loop in try/catch. Verified: executor change behavior-identical for existing callers; inbound secret space-scoped on write + read; tenant isolation from the looked-up connection; ingested `detail` renders as escaped JSX (no XSS); audit log stays immutable from the client (integration-tested). Accepted trade-offs (documented): decrypt-flood needs an unguessable uuid; `resolved` count is mild info to the already-trusted relay; the content-length guard is best-effort behind the platform body limit.
|
||||
|
||||
**Deploy state.** NOT deployed; awaiting the per-request go (ASK-before-deploy). Migration 053 is applied to the LOCAL stack only. No new env var (reuses CRON_SECRET); the door-ingest crontab line is documented in DEPLOYMENT but optional (only spaces that enable inbound need it). Inert until an admin turns on `inbound_enabled` for a connection.
|
||||
|
||||
**Open / next.** Door epic **Phase 5** (universal API-call UI builder, `api_buttons`) is the remaining door work; it needs its own design pass (HTTP-method scope + per-button vs fixed invoke permission forks) and extends the executor to method+headers+body. Browser click-through of the `/door/manage` inbound panel still pending (auth-gated; can't curl). Minor deferred: a far-future-date relay can't reorder but a near-now spoof is accepted (trusted relay); `resolved` count is mild info to the trusted relay.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-20 (pass 62): Diversified surface audit + fixes (DEPLOYED)
|
||||
|
||||
Branch `main`. Audited surfaces I had NOT recently touched, via three parallel independent reviews: (1) anonymous/public + all `app/api/**` route handlers, (2) XSS / unsafe rendering across the whole app, (3) authorization + tenant-isolation in legacy server actions (tasks/projects/members/kb/secrets/contacts/comms/classes/equipment/door/presence/certs/invites/roles/permissions/onboarding/forms/governance). **No P0 on any surface.** Suite 594 / build green.
|
||||
|
||||
Reference in New Issue
Block a user