Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# `@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 the `auth.session` capability. Reads cookies/bearer per Better-Auth conventions, maps Better-Auth's session shape into `Principal`.
- ADDED: `createBetterAuthProvider(opts)` — implements the `auth.provider` capability. Mounts account-lifecycle routes (default prefix `/auth/*`, configurable via `routePrefix`). 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 a `Principal` from Better-Auth's shapes. Default mapping; replaceable via `principalMapping` option.
- 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: `./testing` subpath — re-export of `runAuthSessionConformance` AND `runAuthProviderConformance` bound 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.session` and `auth.provider` contracts — those live in `api-auth` and `api-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 over `mapBetterAuthUserToPrincipal`; missing-fields, extra-claims, scope translation.
- `src/session/create-session.spec-conformance.test.ts` — runs `runAuthSessionConformance` against the factory backed by a real Postgres container.
- `src/provider/create-provider.spec-conformance.test.ts` — runs `runAuthProviderConformance` against 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 raises `AUTH_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, verify `ctx.principal` reflects the Better-Auth user.
## Migration impact
None — additive only.