first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user