Skip to content

@j0nathan-ll0yd/auth

Better Auth integration for session-based authentication in Lambda handlers, with Drizzle ORM adapter, schema mapping, and permission exports for the CLI.

Setup

createAuth(db, config)

Create and cache a Better Auth instance with a Drizzle database adapter. Cached for Lambda container reuse.

typescript
import { createAuth } from "@j0nathan-ll0yd/auth";
import { getRequiredEnv } from "@j0nathan-ll0yd/env";

const auth = createAuth(db, {
  secret: getRequiredEnv("AUTH_SECRET"),
  baseURL: getRequiredEnv("AUTH_BASE_URL"),
  socialProviders: {
    apple: {
      clientId: getRequiredEnv("APPLE_CLIENT_ID"),
      clientSecret: getRequiredEnv("APPLE_CLIENT_SECRET"),
      appBundleIdentifier: "com.example.app",
    },
    google: {
      clientId: getRequiredEnv("GOOGLE_CLIENT_ID"),
      clientSecret: getRequiredEnv("GOOGLE_CLIENT_SECRET"),
    },
  },
});

getAuth(getDb, config)

Lazily initialize auth with an async database factory function. Returns the cached instance on subsequent calls.

typescript
import { getAuth } from "@j0nathan-ll0yd/auth";

const auth = await getAuth(() => getDrizzleClient({ provider: "aurora-dsql", endpoint: "..." }), {
  secret: "...",
  baseURL: "...",
});

resetAuth()

Reset the cached auth instance so the next call to createAuth or getAuth creates a fresh one. Intended for test isolation.

Session Validation

validateSession(auth, token)

Validate a bearer token against Better Auth and return the authenticated user and session.

typescript
import { validateSession } from "@j0nathan-ll0yd/auth";

const result = await validateSession(auth, bearerToken);
// result.user.id, result.user.email, result.session.expiresAt

Throws UnauthorizedError when the token is missing, invalid, or expired.

validateSessionFromHeader(auth, authorizationHeader)

Extract a bearer token from the Authorization header and validate it in one call.

typescript
const result = await validateSessionFromHeader(auth, event.headers.Authorization);

Throws UnauthorizedError when the header is missing, malformed, or the token is invalid.

extractBearerToken(header)

Parse a bearer token from an Authorization header value. Returns null if the header is missing or malformed.

typescript
const token = extractBearerToken("Bearer abc123");
// 'abc123'

expireSession(auth, token, db)

Expire a session by setting its expiresAt to now, preserving the row for audit and cleanup. Unlike Better Auth's revokeSession() which deletes the session row, this keeps it visible for scheduled cleanup.

typescript
import { expireSession } from "@j0nathan-ll0yd/auth";

await expireSession(auth, bearerToken, db);

Throws UnauthorizedError when the token is invalid or already expired.

getSessionExpirationISO(session)

Return the session expiration as an ISO 8601 string.

typescript
const iso = getSessionExpirationISO(sessionResult);
// '2026-04-28T12:00:00.000Z'

refreshSession(auth, token) :badge[deprecated]

Validate and refresh a session. Deprecated -- use validateSession instead. BetterAuth's getSession() auto-extends sessions when updateAge has elapsed, making a separate refresh function unnecessary.

Session Auth Configuration

One-time setup for defineApiHandler with auth: 'session'.

configureSessionAuth(getDb, config)

Register a session auth factory for use by defineApiHandler with auth: 'session'. Call once at module scope in your Lambda entry file, before any handler that uses session auth.

typescript
import { configureSessionAuth } from "@j0nathan-ll0yd/auth";

configureSessionAuth(() => getDrizzleClient({ provider: "aurora-dsql", endpoint: "..." }), {
  secret: getRequiredEnv("AUTH_SECRET"),
  baseURL: getRequiredEnv("AUTH_BASE_URL"),
});

getConfiguredSessionAuth()

Return the configured Better Auth instance, resolving it via the registered factory. Throws if configureSessionAuth has not been called.

resetSessionAuth()

Reset the session auth factory. Intended for test isolation.

Webhook Signature Validation

Timing-safe HMAC signature validation for machine-to-machine webhook callers. Where the session helpers above authenticate an end user by bearer token, these authenticate a sending service by shared secret. All functions throw UnauthorizedError from @j0nathan-ll0yd/errors on failure.

validateWebhookSignature(options)

Generic HMAC webhook signature validation with configurable algorithm and prefix.

typescript
import { validateWebhookSignature } from "@j0nathan-ll0yd/auth";

validateWebhookSignature({
  payload: rawBody,
  signature: signatureHeader,
  secret: webhookSecret,
  algorithm: "sha256",
  prefix: "sha256=",
  headerName: "X-Signature",
});

WebhookValidationOptions

typescript
interface WebhookValidationOptions {
  payload: string | Buffer; // Raw request body
  signature: string; // Signature header value
  secret: string; // HMAC secret
  algorithm?: string; // HMAC algorithm (default: 'sha256')
  prefix?: string; // Prefix stripped before comparison (default: '')
  headerName?: string; // Header name for error messages (default: 'X-Signature')
}

validateGitHubWebhook(payload, signatureHeader, secret)

Validate a GitHub webhook signature (X-Hub-Signature-256). GitHub signs payloads using HMAC-SHA256 with a sha256= prefix.

typescript
import { validateGitHubWebhook } from "@j0nathan-ll0yd/auth";

validateGitHubWebhook(rawBody, headers["x-hub-signature-256"], secret);

Pass the raw request body, not a re-serialized parsed object -- HMAC is computed over exact bytes.

validateStripeWebhook(payload, signatureHeader, secret, toleranceSeconds?)

Validate a Stripe webhook signature (Stripe-Signature). Parses the t=<timestamp>,v1=<hex> header format and includes replay protection via timestamp age validation.

typescript
import { validateStripeWebhook } from "@j0nathan-ll0yd/auth";

validateStripeWebhook(rawBody, headers["stripe-signature"], endpointSecret);

// Custom tolerance (default: 300 seconds)
validateStripeWebhook(rawBody, headers["stripe-signature"], endpointSecret, 600);

Parameters:

ParameterTypeDefaultDescription
payloadstring | Buffer-Raw request body
signatureHeaderstring-Value of the Stripe-Signature header
secretstring-Stripe webhook endpoint secret
toleranceSecondsnumber300Maximum age of the timestamp in seconds

Permissions

AUTH_TABLE_PERMISSIONS

Table permissions required by Better Auth for session management. Consumed by mantle generate permissions to auto-generate DSQL role grants for Lambdas that import @j0nathan-ll0yd/auth.

typescript
const AUTH_TABLE_PERMISSIONS: TablePermission[] = [
  { table: "users", operations: [Select, Insert, Update] },
  { table: "sessions", operations: [Select, Insert, Update, Delete] },
  { table: "accounts", operations: [Select, Insert, Delete] },
  { table: "verification", operations: [Select, Insert, Delete] },
];

AUTH_MIGRATION_SQL

SQL string to create all Better Auth tables and indexes. Idempotent (uses IF NOT EXISTS). Creates: users, sessions, accounts, verification tables with appropriate indexes.

Schema

Reusable Drizzle ORM column definitions and table schemas for Better Auth. Instances can spread these into their own pgTable() calls to extend auth tables with additional columns.

Column Definitions

typescript
import {
  AUTH_USER_COLUMNS,
  AUTH_SESSION_COLUMNS,
  AUTH_ACCOUNT_COLUMNS,
  AUTH_VERIFICATION_COLUMNS,
} from "@j0nathan-ll0yd/auth";

Each is a Record<string, AnyPgColumn> that can be spread into pgTable() calls:

typescript
import { pgTable, text } from "drizzle-orm/pg-core";
import { AUTH_USER_COLUMNS } from "@j0nathan-ll0yd/auth";

export const users = pgTable("users", {
  ...AUTH_USER_COLUMNS,
  customField: text("custom_field"),
});

Default Tables

Pre-built Drizzle tables using the standard column definitions:

typescript
import { authUsers, authSessions, authAccounts, authVerification } from "@j0nathan-ll0yd/auth";

AUTH_SCHEMA_MAPPING

Schema mapping in the format expected by Better Auth's drizzleAdapter. Maps Better Auth model names to Drizzle tables:

typescript
const AUTH_SCHEMA_MAPPING = {
  user: authUsers,
  session: authSessions,
  account: authAccounts,
  verification: authVerification,
};

Pass to drizzleAdapter({ schema: AUTH_SCHEMA_MAPPING }) or let createAuth() apply it automatically.

Types

AuthConfig

typescript
interface AuthConfig {
  secret: string;
  baseURL: string;
  trustedOrigins?: string[];
  sessionExpiresIn?: number; // seconds, default: 30 days
  sessionUpdateAge?: number; // seconds, default: 24 hours
  socialProviders?: {
    apple?: SocialProviderConfig;
    google?: SocialProviderConfig;
    github?: SocialProviderConfig;
    custom?: CustomOAuthProvider[];
  };
  schema?: Record<string, unknown>;
  useSecureCookies?: boolean; // default: true
  databaseGenerateId?: boolean;
  encryptOAuthTokens?: boolean; // default: false
  experimentalJoins?: boolean; // default: false (BetterAuth v1.5+)
}

SocialProviderConfig

typescript
interface SocialProviderConfig {
  clientId: string;
  clientSecret: string;
  enabled?: boolean;
  appBundleIdentifier?: string;
}

CustomOAuthProvider

typescript
interface CustomOAuthProvider {
  providerId: string;
  clientId: string;
  clientSecret: string;
  enabled?: boolean;
  options?: Record<string, unknown>;
}

SessionResult

typescript
interface SessionResult {
  user: UserDetails;
  session: { id: string; token: string; expiresAt: Date };
}

UserDetails

typescript
interface UserDetails {
  id: string;
  email: string;
  name?: string;
  image?: string;
  emailVerified: boolean;
}

BetterAuthInstance

Opaque return type of betterAuth() used throughout the auth package.