Skip to content

Search docs

Jump between documentation pages.

Browse docs

API reference: Feature modules

The per-feature subpath modules: OpenAPI generation, the typed client, contract tests, MCP, docs UIs, streaming, multipart, WebSocket primitives, tracing, metrics stores, and the CLI internals. Each module also re-exports from the root barrel unless noted. See the API reference overview for the complete module map.

One contract, many outputs
single source of truthApp route contractsmethod + path + request + responses
/openapiOpenAPI 3.1 documentgenerateOpenAPI(app, opts)
/client · /contractTyped client & contract testscreateClient(app), runContractTests(app)
/docs · /mcp · /a2aDocs UIs, MCP tools & A2A agentsscalarHtml(), createMcpHandler(), createA2aHandler()
Every module below reads the same route contracts your handlers are typed against, so the spec, client, docs, and MCP surface can never drift from the code.

@daloyjs/core/openapi

ts
generateOpenAPI(app: App, opts: OpenAPIOptions): Record<string, unknown>;
openapiToYAML(doc: Record<string, unknown>): string;

interface OpenAPIOptions {
  info: OpenAPIInfo;
  servers?: { url: string; description?: string }[];
  securitySchemes?: SecuritySchemeMap;
  webhooks?: Record<string, WebhookDefinition | WebhookDefinition[]>;
}

interface OpenAPIInfo {
  title: string;
  version: string;
  description?: string;
  termsOfService?: string;
  contact?: { name?: string; email?: string; url?: string };
  license?: { name: string; identifier?: string; url?: string };
  summary?: string;
}

// OpenAPI 3.1 top-level webhooks. Mirrors RouteDefinition minus path + handler.
interface WebhookDefinition {
  method: HttpMethod;
  operationId?: string;
  summary?: string;
  description?: string;
  tags?: string[];
  deprecated?: boolean;
  request?: RequestSchemas;
  responses: ResponsesMap;
  auth?: AuthSpec;
}

Discriminated unions (OpenAPI)

ts
discriminator(opts: DiscriminatorObject): unknown;            // { propertyName, mapping? }
discriminatedUnion(prop: string, branches: StandardSchemaV1[],
                   opts?: DiscriminatedUnionOptions): StandardSchemaV1;

@daloyjs/core/client

ts
createClient<A extends App>(app: A, opts: ClientOptions): ClientFor<A>;
createInProcessClient<A extends App>(app: A, opts?: InProcessClientOptions): ClientFor<A>;

interface ClientOptions {
  baseUrl: string;
  fetch?: typeof fetch;
  headers?: Record<string, string>;
}

interface InProcessClientOptions {
  baseUrl?: string; // default: http://daloy.local
  headers?: Record<string, string>;
}

// ClientFor<A> is keyed by operationId; each method takes
// { params?, query?, headers?, body? } and returns a discriminated union
// keyed by status: { status, body, headers }.
type ClientFor<A extends App>  = { /* generated from A["routes"] */ };
type RoutesOf<A extends App>   = A["routes"][number];

// Path params are filled by whole segment and percent-encoded. A missing,
// empty, "." or ".." param throws TypeError before any request is sent.
// Wildcard routes ("*name") accept "/"-separated values, each segment checked.

@daloyjs/core/contract

ts
runContractTests(app: App, opts?: ContractTestOptions): Promise<ContractReport>;

interface ContractTestOptions {
  requireOperationId?: boolean;     // default: true
  allowBodyOnSafeMethods?: boolean; // default: false
}

interface ContractReport { ok: boolean; checked: number; issues: ContractIssue[] }
interface ContractIssue  { route: string; method: HttpMethod; code: string; message: string }

@daloyjs/core/mcp

ts
const MCP_PROTOCOL_VERSION = "2026-07-28";
const MCP_MODERN_ERA_MIN_VERSION = "2026-07-28";
const MCP_PROTOCOL_VERSIONS: readonly string[];  // 2024-11-05 … 2026-07-28
const MCP_DEFAULT_MAX_BODY_BYTES = 262144;
const MCP_MAX_REQUEST_STATE_LENGTH = 8192;
const MCP_META_KEYS: {
  protocolVersion: "io.modelcontextprotocol/protocolVersion";
  clientInfo: "io.modelcontextprotocol/clientInfo";
  clientCapabilities: "io.modelcontextprotocol/clientCapabilities";
  logLevel: "io.modelcontextprotocol/logLevel";
  serverInfo: "io.modelcontextprotocol/serverInfo";
};
const MCP_ERROR_CODES: {
  headerMismatch: -32020;
  missingRequiredClientCapability: -32021;
  unsupportedProtocolVersion: -32022;
};

createMcpHandler(options: McpHandlerOptions): McpHandler;
mcpRoutes(path: PathString, handler: McpHandler, options?: McpRoutesOptions):
  RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
validateMcpInput(schema: McpJsonSchema, value: unknown): string[];
isModernProtocolVersion(version: string): boolean;
class McpToolError extends Error {}

type McpHandler = (request: Request) => Promise<Response>;
type McpJsonValue = null | boolean | number | string | McpJsonValue[] | { [key: string]: McpJsonValue };
type McpJsonObject = { [key: string]: McpJsonValue };
type McpJsonSchema = McpJsonObject;
type McpJsonRpcId = string | number | null;
type McpProtocolEra = "modern" | "legacy";

interface McpRoutesOptions {
  public?: boolean;  // default false; opt out of the production auth boot guard
  hooks?: Hooks;  // since 1.7.0; applied to the POST transport only
  methodAuth?: McpMethodAuthOptions;  // since 1.8.0; not with public, or with auth in hooks
}

const MCP_DISCOVERY_METHODS: readonly [
  "server/discover", "tools/list", "resources/list",
  "resources/templates/list", "prompts/list",
];
type McpDiscoveryMethod = (typeof MCP_DISCOVERY_METHODS)[number];

interface McpMethodAuthOptions {
  call: Hooks;                       // auth hook for everything else (incl. no Mcp-Method header)
  discovery: Hooks | "public";       // auth hook for discovery methods, or no credential
  discoveryMethods?: readonly McpDiscoveryMethod[];  // default: all of MCP_DISCOVERY_METHODS
}

interface McpCacheHints {
  ttlMs?: number;                 // default 0 (revalidate every call)
  scope?: "public" | "private";   // default "private"
}

interface McpHandlerOptions {
  serverInfo: McpServerInfo;
  instructions?: string;
  tools?: readonly McpTool[];
  resources?: readonly McpResourceDefinition[];
  resourceTemplates?: readonly McpResourceTemplateDefinition[];
  prompts?: readonly McpPromptDefinition[];
  extensions?: Record<string, McpJsonObject>;  // capabilities.extensions
  cache?: McpCacheHints;                    // ttlMs / cacheScope on cacheable results
  allowedOrigins?: readonly string[];       // bare origins, plus optional "null"
  protocolVersions?: readonly string[];     // default MCP_PROTOCOL_VERSIONS
  preferredProtocolVersion?: string;        // default MCP_PROTOCOL_VERSION
  maxBodyBytes?: number;                    // default MCP_DEFAULT_MAX_BODY_BYTES
  headers?: Record<string, string>;
  exposeInternalErrors?: boolean;           // default: on only when NODE_ENV is "development" or "test"
  maxResourceUriLength?: number;            // default 8192; longer resources/read URIs get -32602
}
// exposeInternalErrors fails closed: when omitted, mcpRoutes() on an App whose
// env is "production" always redacts. An explicit value wins.

interface McpImplementation { name: string; version: string; title?: string }

interface McpInputRequest {
  method: "elicitation/create" | "sampling/createMessage" | "roots/list";
  params?: McpJsonObject;
}
type McpInputRequests = { [id: string]: McpInputRequest };
type McpInputResponses = { [id: string]: McpJsonValue };

interface McpInputRequiredResult {
  resultType: "input_required";
  inputRequests?: McpInputRequests;
  requestState?: string;
}

interface McpServerInfo {
  name: string;
  version: string;
  title?: string;
  description?: string;
  websiteUrl?: string;
  icons?: McpIcon[];
}

interface McpTool<TArgs extends Record<string, unknown> = Record<string, unknown>> {
  name: string;
  description: string;
  title?: string;
  inputSchema: McpJsonSchema;
  outputSchema?: McpJsonSchema;
  annotations?: McpToolAnnotations;
  icons?: McpIcon[];
  handler: McpToolHandler<TArgs>;
}

type McpToolHandler<TArgs extends Record<string, unknown> = Record<string, unknown>> =
  (args: TArgs, ctx: McpRequestContext) =>
    | string | McpToolResult | McpInputRequiredResult
    | Promise<string | McpToolResult | McpInputRequiredResult>;

interface McpToolResult {
  content?: McpContent[];
  structuredContent?: McpJsonObject;
  isError?: boolean;
}

interface McpRequestContext {
  request: Request;
  protocolVersion: string;
  era: McpProtocolEra;
  id: McpJsonRpcId;
  method: string;
  clientCapabilities: McpJsonObject;   // {} on legacy requests
  clientInfo?: McpImplementation;      // advisory only, never a security input
  logLevel?: string;
  inputResponses?: McpInputResponses;  // present on a multi round-trip retry
  requestState?: string;               // attacker-controlled; verify before trusting
}

// One endpoint, both protocol eras. Modern (2026-07-28+) requests carry version,
// identity, and capabilities in params._meta and must mirror them into the
// MCP-Protocol-Version / Mcp-Method / Mcp-Name headers; a missing or mismatched
// header is rejected with 400 and -32020. Legacy (2025-11-25 and earlier) clients
// keep the initialize handshake unchanged.
// tools/call arguments are validated against inputSchema before the handler runs.
// Schema violations return JSON-RPC -32602. Unsupported keywords such as
// pattern, format, $ref, anyOf, oneOf, and allOf are advertised but not enforced.
// In production secureDefaults apps, mcpRoutes() must be covered by an auth hook
// unless mounted with mcpRoutes(path, handler, { public: true }).

@daloyjs/core/a2a

ts
const A2A_PROTOCOL_VERSION = "1.0";
const A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
const A2A_DEFAULT_MAX_BODY_BYTES = 262144;
const A2A_ERROR_CODES: {
  parseError: -32700; invalidRequest: -32600; methodNotFound: -32601;
  invalidParams: -32602; internalError: -32603;
  taskNotFound: -32001; taskNotCancelable: -32002;
  pushNotificationNotSupported: -32003; unsupportedOperation: -32004;
  contentTypeNotSupported: -32005; invalidAgentResponse: -32006;
  extendedAgentCardNotConfigured: -32007; extensionSupportRequired: -32008;
  versionNotSupported: -32009;
};

createA2aHandler(options: A2aHandlerOptions): A2aHandler;
a2aRoutes(path: PathString, handler: A2aHandler, options?: A2aRoutesOptions):
  RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
memoryTaskStore(options?: { maxTasks?: number; ttlMs?: number }): A2aTaskStore;
a2aText(text: string, mediaType?: string): A2aPart;
a2aData(data: unknown, mediaType?: string): A2aPart;
class A2aError extends Error { readonly code: number }  // -32602 or -32001..-32099

// Calling other agents (since 1.5.0)
createA2aClient(options: A2aClientOptions): A2aClient;
interface A2aClientOptions {
  url: string;                      // agent base URL or full card URL; https: unless loopback
  fetch?: typeof fetch;             // default fetchGuard() (SSRF-guarded)
  headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
  allowedOrigins?: readonly string[]; // extra origins the card's endpoint may use (default: card origin)
  allowInsecureHttp?: boolean;      // default false
  timeoutMs?: number;               // default 10_000
  maxResponseBytes?: number;        // default 1 MiB
  cardMaxAgeMs?: number;            // default 300_000
  extensions?: readonly string[];   // A2A-Extensions
  propagateTrace?: boolean | ((headers: Headers, request?: Request) => void); // default off
}
interface A2aClient {
  getAgentCard(opts?): Promise<A2aAgentCard>;
  sendMessage(message: string | A2aPart[] | { parts, taskId?, contextId?, ... }, opts?): Promise<{ message } | { task }>;
  getTask(id, opts?): Promise<A2aTask>;
  cancelTask(id, opts?): Promise<A2aTask>;
  listTasks(query?, opts?): Promise<A2aListTasksResult>;
}
class A2aClientError extends Error { code: number; data?: unknown; status?: number }  // code 0 = client-side refusal

interface A2aHandler {
  readonly agentCard: Readonly<A2aAgentCard>;
  handleCard(request: Request): Promise<Response>;
  handleRpc(request: Request, host?: { state: Record<string, unknown> }): Promise<Response>;
}

interface A2aHandlerOptions {
  card: A2aAgentCardInput;        // name, description, version, url, skills, securitySchemes, ...
  onMessage(ctx: A2aMessageContext): A2aReply | Promise<A2aReply>;
  taskStore?: A2aTaskStore;       // enables GetTask / CancelTask / ListTasks / multi-turn
  taskOwner?(host: { request: Request; state: Record<string, unknown> }): string | undefined | null;
  allowedOrigins?: readonly string[];
  maxBodyBytes?: number;          // default 256 KiB
  maxHistory?: number;            // default 50
  cardMaxAgeSeconds?: number;     // default 300
  exposeInternalErrors?: boolean; // fails closed; forced off on a production App
  headers?: Record<string, string>;
}

type A2aReply =
  | string
  | { reply: string | readonly A2aPart[]; metadata?: Record<string, unknown> }
  | { status: "completed" | "failed" | "rejected" | "input-required" | "auth-required";
      message?: string | readonly A2aPart[]; artifacts?: readonly A2aArtifactInput[];
      metadata?: Record<string, unknown> };

interface A2aRoutesOptions {
  public?: boolean;               // opt out of the production auth boot guard
  hooks?: Hooks;                  // applied to the JSON-RPC POST only
  cardPath?: PathString | false;  // default A2A_AGENT_CARD_PATH
}

// Capabilities are derived: streaming, pushNotifications, and extendedAgentCard
// are false and their methods return -32004 / -32003. A task store requires
// taskOwner; an unresolved owner is refused with 401. In production
// secureDefaults apps, a2aRoutes() must be covered by an auth hook unless
// mounted with { public: true }, and the card url must be https: (non-loopback).

@daloyjs/core/docs

ts
scalarHtml(opts: ScalarHtmlOptions): string;
swaggerUiHtml(opts: SwaggerUiHtmlOptions): string;
redocHtml(opts: RedocHtmlOptions): string;
docsContentSecurityPolicy(opts?: DocsContentSecurityPolicyOptions): string;
htmlResponse(html: string, opts?: HtmlResponseOptions): Response;

interface DocsOptions { specUrl: string; title?: string; assets?: DocsAssetOptions; scriptNonce?: string }
interface ScalarHtmlOptions extends DocsOptions { configuration?: ScalarReferenceConfiguration }
interface SwaggerUiHtmlOptions extends DocsOptions { configuration?: SwaggerUiConfiguration }
interface RedocHtmlOptions  extends DocsOptions { configuration?: RedocConfiguration }

interface DocsAssetOptions {
  // version-pinned URL + matching SRI hash per asset (see /docs/docs-asset-integrity)
  scalarScriptUrl?: string;       scalarScriptIntegrity?: string;
  swaggerUiCssUrl?: string;       swaggerUiCssIntegrity?: string;
  swaggerUiBundleUrl?: string;    swaggerUiBundleIntegrity?: string;
  redocScriptUrl?: string;        redocScriptIntegrity?: string;
  asyncapiScriptUrl?: string;     asyncapiScriptIntegrity?: string;
  asyncapiStyleUrl?: string;      asyncapiStyleIntegrity?: string;
  crossOrigin?: "anonymous" | "use-credentials";  // default: "anonymous"
}

// RedocConfiguration is an index-signature bag of JSON-serializable Redoc
// standalone options (disableSearch, hideDownloadButtons, sortPropsAlphabetically,
// theme, ...), forwarded verbatim to Redoc.init(specUrl, configuration, element).

interface DocsContentSecurityPolicyOptions {
  assetOrigins?: readonly string[];
  connectOrigins?: readonly string[];
  scriptNonce?: string;
  allowInlineStyles?: boolean;
  allowBlobWorkers?: boolean;     // append worker-src 'self' blob: (Redoc needs it)
}

interface HtmlResponseOptions extends DocsContentSecurityPolicyOptions {
  contentSecurityPolicy?: string;
}

@daloyjs/core/streaming

ts
interface SSEMessage { data?: unknown; event?: string; id?: string; retry?: number; comment?: string }

sseStream  (source, opts?: SSEStreamOptions):   ReadableStream<Uint8Array>;
sseResponse(source, opts?: SSEResponseOptions): Response;
ndjsonStream  (source, opts?: StreamOptions):       ReadableStream<Uint8Array>;
ndjsonResponse(source, opts?: NDJSONResponseOptions): Response;

interface StreamOptions       { signal?: AbortSignal }
interface SSEStreamOptions    extends StreamOptions { keepAliveMs?: number }
interface SSEResponseOptions  extends SSEStreamOptions { status?: number; headers?: HeadersInit }
interface NDJSONResponseOptions extends StreamOptions { status?: number; headers?: HeadersInit }

// data and comment split on CRLF, lone CR, and lone LF, so a stray "\r" cannot
// forge a field. CR/LF runs in event (and CR/LF/NUL in id) become a space.
// The Node adapter cancels a streamed body when the client disconnects.

@daloyjs/core/multipart

ts
fileField(opts?: FileFieldOptions): FileFieldSchema<UploadedFile>;
multipartObject<S>(shape: S, opts?: MultipartObjectOptions): StandardSchemaV1;
isFileFieldSchema(value: unknown): boolean;
isMultipartObjectSchema(value: unknown): boolean;

type UploadedFile = Blob & { readonly name?: string };

interface FileFieldOptions {
  maxBytes?: number;
  accept?: string | readonly string[];      // MIME or extension allowlist
  filename?: { maxLength?: number; pattern?: RegExp };
  magicBytes?: FileMagicBytesOption;        // refuses content-type spoofing
  optional?: boolean;
  format?: string;
}

interface MultipartObjectOptions { strict?: boolean }  // refuses unknown fields by default

@daloyjs/core/websocket

ts
defineWebSocket<P, S, TData>(handler: WebSocketHandler<P, S, TData>): WebSocketHandler<P, S, TData>;

interface WebSocketHandler<P, S = AppState, TData = unknown> {
  beforeUpgrade?: (ctx: WebSocketContext<P, S>) => void | Response | Promise<void | Response>;
  open?:    (conn: WebSocketConnection<TData>, ctx) => void | Promise<void>;
  message?: (conn, msg: MessageEvent, ctx) => void | Promise<void>;
  close?:   (conn, code: number, reason: string, ctx) => void | Promise<void>;
  error?:   (conn, err: Error, ctx) => void | Promise<void>;
  // limits
  maxPayloadLength?:       number;   // default: 1 MiB
  backpressureLimit?:      number;   // default: 1 MiB
  idleTimeoutSeconds?:     number;   // default: 120
  allowedSubprotocols?:    readonly string[];
  origin?:                 string | readonly string[] | ((origin) => boolean);
}

wsRateLimit(opts: { windowMs; max; groupId?; keyGenerator?; store? }): WebSocketBeforeUpgrade;
  // store / keyGenerator failures are rethrown and answered with a generic 500,
  // never echoed to the client
normalizeWebSocketOptions(handler, ctx): NormalizedWebSocketOptions;

// Constants
WS_GUID; WS_READY_STATE; WS_OPCODE; WS_CLOSE_CODE; WS_MAX_CONTROL_PAYLOAD;
DEFAULT_WS_BACKPRESSURE_LIMIT;      // 1 MiB
DEFAULT_WS_MAX_PAYLOAD_LENGTH;      // 1 MiB
DEFAULT_WS_MAX_MESSAGE_FRAGMENTS;   // 4096 data frames per message; more closes with 1002 (since 1.3.7)
DEFAULT_WS_IDLE_TIMEOUT_SECONDS;    // 120

// Frame primitives (for custom adapters)
parseSubprotocols(header: string | null | undefined): string[];
validateSelectedSubprotocol(selected, allowed): boolean;
checkWebSocketOrigin(origin, allowed): boolean;
parseFrame(buf: Uint8Array, opts?): ParsedFrame | typeof FRAME_INCOMPLETE;
encodeFrame(opts): Uint8Array;
encodeClosePayload(code: number, reason?: string): Uint8Array;
decodeClosePayload(payload: Uint8Array): { code: number; reason: string };
encodeSendPayload(data: string | ArrayBufferLike | ArrayBufferView): Uint8Array;
computeAcceptKey(secWebSocketKey: string): string;

class WebSocketRegistry {}
class WebSocketProtocolError extends Error {}
class WebSocketPayloadTooLargeError extends WebSocketProtocolError {}
class FrameSink { /* event emitter over an async byte stream */ }

@daloyjs/core/tracing

ts
otelTracing(opts: OtelTracingOptions): Hooks;   // BYO @opentelemetry/api tracer

interface OtelTracingOptions {
  tracer: TracingTracer;
  serviceName?: string;
  includeRequestHeaders?: readonly string[];
  includeResponseHeaders?: readonly string[];
  recordExceptions?: boolean;
  redactQuery?: false | ((query: string) => string | undefined);
    // default: url.query redacted via sanitizeUrlQueryForLog(); false records it raw (unsafe) (since 1.3.7)
}

const TRACING_SPAN_KIND_SERVER:   number;
const TRACING_SPAN_STATUS_UNSET:  number;
const TRACING_SPAN_STATUS_OK:     number;
const TRACING_SPAN_STATUS_ERROR:  number;

@daloyjs/core/rate-limit-redis

ts
redisRateLimitStore(opts: RedisRateLimitStoreOptions): RateLimitStore;
redisAutoBanStore(opts: RedisAutoBanStoreOptions): AutoBanStore;  // atomic strike() for autoBan() (since 1.3.7)
ioredisAdapter (client: IoredisLike):  RedisCommands;
nodeRedisAdapter(client: NodeRedisLike): RedisCommands;

interface RedisRateLimitStoreOptions {
  client:  RedisCommands;
  prefix?: string;             // default: "daloy:rl:"
  scriptCacheKey?: string;
}
interface RedisAutoBanStoreOptions {
  client:  RedisCommands;
  prefix?: string;             // default: "daloy:ab:"
}
interface RedisCommands {
  evalsha?: (...) => Promise<unknown>;
  eval?:    (...) => Promise<unknown>;
  // ... narrow subset; adapters provided for ioredis + node-redis.
}

@daloyjs/core/banner

ts
formatStartupBanner(opts: StartupBannerOptions): string;
printStartupBanner (opts: StartupBannerOptions): void;

@daloyjs/core/cli

ts
// Internals used by bin/daloy.mjs. Most users will not import this directly,
// but the surface is public-typed so wrappers can compose it.
type DevRuntime = "node" | "bun" | "deno";
detectRuntime(): DevRuntime;
buildDevCommand(runtime: DevRuntime, entry: string): { command: string; args: string[] };
parseArgs(argv: readonly string[]): { command: string; opts: CliOptions };
buildAiDump(app: App, opts: CliOptions): Record<string, unknown>;
assertSafeEntryPath(entry: string, context: string): void;
normalizeEntryArg(entry: string): string;

Next up: runtime adapters.