Endpoints & webhooks
Most browser writes should be mutation() forms: schema input, CSRF, no-JS behavior, replay, guards,
transactions, query truth, and typed failure UI. Use raw ingress when the caller is not a browser
form or when the protocol itself needs raw HTTP control: OAuth callbacks, third-party webhooks,
downloads, health checks, adapter-owned mounts, and externally authenticated machine writes.
CSV/TSV/spreadsheet exports also live here if an app needs them, but they are ordinary app-owned
raw response code, not a Kovo safe-by-default helper lane.
endpoint() for raw HTTP#
An endpoint is registry-visible and receives the raw Request before body parsing:
import { endpoint } from '@kovojs/server';
export const oauthCallback = endpoint('/auth/callback', {
method: 'GET',
reason: 'OAuth provider callback',
auth: { kind: 'none', justification: 'OAuth state parameter validates callback' },
csrf: false,
csrfJustification: 'OAuth provider callback is not a browser form submission',
response: { appOwnedSafety: true, body: 'redirect', cache: 'no-store' },
handler: finishOAuth,
});Exact and prefix mounts are declared. Cookies are not interpreted into ambient req.session, and an
endpoint does not get the mutation lifecycle unless you build it deliberately. A CSRF exemption is
sound here because auth does not ride browser ambient authority; verifier tokens, signatures, OAuth
state, or another explicit scheme authenticate the request. The handler can still parse the raw
request when the protocol needs it.
webhook() for provider POSTs#
Use webhook() for third-party POSTs that write Kovo-owned data:
import { hmacSignature } from '@kovojs/core/webhooks';
import { domain, s } from '@kovojs/server';
import { webhook, webhookReplayIdentity } from '@kovojs/server/webhooks';
const order = domain('order');
declare const providerWebhookReplayStore: any;
declare const applyOrderEvent: any;
export const orderWebhook = webhook('/hooks/order-provider', {
verify: hmacSignature({
encoding: 'hex',
header: 'x-provider-signature',
payload: (request) => request.payload,
secret: process.env.PROVIDER_WEBHOOK_SECRET!,
tolerance: { header: 'x-provider-timestamp', seconds: 5 * 60 },
}),
input: s.object({
id: s.string(),
occurredAtMs: s.number().int(),
type: s.string(),
}),
idempotency: (event) => webhookReplayIdentity(event.id, event.occurredAtMs),
replayStore: providerWebhookReplayStore,
writes: [order],
async handler(event, context) {
await context
.declareSystemWrite('Provider webhook applies provider-confirmed order events')
.runMutation(applyOrderEvent, event);
context.recordChange(order, { keys: [event.id] });
},
});The path is explicit because it is the provider-facing address. The webhook registry identity is
derived from orderWebhook and its module path, so replay and audit names follow the source instead
of duplicating a string.
Replace applyOrderEvent with your app mutation or write helper. A webhook that can write
Kovo-owned data must declare both idempotency and replayStore, then choose an explicit
actAs(...) or declareSystemWrite(...) posture before composing through mutations or managed DB
work.
Use the provider event's own signed occurrence timestamp for occurredAtMs. Do not use Date.now()
or the signature delivery timestamp. Kovo accepts up to five minutes of future clock skew and keeps
replay truth for 30 days from that authenticated occurrence. Stale or farther-future events return
422 before the replay store or handler runs.
The lifecycle is fixed: capture raw bytes, verify, parse/coerce a loose input schema, reserve replay
by the opaque provider event identity, run the handler with tx, commit, emit every
recordChange() as the unified change record, and return the provider-appropriate 2xx. A
redelivered event inside the horizon replays the stored response and does not re-execute the
handler. The writes list is the static audit fact; the recordChange() calls are the runtime
key-level records.
The verifier kit includes generic HMAC and Standard Webhooks helpers. Provider-specific recipes, including Stripe's exact signature format, can live in app/example code on top of those helpers; the audit prints the resolved verifier scheme or a named custom/none justification.
Set response metadata#
Use the named file options for fields that change how a response is interpreted. The direct header bag is only for cache metadata:
import { respond } from '@kovojs/server';
declare const pdfBytes: Uint8Array;
function downloadInvoice() {
return respond.file(pdfBytes, {
contentType: 'application/pdf',
etag: '"invoice-42"',
filename: 'invoice-42.pdf',
headers: {
'Cache-Control': 'private, no-store',
Vary: 'Cookie',
},
});
}respond.file() and respond.stream() accept Cache-Control, Last-Modified, and Vary in
headers. Use contentType, etag, and filename/disposition for the dedicated fields. Return
redirect() for a location. Mutation cookies go through context.setCookie().
Raw endpoint() responses may need other end-to-end integration headers. Return a Web Response
for those. Kovo still rejects Content-Length, Connection, Transfer-Encoding, and the other
adapter-owned framing or hop-by-hop fields before anything reaches the socket. Header controls such
as CR, LF, and NUL also fail closed.
Send CSRF tokens to raw endpoints#
Default-CSRF unsafe endpoints accept the same token carriers Kovo can parse before the handler runs:
form fields and JSON bodies. For the first anonymous page, use mintCsrfField or mintCsrfToken so
the response can also set Kovo's anonymous CSRF cookie. The ordinary browser path is a route plus a
mutation() form, where Kovo owns this lifecycle automatically. A raw bootstrap is an advanced
integration: its endpoint must execute a verifier (or be a framework-owned self-verifying adapter)
before it can emit browser state.
Call the helper inside that verified endpoint while it constructs its Response. When the endpoint
runs through createRequestHandler(), Kovo privately captures a first-anonymous binding cookie and
attaches it during final response reconstruction. Do not copy it into app-owned headers yourself:
an explicit Set-Cookie is still app-authored browser state and is rejected without the endpoint's
browser-state authorization proof.
import { mintCsrfField } from '@kovojs/server/security';
export async function renderUploadForm(request: Request) {
const csrf = mintCsrfField(request, {
...appCsrf,
audience: 'endpoint:/files',
});
return new Response(
`<form method="post" action="/files">${csrf.html}<button>Upload</button></form>`,
);
}Call renderUploadForm(request) only from the verified raw endpoint handler. The public mint
helpers do not accept a mutation target. Route mutation forms use typed
<form mutation={definition}> authoring so Kovo emits CSRF and Kovo-Idem together. The returned
setCookie remains available for
non-Kovo response integrations; explicitly attaching its exact bytes to a managed raw response is
deduplicated, but it still requires the same endpoint browser-state proof and reserved-header
posture as any app-authored Set-Cookie.
For verified JSON bootstraps, send
mintCsrfToken(request, appCsrf, { audience: 'endpoint:/bootstrap' }).token as kovo-csrf; the
managed finalizer delivers a first binding cookie. A detached call can still use a session or an
anonymous cookie already on the request. It cannot create the first anonymous binding: Kovo rejects
that call because there is no active response lifecycle proving the new cookie can reach the
browser. Direct runEndpoint() execution has no managed cookie sink and likewise rejects a first
anonymous mint. If your protocol needs text/plain, bytes, or a signed raw body, use
csrf: false with a verifier, OAuth state, or another non-browser auth scheme and name the
justification. Browser credential forms should stay as mutation() forms so Kovo owns the CSRF
field, no-JS response, replay, and typed failure UI.
Audit the ingress surface#
kovo explain endpoints is the stable security-review table. It lists every declared endpoint and
webhook, every mutation, and every route returning respond.file() or respond.stream():
kovo-explain/v1
ENDPOINTS
ENDPOINT app-shell/order-paid surface=webhook method=POST path=/webhooks/order-paid mount=exact auth=verifier:stripe-signature csrf=exempt:signed stripe webhook cache=no-store body=raw bodySize=- rateLimit=webhook:stripe headers=Stripe-Signature files=- dynamic=- writes=order
ENDPOINT echo surface=endpoint method=POST path=/api/echo-json mount=exact auth=public:public echo endpoint is CSRF checked csrf=checked cache=no-store body=json bodySize=- rateLimit=- headers=- files=- dynamic=- writes=-
ENDPOINT health surface=endpoint method=GET path=/healthz mount=exact auth=none:public uptime probe csrf=safe:read-only cache=no-store body=json bodySize=- rateLimit=- headers=- files=- dynamic=- writes=-
ENDPOINT inventory/download surface=route-file method=GET path=/downloads/inventory.bin mount=exact auth=custom:api-key csrf=safe:read-only cache=private,no-store body=bytes bodySize=stream rateLimit=download:user headers=Content-Disposition,Content-Type files=inventory.bin dynamic=- writes=-
SUMMARY total=4Each row is key/value text: surface, method, path, mount mode, auth scheme, CSRF posture, cache/body
posture, response headers, file fields, dynamic-route posture, and the write-to-domain chain where
one exists. A csrf: false mutation appears here too, and the CSRF/session gate guarantees that it
does not read ambient session authority. If that mutation uses replay storage, declare
machineReplayPrincipal from the post-verification request so one caller cannot replay another
caller's response.
Handle failure#
Keep provider and machine failures explicit:
- Verifier failures return the fixed unauthorized/bad-request response for that verifier before the handler runs.
context.fail(code, payload, { status, retryAfter })is the expected provider-facing failure path when the request is valid but the event cannot be accepted.- Handler throws roll back the transaction and do not publish
recordChange()output. - Replay conflicts reuse the stored response for the same provider event id instead of running the handler twice.
That posture keeps "bad signature", "valid but rejected event", and "operational failure" separate in logs and provider retries.
Next#
- Security & authorization — CSRF, guards, and audit posture.
- Request shell — where endpoints sit in dispatch order.
- Reading kovo check & kovo explain — keeping endpoint audits in CI.
- Server API reference — generated reference for
endpoint(),webhook(), and response helpers.
Spec & diagnostics
Raw endpoint(), webhook(), verifier kits, no ambient session, CSRF exemptions, response header
channel, typed cookie builder, and KV415: SPEC §9.1. csrf: false mutation restrictions and KV418:
SPEC §6.6. The kovo explain endpoints audit and printed row vocabulary: SPEC §11.4.
API reference: @kovojs/core, @kovojs/server.