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>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
# schemas/options
## Purpose
The options module exports `AuthPluginOptionsSchema`, the Zod validator
that `createAuthPlugin` runs at the factory boundary. The schema both
validates user input and applies defaults so the descriptor's lifecycle
hooks read a fully-resolved options object — never a partial input.
## Features
- Required `betterAuth` field (forwarded verbatim to Better-Auth's own
`betterAuth(options)` factory) typed as `Record<string, unknown>`.
- Default `routePrefix: "/auth"` with a regex that rejects missing
leading slash and trailing slashes (which produce broken Hono routes).
- Default `applySchemaOnInit: true` — set `false` to disable the
built-in SurrealQL schema apply step when operators manage migrations
externally.
- Optional `principalMapping` callback for projecting Better-Auth output
onto a custom `Principal` extension. Validated as a function.
- Optional `LoggerLike`-compatible `logger`. Defaults to the
`PluginContext` logger when omitted.
## Usage: Basic
```ts
import { AuthPluginOptionsSchema } from "@suaveplan/api-plugin-auth/schemas/options";
const opts = AuthPluginOptionsSchema.parse({ betterAuth: {} });
// opts.routePrefix === "/auth"; opts.applySchemaOnInit === true
```
## Usage: Advanced
Use the schema in a custom CLI that asks the operator for a non-default
prefix and a custom mapping function. `safeParse` lets you surface the
ZodError directly to the user instead of letting `createAuthPlugin`
throw:
```ts
import { AuthPluginOptionsSchema } from "@suaveplan/api-plugin-auth/schemas/options";
import { mapBetterAuthUserToPrincipal } from "@suaveplan/api-plugin-auth/principal/principal";
const result = AuthPluginOptionsSchema.safeParse({
betterAuth: { secret: process.env["BETTER_AUTH_SECRET"] },
routePrefix: "/api/v1/auth",
applySchemaOnInit: false,
principalMapping: (view) => ({
...mapBetterAuthUserToPrincipal(view as never),
roles: ["service"],
}),
});
if (!result.success) {
console.error(result.error.format());
process.exit(1);
}
```
## API Reference
### `AuthPluginOptionsSchema: ZodObject`
Required:
- `betterAuth: Record<string, unknown>` — forwarded to Better-Auth's
factory.
With defaults:
- `routePrefix: string = "/auth"` — must match
`^/[A-Za-z0-9\-_/]*[A-Za-z0-9\-_]$` or be exactly `/`.
- `applySchemaOnInit: boolean = true`.
Optional:
- `principalMapping?: (view: unknown) => unknown`.
- `logger?: LoggerLike` with all 4 methods (`debug`, `info`, `warn`,
`error`).
### `AuthPluginOptionsInput` / `AuthPluginOptionsResolved`
`z.input` and `z.output` aliases so consumers can reference both the
caller surface (defaults optional) and the post-parse surface (defaults
present) without re-deriving the types.
## Implementation Notes
- `betterAuth` is typed as `Record<string, unknown>` rather than the
upstream `BetterAuthOptions` because pinning to a specific Better-Auth
type version would couple the plugin to a single minor. Runtime
validation is delegated to `betterAuth(...)` itself.
- The route-prefix regex accepts nested paths (`/api/v1/auth`) and
hyphenated segments while rejecting `auth` (no leading slash) and
`/auth/` (trailing slash). The exact-root `/` form is allowed because
some hosts mount auth at the root path.
- `logger` is validated structurally rather than nominally (via a
branded type) so any LoggerLike-shaped object from
`@suaveplan/types` or a third-party adapter is accepted without
cross-package coupling.
- `principalMapping` is typed loosely on input (`(view: unknown) =>
unknown`); the plugin re-applies the strict `BetterAuthSessionView`
type at the call site so authors get a typed `view` argument inside
their mapping function.

View File

@@ -0,0 +1,90 @@
# schemas/principal
## Purpose
This module exports the canonical Zod schemas for the auth boundary —
`PrincipalSchema`, `SessionSchema`, `AccountSchema` — and their inferred
TypeScript types. Sibling plugins (`@suaveplan/api-plugin-user`,
`@suaveplan/api-plugin-vault`, …) consume these schemas directly via
the `./schemas/principal` subpath so the wire shape of an authenticated
user is one type, defined once.
## Features
- `PrincipalSchema` matches the runtime `Principal` interface from
`../principal/principal.ts` field-for-field.
- `SessionSchema` matches the Better-Auth `session` row 1:1 (including
optional plugin-extended fields used by the organisations plugin).
- `AccountSchema` matches the Better-Auth `account` row — exposed so
downstream test fixtures can validate persisted rows without
duplicating the schema.
- Every schema exports its `z.infer` companion type so consumers can
reference `PrincipalSchemaType` without re-deriving it.
## Usage: Basic
```ts
import { PrincipalSchema } from "@suaveplan/api-plugin-auth/schemas/principal";
const parsed = PrincipalSchema.parse({
id: "u1",
emailVerified: true,
roles: [],
});
```
## Usage: Advanced
Compose the schema into route response envelopes via
`@suaveplan/api-openapi`'s `successResponse` helper so the OpenAPI
document carries the full principal shape on every route that returns it:
```ts
import z from "zod";
import {
PrincipalSchema,
SessionSchema,
} from "@suaveplan/api-plugin-auth/schemas/principal";
import { successResponse } from "@suaveplan/api-openapi";
const SignInResponseSchema = successResponse(
z.object({ user: PrincipalSchema, session: SessionSchema })
);
```
## API Reference
### `PrincipalSchema`
Zod object with: `id` (non-empty string, required), `email` (email,
optional), `emailVerified` (boolean, required), `roles` (string array,
required), `tenantId` (non-empty string, optional), `metadata`
(`Record<string, unknown>`, optional). Inferred type:
`PrincipalSchemaType`.
### `SessionSchema`
Zod object mirroring the Better-Auth session row. Every field optional
to tolerate plugin-extended sessions. Inferred type:
`SessionSchemaType`.
### `AccountSchema`
Zod object mirroring the Better-Auth account row. `id`, `userId`,
`providerId`, `accountId` are required; OAuth token fields and credential
password are optional. Inferred type: `AccountSchemaType`.
## Implementation Notes
- `PrincipalSchema` enforces `email` is an RFC-5322-ish string when
present — this matches Better-Auth's own validation surface and is
forwarded to the OpenAPI spec as `format: email`.
- `roles` is the canonical permission carrier; the schema accepts a
plain string array (the runtime type is `readonly`, but Zod treats
arrays uniformly so consumers can pass either).
- `SessionSchema` accepts `expiresAt` as either ISO string or `Date` so
it round-trips both wire and in-process shapes.
- `AccountSchema` does NOT mark `password` as sensitive at the Zod
layer; redaction is the responsibility of the route serialiser. The
schema is exposed so test code can read/assert account rows from
SurrealDB directly.

View File

@@ -0,0 +1,71 @@
# schemas/routes/forgot-password
## Purpose
Schemas for `POST /auth/forgot-password`. The body schema accepts a
single email and the response is `{ sent: true }`. The response shape
is deliberately uninformative — leaking whether the email exists in the
user table is a known enumeration vector, so the route returns the same
"sent" token whether or not it actually emitted an email.
## Features
- `ForgotPasswordBodySchema` — `{ email: string (RFC-5322) }`.
- `ForgotPasswordResponseSchema` — `{ sent: literal(true) }`.
- Inferred companions `ForgotPasswordBody` and `ForgotPasswordResponse`.
## Usage: Basic
```ts
import { ForgotPasswordBodySchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const body = ForgotPasswordBodySchema.parse(await c.req.json());
```
## Usage: Advanced
Combine with a rate-limit middleware so the route can't be used to
probe email addresses by timing the response. The schema keeps the
response shape opaque even when the request is rate-limited:
```ts
import { POST, successResponse } from "@suaveplan/api-openapi";
import {
ForgotPasswordBodySchema,
ForgotPasswordResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const forgotRoute = POST("/auth/forgot-password")
.tags("auth")
.summary("Send a password-reset email")
.body(ForgotPasswordBodySchema)
.response(successResponse(ForgotPasswordResponseSchema))
.build();
```
## API Reference
### `ForgotPasswordBodySchema`
Required: `email: string (email)`. Inferred type `ForgotPasswordBody`.
### `ForgotPasswordResponseSchema`
`{ sent: true }`. Inferred type `ForgotPasswordResponse`.
## Implementation Notes
- The schema rejects malformed emails at the boundary so Better-Auth's
email delivery layer never has to handle garbage input.
- The response always returns `sent: true` regardless of whether the
email exists in the user table — the canonical pattern for avoiding
account enumeration through forgot-password endpoints.
- The schema does not include locale or template selection; those are
Better-Auth configuration concerns, not request-body concerns.
- Operators that want different responses for known vs unknown emails
(e.g. throttling differently) should configure rate limiting at the
network edge rather than altering the schema — the constant-shape
response is the privacy guarantee the schema exists to enforce.
- The route mounts under the configured `routePrefix`, so the fully
qualified path is typically `/auth/forgot-password` when the
default prefix is used.

View File

@@ -0,0 +1,106 @@
# schemas/routes/oauth
## Purpose
Schemas for the OAuth pair `GET /auth/oauth/:provider/start` and
`GET /auth/oauth/:provider/callback`. The start endpoint initiates an
OAuth flow (typically redirecting to the provider's authorization
endpoint); the callback receives the provider's `code` + `state` and
finishes the flow.
## Features
- `OAuthStartParamsSchema` — `{ provider: string (min 1) }` for the
path parameter.
- `OAuthStartQuerySchema` — `{ redirectTo?: string (url) }` so callers
can request a specific post-login redirect.
- `OAuthStartResponseSchema` — `{ redirectTo: string (url) }`; the
body shape used when the response is a 200 + JSON pointer rather than
a direct 302.
- `OAuthCallbackParamsSchema` + `OAuthCallbackQuerySchema` — `{ code,
state }` from the provider.
- `OAuthCallbackResponseSchema` — `{ redirectTo: string (url) }` for
the same 200 + JSON pointer variant.
## Usage: Basic
```ts
import { OAuthStartQuerySchema, OAuthCallbackQuerySchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const startQuery = OAuthStartQuerySchema.parse({ redirectTo: "https://example.test/" });
const callbackQuery = OAuthCallbackQuerySchema.parse({ code: "abc", state: "xyz" });
```
## Usage: Advanced
Use the params + query + response shapes inside a `RouteBuilder` chain
so the OpenAPI doc advertises a fully-typed OAuth flow including the
required `state` field on the callback:
```ts
import { GET, successResponse } from "@suaveplan/api-openapi";
import {
OAuthCallbackParamsSchema,
OAuthCallbackQuerySchema,
OAuthCallbackResponseSchema,
OAuthStartParamsSchema,
OAuthStartQuerySchema,
OAuthStartResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const startRoute = GET("/auth/oauth/{provider}/start")
.tags("auth", "oauth")
.summary("Begin OAuth flow with the named provider")
.params(OAuthStartParamsSchema)
.query(OAuthStartQuerySchema)
.response(successResponse(OAuthStartResponseSchema))
.build();
const callbackRoute = GET("/auth/oauth/{provider}/callback")
.tags("auth", "oauth")
.summary("OAuth callback — exchange code for session")
.params(OAuthCallbackParamsSchema)
.query(OAuthCallbackQuerySchema)
.response(successResponse(OAuthCallbackResponseSchema))
.build();
```
## API Reference
### `OAuthStartParamsSchema`
`{ provider: string (min 1) }`. Inferred type `OAuthStartParams`.
### `OAuthStartQuerySchema`
`{ redirectTo?: string (url) }`. Inferred type `OAuthStartQuery`.
### `OAuthStartResponseSchema`
`{ redirectTo: string (url) }`.
### `OAuthCallbackParamsSchema`
`{ provider: string (min 1) }`. Inferred type `OAuthCallbackParams`.
### `OAuthCallbackQuerySchema`
`{ code: string (min 1); state: string (min 1) }`. Inferred type
`OAuthCallbackQuery`.
### `OAuthCallbackResponseSchema`
`{ redirectTo: string (url) }`.
## Implementation Notes
- The OAuth pair is documented as 200-with-JSON-redirect in OpenAPI for
ergonomics — Better-Auth's runtime handler may return a 302 directly,
which is compatible with the OpenAPI doc because consumers reading
the spec see the "redirectTo" pointer either way.
- The callback's `state` field is required (Better-Auth always emits
it) so CSRF protection is enforced at the schema layer; callers that
drop `state` fail Zod validation before Better-Auth ever runs.
- Provider validation is structural — the route accepts any provider
string; Better-Auth's configured providers map decides whether the
call succeeds at runtime.

View File

@@ -0,0 +1,77 @@
# schemas/routes/reset-password
## Purpose
Schemas for `POST /auth/reset-password`. The body accepts the
single-use token previously emailed to the user plus the new password;
the response is `{ success: true }`. Better-Auth's downstream handler
validates the token, hashes the new password, and clears the
verification row.
## Features
- `ResetPasswordBodySchema` — `{ token: string (min 1), password:
string (min 8) }`.
- `ResetPasswordResponseSchema` — `{ success: literal(true) }`.
- Inferred companions `ResetPasswordBody` and `ResetPasswordResponse`.
## Usage: Basic
```ts
import { ResetPasswordBodySchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const body = ResetPasswordBodySchema.parse(await c.req.json());
```
## Usage: Advanced
Combine the schema with the standard error response so the OpenAPI
document advertises the 400 (invalid token) and 422 (password too weak)
cases consumers need to handle:
```ts
import { POST, successResponse } from "@suaveplan/api-openapi";
import {
ResetPasswordBodySchema,
ResetPasswordResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const resetRoute = POST("/auth/reset-password")
.tags("auth")
.summary("Apply a password reset using a single-use token")
.body(ResetPasswordBodySchema)
.response(successResponse(ResetPasswordResponseSchema))
.build();
```
## API Reference
### `ResetPasswordBodySchema`
Required: `token: string (min 1)`, `password: string (min 8)`. Inferred
type `ResetPasswordBody`.
### `ResetPasswordResponseSchema`
`{ success: true }`. Inferred type `ResetPasswordResponse`.
## Implementation Notes
- The 8-character minimum on `password` matches Better-Auth's default
password policy. Stricter policies are configured downstream; the
schema is a floor, not a ceiling.
- The token is opaque from the schema's perspective — its structure
(HMAC, JWT, or random base64) is a Better-Auth implementation detail
and the schema only ensures the field is non-empty.
- The response is a literal `true` so the typed return narrows
correctly on the client side.
- Better-Auth's downstream handler enforces single-use semantics —
the schema does not need to track whether the token was previously
redeemed. Replaying a consumed token surfaces a 400 from
Better-Auth, which the host's error-handler middleware translates
into the standard error envelope.
- Hashing the new password happens inside Better-Auth using its
configured hashing strategy (bcrypt, argon2, scrypt). The schema
only checks length; the policy layer (zxcvbn, breached-password
checks) is configured upstream and runs before this route is
invoked.

View File

@@ -0,0 +1,64 @@
# schemas/routes/session
## Purpose
Response schema for `GET /auth/session`. Returns the current
authenticated `Principal` and the matching Better-Auth session row, or
`null` for both fields when the request is anonymous. The endpoint is
the canonical "who am I" call for client SDKs and SSR hydration.
## Features
- `GetSessionResponseSchema` — `{ user: PrincipalSchema | null;
session: SessionSchema | null }` so the response carries the exact
same wire shape as `c.get("user")` plus the session row.
- Anonymous callers see `{ user: null, session: null }` — a 200 with
null fields rather than a 401, so client SDKs can probe identity
without error handling.
## Usage: Basic
```ts
import { GetSessionResponseSchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const parsed = GetSessionResponseSchema.parse({ user: null, session: null });
```
## Usage: Advanced
Pair the schema with a route definition that lists both anonymous and
authenticated cases in the OpenAPI document so consumers know the
endpoint never 401s:
```ts
import { GET, successResponse } from "@suaveplan/api-openapi";
import {
GetSessionResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const sessionRoute = GET("/auth/session")
.tags("auth")
.summary("Resolve the current session, if any")
.response(successResponse(GetSessionResponseSchema))
.build();
```
## API Reference
### `GetSessionResponseSchema`
`{ user: PrincipalSchema | null; session: SessionSchema | null }`.
Inferred type `GetSessionResponse`.
## Implementation Notes
- Returning `null` for the anonymous case keeps the endpoint cacheable
per cookie — a 401 response would force the CDN to either skip caching
or treat the page as private. Some host setups place this endpoint
behind a Service-Worker cache.
- The schema mirrors the runtime shape installed by the session
middleware so consumers can use the same type to read either the HTTP
response body or `c.get("user")` / `c.get("session")`.
- Better-Auth's session row carries plugin-extended fields (org
membership, impersonation) — those are tolerated via the loose schema
shape on `SessionSchema` so the API is forward-compatible.

View File

@@ -0,0 +1,73 @@
# schemas/routes/sign-in
## Purpose
Request/response Zod schemas for `POST /auth/sign-in`. The body schema
validates email + password credential input; the response describes
the user + session pair Better-Auth returns on a successful sign-in.
These schemas form one half of the canonical credential lifecycle —
sign-up and sign-in share the same response shape so client code can
treat the two endpoints uniformly.
## Features
- `SignInBodySchema` — `email` (RFC-5322), `password` (min 1, since
Better-Auth performs the policy check downstream and the schema only
guards against empty strings here).
- `SignInResponseSchema` — `{ user: PrincipalSchema; session:
SessionSchema }` matching the wire shape Better-Auth produces.
- Inferred companions `SignInBody` and `SignInResponse`.
## Usage: Basic
```ts
import { SignInBodySchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const body = SignInBodySchema.parse(await c.req.json());
```
## Usage: Advanced
Combine the schema with the `requireAuth` middleware downstream so
consumers can build a `/sign-in-then-call-private-route` flow with full
type inference:
```ts
import { POST, successResponse } from "@suaveplan/api-openapi";
import {
SignInBodySchema,
SignInResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const signInRoute = POST("/auth/sign-in")
.tags("auth")
.summary("Email + password sign-in")
.body(SignInBodySchema)
.response(successResponse(SignInResponseSchema))
.build();
```
## API Reference
### `SignInBodySchema`
Required: `email: string (email)`, `password: string (min 1)`. Inferred
type `SignInBody`.
### `SignInResponseSchema`
`{ user: PrincipalSchema; session: SessionSchema }`. Inferred type
`SignInResponse`.
## Implementation Notes
- The `password` field only requires a non-empty string — Better-Auth's
policy layer enforces strength rules with full error reporting.
Duplicating policy at the schema layer would force the client SDK to
re-implement Better-Auth's exact rules.
- The response intentionally mirrors `SignUpResponseSchema` so consumer
code can use one type for both the post-sign-up and post-sign-in
redirect handlers — a deliberate ergonomics choice.
- The schema does not include rate-limit headers or CSRF tokens; those
are handled by `@suaveplan/api`'s middleware chain, not the route
schema itself.

View File

@@ -0,0 +1,65 @@
# schemas/routes/sign-out
## Purpose
Schemas for `POST /auth/sign-out`. The body is optional (Better-Auth
identifies the session from the cookie or `Authorization` header) and
the response is a single `{ success: true }` literal so client code
can branch on success without parsing additional fields.
## Features
- `SignOutBodySchema` — optional empty object; accepts both `undefined`
and `{}` so clients can send a body or skip it.
- `SignOutResponseSchema` — `{ success: literal(true) }` so the
response type is `{ success: true }` rather than `{ success: boolean }`.
- Inferred companion `SignOutResponse`.
## Usage: Basic
```ts
import { SignOutResponseSchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const parsed = SignOutResponseSchema.parse({ success: true });
```
## Usage: Advanced
Pair the schema with the `requireAuth` middleware so the OpenAPI
document advertises a 401 response for anonymous callers and a 200
response with the typed body for authenticated ones:
```ts
import { POST, successResponse } from "@suaveplan/api-openapi";
import {
SignOutResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const signOutRoute = POST("/auth/sign-out")
.tags("auth")
.summary("End the current session")
.response(successResponse(SignOutResponseSchema))
.build();
```
## API Reference
### `SignOutBodySchema`
Optional empty object. Accepts `undefined` or `{}`.
### `SignOutResponseSchema`
`{ success: true }` literal. Inferred type `SignOutResponse`.
## Implementation Notes
- The body schema is `optional` rather than absent so the OpenAPI
document still advertises the route as accepting a JSON body. Clients
may send `{}` to follow the route shape or omit the body entirely.
- The response is a literal `true` rather than `boolean` so the typed
return value at the client side narrows correctly — `if
(response.success) { … }` is statically known to execute.
- Better-Auth's downstream cookie clearing is a side effect of the
handler, not part of the schema. The response only describes the JSON
body so the schema stays orthogonal to the cookie machinery.

View File

@@ -0,0 +1,77 @@
# schemas/routes/sign-up
## Purpose
Request/response Zod schemas for `POST /auth/sign-up`. The body schema
constrains the email + password + optional name input; the response
schema describes the typed user + session pair Better-Auth returns on
a successful sign-up. Sibling plugins import these schemas directly so
client SDK generators and downstream tests share one canonical wire
shape.
## Features
- `SignUpBodySchema` — `email` (RFC-5322), `password` (min 8 chars),
optional `name` (min 1 char).
- `SignUpResponseSchema` — Better-Auth user + session payload typed
via `PrincipalSchema` and `SessionSchema` from `../principal.ts` so
the response carries the same wire-level user identity that
`c.get("user")` produces on every other route.
- Inferred TypeScript companions `SignUpBody` and `SignUpResponse`.
## Usage: Basic
```ts
import { SignUpBodySchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const body = SignUpBodySchema.parse(await c.req.json());
```
## Usage: Advanced
Use the schemas inside a `RouteBuilder` chain so the OpenAPI document
carries both the request body and the success response shapes:
```ts
import { POST, successResponse } from "@suaveplan/api-openapi";
import {
SignUpBodySchema,
SignUpResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const signUpRoute = POST("/auth/sign-up")
.tags("auth")
.summary("Email + password sign-up")
.body(SignUpBodySchema)
.response(successResponse(SignUpResponseSchema))
.build();
```
## API Reference
### `SignUpBodySchema`
Required: `email: string (email)`, `password: string (min 8)`.
Optional: `name: string (min 1)`.
### `SignUpResponseSchema`
`{ user: PrincipalSchema; session: SessionSchema }`. Inferred type
`SignUpResponse`.
### `SignUpBody`
`z.infer<typeof SignUpBodySchema>`.
## Implementation Notes
- The 8-character minimum on `password` matches Better-Auth's default
password policy; consumers can layer stricter policies via Better-Auth
configuration without altering the schema.
- The response shape uses the shared `PrincipalSchema` / `SessionSchema`
rather than a route-local one — this is how the contract that
`c.get("user")` returns the same `Principal` everywhere stays a
single-source-of-truth thing.
- Sensitive fields (`password`) are intentionally excluded from the
response schema so the OpenAPI document never advertises a password
surface to consumers.

View File

@@ -0,0 +1,72 @@
# schemas/routes/verify-email
## Purpose
Schemas for `POST /auth/verify-email`. The body carries the
single-use verification token (typically emailed from a sign-up
event); the response is `{ verified: true }`. The route is idempotent
in Better-Auth — submitting an already-verified token returns `verified:
true` rather than failing.
## Features
- `VerifyEmailBodySchema` — `{ token: string (min 1) }`.
- `VerifyEmailResponseSchema` — `{ verified: literal(true) }`.
- Inferred companions `VerifyEmailBody` and `VerifyEmailResponse`.
## Usage: Basic
```ts
import { VerifyEmailBodySchema } from "@suaveplan/api-plugin-auth/schemas/routes";
const body = VerifyEmailBodySchema.parse(await c.req.json());
```
## Usage: Advanced
Wire the route into the host so the OpenAPI document advertises the 400
case (invalid / expired token) and the 200 case (verified). The schema
covers the 200 case; standard error responses come from
`StandardResponses`:
```ts
import { POST, successResponse } from "@suaveplan/api-openapi";
import {
VerifyEmailBodySchema,
VerifyEmailResponseSchema,
} from "@suaveplan/api-plugin-auth/schemas/routes";
const verifyRoute = POST("/auth/verify-email")
.tags("auth")
.summary("Verify an email address using a single-use token")
.body(VerifyEmailBodySchema)
.response(successResponse(VerifyEmailResponseSchema))
.build();
```
## API Reference
### `VerifyEmailBodySchema`
Required: `token: string (min 1)`. Inferred type `VerifyEmailBody`.
### `VerifyEmailResponseSchema`
`{ verified: true }`. Inferred type `VerifyEmailResponse`.
## Implementation Notes
- The token is opaque to the schema — Better-Auth's downstream handler
validates structure, expiry, and single-use semantics.
- The response is the literal `true` so client code can branch on
success without a wider type comparison.
- Repeated successful verification calls return the same `verified:
true` shape — the schema is intentionally idempotent so client retry
loops do not need a special-case "already verified" code.
- Token expiry is handled by Better-Auth's verification flow — the
schema does not impose a TTL; expired tokens return a 400 from the
Better-Auth handler, which the host's error-handler middleware
translates to the standard error envelope.
- The route is mounted under the configured `routePrefix` so the
fully-qualified path is typically `/auth/verify-email` (or the
caller's custom prefix when wiring the plugin).