196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
# @suaveplan/api-plugin-auth - Better-Auth-first authentication plugin for @suaveplan/api. Wraps Better-Auth as a @suaveplan/api-plugin definePlugin descriptor, applies the canonical 17-table SurrealDB schema at boot via @suaveplan/api-migrations, mounts 9 OpenAPI routes through @suaveplan/api-openapi, registers a typed authService in the host ServiceRegistry, installs a global session-resolver middleware that puts a typed Principal on every Hono context, and exports requireAuth / requireRole / requireEmailVerified middleware factories. Server-tier — runs in Bun and Node. Reference implementation of the 3-way plug-and-play contract (.surql DDL + Zod schemas at boundary + OpenAPI route registration) every @suaveplan/api-plugin-* follows.
|
|
|
|
Better-Auth-first authentication plugin for `@suaveplan/api`. The
|
|
canonical exemplar of the 3-way plug-and-play contract every
|
|
`@suaveplan/api-plugin-*` package follows: a plugin owns its `.surql`
|
|
DDL, its Zod boundary schemas, and its OpenAPI route definitions —
|
|
all wired into the host's `PluginRegistry`, `MigrationRunner`, and
|
|
`OpenAPIRegistry` from a single `definePlugin` descriptor.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module owns the stable `AUTH.*` code registry and the `AuthError` class used by every production failur…
|
|
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module owns the stable `AUTH.*` code registry and the `AuthError` class used by every production failur…
|
|
- [context-augmentation](./src/middleware/context-augmentation.md) — This module installs the canonical Hono `ContextVariableMap` augmentation that lets every consumer call `c.get("user")`…
|
|
- [require-auth](./src/middleware/require-auth.md) — `requireAuth()` is the canonical Hono middleware factory for gating a route or sub-route on an authenticated principal.…
|
|
- [require-email-verified](./src/middleware/require-email-verified.md) — `requireEmailVerified()` is the Hono middleware factory that gates a route on the principal's email having been verifie…
|
|
- [require-role](./src/middleware/require-role.md) — `requireRole(...roles)` builds a Hono middleware that gates a route on the principal carrying at least one of the suppl…
|
|
- [session](./src/middleware/session.md) — `sessionMiddleware` is the global Hono middleware that resolves every incoming request's Better-Auth session and expose…
|
|
- [otel](./src/otel/instruments.md) — The instruments module owns the OpenTelemetry counters and histogram emitted by every code path inside `@suaveplan/api-…
|
|
- [instruments](./src/otel/instruments.md) — The instruments module owns the OpenTelemetry counters and histogram emitted by every code path inside `@suaveplan/api-…
|
|
- [plugin](./src/plugin/plugin.md) — `createAuthPlugin(options)` is the factory that returns a typed `PluginDescriptor<AuthPluginConfig>` ready to register…
|
|
- [plugin](./src/plugin/plugin.md) — `createAuthPlugin(options)` is the factory that returns a typed `PluginDescriptor<AuthPluginConfig>` ready to register…
|
|
- [principal](./src/principal/principal.md) — The principal module defines the canonical authenticated-user shape used across the entire `@suaveplan/api-plugin-*` fa…
|
|
- [principal](./src/principal/principal.md) — The principal module defines the canonical authenticated-user shape used across the entire `@suaveplan/api-plugin-*` fa…
|
|
- [routes](./src/routes/build-routes.md) — `buildAuthRoutes(auth)` returns the 9 `PluginRouteEntry` values that `createAuthPlugin` mounts on the host's Hono app v…
|
|
- [build-routes](./src/routes/build-routes.md) — `buildAuthRoutes(auth)` returns the 9 `PluginRouteEntry` values that `createAuthPlugin` mounts on the host's Hono app v…
|
|
- [schema](./src/schema/schema.md) — This module re-exports the canonical Better-Auth SurrealQL schema shipped by `@suaveplan/better-auth-surrealdb` so cons…
|
|
- [schema](./src/schema/schema.md) — This module re-exports the canonical Better-Auth SurrealQL schema shipped by `@suaveplan/better-auth-surrealdb` so cons…
|
|
- [forgot-password](./src/schemas/routes/forgot-password.md) — Schemas for `POST /auth/forgot-password`. The body schema accepts a single email and the response is `{ sent: true }`.…
|
|
- [oauth](./src/schemas/routes/oauth.md) — Schemas for the OAuth pair `GET /auth/oauth/:provider/start` and `GET /auth/oauth/:provider/callback`. The start endpoi…
|
|
- [reset-password](./src/schemas/routes/reset-password.md) — Schemas for `POST /auth/reset-password`. The body accepts the single-use token previously emailed to the user plus the…
|
|
- [session](./src/schemas/routes/session.md) — Response schema for `GET /auth/session`. Returns the current authenticated `Principal` and the matching Better-Auth ses…
|
|
- [sign-in](./src/schemas/routes/sign-in.md) — Request/response Zod schemas for `POST /auth/sign-in`. The body schema validates email + password credential input; the…
|
|
- [sign-out](./src/schemas/routes/sign-out.md) — Schemas for `POST /auth/sign-out`. The body is optional (Better-Auth identifies the session from the cookie or `Authori…
|
|
- [sign-up](./src/schemas/routes/sign-up.md) — Request/response Zod schemas for `POST /auth/sign-up`. The body schema constrains the email + password + optional name…
|
|
- [verify-email](./src/schemas/routes/verify-email.md) — Schemas for `POST /auth/verify-email`. The body carries the single-use verification token (typically emailed from a sig…
|
|
- [options](./src/schemas/options.md) — The options module exports `AuthPluginOptionsSchema`, the Zod validator that `createAuthPlugin` runs at the factory bou…
|
|
- [principal](./src/schemas/principal.md) — This module exports the canonical Zod schemas for the auth boundary — `PrincipalSchema`, `SessionSchema`, `AccountSchem…
|
|
- [auth-service](./src/services/auth-service.md) — The `auth-service` module owns the `AuthService` interface, its constructor (`createAuthService`), and the side-effect…
|
|
- [registry-augmentation](./src/services/registry-augmentation.md) — Module augmentation that wires the `authService` slot into `@suaveplan/api-plugin/service-registry`'s `ServiceRegistry`…
|
|
- [types](./src/types/types.md) — Top-level type definitions for `@suaveplan/api-plugin-auth`. This module exports the `AuthPluginOptions` shape (what ca…
|
|
- [types](./src/types/types.md) — Top-level type definitions for `@suaveplan/api-plugin-auth`. This module exports the `AuthPluginOptions` shape (what ca…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
import { defineApp } from "@suaveplan/api";
|
|
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb";
|
|
import { createAuthPlugin } from "@suaveplan/api-plugin-auth";
|
|
|
|
const auth = await defineApp()
|
|
.name("my-api")
|
|
.use(
|
|
createAuthPlugin({
|
|
betterAuth: {
|
|
database: betterAuthSurrealAdapter({ client: surreal }),
|
|
secret: process.env.BETTER_AUTH_SECRET!,
|
|
baseURL: "http://localhost:3000",
|
|
emailAndPassword: { enabled: true },
|
|
},
|
|
})
|
|
)
|
|
.start();
|
|
```
|
|
|
|
`createAuthPlugin` returns a typed `PluginDescriptor`. The host's
|
|
`PluginRegistry` calls `onInit(ctx)` which:
|
|
|
|
1. Resolves `app`, `openAPIRegistry`, `migrationRunner`, and `db` from
|
|
the host `ServiceRegistry`.
|
|
2. Applies `BETTER_AUTH_SURREAL_SCHEMA` (the canonical 17-table DDL
|
|
shipped by `@suaveplan/better-auth-surrealdb`) — uses the
|
|
`migrationRunner` when registered, otherwise calls
|
|
`applyBetterAuthSchema(db)`.
|
|
3. Constructs the Better-Auth instance.
|
|
4. Registers `authService` in the typed `ServiceRegistry`.
|
|
5. Mounts 9 OpenAPI routes via `definePluginRoutes` so they appear in
|
|
the unified `/openapi.json` doc with full Zod schemas.
|
|
6. Installs a global session-resolver middleware so every request's
|
|
`c.get("user")` is a typed `Principal | null`.
|
|
|
|
## The 3-way plug-and-play contract
|
|
|
|
The auth plugin is the canonical example of the plugin
|
|
contract. Three artefacts move together; the plugin owns all three.
|
|
|
|
| Surface | Implementation |
|
|
|---|---|
|
|
| `.surql` DDL | Re-exports `BETTER_AUTH_SURREAL_SCHEMA` (17 tables) and applies it via the host's `MigrationRunner` |
|
|
| Zod schemas | Per-route bodies + responses + the canonical `PrincipalSchema` / `SessionSchema` / `AccountSchema` at `./schemas/principal` |
|
|
| OpenAPI routes | `buildAuthRoutes(auth)` returns 9 `RouteBuilder` entries; `definePluginRoutes` writes both the Hono app AND the `OpenAPIRegistry` |
|
|
|
|
User and vault plugins follow the same shape — read this README to
|
|
understand the contract, then look at `api-plugin-user` / `api-plugin-vault`
|
|
for second and third examples.
|
|
|
|
## Middleware recipes
|
|
|
|
```ts
|
|
import {
|
|
requireAuth,
|
|
requireRole,
|
|
requireEmailVerified,
|
|
} from "@suaveplan/api-plugin-auth";
|
|
|
|
app.use("/me", requireAuth());
|
|
app.use("/admin/*", requireAuth(), requireRole("admin"));
|
|
app.use("/billing/*", requireAuth(), requireEmailVerified());
|
|
```
|
|
|
|
Each middleware throws a typed `AuthError(AUTH_*)` carrying
|
|
`context.statusCode` so the host's error-handler returns the right
|
|
HTTP status (401 / 403). The error-code registry has 10 entries —
|
|
see `./src/error-codes/error-codes.md` for the full list.
|
|
|
|
## OAuth + organisations + 2FA
|
|
|
|
Better-Auth handles every plugin / provider option. Pass them through
|
|
`options.betterAuth` verbatim:
|
|
|
|
```ts
|
|
createAuthPlugin({
|
|
betterAuth: {
|
|
database: betterAuthSurrealAdapter({ client: surreal }),
|
|
secret: process.env.BETTER_AUTH_SECRET!,
|
|
baseURL: "https://app.example.test",
|
|
emailAndPassword: { enabled: true },
|
|
socialProviders: {
|
|
github: {
|
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
The plugin re-emits Better-Auth's full router under `routePrefix` —
|
|
9 OpenAPI-typed routes are advertised; the remaining surface (e.g.
|
|
the OAuth provider list) is still reachable but not documented in the
|
|
served OpenAPI doc.
|
|
|
|
## Schema apply + migrations integration
|
|
|
|
The plugin applies the 17-table DDL during `onInit` by default. Two
|
|
paths are supported:
|
|
|
|
- **`migrationRunner` (preferred)** — the runner registers the schema
|
|
as a tracked migration named `0000-better-auth-schema` so subsequent
|
|
custom migrations can build on it.
|
|
- **`db` fallback** — when no runner is registered, the plugin calls
|
|
`applyBetterAuthSchema(db)` directly. The DDL is idempotent
|
|
(`DEFINE TABLE OVERWRITE`), so re-running is safe.
|
|
|
|
Disable the apply step entirely with `applySchemaOnInit: false` when
|
|
operators manage migrations externally.
|
|
|
|
## Customising the principal
|
|
|
|
Override the Better-Auth → `Principal` mapping with
|
|
`options.principalMapping` to project plugin-extended user fields
|
|
(organisations, 2FA flags) onto the resolved principal. The default
|
|
mapping (`mapBetterAuthUserToPrincipal`) covers email, verification
|
|
state, name/image metadata, and `activeOrganizationId` → `tenantId`.
|
|
|
|
```ts
|
|
import { mapBetterAuthUserToPrincipal } from "@suaveplan/api-plugin-auth";
|
|
|
|
createAuthPlugin({
|
|
betterAuth,
|
|
principalMapping: (view) => {
|
|
const base = mapBetterAuthUserToPrincipal(view);
|
|
const session = view.session as { roles?: string[] };
|
|
return { ...base, roles: [...base.roles, ...(session.roles ?? [])] };
|
|
},
|
|
});
|
|
```
|
|
|
|
## Subpath exports
|
|
|
|
`.`, `./plugin/plugin`, `./middleware/require-auth`,
|
|
`./middleware/require-role`, `./middleware/require-email-verified`,
|
|
`./middleware/session`, `./principal/principal`,
|
|
`./services/auth-service`, `./schemas/principal`, `./schemas/options`,
|
|
`./schemas/routes`, `./routes/routes`, `./schema/schema`,
|
|
`./error-codes/error-codes`, `./otel/instruments`, `./types/types`.
|
|
|
|
## Peer deps
|
|
|
|
- `better-auth ^1.4` (resolves to 1.6.x via the catalog).
|
|
- `hono`, `@hono/zod-openapi`, `zod` — supplied by the host
|
|
application; the plugin imports them directly.
|
|
|