Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
import type {
Claims,
Principal,
PrincipalId,
Scope,
} from "@suaveplan/api-types/principal";
import { AUTH_BA_CODES, BetterAuthError } from "../error-codes/error-codes.js";
import type {
BetterAuthSessionRecord,
BetterAuthSessionView,
BetterAuthUserView,
PrincipalMapping,
} from "../options/options.types.js";
const SENSITIVE_USER_FIELDS = new Set([
"password",
"passwordHash",
"salt",
"twoFactorSecret",
"magicLinkToken",
"resetPasswordToken",
]);
/**
* Default Better-Auth user/session → `Principal` mapping. Produces a
* stable `Principal` carrying:
*
* - `id` ← `user.id` (branded as `PrincipalId`).
* - `scopes` ← `user.roles` (branded as `Scope[]`); empty array when
* no roles are attached. Plugins/middleware further upstream
* enrich the scope set; the mapper itself does no role-to-scope
* translation beyond identity.
* - `tenantId` ← `session.activeOrganizationId` (when present), else
* `user.tenantId` (when present), else `undefined`.
* - `claims` ← `user` minus sensitive fields plus a `session_id` claim
* carrying the Better-Auth session id (when present).
*
* Throws {@link BetterAuthError} with code
* `AUTH_BA.PRINCIPAL_MAPPING_FAILED` when `user.id` is missing — without a
* stable id the principal cannot be persisted across requests.
*/
export const mapBetterAuthUserToPrincipal: PrincipalMapping = (
view: BetterAuthSessionView
): Principal => {
const { user, session } = view;
if (typeof user.id !== "string" || user.id.length === 0) {
throw new BetterAuthError(
AUTH_BA_CODES.PRINCIPAL_MAPPING_FAILED,
"Better-Auth user is missing a non-empty `id`",
{ context: { received: { idType: typeof user.id } } }
);
}
const id = user.id as unknown as PrincipalId;
const scopes = pickScopes(user.roles);
const tenantId = pickTenant(user, session);
const claims = buildClaims(user, session);
if (tenantId !== undefined) {
return Object.freeze({ id, scopes, tenantId, claims }) as Principal;
}
return Object.freeze({ id, scopes, claims }) as Principal;
};
function pickScopes(
roles: readonly string[] | null | undefined
): readonly Scope[] {
if (!Array.isArray(roles)) return [];
const out: Scope[] = [];
for (const role of roles) {
if (typeof role === "string" && role.length > 0) {
out.push(role as unknown as Scope);
}
}
return Object.freeze(out);
}
function pickTenant(
user: BetterAuthUserView,
session: BetterAuthSessionRecord
): import("@suaveplan/api-types/principal").Principal["tenantId"] | undefined {
const fromSession = session.activeOrganizationId;
if (typeof fromSession === "string" && fromSession.length > 0) {
return fromSession as unknown as import("@suaveplan/api-types/principal").Principal["tenantId"];
}
const fromUser = user.tenantId;
if (typeof fromUser === "string" && fromUser.length > 0) {
return fromUser as unknown as import("@suaveplan/api-types/principal").Principal["tenantId"];
}
return undefined;
}
function buildClaims(
user: BetterAuthUserView,
session: BetterAuthSessionRecord
): Claims {
const claims: Record<string, unknown> = {};
for (const [key, value] of Object.entries(user)) {
if (SENSITIVE_USER_FIELDS.has(key)) continue;
if (value === undefined) continue;
claims[key] = value;
}
if (typeof session.id === "string" && session.id.length > 0) {
claims["session_id"] = session.id;
}
return Object.freeze(claims);
}