Files
2026-05-27 23:29:53 +01:00

11 KiB

@suaveplan/api-auth-better-auth — Better-Auth adapter for the @suaveplan/api foundation. Ships two independently-wireable factories implementing the auth.session and auth.provider capability contracts against a Better-Auth instance backed by its own Postgres connection. Maps Better-Auth user+session onto the @suaveplan/api Principal contract, mounts canonical PROVIDER_ROUTE_SURFACE routes (translating Better-Auth's /sign-up etc. idiom), exposes a GitHub OAuth flow via Better-Auth's genericOAuth plugin (so the mock OAuth IdP can be wired end-to-end), guards route-prefix collisions against @suaveplan/api-host's reserved-prefix contract, and emits OTel sessions/oauth/mapping instruments. The ./testing subpath re-exports both conformance suites bound to Postgres-backed factories.

Stage-6 Better-Auth adapter for @suaveplan/api. Ships two independently-wireable capability surfaces — createBetterAuthSession (implements auth.session from @suaveplan/api-auth) and createBetterAuthProvider (implements auth.provider from @suaveplan/api-auth-provider). Operators MAY wire either side in isolation OR both together; sharing one options bag between the two factories shares the underlying Better-Auth instance + DB pool.

Two capability surfaces, wire independently. The session adapter reads cookies + bearer tokens; the provider adapter mounts the canonical PROVIDER_ROUTE_SURFACE (9 routes) under a configurable prefix. They share no required code path — wiring only one side does NOT install the other side's surface.

Documentation tree

  • db — Lazy holder for the underlying Better-Auth instance plus a helper that projects {@link BetterAuthOptions} onto the loos…
    • database — Lazy holder for the underlying Better-Auth instance plus a helper that projects {@link BetterAuthOptions} onto the loos…
  • error-codes — Stable, tracked error-code registry for @suaveplan/api-auth-better-auth. Every fault path inside the adapter…
    • error-codes — Stable, tracked error-code registry for @suaveplan/api-auth-better-auth. Every fault path inside the adapter…
  • independence — Pin the design invariant from the Stage-6 spec change: the Better-Auth adapter ships two factories — `createBetterA…
    • session-without-provider — Pin the design invariant from the Stage-6 spec change: the Better-Auth adapter ships two factories — `createBetterA…
  • instruments — Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter. Built once per process against the current gl…
    • instruments — Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter. Built once per process against the current gl…
  • default-factory — Default betterAuthFactory used when an operator does not supply one via BetterAuthOptions.betterAuthFactory. Dynami…
  • github — Project Suaveplan's {@link BetterAuthOauthProvider} entries onto the genericOAuth plugin config Better-Auth consumes.…
  • options — Zod-validated boundary for the Better-Auth adapter's configuration bag. Both createBetterAuthSession and `createBette…
    • options — Zod-validated boundary for the Better-Auth adapter's configuration bag. Both createBetterAuthSession and `createBette…
  • principal — Default Better-Auth (user, session) → Principal mapping consumed by both createBetterAuthSession and `createBette…
    • map-user-to-principal — Default Better-Auth (user, session) → Principal mapping consumed by both createBetterAuthSession and `createBette…
  • create-provider — Factory for the auth.provider half of the Stage-6 split. Returns an AuthProviderAdapter (per `@suaveplan/api-auth-p…
  • route-translation — Translation table mapping every entry in PROVIDER_ROUTE_SURFACE (the canonical 9-route foundation surface) to the mat…
  • route-prefix — Resolve an operator-supplied routePrefix against the host's reserved-prefix contract from `@suaveplan/api-host/reserv…
    • route-prefix — Resolve an operator-supplied routePrefix against the host's reserved-prefix contract from `@suaveplan/api-host/reserv…
  • session — Factory for the auth.session half of the Stage-6 split. Returns a SessionResolver that asks Better-Auth's `auth.api…
    • create-session — Factory for the auth.session half of the Stage-6 split. Returns a SessionResolver that asks Better-Auth's `auth.api…
  • testing — @suaveplan/api-auth-better-auth/testing is the subpath downstream packages import when they want to assert their own…
    • testing — @suaveplan/api-auth-better-auth/testing is the subpath downstream packages import when they want to assert their own…

Modules

  • error-codes/ — 10-code AUTH_BA.* registry + BetterAuthError.
  • options/ — Zod-validated BetterAuthOptions boundary + canonical defaults.
  • db/ — LazyBetterAuth (lazy + cached + concurrency-safe) + buildBetterAuthFactoryArgs (outer-options → Better-Auth-args projection).
  • principal/ — default Better-Auth (user, session) → Principal mapping; replaceable via BetterAuthOptions.principalMapping.
  • session/ — createBetterAuthSession(opts) factory implementing the SessionResolver contract.
  • provider/ — createBetterAuthProvider(opts) factory implementing the AuthProviderAdapter contract; mounts canonical routes that rewrite onto Better-Auth's native sub-paths.
  • route-prefix/ — assertRoutePrefixAvailable collision check against @suaveplan/api-host/reserved-prefix.
  • oauth/ — genericOAuth plugin config builder + default betterAuth(...) factory that dynamically loads the plugin when OAuth providers are configured. The GitHub OAuth flow is the canonical reference; the implementation works against the real GitHub IdP in production and the mock IdP from @suaveplan/testing-integration-oauth-mock in tests because we use genericOAuth (which accepts the full URL triple) rather than the hard-coded github social-provider.
  • instruments/ — OTel counters + histograms per the spec's metrics contract.
  • independence/ — invariant pin: session-only / provider-only wiring exposes nothing from the other side.
  • testing/ — ./testing subpath re-exporting both conformance harnesses bound to Better-Auth factories.

Usage

import { Pool } from "pg";
import {
    createBetterAuthProvider,
    createBetterAuthSession,
} from "@suaveplan/api-auth-better-auth";

const pool = new Pool({ connectionString: process.env.BETTER_AUTH_DB_URL! });
const opts = {
    database: pool,
    baseUrl: "https://api.example.com",
    secret: process.env.BETTER_AUTH_SECRET!,
    routePrefix: "/auth",
    oauth: {
        providers: {
            github: {
                providerId: "github",
                clientId: process.env.GITHUB_CLIENT_ID!,
                clientSecret: process.env.GITHUB_CLIENT_SECRET!,
                authorizationUrl: "https://github.com/login/oauth/authorize",
                tokenUrl: "https://github.com/login/oauth/access_token",
                userInfoUrl: "https://api.github.com/user",
            },
        },
    },
};

// Wire both sides — they share one Better-Auth instance via per-options WeakMap:
host.registerAuth({
    resolver: createBetterAuthSession(opts),
    csrf: true,
});
host.registerAuthProvider(createBetterAuthProvider(opts));

Error codes

Code Surface When thrown
AUTH_BA.INVALID_SESSION session unexpected getSession failure
AUTH_BA.DB_UNREACHABLE db betterAuth({...}) construction threw
AUTH_BA.OAUTH_PROVIDER_REJECTED oauth provider returned 403
AUTH_BA.OAUTH_STATE_MISMATCH oauth state cookie did not match
AUTH_BA.USER_NOT_FOUND mapping custom mapper signalled missing user
AUTH_BA.EMAIL_NOT_VERIFIED session session present but email unverified
AUTH_BA.RATE_LIMITED provider Better-Auth rate-limiter blocked
AUTH_BA.INVALID_REDIRECT oauth redirect URI outside allow-list
AUTH_BA.PRINCIPAL_MAPPING_FAILED mapping mapper threw
AUTH_BA.ROUTE_PREFIX_COLLISION mount prefix collides with host-reserved

Conformance

Conformance tests live at:

  • src/session/create-session.spec-conformance.test.ts — drives runAuthSessionConformance against a Postgres-shaped stubbed BetterAuthLike (the contract asserts the abstract SessionResolver invariants; DB backing is orthogonal).
  • src/provider/create-provider.spec-conformance.test.ts — drives runAuthProviderConformance against createBetterAuthProvider with the same stubbing topology.
  • tests/integration/postgres-better-auth.test.ts — real Postgres container + real Better-Auth + register/login/resolve. Gracefully skips with a logged reason when Docker / pg / schema bootstrap is unavailable in the runner environment.

Architecture

              host config
                 │
                 ▼
   ┌─────────────────────────────┐
   │  BetterAuthOptions (Zod)    │  parseBetterAuthOptions(...)
   └──────────────┬──────────────┘
                  │
        per-opts WeakMap
                  │
                  ▼
         ┌──────────────┐
         │ LazyBetterAuth│  ── betterAuth({ ...args, plugins: [genericOAuth(...)] })
         └─────┬────────┘
               │ shared (single connection pool)
       ┌───────┴────────┐
       ▼                ▼
createBetterAuthSession  createBetterAuthProvider
  (SessionResolver)        (AuthProviderAdapter)
       │                    │
       │                    ├── mounts /register, /login, /logout, /refresh,
       │                    │   /oauth/:provider/start, /oauth/:provider/callback,
       │                    │   /verify-email/:token, /forgot-password, /reset-password
       ▼                    ▼
  Principal              auth.handler(rewrittenRequest)