← all parts

Part · auth.session

What's actually behind auth.session

The part exactly as partkit add auth.session vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

auth.sessionv1.2.0

✓ attested🔒 read-onlyauth.session@1

Lives at parts/auth.session/ in your repo — open, owned, readable. Not buried in node_modules. 385 lines of source you can audit.

9 conformance tests passedverified 2026-06-14↗ CI run
content hash 4685c30dd2…028cf8pinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0better-auth 1.6.18pg 8.21.0

Public API — what your seam calls

  • authHandler(request: Request): Promise<Response>
  • getSession(headers: Headers): Promise<SessionResult | null>
  • requireSession(headers: Headers): Promise<SessionResult>
  • signUp(input: SignUpInput): Promise<AuthResult>
  • signIn(input: SignInInput): Promise<AuthResult>
  • signOut(headers: Headers): Promise<void>
  • class AuthError extends Error { code: AuthErrorCode }
  • types: AuthUser, AuthSession, SessionResult, AuthResult, SignUpInput, SignInInput, AuthErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; configuration (secret, database URL, base URL) is validated lazily at first use with typed errors
  2. signUp creates a user and an active session; the password is stored only as a hash in auth_account (never plaintext), and a duplicate email is rejected with a typed error
  3. signIn with valid credentials issues a session that getSession resolves; an unknown email or a wrong password is rejected with the same typed error (no account enumeration)
  4. getSession returns the authenticated user and session for a valid session token and null for a missing, malformed, or expired one; requireSession throws a typed AuthError when there is no valid session
  5. signOut invalidates the session so the same token no longer resolves
  6. authHandler is a mountable fetch handler that serves the auth routes; importing or constructing it performs no I/O
  7. The part owns only the auth_* tables and surfaces every failure as a typed AuthError — Better Auth's internal errors never escape raw, and secrets never appear in messages
  8. Social sign-in is opt-in by env: a provider (google, github) is enabled only when BOTH its client id and secret are set; the mounted handler then serves that provider's sign-in/callback routes and links the account in auth_account. No provider is configured by default, and an unconfigured provider is rejected rather than silently attempted

Owns in your Postgres

auth_userauth_sessionauth_accountauth_verification

Dependencies

better-auth ^1.6.0pg ^8.0.0
SOURCEparts/auth.session/13 files · click to read
parts/auth.session/src/index.tstypescript · 4,156 bytes
/**
 * auth.session — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 */
import {
  firstCookiePair,
  getAuth,
  mapAuthFailure,
  statusOf,
  toSessionResult,
} from "./internal/auth";
import { redactSecrets } from "./internal/config";
import { AuthError } from "./internal/errors";
import type {
  AuthResult,
  SessionResult,
  SignInInput,
  SignUpInput,
} from "./internal/types";

export { AuthError } from "./internal/errors";
export type { AuthErrorCode } from "./internal/errors";
export type {
  AuthResult,
  AuthSession,
  AuthUser,
  SessionResult,
  SignInInput,
  SignUpInput,
} from "./internal/types";

/**
 * Mount as the auth catch-all route (`app/api/auth/[...all]/route.ts`):
 *   export const { GET, POST } = { GET: authHandler, POST: authHandler };
 * This is how the Better Auth client (browser) signs in/out and reads sessions;
 * cookies are managed for you. Constructing it performs no I/O (invariant 6).
 */
export async function authHandler(request: Request): Promise<Response> {
  try {
    return await getAuth().handler(request);
  } catch (e) {
    throw new AuthError("auth", redactSecrets(e instanceof Error ? e.message : String(e)));
  }
}

/** Resolve the signed-in user+session from request headers, or null. */
export async function getSession(headers: Headers): Promise<SessionResult | null> {
  try {
    const raw = await getAuth().api.getSession({ headers });
    return toSessionResult(raw);
  } catch (e) {
    if (e instanceof AuthError) throw e;
    throw new AuthError("auth", redactSecrets(e instanceof Error ? e.message : String(e)));
  }
}

/** Like getSession, but throws AuthError("unauthenticated") when there is none. */
export async function requireSession(headers: Headers): Promise<SessionResult> {
  const session = await getSession(headers);
  if (session === null) throw new AuthError("unauthenticated", "Authentication required");
  return session;
}

async function establish(
  context: "signup" | "signin",
  run: () => Promise<Response>,
): Promise<AuthResult> {
  let res: Response;
  try {
    res = await run();
  } catch (e) {
    if (e instanceof AuthError) throw e;
    const status = statusOf(e);
    if (status !== null) throw mapAuthFailure(status, context);
    throw new AuthError("auth", redactSecrets(e instanceof Error ? e.message : String(e)));
  }
  if (!res.ok) throw mapAuthFailure(res.status, context);

  const setCookie = res.headers.get("set-cookie") ?? "";
  const session = await getSession(new Headers({ cookie: firstCookiePair(setCookie) }));
  if (session === null) throw new AuthError("auth", `${context} did not establish a session`);
  return { ...session, setCookie };
}

/**
 * Create an account and an active session. Returns the session and the
 * `Set-Cookie` to attach to your response (the browser flow via authHandler
 * sets it automatically; this is for server-side sign-up). A duplicate email
 * fails with AuthError("email_taken"); the password is stored only hashed
 * (contract invariant 2).
 */
export async function signUp(input: SignUpInput): Promise<AuthResult> {
  return establish("signup", () =>
    getAuth().api.signUpEmail({
      body: { email: input.email, password: input.password, name: input.name },
      asResponse: true,
    }),
  );
}

/**
 * Verify credentials and establish a session. An unknown email or a wrong
 * password both fail with AuthError("invalid_credentials") and the same
 * message — no account enumeration (contract invariant 3).
 */
export async function signIn(input: SignInInput): Promise<AuthResult> {
  return establish("signin", () =>
    getAuth().api.signInEmail({
      body: { email: input.email, password: input.password },
      asResponse: true,
    }),
  );
}

/** Invalidate the session referenced by the request's cookie (invariant 5). */
export async function signOut(headers: Headers): Promise<void> {
  try {
    await getAuth().api.signOut({ headers });
  } catch (e) {
    if (e instanceof AuthError) throw e;
    throw new AuthError("auth", redactSecrets(e instanceof Error ? e.message : String(e)));
  }
}