first commit
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# `principal-projection`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user