first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
# `map-user-to-principal`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Default Better-Auth `(user, session)` → `Principal` mapping consumed by both
`createBetterAuthSession` and `createBetterAuthProvider`. Pure function; given
the same view it always returns the same `Principal`. Operators MAY supply a
custom `principalMapping` in `BetterAuthOptions` to project Better-Auth's
user row onto a custom claims topology, but the default is good enough for
most deployments — it drops sensitive fields, brands `id` and `scopes`, and
prefers `session.activeOrganizationId` over `user.tenantId` so the
organisation plugin's tenant scoping wins automatically when wired.
## Features
- `id` ← `user.id` branded as `PrincipalId`.
- `scopes` ← `user.roles` branded as `Scope[]` (empty array when absent).
- `tenantId` ← `session.activeOrganizationId` → `user.tenantId` fallback.
- `claims` ← rest of `user` minus sensitive fields; `session_id` appended.
- Throws `BetterAuthError(AUTH_BA.PRINCIPAL_MAPPING_FAILED)` on missing id.
- Returned `Principal` and its `claims` are frozen.
## Usage: Basic
```ts
import { mapBetterAuthUserToPrincipal } from "@suaveplan/api-auth-better-auth/principal";
const principal = mapBetterAuthUserToPrincipal({
user: { id: "u_1", email: "a@b.c", roles: ["admin"] },
session: { id: "s_1" },
});
// → { id: "u_1", scopes: ["admin"], claims: { id, email, roles, session_id } }
```
## Usage: Advanced
```ts
import type { Principal } from "@suaveplan/api-types/principal";
import type { PrincipalMapping } from "@suaveplan/api-auth-better-auth/options";
const customMapping: PrincipalMapping = ({ user, session }): Principal => ({
id: `tenant_${session.activeOrganizationId}_${user.id}` as never,
scopes: ((user.roles ?? []) as readonly string[]).flatMap((role) =>
role === "admin"
? ["billing:write", "billing:read", "users:write"]
: [role]
) as never[],
claims: { email: user.email, source: "better-auth" },
});
// Hand the custom mapping through BetterAuthOptions:
// { ..., principalMapping: customMapping }
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `mapBetterAuthUserToPrincipal(view)` | `PrincipalMapping` | Default mapping consumed by both factories |
## Implementation Notes
The frozen-object return shape (`Object.freeze` on both the `Principal` and
its `claims` bag) defends against handlers downstream mutating a single
returned principal and observing the mutation on later requests — the same
hardening applied by `@suaveplan/api-auth-static-principal`. The sensitive
field list (`password`, `passwordHash`, `salt`, `twoFactorSecret`,
`magicLinkToken`, `resetPasswordToken`) covers Better-Auth's stock plugin
set; operators who define additional sensitive fields via a custom
Better-Auth plugin SHOULD supply a custom `principalMapping` to drop those
too. The mapping deliberately does NOT translate roles into a stable
scope vocabulary — that's a deployment concern; the default is identity so
the topology stays predictable.