Files
hackerspace.sh/lib/actions/door/_audit.ts
T
Moheeb ZaraandClaude Opus 4.8 39bcdd9677 refactor(actions): split door.ts into a folder (slice 4)
813-line lib/actions/door.ts → lib/actions/door/{connections,operate,self-entry}.ts
('use server') + _guards.ts + _audit.ts + index.ts barrel. All 14 actions
preserved verbatim; lib/door/ (executor/ingest/secret) untouched. tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:01:02 -07:00

113 lines
3.9 KiB
TypeScript

import { createAdminClient } from '@/lib/supabase/admin'
import { redactDoorSecrets } from '@/lib/door-logic'
import { pickLowestFreeSlot, slotCapacity } from '@/lib/door-slots-logic'
import { resolveDoorSecret } from '@/lib/door/secret'
// Connection door password is loaded + decrypted via the shared
// resolveDoorSecret (lib/door/secret.ts); service client, never returned to
// the browser. Aliased here to keep the existing call sites unchanged.
export const resolveSecret = resolveDoorSecret
export function isUniqueViolation(message: string): boolean {
return /duplicate key value|already exists|unique constraint/i.test(message)
}
type ConnRow = {
id: string
adapter: string
base_url: string
pinned_host: string
auth_param: string | null
secret_ref: string | null
verbs: Record<string, string> | null
is_enabled: boolean
}
export async function loadEnabledConnection(
admin: ReturnType<typeof createAdminClient>,
spaceId: string,
connectionId: string,
): Promise<{ ok: true; conn: ConnRow } | { ok: false; error: string }> {
const { data } = await admin
.from('door_connections')
.select('id, adapter, base_url, pinned_host, auth_param, secret_ref, verbs, is_enabled')
.eq('id', connectionId)
.eq('space_id', spaceId)
.maybeSingle()
if (!data) return { ok: false, error: 'Connection not found' }
if (!data.is_enabled) return { ok: false, error: 'This connection is disabled.' }
return { ok: true, conn: data as unknown as ConnRow }
}
export async function auditDoor(
admin: ReturnType<typeof createAdminClient>,
row: {
spaceId: string
connectionId: string
actorMemberId: string
targetMemberId?: string | null
action: string
success: boolean
detail: string
password?: string | null
authParam?: string | null
},
) {
await admin.from('door_access_log').insert({
space_id: row.spaceId,
connection_id: row.connectionId,
actor_member_id: row.actorMemberId,
target_member_id: row.targetMemberId ?? null,
action: row.action,
success: row.success,
detail: redactDoorSecrets(row.detail.slice(0, 1000), row.password, row.authParam),
})
}
// Reserve (or reuse) the card's slot on this connection. The DB unique
// constraints arbitrate concurrency: (connection_id, card_id) makes re-grant
// idempotent; (connection_id, slot) makes a slot race fail loudly so we retry.
export async function reserveSlot(
admin: ReturnType<typeof createAdminClient>,
spaceId: string,
connectionId: string,
cardId: string,
createdBy: string,
): Promise<
| { ok: true; slot: number; created: boolean }
| { ok: false; error: string }
> {
for (let attempt = 0; attempt < 3; attempt++) {
const { data: existing } = await admin
.from('door_card_slots')
.select('slot')
.eq('connection_id', connectionId)
.eq('card_id', cardId)
.maybeSingle()
if (existing) return { ok: true, slot: existing.slot as number, created: false }
const { data: used } = await admin
.from('door_card_slots')
.select('slot')
.eq('connection_id', connectionId)
const pick = pickLowestFreeSlot((used ?? []).map(r => r.slot as number))
if (!pick.ok) {
const cap = slotCapacity()
return { ok: false, error: `This controller is full (${cap}/${cap} card slots used). Revoke an unused card before granting another.` }
}
const { error: insErr } = await admin.from('door_card_slots').insert({
space_id: spaceId,
connection_id: connectionId,
card_id: cardId,
slot: pick.slot,
created_by: createdBy,
})
if (!insErr) return { ok: true, slot: pick.slot, created: true }
// Unique violation = a concurrent grant took this slot or this card.
// Loop: re-read (the card row may now exist → reuse it; else re-pick).
if (!isUniqueViolation(insErr.message)) return { ok: false, error: insErr.message }
}
return { ok: false, error: 'Could not allocate a free slot (contention). Try again.' }
}