Plugins
Web-layer plugins — authentication, CORS and OpenAPI/Swagger docs — that hook the gateway.
@ozaco/server/plugin/authThese plugins extend the web layer by hooking the single Gateway protocol. Each is a std plugin you install once: it mounts routes with Gateway.actions.mount, and/or registers Gateway.before / Gateway.after hooks around the REST transformer and server lifecycle, so behaviour applies uniformly to every dispatch.
Authentication ships two interchangeable JWT strategies over one shared provider contract, plus request helpers for reading the bearer token inside an action. CORS is a zero-config header/preflight layer. Docs compiles your mounted services into an OpenAPI document and serves Swagger UI.
@ozaco/server/plugin/authJWT authentication with two strategies (access+refresh and single-token session) over a pluggable provider, role/permission checks, email-style verification tokens and SSO.@ozaco/server/plugin/corsCross-Origin Resource Sharing: applies CORS response headers and answers preflight `OPTIONS` requests, with flexible `origin` matching and credentials support.@ozaco/server/plugin/docsGenerates an OpenAPI spec from mounted services and serves it plus Swagger UI at configurable routes, logging the URLs once the gateway starts.
Auth
JWT authentication strategies, request helpers and shared types.
AccessRefreshAuth
Short-lived access token + long-lived, rotating refresh token strategy.
// from '@ozaco/server/plugin/auth'
const AccessRefreshAuth // a std Plugin; install with AuthDef.AccessRefreshOptions
install(AccessRefreshAuth, options: AuthDef.AccessRefreshOptions)
AccessRefreshAuth.actions.provide(provider: AuthDef.Provider): Future<void>
AccessRefreshAuth.actions.signIn(credentials: unknown): Future<{
user: AuthDef.User
session: AuthDef.Session
} & AuthDef.AccessRefreshTokens>
AccessRefreshAuth.actions.refresh(refreshToken: string): Future<AuthDef.AccessRefreshTokens>
AccessRefreshAuth.actions.signOut(refreshToken: string): Future<void>
AccessRefreshAuth.actions.authorize(token: string): Future<AuthDef.Session>Issues an access token (default TTL '15m') alongside a refresh token (default '7d', rotate defaulting to true) — refresh swaps the pair, atomically when the provider implements rotateRefreshToken and via non-atomic revoke+save otherwise. Signing uses secret (HMAC) or privateKey/publicKey (RS/ES/EdDSA PEM or CryptoKey; raw asymmetric bytes are rejected) with algorithm (default 'HS256').
You must call provide(provider) once to supply the AuthDef.Provider (authenticate, loadUser, refresh-token store, …). Beyond the token lifecycle the built plugin also exposes authorization checks (hasRole, hasPermission, requireRole, requirePermission), verification tokens (issueVerification, confirmVerification), SSO (ssoAuthorize, ssoCallback) and an events() emitter over AuthDef.Events.
import { install } from '@ozaco/std/plugin'
import { AccessRefreshAuth } from '@ozaco/server/plugin/auth'
yield* install(AccessRefreshAuth, {
secret: process.env.JWT_SECRET,
access: { expiresIn: '15m' },
refresh: { expiresIn: '30d', rotate: true },
})
yield* AccessRefreshAuth.actions.provide(myProvider)
const { user, session, accessToken, refreshToken } =
yield* AccessRefreshAuth.actions.signIn(credentials)JWTSessionAuth
Classic single-token JWT session strategy (no refresh token).
// from '@ozaco/server/plugin/auth'
const JWTSessionAuth // a std Plugin; install with AuthDef.JWTSessionOptions
install(JWTSessionAuth, options: AuthDef.JWTSessionOptions)
JWTSessionAuth.actions.provide(provider: AuthDef.Provider): Future<void>
JWTSessionAuth.actions.signIn(credentials: unknown): Future<{
user: AuthDef.User
session: AuthDef.Session
} & AuthDef.JWTSessionTokens>
JWTSessionAuth.actions.signOut(token: string): Future<void>
JWTSessionAuth.actions.authorize(token: string): Future<AuthDef.Session>A single self-contained session token (default TTL '7d') — there is no refresh token, so signOut merely emits the signed-out event (stateless logout is handled client-side by dropping the token). It shares the same provider contract, signing options and the full authorization / verification / SSO / events() surface as AccessRefreshAuth; choose it when you do not need refresh-token rotation.
import { install } from '@ozaco/std/plugin'
import { JWTSessionAuth } from '@ozaco/server/plugin/auth'
yield* install(JWTSessionAuth, {
secret: process.env.JWT_SECRET,
session: { expiresIn: '7d' },
})
yield* JWTSessionAuth.actions.provide(myProvider)useAuth
Authorize the current request by verifying its bearer token against a strategy.
function useAuth<T extends AuthDef.Strategy>(
strategy: T,
): Operation<AuthDef.Session>Reads the incoming request's Authorization: Bearer <token> header via getBearerToken, then calls strategy.actions.authorize(token) to verify and decode it into an AuthDef.Session. Use it inside an action to gate access; it fails with AuthErrorCode.MissingToken when no bearer token is present, and propagates the strategy's verification failures otherwise.
import { useAuth, AccessRefreshAuth } from '@ozaco/server/plugin/auth'
import { defineAction } from '@ozaco/server/core'
const me = defineAction(function* () {
const session = yield* useAuth(AccessRefreshAuth)
return session.user
})getBearerToken
Read the raw bearer token from the current request, failing if absent.
function getBearerToken(): Operation<string>Pulls the authorization (or Authorization) header off the request read via useRequest, strips the Bearer prefix and returns the token. Fails with AuthErrorCode.MissingToken when the header is missing or not a bearer token. useAuth builds on it; call it directly when you need the raw token rather than a verified session.
AuthDef
The shared auth types: users, providers, sessions, tokens and per-strategy options.
namespace AuthDef {
interface User {
id: string
[key: string]: unknown
}
interface Session<TUser extends User = User> {
sub: string
jti: string
iat: number
exp: number
type: string
user: TUser
roles: string[]
permissions: string[]
}
interface Provider<TUser extends User = User, TCredentials = unknown> {
authenticate(credentials: TCredentials): Operation<TUser | null>
loadUser(userId: string): Operation<TUser | null>
saveRefreshToken?(record: RefreshRecord): Operation<void>
findRefreshToken?(jti: string): Operation<RefreshRecord | null>
revokeRefreshToken?(jti: string): Operation<void>
rotateRefreshToken?(oldJti: string, newRecord: RefreshRecord): Operation<void>
getRoles?(user: TUser): Operation<string[]>
getPermissions?(user: TUser): Operation<string[]>
ssoProviders?: Record<string, SSOProvider>
// ...verification + linkSSO hooks
}
interface AccessRefreshOptions extends BaseOptions {
access?: { expiresIn?: string }
refresh?: { expiresIn?: string; rotate?: boolean }
}
interface JWTSessionOptions extends BaseOptions {
session?: { expiresIn?: string }
}
// also: BaseOptions, JWTAlgorithm, JWTKey, RefreshRecord, VerificationRecord,
// SSOProvider, SSOProfile, Events, AccessRefreshTokens, JWTSessionTokens, Strategy
}BaseOptions carries the signing material (secret for HMAC, or privateKey/publicKey for asymmetric), issuer, audience, algorithm and a verification.expiresIn. Events maps lifecycle events (signed-in, signed-out, refreshed, authorized, denied, verified, sso-linked) to their payloads. Strategy is the minimal { actions: { authorize } } shape useAuth accepts.
AuthErrorCode
The tag values auth failures are raised with.
const AuthErrorCode: {
InvalidCredentials: 'invalid-credentials'
InvalidToken: 'invalid-token'
ExpiredToken: 'expired-token'
RevokedToken: 'revoked-token'
MissingToken: 'missing-token'
NotProvided: 'not-provided'
UnknownProvider: 'unknown-provider'
VerificationConsumed: 'verification-consumed'
InvalidDuration: 'invalid-duration'
InvalidState: 'invalid-state'
}Auth actions yield* fail(AuthErrorCode.InvalidCredentials, message) (etc.) instead of throwing, so callers can pattern-match on the tag. NotProvided signals that an optional Provider hook required by the requested operation was not implemented.
CORS
Cross-Origin Resource Sharing headers and preflight.
Cors
Apply CORS response headers and answer preflight `OPTIONS` requests.
// from '@ozaco/server/plugin/cors'
const Cors // a std Plugin; install with CorsDef.Options (no actions)
install(Cors, options?: CorsDef.Options)On install, Cors mounts a catch-all preflight route and registers a Gateway.before({ fromInternal }) hook that writes the resolved CORS headers onto every response and short-circuits OPTIONS requests with preflightStatus. origin accepts '*', true, a string, a string list, a RegExp, or a predicate; set credentials: true to emit Access-Control-Allow-Credentials. methods, allowedHeaders, exposedHeaders and maxAge fill the remaining preflight headers. The plugin exposes no actions — configuration is entirely through install options.
import { install } from '@ozaco/std/plugin'
import { Cors } from '@ozaco/server/plugin/cors'
yield* install(Cors, {
origin: ['https://app.example.com'],
credentials: true,
maxAge: '10m',
})CorsDef
Origin matcher, install options and resolved context for the CORS plugin.
namespace CorsDef {
type Origin =
| '*'
| true
| string
| readonly string[]
| RegExp
| ((origin: string) => boolean)
interface Options {
origin?: CorsDef.Origin
methods?: readonly string[]
allowedHeaders?: readonly string[]
exposedHeaders?: readonly string[]
credentials?: boolean
maxAge?: number | string
preflightStatus?: number
}
interface Context {
origin: CorsDef.Origin
methods: string
allowedHeaders: string
exposedHeaders: string | null
credentials: boolean
maxAge: string
preflightStatus: number
}
interface ResolvedOrigin {
allow: string | null
vary: boolean
}
}Docs
OpenAPI generation and Swagger UI.
Docs
Compile mounted services into an OpenAPI spec and serve it plus Swagger UI.
// from '@ozaco/server/plugin/docs'
const Docs // a std Plugin; install with DocsDef.Options
install(Docs, options?: DocsDef.Options)
Docs.actions.from(...services: Service[]): Future<void>On install, Docs mounts two routes — the OpenAPI JSON at openapi (default /docs/openapi) and Swagger UI at swagger (default /docs/swagger) — and registers a Gateway.after({ start }) hook that logs both URLs once the server is listening (when a Logger is installed and silent is false). Call from(...services) to (re)compile the spec from the given services' action metadata; calling it again replaces the entries for those services and leaves others intact. auth (true or an AuthOptions) adds a security scheme to the document.
import { install } from '@ozaco/std/plugin'
import { Docs } from '@ozaco/server/plugin/docs'
yield* install(Docs, { title: 'My API', version: '1.0.0', auth: true })
yield* Docs.actions.from(UserService, OrderService)
// Swagger UI at /docs/swagger, spec at /docs/openapiDocsDef
Install options, context and the OpenAPI document model.
namespace DocsDef {
interface Options {
title?: string // default 'Docs'
version?: string // default '0.0.0'
description?: string
swagger?: string // default '/docs/swagger'
openapi?: string // default '/docs/openapi'
silent?: boolean // default false
auth?: boolean | AuthOptions
}
interface AuthOptions {
type?: 'bearer' | 'basic' | 'apiKey'
name?: string
in?: 'header' | 'query' | 'cookie'
bearerFormat?: string
description?: string
}
interface Context {
title: string
version: string
description: string
swagger: string
openapi: string
silent?: boolean
auth: AuthOptions | null
}
// plus the OpenAPI model: OpenAPIDocument, OperationObject, ParameterObject,
// JsonSchema, SecurityScheme, CompiledEntry, SwaggerHtmlOptions
}