66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
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 {};
|
|
}
|