3.0 KiB
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.idbranded asPrincipalId.scopes←user.rolesbranded asScope[](empty array when absent).tenantId←session.activeOrganizationId→user.tenantIdfallback.claims← rest ofuserminus sensitive fields;session_idappended.- Throws
BetterAuthError(AUTH_BA.PRINCIPAL_MAPPING_FAILED)on missing id. - Returned
Principaland itsclaimsare frozen.
Usage: Basic
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
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.