Middleware

Dawn supports a single global request middleware for authentication, request shaping, and per-request context. The middleware runs once per request, before any route executes, and its decision (allow or reject) gates execution.

File location

Define middleware as a default-exported function in src/middleware.ts (or middleware.ts at the app root):

src/middleware.ts
import { allow, defineMiddleware, reject } from "@dawn-ai/sdk"
 
export default defineMiddleware(async (req) => {
  if (!req.headers["x-api-key"]) {
    return reject(401, { error: "Missing x-api-key" })
  }
  return allow()
})

If src/middleware.ts is absent, every request is allowed.

API

defineMiddleware(fn)

Identity helper that types fn as DawnMiddleware. Use it for editor inference:

ts
type DawnMiddleware = (
  req: MiddlewareRequest,
) => Promise<MiddlewareResult> | MiddlewareResult

MiddlewareRequest

The argument every middleware receives. All values are pre-parsed:

FieldShapeNotes
headersReadonly<Record<string, string>>Lowercase keys (Node convention). Multi-value headers joined with , .
paramsReadonly<Record<string, string>>Dynamic-segment values extracted from the request input, e.g. { tenant: "acme" } for /hello/[tenant].
routeIdstringE.g. "/hello/[tenant]".
assistantIdstringE.g. "/hello/[tenant]#agent".
methodstringHTTP method.
urlstringPath + query, e.g. "/runs/wait".

reject(status, body?)

Stops execution and responds with the given HTTP status. Optional body is JSON-encoded into the response.

ts
return reject(401, { error: "Unauthorized" })
return reject(403)  // body omitted

allow(context?)

Lets the request proceed. Optional context is a record of arbitrary values that flows into every tool invocation as ctx.middleware. See Context flow to tools below.

ts
return allow()
return allow({ userId, plan: "pro" })

Context flow to tools

Whatever you pass to allow({ ... }) is delivered to every tool call for that request via the second argument:

export default defineMiddleware(async (req) => {
  const userId = await verifyJwt(req.headers.authorization)
  return allow({ userId })
})

Context is per-request — there is no shared state between requests. The middleware field is undefined if no middleware is defined, or if the middleware called allow() without arguments.

Single-function model

Dawn middleware is intentionally a single global function, not an array of layered middlewares. If you need branching by route, do it with normal control flow inside the function:

ts
export default defineMiddleware(async (req) => {
  if (req.routeId.startsWith("/admin/")) {
    return await requireAdmin(req)
  }
  return await requireApiKey(req)
})

This keeps the request lifecycle predictable and avoids ordering questions about per-route middleware.

Where middleware runs

Middleware runs in the local dev server (dawn dev). Both local /runs/wait and /runs/stream requests invoke middleware before any route executes. The /healthz endpoint bypasses middleware because it is a liveness probe.

dawn build does not currently materialize this middleware into the generated LangSmith entry files. Treat middleware as a local Dawn runtime feature unless your deployment target wires it in separately.

Errors thrown from middleware

If the middleware function throws, the request is rejected with HTTP 500. Prefer explicit reject(status, body?) over throwing — it gives users a meaningful response.

Related