102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
import type { SessionResolver } from "@suaveplan/api-auth";
|
|
import type { Principal } from "@suaveplan/api-types/principal";
|
|
import { LazyBetterAuth } from "../db/database.js";
|
|
import { AUTH_BA_CODES, BetterAuthError } from "../error-codes/error-codes.js";
|
|
import { getBetterAuthInstruments } from "../instruments/instruments.js";
|
|
import { defaultBetterAuthFactory } from "../oauth/default-factory.js";
|
|
import { parseBetterAuthOptions } from "../options/options.js";
|
|
import type { BetterAuthOptions } from "../options/options.types.js";
|
|
import { mapBetterAuthUserToPrincipal } from "../principal/map-user-to-principal.js";
|
|
|
|
/**
|
|
* Construct a `SessionResolver` that asks Better-Auth's `auth.api.getSession`
|
|
* to materialise the current session from the request's cookie/bearer, then
|
|
* maps the resulting `(user, session)` view onto a `Principal` per
|
|
* `@suaveplan/api-types`. Returns `null` when Better-Auth reports no
|
|
* session — the host's resolver chain then advances to the next adapter
|
|
* (typically `api-auth-anonymous`).
|
|
*
|
|
* Operators MAY share the underlying Better-Auth instance with
|
|
* `createBetterAuthProvider(opts)` by passing the same options bag — the
|
|
* factories memoise per-options so both sides resolve through one pool.
|
|
* Wiring only the session adapter is the documented "Better-Auth for
|
|
* session reads, Clerk for account lifecycle" topology and works without
|
|
* standing up the provider routes.
|
|
*/
|
|
export function createBetterAuthSession(
|
|
rawOptions: BetterAuthOptions
|
|
): SessionResolver {
|
|
const opts = parseBetterAuthOptions(rawOptions);
|
|
const factory = opts.betterAuthFactory ?? defaultBetterAuthFactory;
|
|
const lazy = new LazyBetterAuth(opts, factory);
|
|
const map = opts.principalMapping ?? mapBetterAuthUserToPrincipal;
|
|
let disposed = false;
|
|
|
|
return {
|
|
async resolve(req: Request): Promise<Principal | null> {
|
|
if (disposed) return null;
|
|
const instruments = getBetterAuthInstruments();
|
|
const started = nowMs();
|
|
let outcome: "resolved" | "null" | "error" = "null";
|
|
try {
|
|
const auth = await lazy.get();
|
|
const getSession = auth.api.getSession;
|
|
if (getSession === undefined) {
|
|
return null;
|
|
}
|
|
const sessionView = await getSession({
|
|
headers: req.headers,
|
|
});
|
|
if (sessionView === null) {
|
|
return null;
|
|
}
|
|
let principal: Principal;
|
|
try {
|
|
principal = map(sessionView);
|
|
} catch (err) {
|
|
instruments.principalMappingErrorsTotal.add(1);
|
|
outcome = "error";
|
|
throw err instanceof BetterAuthError
|
|
? err
|
|
: new BetterAuthError(
|
|
AUTH_BA_CODES.PRINCIPAL_MAPPING_FAILED,
|
|
"principalMapping threw during session resolution",
|
|
{ cause: err }
|
|
);
|
|
}
|
|
outcome = "resolved";
|
|
return principal;
|
|
} catch (err) {
|
|
outcome = "error";
|
|
if (err instanceof BetterAuthError) {
|
|
throw err;
|
|
}
|
|
throw new BetterAuthError(
|
|
AUTH_BA_CODES.INVALID_SESSION,
|
|
"Better-Auth session resolution failed",
|
|
{ cause: err }
|
|
);
|
|
} finally {
|
|
const duration = nowMs() - started;
|
|
instruments.sessionsResolvedTotal.add(1, { outcome });
|
|
instruments.sessionResolveDurationMs.record(duration, {
|
|
outcome,
|
|
});
|
|
}
|
|
},
|
|
async dispose(): Promise<void> {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
lazy.reset();
|
|
},
|
|
};
|
|
}
|
|
|
|
/* istanbul ignore next -- `performance` is universally present on Node 18+ + Bun; the Date.now() fallback exists only as a defensive guard for stripped-down runtimes. */
|
|
function nowMs(): number {
|
|
return typeof performance === "object" &&
|
|
typeof performance.now === "function"
|
|
? performance.now()
|
|
: Date.now();
|
|
}
|