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,110 @@
# `create-provider`
## Purpose
Factory for the `auth.provider` half of The split. Returns an
`AuthProviderAdapter` (per `@suaveplan/api-auth-provider`) that mounts the
canonical `PROVIDER_ROUTE_SURFACE` (9 routes) under `routePrefix` (default
`/auth`). Each handler internally rewrites the incoming `Request` onto the
Better-Auth-native sub-path via `ROUTE_TRANSLATION` and delegates to
`auth.handler(...)`. This indirection means downstream consumers of the
provider see the canonical foundation surface — never Better-Auth's idioms —
while Better-Auth still owns the precise wire response (cookies, 302
redirects, error envelopes).
## Features
- `id: "better-auth"`; `pathPrefix` validated as `AuthPathPrefix`.
- All 9 canonical route names registered:
- `POST /register` → `POST /sign-up/email`
- `POST /login` → `POST /sign-in/email`
- `POST /logout` → `POST /sign-out`
- `POST /refresh` → `GET /get-session`
- `GET /oauth/:provider/start` → `POST /oauth2/sign-in/:provider`
- `GET /oauth/:provider/callback` → `GET /oauth2/callback/:provider`
- `GET /verify-email/:token` → `GET /verify-email`
- `POST /forgot-password` → `POST /forget-password`
- `POST /reset-password` → `POST /reset-password`
- `routePrefix` collision-checked against
`@suaveplan/api-host/reserved-prefix` — refused with
`AUTH_BA.ROUTE_PREFIX_COLLISION` on collision.
- Better-Auth instance materialised during `lifecycle.install` so DB
unreachable errors fail before traffic.
- OAuth flow conclusions emit `oauth_flows_total{provider,outcome}`.
- Per-options `WeakMap` so the same Better-Auth instance is shared with
`createBetterAuthSession(opts)` when wired with the same options object.
## Usage: Basic
```ts
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth";
const provider = createBetterAuthProvider({
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
});
// Mount via the host's plugin lifecycle:
host.registerAuthProvider(provider);
```
## Usage: Advanced
```ts
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth";
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
// Operators MUST share the SAME options bag between the two factories to
// share the Better-Auth instance (and thus the DB pool):
const opts = {
database: pool,
baseUrl: "https://api.example.com",
secret: process.env.BETTER_AUTH_SECRET!,
routePrefix: "/api/auth",
oauth: {
providers: {
github: {
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ["read:user", "user:email"],
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
},
},
},
};
host.registerAuth({
resolver: createBetterAuthSession(opts),
csrf: true,
});
host.registerAuthProvider(createBetterAuthProvider(opts));
```
## API Reference
| Export | Shape | Purpose |
| ------ | ----- | ------- |
| `createBetterAuthProvider(opts)` | `(BetterAuthOptions) => AuthProviderAdapter` | Build the provider |
| `ROUTE_TRANSLATION` | `Record<ProviderRouteName, NativeRoute>` | Re-exported for callers wanting to introspect the mapping |
| `buildNativeUrl(args)` | function | Re-exported for advanced wrappers |
## Implementation Notes
The provider's `routes(router, ctx)` mounts every canonical entry from
`PROVIDER_ROUTE_SURFACE` with `auth: null` because the routes are mounted
INSIDE the auth provider's own prefix and the routes themselves are the
authentication flow — they MUST be reachable unauthenticated. The handler's
inner rewrite reads `(ctx as { params })` because the abstract
`RequestContext` doesn't declare `params`, but the request pipeline driver
(`@suaveplan/api-host/request-pipeline-driver.ts`) attaches them at runtime.
The cast is documented inline. The OAuth outcome heuristic maps status codes
to labels — 2xx/3xx → `success`, 403 → `provider_rejected`, anything else
→ `state_mismatch`. The semantic split between "state_mismatch" and
"provider_rejected" is intentional even though a single 400 covers both
wire-wise: the conformance suite distinguishes the two via the underlying
mock IdP's `primeError` API. The per-options `WeakMap` is keyed by object
identity, not deep equality — sharing requires literal same-object reuse.