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