import { resolveRequestPrincipal, type PrincipalResolution, type RequestPrincipalResolver, } from '@/lib/server/authz/principal'; import { decideResourceAuthorization, type ResourceAuthorizationAction, type ResourceAuthorizationDecision, type ResourceOwnership, } from '@/lib/server/authz/resource-ownership'; export interface ResourceAuthorizationObservation { readonly resourceType: string; readonly resourceId: string; readonly action: ResourceAuthorizationAction; readonly principalState: PrincipalResolution['state']; readonly ownershipState: ResourceOwnership['state']; readonly decision: ResourceAuthorizationDecision; } export type ResourceAuthorizationObserver = ( observation: ResourceAuthorizationObservation, ) => void | Promise; export interface ObserveResourceAuthorizationInput { readonly request: Request; readonly resourceType: string; readonly resourceId: string; readonly action: ResourceAuthorizationAction; readonly ownership: ResourceOwnership; readonly overrideCapability?: string; } export interface ResourceAuthorizationObservationDeps { readonly resolvePrincipal?: RequestPrincipalResolver; readonly observer?: ResourceAuthorizationObserver; } const ignoreObservation: ResourceAuthorizationObserver = () => undefined; /** * Observe a future authorization decision without enforcing it. * * The function intentionally returns no decision, and every resolver/observer * failure is contained. A caller's existing response must never depend on this * migration seam. */ export async function observeResourceAuthorization( input: ObserveResourceAuthorizationInput, deps: ResourceAuthorizationObservationDeps = {}, ): Promise { try { const principal = await (deps.resolvePrincipal ?? resolveRequestPrincipal)(input.request); const decision = decideResourceAuthorization({ principal, ownership: input.ownership, action: input.action, ...(input.overrideCapability ? { overrideCapability: input.overrideCapability } : {}), }); await (deps.observer ?? ignoreObservation)({ resourceType: input.resourceType, resourceId: input.resourceId, action: input.action, principalState: principal.state, ownershipState: input.ownership.state, decision, }); } catch { // Observe-only means failures must never change route status/body or throw. } }