Composable middleware, guards, and form utilities for SvelteKit
Wrap actions, loads, methods, and hooks with composable enhancers. Stack auth guards, feature flags, request tracing, and form parsing without touching SvelteKit's internals.
npm install @sourceregistry/sveltekit-enhance
Peer dependency: @sveltejs/kit ^2.58.0
import { enhance, Auth, RequestCorrelation, RequestMonitor, Form } from '@sourceregistry/sveltekit-enhance';
// hooks.server.ts
export const handle = enhance.handle(
async ({ event, resolve }) => resolve(event),
RequestCorrelation.attach,
RequestMonitor.trace({ logger: myLogger, record: metrics.record }),
);
// +server.ts
export const POST = enhance.method(
async (event) => new Response(JSON.stringify(event.context)),
Auth.Bearer,
FeatureFlag.all('PUBLIC_API_ENABLED'),
);
// +page.server.ts
export const actions = {
default: enhance.action(
async (event) => {
const name = event.context.form.string$('name');
return success({ name });
},
Form.schema(myValidator),
),
};
Import from @sourceregistry/sveltekit-enhance.
enhance.handleWraps SvelteKit's handle hook. Enhancers run left-to-right before the handler; their return values are merged into context.
import { enhance } from '@sourceregistry/sveltekit-enhance';
// src/hooks.server.ts
export const handle = enhance.handle(
async ({ event, resolve, context }) => resolve(event),
enhancerA,
enhancerB,
);
enhance.handle automatically detects Server-Sent Events and other streaming requests (via Accept: text/event-stream) and bypasses the main handler entirely — going straight to SvelteKit's resolve. This prevents the handler from blocking indefinitely on await resolve(event), which never settles for a streaming response.
Consequences:
responseHandlers (added by enhancers via contextInput.responseHandlers.push(...)) are also skipped for responses with Content-Type: text/event-stream.// This handler is skipped for SSE requests — no change needed on your end.
export const handle = enhance.handle(
async ({ event, resolve }) => {
const response = await resolve(event); // not called for SSE
response.headers.set('x-custom', 'value');
return response;
},
Auth.Bearer, // still runs — can reject unauthorized SSE requests
SessionGuard.hook, // still runs
);
If you need to set response headers on SSE routes, do it in the route handler itself (e.g. +server.ts) rather than in the enhance.handle main handler.
enhance.loadWraps server load functions.
// +page.server.ts
export const load = enhance.load(
async (event) => ({ user: event.context.user }),
Auth.Bearer,
);
enhance.actionWraps form actions.
// +page.server.ts
export const actions = {
submit: enhance.action(
async (event) => success(event.context),
Auth.Bearer,
Form.schema(myValidator),
),
};
enhance.methodWraps +server.ts endpoint handlers.
// +server.ts
export const GET = enhance.method(
async (event) => new Response(JSON.stringify(event.context)),
Auth.Bearer,
);
import { fail, error, success, not_good } from '@sourceregistry/sveltekit-enhance';
fail(400, { message: 'bad input' }); // throws ActionFailure — use inside actions
error(404, { message: 'not found' }); // throws HttpError
success({ id: 1 }); // typed identity helper
not_good(input, 403); // delegates to fail or error based on callType
All helpers are available from @sourceregistry/sveltekit-enhance or @sourceregistry/sveltekit-enhance/helpers.
CSRFBlocks cross-site form submissions on mutating methods (POST, PUT, PATCH, DELETE) with form content types. Checks the Origin header against the request origin. Absent Origin (server-side fetch, curl) is allowed through. Returns 403 — JSON body when Accept: application/json, SvelteKit error() otherwise.
import { CSRF, CSRFChecker } from '@sourceregistry/sveltekit-enhance';
export const handle = enhance.handle(
myHandler,
CSRF.inspect(
CSRFChecker.list('/api/webhooks/stripe'), // bypass paths
myLogger, // optional, defaults to console
),
);
Built-in bypass checkers:
| Checker | Description |
|---|---|
CSRFChecker.list(...paths) |
Exact pathname match |
CSRFChecker.regex(...patterns) |
RegExp match against pathname |
Custom checker — any (input: EnhanceInput) => MaybePromise<boolean>:
// true = bypass CSRF check
CSRF.inspect((input) => input.url.pathname.startsWith('/api/public'))
Returns { csrf_valid: true } on pass. Locals set: none.
AuthExtracts and validates Authorization: Bearer <token> headers.
import { Auth } from '@sourceregistry/sveltekit-enhance';
export const GET = enhance.method(
async (event) => new Response(event.context.token),
Auth.Bearer,
);
Returns { token: string }. Throws 401 if the header is missing or malformed.
DevtoolsSilences Chrome DevTools probe requests (/.well-known/appspecific/com.chrome.devtools.json) with a 204 No Content. Logs in dev mode.
import { Devtools } from '@sourceregistry/sveltekit-enhance';
export const handle = enhance.handle(myHandler, Devtools.ignore);
FeatureFlagGuards routes behind SvelteKit public env vars ($env/dynamic/public). Always passes in dev mode.
import { FeatureFlag } from '@sourceregistry/sveltekit-enhance';
// All listed flags must be enabled
FeatureFlag.all('PUBLIC_FEATURE_A', 'PUBLIC_FEATURE_B')
// At least one flag must be enabled
FeatureFlag.oneOf('PUBLIC_FEATURE_A', 'PUBLIC_FEATURE_B')
Truthy values: true, TRUE, on, ON, 1. Returns { flags } or throws 503 Feature not enabled.
RequestCorrelationPropagates a correlation ID across the request/response cycle.
x-correlation-id or x-request-id from incoming headers[A-Za-z0-9._:-]+x-correlation-id response headerimport { RequestCorrelation } from '@sourceregistry/sveltekit-enhance';
export const handle = enhance.handle(myHandler, RequestCorrelation.attach);
| Local | Type | Description |
|---|---|---|
correlation_id |
string |
Resolved correlation ID |
request_started_at |
number |
Date.now() at attach time |
RequestMonitorStructured HTTP request logging and optional metrics collection. Instruments the full lifecycle: start, completion (log level by status), and unhandled errors — all with elapsed duration.
import { RequestMonitor } from '@sourceregistry/sveltekit-enhance';
export const handle = enhance.handle(
myHandler,
RequestCorrelation.attach,
RequestMonitor.trace({
logger: myLogger, // optional, defaults to console
record: metrics.record, // optional
}),
);
TraceOptions| Option | Type | Default | Description |
|---|---|---|---|
logger |
TraceLogger |
console |
Must implement debug, info, warn, error |
record |
(entry: RecordTraceMetricEntry) => any |
— | Called after every request |
| Event | Level | Condition |
|---|---|---|
http.request.started |
debug |
Before resolve |
http.request.completed |
info |
status < 400 |
http.request.completed |
warn |
status 4xx |
http.request.completed |
error |
status 5xx |
http.request.failed |
error |
Unhandled throw |
type RecordTraceMetricEntry = { method: string; path: string; status: number; durationMs: number }
type TraceLogger = { debug(...args: any[]): any; info(...args: any[]): any; warn(...args: any[]): any; error(...args: any[]): any }
Locals set: trace: { id: string; started_at: bigint }.
FormTyped, ergonomic FormData extraction. Works standalone or as an enhancer.
Form.schemaValidates and deserializes form data via a Validator<T> before the handler runs.
import { Form, enhance, success } from '@sourceregistry/sveltekit-enhance';
export const actions = {
default: enhance.action(
async (event) => success(event.context.form.result),
Form.schema(myValidator),
),
};
Form.handleimport { Form } from '@sourceregistry/sveltekit-enhance';
await Form.handle(request, ({ form }) => {
const name = form.string$('name');
const age = form.number('age');
return { name, age };
});
Optional variants return undefined when the field is absent. Required variants ($ suffix) throw fail(400).
| Method | Returns | Notes |
|---|---|---|
string(name) / string$(name) |
string | null | undefined |
|
pattern$(name, pattern) |
string |
RegExp or pattern string |
number(name) / number$(name) |
number | undefined |
|
boolean(name) / boolean$(name) |
boolean | undefined |
Accepts true/false, 1/0, on/off |
date(name, parser?) / date$(name, parser) |
Date | undefined |
Custom parser supported |
json<T>(name, transformer?) / json$(name) |
T | undefined |
Optional transform fn |
jsond(options) |
any |
All FormData → nested object via dot-notation keys |
file(name) / file$(name) |
File | null | undefined |
|
files(name) |
File[] |
Non-empty, named files only |
fileRecord(prefix, removePrefix?) |
Record<string, File[]> |
Groups files by key prefix |
array<T>(name, mapper?) / array$(name) |
T[] | undefined |
|
enum(name, Enum) / enum$(name, Enum) |
keyof E | undefined |
|
record(options?) |
Record<string, any> |
All entries; optional filter / transformer |
validate(schema, options?) |
T |
Runs Validator<T>, throws fail(400) on failure |
form.onlyIf(condition, trueVal, falseVal?)
form.onlyIfPresent(key, (entry) => ..., fallback?)
form.onlyIfArrayPresent(key, (entries) => ..., fallback)
form.selector({ fieldName: (entry, key) => ..., $default?, $error? })
form.selector$({ ... }) // throws fail(400) if no case matches
form.basedOn(val, processor)
form.process(name, parser, processor)
All FormContext methods are also exported as standalone functions taking FormData as the first argument, plus:
arrayString(formdata, name, delimiter, mapper?)
hasOneOf(formdata, names)
reviver(key, value) // JSON.parse reviver — coerces strings to typed primitives
// Core
EnhanceInput<CallType> // event passed to all enhancers
EnhanceFunction<CallType> // (event: EnhanceInput) => MaybePromise<object | Response>
EnhanceHandle // final handle fn — ({ event, resolve, context }) => MaybePromise<Response>
EnhanceLoad // final load fn
EnhanceAction // final action fn
EnhanceMethod // final method fn
MaybePromise<T> // T | Promise<T>
// Form
Validator<T> // (value: unknown, path?: ValidationPath) => ValidationResult<T>
ValidationResult<T> // { success: true; data: T } | { success: false; errors: ValidationIssue[] }
ValidationIssue // { path: string; message: string; code?: string }
FormContext // fluent API returned by Form.enhance / Form.handle
InferValidator<V> // infers T from Validator<T>
// Helpers
TraceLogger // { debug, info, warn, error }
TraceOptions // { logger?: TraceLogger; record?: (entry) => any }
RecordTraceMetricEntry // { method: string; path: string; status: number; durationMs: number }
RequestTraceLocals // { trace?: { id: string; started_at: bigint } }
RequestCorrelationLocals // { correlation_id?: string; request_started_at?: number }
CSRFChecker // { regex(...patterns): checker; list(...paths): checker }