first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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