← all parts

Part · auth.apikey

What's actually behind auth.apikey

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

auth.apikeyv1.1.0

✓ attested🔒 read-onlyauth.apikey@1

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

13 conformance tests passedverified 2026-06-15↗ CI run
content hash 0eca059b97…8aafbapinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0

Public API — what your seam calls

  • apiKeys(db: SqlExecutor): ApiKeyStore
  • ApiKeyStore { issueKey(input: IssueKeyInput): Promise<IssuedKey>; verifyKey(presented: string, opts?: VerifyOptions): Promise<ApiKeyContext>; rotateKey(id: string, opts?: RotateOptions): Promise<IssuedKey>; revokeKey(id: string): Promise<void>; listKeys(ownerId: string): Promise<ApiKeyInfo[]>; requireApiKey(scopes?: string[]): (request: Request) => Promise<ApiKeyContext> }
  • class ApiKeyError extends Error { code: ApiKeyErrorCode }
  • types: SqlExecutor, ApiKeyStore, IssueKeyInput, IssuedKey, VerifyOptions, RotateOptions, ApiKeyContext, ApiKeyInfo, ApiKeyErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; inputs are validated at call time, and every failure — invalid input or a storage error from the executor — surfaces as a typed ApiKeyError (raw driver errors never escape)
  2. issueKey returns the plaintext key exactly once; only a salted one-way hash and a non-secret display prefix persist, so the key cannot be recovered from storage, and listKeys never returns plaintext or hash material
  3. verifyKey resolves a valid active key to its context via a single indexed prefix lookup and a constant-time compare; a wrong secret, an unknown prefix, and a wrong-length secret are indistinguishable — all rejected as `invalid`, with no oracle revealing whether a prefix exists
  4. A revoked key is rejected as `revoked` and a (naturally- or rotation-) expired key as `expired`, but only to a caller presenting the correct secret; revocation takes effect immediately and an expiry takes effect at its recorded time
  5. requireScopes is all-of: a key missing any required scope is rejected as `forbidden` and never silently downgraded; requireApiKey enforces the same over the `Authorization: Bearer` header and rejects a missing/garbled header as `malformed`
  6. rotateKey issues a new key and keeps the old one valid only until a bounded, recorded grace window elapses (default 0 = immediate); after the window the old key is rejected as `expired` while the new key verifies
  7. Secret material (plaintext keys, hashes, salts) never appears in error messages, in ApiKeyError, or in any value the part returns
  8. The part operates solely through the provided SqlExecutor seam — it imports no database driver — and every statement it issues targets only its own auth_apikey_keys table; every input is parameterized, so SQL metacharacters are stored literally and never executed

Owns in your Postgres

auth_apikey_keys

Dependencies

zero-dep — runs on your Postgres
SOURCEparts/auth.apikey/16 files · click to read
parts/auth.apikey/src/index.tstypescript · 1,427 bytes
/**
 * auth.apikey — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 *
 * Issue, verify, scope, rotate, and revoke long-lived API keys — the
 * programmatic sibling of auth.session. Bind it to a database connection (the
 * SqlExecutor seam) and use the returned store; constructing it performs no I/O.
 */
import { createStore } from "./internal/store";
import type { ApiKeyStore, SqlExecutor } from "./internal/types";

export { ApiKeyError } from "./internal/errors";
export type { ApiKeyErrorCode } from "./internal/errors";
export type {
  ApiKeyContext,
  ApiKeyInfo,
  ApiKeyStore,
  IssueKeyInput,
  IssuedKey,
  RotateOptions,
  SqlExecutor,
  VerifyOptions,
} from "./internal/types";

/**
 * Bind the API-key store to a database connection (the SqlExecutor seam).
 * Constructing it performs no I/O and never throws (contract invariant 1) —
 * the database is touched only when a method runs, so it is serverless-safe.
 * Pass a per-request executor from your pool; for rotation atomicity hand a
 * transaction-bound executor (seams.md §5).
 *
 *   const keys = apiKeys(db);
 *   const { plaintext } = await keys.issueKey({ ownerId, scopes: ["models.read"] });
 *   const ctx = await keys.verifyKey(presented, { requireScopes: ["models.read"] });
 */
export function apiKeys(db: SqlExecutor): ApiKeyStore {
  return createStore(db);
}