5.9 KiB
@suaveplan/api-auth-better-auth — Better-Auth adapter implementing both auth contracts
Package: @suaveplan/api-auth-better-auth
Status: PROPOSED
Type: new-package
Stage: 6 (Distributed transactions + Better-Auth) — see umbrella openspec/changes/api-plugin-foundation/
Why
Per design doc §2.3 the foundation deliberately separates two auth contracts: auth.session (Request → Principal | null, on every request) and auth.provider (account-lifecycle routes — register / login / logout / oauth / verify-email). Conflating them is the failure mode that makes most plugin systems painful to integrate with third-party auth. Shipping Better-Auth on day one is how we know those contracts hold up against a real non-trivial third-party library — wrapping it surfaces the contract pinch points before they ossify.
Better-Auth is opinionated about owning its own DB connection. This package reflects that reality: it consumes its own DB rather than the host's main DB. It then maps Better-Auth's user/session shapes onto our Principal contract so plugins never see Better-Auth-shaped types — only the foundation's Principal.
What changes
Net-new package depending on @suaveplan/api-types, @suaveplan/api-auth (session contract), @suaveplan/api-auth-provider (provider contract), @suaveplan/api-router, @suaveplan/error, @suaveplan/logger, @suaveplan/opentelemetry, better-auth (peer), and zod.
- ADDED:
createBetterAuthSession(opts)— implements theauth.sessioncapability. Reads cookies/bearer per Better-Auth conventions, maps Better-Auth's session shape intoPrincipal. - ADDED:
createBetterAuthProvider(opts)— implements theauth.providercapability. Mounts account-lifecycle routes (default prefix/auth/*, configurable viaroutePrefix). Includes OAuth flow. - ADDED:
BetterAuthOptions— Zod-validated config:database(separate from host main DB),baseUrl,routePrefix(default/auth),secret,oauth.providers(GitHub mandatory in spec, others optional),session.cookieName,session.cookieSameSite,principalMapping(function mapping Better-Auth user →Principal). - ADDED:
mapBetterAuthUserToPrincipal(user, session)— pure helper producing aPrincipalfrom Better-Auth's shapes. Default mapping; replaceable viaprincipalMappingoption. - ADDED: Wired-only-one-side support — host config can mount the session adapter WITHOUT the provider adapter (operator wants Better-Auth's session resolver but a different account-lifecycle path, e.g. Clerk for accounts but Better-Auth as the session reader against a shared cookie). Each factory is independent.
- ADDED: GitHub OAuth integration as the canonical reference flow.
- ADDED:
AUTH_BA_*error codes:AUTH_BA_INVALID_SESSION,AUTH_BA_DB_UNREACHABLE,AUTH_BA_OAUTH_PROVIDER_REJECTED,AUTH_BA_OAUTH_STATE_MISMATCH,AUTH_BA_USER_NOT_FOUND,AUTH_BA_EMAIL_NOT_VERIFIED,AUTH_BA_RATE_LIMITED,AUTH_BA_INVALID_REDIRECT,AUTH_BA_PRINCIPAL_MAPPING_FAILED,AUTH_BA_ROUTE_PREFIX_COLLISION. - ADDED: OTel instruments:
auth.better_auth.sessions_resolved_total,auth.better_auth.session_resolve_duration_ms,auth.better_auth.oauth_flows_total { provider, outcome },auth.better_auth.principal_mapping_errors_total. - ADDED:
./testingsubpath — re-export ofrunAuthSessionConformanceANDrunAuthProviderConformancebound to factories backed by an integration Postgres container.
Out of scope
- Re-export of Better-Auth's broader feature surface (passkeys, magic links, 2FA flows beyond OTP, organisation management). Operators wire those via Better-Auth config directly; the wrapper just plumbs them through.
- A non-Postgres backend for Better-Auth. Better-Auth supports multiple databases; the conformance test pins Postgres because that's what the design doc says is integration-tested. Other backends MAY work but are not contracted.
- The abstract
auth.sessionandauth.providercontracts — those live inapi-authandapi-auth-provider(Stage 1).
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
Implements both auth.session AND auth.provider |
§2.3 (bullet 4) |
| Owns its own DB connection (separate from plugin DBs) | §2.3 (bullet 4) |
Maps Better-Auth user/session onto our Principal |
§2.3 (bullet 4) |
| Integration-tested against real Postgres | §2.3 (bullet 4) |
Default route prefix /auth/* (configurable) |
§2.3 (paragraph 2) |
| Operator MAY wire only the session side OR only the provider side | §2.3 (paragraph 2 "Mix-and-match") |
| Surfaces contract pinch points before they ossify (= our verification stance) | §2.3 (bullet 4) |
Verification approach
src/session/create-session.test.ts— pure-unit tests overmapBetterAuthUserToPrincipal; missing-fields, extra-claims, scope translation.src/session/create-session.spec-conformance.test.ts— runsrunAuthSessionConformanceagainst the factory backed by a real Postgres container.src/provider/create-provider.spec-conformance.test.ts— runsrunAuthProviderConformanceagainst the factory backed by a real Postgres container.src/oauth/github-oauth.test.ts(integration, 60000ms) — end-to-end GitHub OAuth flow against a mock OAuth server; pins state-cookie handling, code exchange, principal mapping.src/route-prefix/route-prefix.test.ts— configurable prefix; collision detection vs host-reserved prefixes raisesAUTH_BA_ROUTE_PREFIX_COLLISION.src/independence/session-without-provider.test.ts— operator wires only the session adapter; assert provider routes are NOT mounted AND session resolution still works.tests/integration/postgres-better-auth.test.ts(60000ms) — full lifecycle: register a user via provider routes, log in, hit a protected route, verifyctx.principalreflects the Better-Auth user.
Migration impact
None — additive only.