import type { PrincipalResolution } from '@/lib/server/authz/principal'; /** Owner metadata stored beside a resource, never inside client-owned content. */ export interface ResourceOwnerBinding { ownerPrincipalId?: string; guestPrincipalId?: string; } export class ResourceOwnerBindingError extends Error { constructor(message: string) { super(message); this.name = 'ResourceOwnerBindingError'; } } export function normalizePrincipalSubjectId( value: string | undefined, label: string, ): string | undefined { if (value === undefined) return undefined; const normalized = value.trim(); if (!normalized || normalized.length > 256 || /[\u0000-\u001f\u007f]/.test(normalized)) { throw new ResourceOwnerBindingError(`Invalid ${label}`); } return normalized; } export function normalizeResourceOwnerBinding( binding: ResourceOwnerBinding | undefined, resourceLabel: string, ): ResourceOwnerBinding { const ownerPrincipalId = normalizePrincipalSubjectId( binding?.ownerPrincipalId, 'owner principal id', ); const guestPrincipalId = normalizePrincipalSubjectId( binding?.guestPrincipalId, 'guest principal id', ); if (ownerPrincipalId && guestPrincipalId) { throw new ResourceOwnerBindingError( `${resourceLabel} cannot have both an account owner and a guest owner`, ); } return { ...(ownerPrincipalId ? { ownerPrincipalId } : {}), ...(guestPrincipalId ? { guestPrincipalId } : {}), }; } /** Permanent account ownership wins; an unbound device is a migratable guest. */ export function resourceOwnerBindingFromPrincipal( principal: PrincipalResolution, ): ResourceOwnerBinding { if (principal.state !== 'authenticated') return {}; if (principal.principal.kind === 'user') { return { ownerPrincipalId: principal.principal.principalId }; } if (principal.principal.kind === 'device') { return principal.principal.userId ? { ownerPrincipalId: principal.principal.userId } : { guestPrincipalId: principal.principal.principalId }; } return {}; }