Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user