2.6 KiB
principal-projection
(no submodules)
Purpose
Projects the package's internal UserRow storage shape onto the abstract
Principal contract published by @suaveplan/api-types. Keeping the
projection in one place ensures that no storage-specific columns leak
into the principal seen by downstream plugins, and that the four
documented principal fields (id, scopes, tenantId?, claims) are
the only surface area authors can rely on.
The projection is deliberately strict: structurally-invalid rows raise
AUTH_ROLLED.PRINCIPAL_PROJECTION_FAILED, surfacing storage-driver bugs
loudly rather than letting malformed data flow into route handlers.
Features
- One-shot pure function — no DB calls, no async, no allocations beyond
the new array of branded
Scopes. - Validates every column the projection touches (
idnon-empty string,scopesarray of non-empty strings,claimsplain object). - Omits
tenantIdfrom the outputPrincipalwhen the storage row's value isnull, matching the optional shape inPrincipal. - Brands strings as
PrincipalId/Scope/TenantIdat the boundary so type narrowing carries through to consumers.
Usage: Basic
import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled";
const principal = projectUserRowToPrincipal(row);
console.log(principal.id, principal.scopes);
Usage: Advanced
import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled";
function resolveSession(row: UserRow): Principal {
try {
return projectUserRowToPrincipal(row);
} catch (err) {
// Storage adapter returned a malformed row — log + treat as an
// anonymous request rather than surfacing the bug to the client.
log.error({ err }, "principal projection failed");
throw err;
}
}
API Reference
projectUserRowToPrincipal(row: UserRow): Principal— pure projection. ThrowsAuthRolledError(PRINCIPAL_PROJECTION_FAILED)on any structural invariant violation.
Implementation Notes
The projection allocates a fresh scopes array rather than spreading the
row's array — this is intentional so that downstream handlers cannot
mutate the source row through the principal reference. claims is a
shallow read-only reference so deep-cloning every claim object on every
request stays optional (consumers wanting deep immutability can wrap with
Object.freeze). The tenantId branding mirrors the contract published
by @suaveplan/api-tenant-resolver-* adapters — keeping the brand at the
projection boundary means downstream code that takes a TenantId never
needs an unsafe cast.