feat(door): P2 connections + SSRF-safe executor + native adapter + audit

Migration 035 adds door_connections (per-space controller integration:
secret_ref -> AES-256-GCM secrets vault, never a plaintext password;
pinned_host SSRF pin; native_heatsync|generic_http adapter;
allow_member_self_entry opt-in for phase 3) and door_access_log
(append-only, secrets redacted, service-client-only writes; immutable).
RLS additive/default-deny: connections CRUD = door.manage; log SELECT =
door.manage/operate.

Safety core is pure and heavily unit-tested (door-logic, suite 409):
validateDoorTarget (request host must equal the pinned host;
metadata/link-local always blocked; http(s) only), the
firmware-verified HeatSync encoders with fixed-width zero-padding
(slot 3 / perm 3 / tag 8 / 4-char password), generic template
substitution, and redactDoorSecrets. lib/door/executor.ts is the single
hardened egress: re-validates the pin, refuses redirects, caps time and
body, redacts before any audit write. The door password is read from the
secrets vault and decrypted server-side only, never returned or logged.
/door/manage (door.manage) configures connections, picks a vault secret,
and runs a safe status-only test. Sidebar Admin link added; "Learn"
moved before "People" per request. Docs updated.
This commit is contained in:
Moheeb Zara
2026-05-16 20:31:43 -07:00
parent 1cbea138ba
commit 59e9fcbd4f
17 changed files with 1203 additions and 11 deletions
+68
View File
@@ -0,0 +1,68 @@
// Server-only. The single hardened egress point for door-controller HTTP.
// Every door call funnels through here so the SSRF guard, no-redirect rule,
// timeout, response cap and secret redaction cannot be bypassed. The secret
// is already embedded in `url` by the caller (per the verified firmware,
// auth is a query param); it is never logged -- only a redacted snippet is
// ever returned.
import { validateDoorTarget, redactDoorSecrets } from '@/lib/door-logic'
export type DoorCallResult =
| { ok: true; status: number; snippet: string }
| { ok: false; reason: string; status?: number }
const MAX_BODY = 4096
const SNIPPET = 500
export async function callDoor(opts: {
url: string
pinnedHost: string
password?: string | null
timeoutMs?: number
}): Promise<DoorCallResult> {
const guard = validateDoorTarget(opts.url, opts.pinnedHost)
if (!guard.ok) return { ok: false, reason: guard.reason }
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 6000)
try {
const res = await fetch(guard.url, {
method: 'GET',
// Never chase a redirect: a 3xx could bounce us off the pinned host.
redirect: 'manual',
signal: controller.signal,
headers: { accept: 'text/plain, text/html' },
cache: 'no-store',
})
if (res.type === 'opaqueredirect' || (res.status >= 300 && res.status < 400)) {
return {
ok: false,
reason: 'The controller returned a redirect, which is refused (anti-SSRF).',
status: res.status || undefined,
}
}
let received = ''
const reader = res.body?.getReader()
if (reader) {
const dec = new TextDecoder()
while (received.length < MAX_BODY) {
const { done, value } = await reader.read()
if (done) break
received += dec.decode(value, { stream: true })
}
try {
await reader.cancel()
} catch {
/* ignore */
}
}
const snippet = redactDoorSecrets(received.slice(0, SNIPPET), opts.password)
return { ok: res.ok, status: res.status, snippet }
} catch (e) {
const msg = e instanceof Error ? e.message : 'request failed'
return { ok: false, reason: redactDoorSecrets(msg, opts.password) }
} finally {
clearTimeout(timer)
}
}