# `map-user-to-principal` _(no submodules)_ ## 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.