Files
skills/docs/api-stack/api/api-auth-rolled/src/principal-projection/principal-projection.md
2026-05-29 08:20:56 +01:00

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 (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

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. 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.