# `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 `Scope`s. - Validates every column the projection touches (`id` non-empty string, `scopes` array of non-empty strings, `claims` plain object). - Omits `tenantId` from the output `Principal` when the storage row's value is `null`, matching the optional shape in `Principal`. - Brands strings as `PrincipalId` / `Scope` / `TenantId` at the boundary so type narrowing carries through to consumers. ## Usage: Basic ```ts import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled"; const principal = projectUserRowToPrincipal(row); console.log(principal.id, principal.scopes); ``` ## Usage: Advanced ```ts 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. Throws `AuthRolledError(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.