Files
skills/plugins/document-navigator/docs/api-stack/api/api-plugin-auth/README.md
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 01:10:00 +01:00

12 KiB

@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.

Documentation tree

  • error-codes — The error-codes module owns the stable AUTH.* code registry and the AuthError class used by every production failur…
    • error-codes — The error-codes module owns the stable AUTH.* code registry and the AuthError class used by every production failur…
  • context-augmentation — This module installs the canonical Hono ContextVariableMap augmentation that lets every consumer call c.get("user")…
  • require-auth — requireAuth() is the canonical Hono middleware factory for gating a route or sub-route on an authenticated principal.…
  • require-email-verified — requireEmailVerified() is the Hono middleware factory that gates a route on the principal's email having been verifie…
  • require-role — requireRole(...roles) builds a Hono middleware that gates a route on the principal carrying at least one of the suppl…
  • session — sessionMiddleware is the global Hono middleware that resolves every incoming request's Better-Auth session and expose…
  • otel — The instruments module owns the OpenTelemetry counters and histogram emitted by every code path inside `@suaveplan/api-…
    • instruments — The instruments module owns the OpenTelemetry counters and histogram emitted by every code path inside `@suaveplan/api-…
  • plugin — createAuthPlugin(options) is the factory that returns a typed PluginDescriptor<AuthPluginConfig> ready to register…
    • plugin — createAuthPlugin(options) is the factory that returns a typed PluginDescriptor<AuthPluginConfig> ready to register…
  • principal — The principal module defines the canonical authenticated-user shape used across the entire @suaveplan/api-plugin-* fa…
    • principal — The principal module defines the canonical authenticated-user shape used across the entire @suaveplan/api-plugin-* fa…
  • routes — buildAuthRoutes(auth) returns the 9 PluginRouteEntry values that createAuthPlugin mounts on the host's Hono app v…
    • build-routes — buildAuthRoutes(auth) returns the 9 PluginRouteEntry values that createAuthPlugin mounts on the host's Hono app v…
  • schema — This module re-exports the canonical Better-Auth SurrealQL schema shipped by @suaveplan/better-auth-surrealdb so cons…
    • schema — This module re-exports the canonical Better-Auth SurrealQL schema shipped by @suaveplan/better-auth-surrealdb so cons…
  • forgot-password — Schemas for POST /auth/forgot-password. The body schema accepts a single email and the response is { sent: true }.…
  • oauth — Schemas for the OAuth pair GET /auth/oauth/:provider/start and GET /auth/oauth/:provider/callback. The start endpoi…
  • reset-password — Schemas for POST /auth/reset-password. The body accepts the single-use token previously emailed to the user plus the…
  • session — Response schema for GET /auth/session. Returns the current authenticated Principal and the matching Better-Auth ses…
  • sign-in — Request/response Zod schemas for POST /auth/sign-in. The body schema validates email + password credential input; the…
  • sign-out — Schemas for POST /auth/sign-out. The body is optional (Better-Auth identifies the session from the cookie or `Authori…
  • sign-up — Request/response Zod schemas for POST /auth/sign-up. The body schema constrains the email + password + optional name…
  • verify-email — Schemas for POST /auth/verify-email. The body carries the single-use verification token (typically emailed from a sig…
  • options — The options module exports AuthPluginOptionsSchema, the Zod validator that createAuthPlugin runs at the factory bou…
  • principal — This module exports the canonical Zod schemas for the auth boundary — PrincipalSchema, SessionSchema, `AccountSchem…
  • auth-service — The auth-service module owns the AuthService interface, its constructor (createAuthService), and the side-effect…
  • registry-augmentation — Module augmentation that wires the authService slot into @suaveplan/api-plugin/service-registry's ServiceRegistry…
  • types — Top-level type definitions for @suaveplan/api-plugin-auth. This module exports the AuthPluginOptions shape (what ca…
    • types — Top-level type definitions for @suaveplan/api-plugin-auth. This module exports the AuthPluginOptions shape (what ca…

Quick start

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

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:

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.

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.