98 lines · 3.2 KB
import { first, run } from "./db";
import { sha256Hex } from "./crypto";

export type AuthTokenKind =
  | "email-verify"
  | "email-verify-spent"
  | "password-reset"
  | "webauthn"
  | "pending-login"
  | "editor-session"
  | "editor-preview"
  | "mcp-authz";

type Row = { user_id: string | null; payload: string; created_at: number };
export type ConsumedToken = { userId: string | null; payload: string; createdAt: number };

/** Stores single-use state keyed by the hash of a secret token. */
export async function storeAuthToken(
  kind: AuthTokenKind,
  token: string,
  userId: string | null,
  payload: string,
  ttlSeconds: number,
): Promise<void> {
  const now = Date.now();
  await run(
    "INSERT INTO auth_tokens (token_hash, kind, user_id, payload, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)",
    await sha256Hex(token),
    kind,
    userId,
    payload,
    now,
    now + ttlSeconds * 1000,
  );
}

/** Atomically consumes a token: concurrent callers can't both succeed. */
export async function consumeAuthToken(kind: AuthTokenKind, token: string): Promise<ConsumedToken | null> {
  const row = await first<Row>(
    "DELETE FROM auth_tokens WHERE token_hash = ? AND kind = ? AND expires_at > ? RETURNING user_id, payload, created_at",