first commit
This commit is contained in:
172
docs/api-stack/api/api-auth-better-auth/README.md
Normal file
172
docs/api-stack/api/api-auth-better-auth/README.md
Normal 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)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user