# `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` | 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.