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:
@@ -903,6 +903,49 @@ export const listMemberCardsSchema = z.object({
|
||||
memberId: z.string().uuid('Invalid member ID'),
|
||||
})
|
||||
|
||||
// ─── Door connections (Door epic P2) ─────────────────────────────────────────
|
||||
|
||||
const httpUrl = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2000)
|
||||
.regex(/^https?:\/\/.+/i, 'Must be an http(s) URL')
|
||||
|
||||
const doorAdapter = z.enum(['native_heatsync', 'generic_http'])
|
||||
const doorAuthMode = z.enum(['none', 'query', 'header', 'bearer'])
|
||||
|
||||
export const createDoorConnectionSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(200),
|
||||
adapter: doorAdapter.optional().default('generic_http'),
|
||||
base_url: httpUrl,
|
||||
pinned_host: z.string().min(1, 'A pinned host is required').max(255),
|
||||
auth_mode: doorAuthMode.optional().default('none'),
|
||||
auth_param: z.string().max(120).optional().nullable(),
|
||||
secret_ref: z.string().uuid('Invalid secret reference').optional().nullable(),
|
||||
// generic adapter per-verb templates (validated as a flat string map)
|
||||
verbs: z.record(z.string().max(2000)).optional().default({}),
|
||||
allow_member_self_entry: z.boolean().optional().default(false),
|
||||
is_enabled: z.boolean().optional().default(true),
|
||||
})
|
||||
|
||||
export const updateDoorConnectionSchema = z.object({
|
||||
connectionId: z.string().uuid('Invalid connection ID'),
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
adapter: doorAdapter.optional(),
|
||||
base_url: httpUrl.optional(),
|
||||
pinned_host: z.string().min(1).max(255).optional(),
|
||||
auth_mode: doorAuthMode.optional(),
|
||||
auth_param: z.string().max(120).optional().nullable(),
|
||||
secret_ref: z.string().uuid('Invalid secret reference').optional().nullable(),
|
||||
verbs: z.record(z.string().max(2000)).optional(),
|
||||
allow_member_self_entry: z.boolean().optional(),
|
||||
is_enabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const doorConnectionIdSchema = z.object({
|
||||
connectionId: z.string().uuid('Invalid connection ID'),
|
||||
})
|
||||
|
||||
// ─── Generic ID schemas ──────────────────────────────────────────────────────
|
||||
|
||||
export const uuidSchema = z.string().uuid('Invalid ID format')
|
||||
|
||||
Reference in New Issue
Block a user