@daloyjs/core 1.0.0-beta.6 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/bun.d.ts +13 -1
  3. package/dist/adapters/bun.js +8 -1
  4. package/dist/adapters/cloudflare.d.ts +7 -1
  5. package/dist/adapters/cloudflare.js +6 -1
  6. package/dist/adapters/deno.d.ts +11 -1
  7. package/dist/adapters/deno.js +8 -1
  8. package/dist/adapters/fastly.d.ts +12 -2
  9. package/dist/adapters/fastly.js +12 -2
  10. package/dist/adapters/lambda.d.ts +37 -1
  11. package/dist/adapters/lambda.js +6 -1
  12. package/dist/adapters/node.d.ts +12 -1
  13. package/dist/adapters/node.js +223 -12
  14. package/dist/adapters/vercel.d.ts +13 -1
  15. package/dist/adapters/vercel.js +12 -1
  16. package/dist/app.d.ts +78 -17
  17. package/dist/app.js +444 -86
  18. package/dist/banner.d.ts +6 -0
  19. package/dist/banner.js +6 -0
  20. package/dist/cli.d.ts +35 -0
  21. package/dist/cli.js +23 -1
  22. package/dist/combine.d.ts +8 -0
  23. package/dist/combine.js +8 -0
  24. package/dist/compression.d.ts +3 -0
  25. package/dist/compression.js +3 -0
  26. package/dist/config.d.ts +4 -0
  27. package/dist/config.js +4 -0
  28. package/dist/conn-info.d.ts +35 -2
  29. package/dist/conn-info.js +35 -2
  30. package/dist/contract.d.ts +2 -0
  31. package/dist/contract.js +2 -0
  32. package/dist/cookie.d.ts +12 -0
  33. package/dist/cookie.js +12 -0
  34. package/dist/dependency.d.ts +4 -0
  35. package/dist/dependency.js +3 -0
  36. package/dist/discriminator.d.ts +13 -0
  37. package/dist/discriminator.js +23 -1
  38. package/dist/docs.d.ts +78 -0
  39. package/dist/docs.js +21 -0
  40. package/dist/errors.d.ts +16 -0
  41. package/dist/errors.js +14 -0
  42. package/dist/etag.d.ts +2 -0
  43. package/dist/etag.js +2 -0
  44. package/dist/fetch-guard.d.ts +7 -0
  45. package/dist/fetch-guard.js +7 -0
  46. package/dist/fetch-resilience.d.ts +4 -0
  47. package/dist/fetch-resilience.js +4 -0
  48. package/dist/http-signatures.d.ts +32 -0
  49. package/dist/http-signatures.js +30 -0
  50. package/dist/index.d.ts +1 -1
  51. package/dist/ip-restriction.d.ts +40 -3
  52. package/dist/ip-restriction.js +35 -3
  53. package/dist/jwk.d.ts +12 -1
  54. package/dist/jwk.js +6 -0
  55. package/dist/jwt.d.ts +14 -0
  56. package/dist/jwt.js +10 -0
  57. package/dist/load-shedding.d.ts +3 -0
  58. package/dist/load-shedding.js +3 -0
  59. package/dist/logger.d.ts +13 -0
  60. package/dist/logger.js +3 -0
  61. package/dist/mcp.d.ts +152 -10
  62. package/dist/mcp.js +223 -19
  63. package/dist/middleware.d.ts +68 -0
  64. package/dist/middleware.js +17 -0
  65. package/dist/mtls.d.ts +19 -2
  66. package/dist/mtls.js +12 -2
  67. package/dist/multipart.d.ts +42 -5
  68. package/dist/multipart.js +41 -5
  69. package/dist/openapi.d.ts +15 -9
  70. package/dist/openapi.js +6 -9
  71. package/dist/rate-limit-redis.d.ts +21 -2
  72. package/dist/rate-limit-redis.js +17 -2
  73. package/dist/safe-redirect.d.ts +6 -0
  74. package/dist/safe-redirect.js +6 -0
  75. package/dist/sbom.cdx.json +9 -9
  76. package/dist/sbom.spdx.json +5 -5
  77. package/dist/scheduler.d.ts +4 -0
  78. package/dist/schema.d.ts +25 -0
  79. package/dist/security-schemes.d.ts +50 -0
  80. package/dist/security-schemes.js +6 -0
  81. package/dist/security.d.ts +33 -0
  82. package/dist/security.js +28 -10
  83. package/dist/session.d.ts +34 -5
  84. package/dist/session.js +31 -5
  85. package/dist/streaming.d.ts +19 -0
  86. package/dist/streaming.js +16 -0
  87. package/dist/subdomains.d.ts +4 -0
  88. package/dist/subdomains.js +4 -0
  89. package/dist/time-claims.d.ts +22 -2
  90. package/dist/time-claims.js +6 -0
  91. package/dist/tracing.d.ts +12 -0
  92. package/dist/tracing.js +6 -0
  93. package/dist/types.d.ts +70 -1
  94. package/dist/waf.js +21 -1
  95. package/dist/webhook-delivery.d.ts +6 -0
  96. package/dist/webhook-delivery.js +5 -0
  97. package/dist/websocket.d.ts +137 -4
  98. package/dist/websocket.js +105 -4
  99. package/package.json +1 -1
package/dist/mcp.js CHANGED
@@ -25,17 +25,51 @@ export const MCP_PROTOCOL_VERSIONS = Object.freeze([
25
25
  * @since 1.0.0
26
26
  */
27
27
  export const MCP_DEFAULT_MAX_BODY_BYTES = 1 << 18;
28
+ /**
29
+ * Protocol revision assumed when an HTTP request carries no
30
+ * `MCP-Protocol-Version` header, as required by the Streamable HTTP spec for
31
+ * backwards compatibility with pre-2025-06-18 clients.
32
+ */
33
+ const LEGACY_ASSUMED_PROTOCOL_VERSION = "2025-03-26";
28
34
  const PARSE_ERROR = -32700;
29
35
  const INVALID_REQUEST = -32600;
30
36
  const METHOD_NOT_FOUND = -32601;
31
37
  const INVALID_PARAMS = -32602;
32
38
  const INTERNAL_ERROR = -32603;
39
+ /**
40
+ * JSON Schema for the JSON-RPC 2.0 envelope every MCP response uses. Exposed
41
+ * through `toJSONSchema()` so the generated OpenAPI document describes the
42
+ * `/mcp` route honestly instead of leaving it an undocumented blind spot.
43
+ */
44
+ const MCP_JSONRPC_ENVELOPE_JSON_SCHEMA = {
45
+ type: "object",
46
+ description: "JSON-RPC 2.0 envelope produced by the MCP Streamable HTTP endpoint.",
47
+ properties: {
48
+ jsonrpc: { type: "string", const: "2.0" },
49
+ id: { oneOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
50
+ result: { description: "Method result. Present on success; shape varies by MCP method." },
51
+ error: {
52
+ type: "object",
53
+ properties: {
54
+ code: { type: "integer" },
55
+ message: { type: "string" },
56
+ data: {},
57
+ },
58
+ required: ["code", "message"],
59
+ additionalProperties: false,
60
+ },
61
+ },
62
+ required: ["jsonrpc"],
63
+ };
33
64
  const MCP_JSON_RESPONSE_SCHEMA = {
34
65
  "~standard": {
35
66
  version: 1,
36
67
  vendor: "daloyjs",
68
+ // Pass-through: the MCP handler fully controls the envelope it builds, so
69
+ // re-validating (or field-stripping) it here would only burn cycles.
37
70
  validate: (value) => ({ value }),
38
71
  },
72
+ toJSONSchema: () => MCP_JSONRPC_ENVELOPE_JSON_SCHEMA,
39
73
  };
40
74
  /**
41
75
  * Caller-correctable MCP tool/resource/prompt error.
@@ -103,21 +137,105 @@ function publicPrompt(prompt) {
103
137
  const { get: _get, ...rest } = prompt;
104
138
  return rest;
105
139
  }
140
+ function publicResourceTemplate(template) {
141
+ const { read: _read, ...rest } = template;
142
+ return rest;
143
+ }
106
144
  function normalizeToolResult(value) {
107
- return typeof value === "string" ? { content: [{ type: "text", text: value }] } : value;
145
+ if (typeof value === "string")
146
+ return { content: [{ type: "text", text: value }] };
147
+ if (value.content && value.content.length > 0)
148
+ return value;
149
+ // Backwards compatibility: clients that predate `structuredContent` only
150
+ // read `content`, so mirror the structured payload into a text block.
151
+ const content = value.structuredContent !== undefined
152
+ ? [{ type: "text", text: JSON.stringify(value.structuredContent) }]
153
+ : [];
154
+ return { ...value, content };
108
155
  }
109
156
  function selectedProtocolVersion(requested, supported, preferred) {
110
157
  return supported.has(requested) ? requested : preferred;
111
158
  }
159
+ function escapeRegExp(literal) {
160
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
162
+ /**
163
+ * Compile a simple RFC 6570 level-1 URI template into a matcher. Each
164
+ * `{name}` variable matches exactly one URI segment (`[^/]+`). Operators such
165
+ * as `{+path}` or `{?query}` are rejected so the handler never advertises a
166
+ * template it cannot match.
167
+ */
168
+ function compileUriTemplate(template) {
169
+ const { uriTemplate } = template;
170
+ const variables = [];
171
+ let pattern = "";
172
+ let index = 0;
173
+ while (index < uriTemplate.length) {
174
+ const open = uriTemplate.indexOf("{", index);
175
+ if (open === -1) {
176
+ pattern += escapeRegExp(uriTemplate.slice(index));
177
+ break;
178
+ }
179
+ pattern += escapeRegExp(uriTemplate.slice(index, open));
180
+ const close = uriTemplate.indexOf("}", open);
181
+ if (close === -1) {
182
+ throw new TypeError(`MCP resource template "${uriTemplate}" has an unterminated "{".`);
183
+ }
184
+ const name = uriTemplate.slice(open + 1, close);
185
+ if (!/^[A-Za-z0-9_]+$/.test(name)) {
186
+ throw new TypeError(`MCP resource template "${uriTemplate}" uses an unsupported expression "{${name}}"; ` +
187
+ "only simple {name} variables are supported.");
188
+ }
189
+ variables.push(name);
190
+ pattern += "([^/]+)";
191
+ index = close + 1;
192
+ }
193
+ return { template, regex: new RegExp(`^${pattern}$`), variables };
194
+ }
195
+ const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
196
+ /**
197
+ * Streamable HTTP DNS-rebinding defense: decide whether a browser `Origin`
198
+ * may talk to this MCP endpoint. Same-origin and loopback origins are always
199
+ * allowed; anything else must be explicitly allowlisted.
200
+ */
201
+ function isAllowedOrigin(origin, request, allowlist) {
202
+ const normalized = origin.toLowerCase();
203
+ if (allowlist.has(normalized))
204
+ return true;
205
+ if (normalized === "null")
206
+ return false;
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(normalized);
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ const hostname = parsed.hostname;
215
+ if (LOOPBACK_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost"))
216
+ return true;
217
+ try {
218
+ return parsed.host === new URL(request.url).host;
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ }
112
224
  /**
113
225
  * Create a dependency-free MCP Streamable HTTP endpoint handler.
114
226
  *
115
227
  * The handler implements the server side of MCP over one HTTP endpoint:
116
228
  * `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
117
- * `resources/read`, `prompts/list`, and `prompts/get`. It accepts JSON-RPC
118
- * requests over `POST`, acknowledges notifications with `202`, validates the
119
- * `MCP-Protocol-Version` header, bounds request bodies, and returns JSON-RPC
120
- * errors for malformed input.
229
+ * `resources/templates/list`, `resources/read`, `prompts/list`, and
230
+ * `prompts/get`. It accepts JSON-RPC requests over `POST`, acknowledges
231
+ * notifications with `202`, validates the `MCP-Protocol-Version` header,
232
+ * bounds request bodies, enforces required prompt arguments, and returns
233
+ * JSON-RPC errors for malformed input.
234
+ *
235
+ * Security: per the Streamable HTTP spec's DNS-rebinding guidance, every
236
+ * request bearing an `Origin` header is validated. Same-origin and loopback
237
+ * origins pass; anything else is rejected with `403` unless listed in
238
+ * {@link McpHandlerOptions.allowedOrigins}.
121
239
  *
122
240
  * It intentionally does not spawn stdio servers, manage OAuth metadata, keep
123
241
  * durable sessions, or open server-initiated SSE streams. Use DaloyJS
@@ -128,6 +246,9 @@ function selectedProtocolVersion(requested, supported, preferred) {
128
246
  * @param options - Server identity, capabilities, limits, and response headers.
129
247
  * @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
130
248
  * or for direct use in any web-standard runtime.
249
+ * @throws {TypeError} at construction for invalid serverInfo, protocol
250
+ * versions, body limits, duplicate names/URIs, malformed `allowedOrigins`
251
+ * entries, or unsupported URI template expressions.
131
252
  *
132
253
  * @example
133
254
  * ```ts
@@ -173,6 +294,7 @@ export function createMcpHandler(options) {
173
294
  }
174
295
  const tools = options.tools ?? [];
175
296
  const resources = options.resources ?? [];
297
+ const resourceTemplates = options.resourceTemplates ?? [];
176
298
  const prompts = options.prompts ?? [];
177
299
  const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
178
300
  const resourceMap = new Map(resources.map((resource) => [resource.uri, resource]));
@@ -183,24 +305,68 @@ export function createMcpHandler(options) {
183
305
  throw new TypeError("MCP resource URIs must be unique.");
184
306
  if (promptMap.size !== prompts.length)
185
307
  throw new TypeError("MCP prompt names must be unique.");
308
+ if (new Set(resourceTemplates.map((template) => template.uriTemplate)).size !==
309
+ resourceTemplates.length) {
310
+ throw new TypeError("MCP resource template URIs must be unique.");
311
+ }
312
+ const compiledTemplates = resourceTemplates.map(compileUriTemplate);
313
+ const allowedOrigins = new Set();
314
+ for (const entry of options.allowedOrigins ?? []) {
315
+ const normalized = entry.toLowerCase();
316
+ if (normalized === "null") {
317
+ allowedOrigins.add(normalized);
318
+ continue;
319
+ }
320
+ let parsed;
321
+ try {
322
+ parsed = new URL(normalized);
323
+ }
324
+ catch {
325
+ parsed = undefined;
326
+ }
327
+ if (!parsed || parsed.origin !== normalized) {
328
+ throw new TypeError(`MCP allowedOrigins entry "${entry}" must be a bare origin such as "https://app.example.com".`);
329
+ }
330
+ allowedOrigins.add(normalized);
331
+ }
186
332
  const exposeInternalErrors = options.exposeInternalErrors ??
187
333
  (typeof process === "object" && process.env?.NODE_ENV !== "production");
188
334
  const headers = options.headers;
335
+ const legacyAssumed = supported.has(LEGACY_ASSUMED_PROTOCOL_VERSION)
336
+ ? LEGACY_ASSUMED_PROTOCOL_VERSION
337
+ : preferred;
189
338
  async function handleRpcRequest(message, request) {
190
339
  const id = (message.id ?? null);
191
340
  const method = message.method;
192
341
  const params = asRecord(message.params);
193
- const protocolVersion = selectedProtocolVersion(typeof params.protocolVersion === "string"
194
- ? params.protocolVersion
195
- : (request.headers.get("mcp-protocol-version") ?? ""), supported, preferred);
342
+ const headerVersion = request.headers.get("mcp-protocol-version");
343
+ // Per the Streamable HTTP spec, a request without the header is assumed
344
+ // to speak 2025-03-26; `initialize` negotiates via params instead.
345
+ const protocolVersion = method === "initialize"
346
+ ? selectedProtocolVersion(typeof params.protocolVersion === "string"
347
+ ? params.protocolVersion
348
+ : (headerVersion ?? ""), supported, preferred)
349
+ : headerVersion !== null
350
+ ? selectedProtocolVersion(headerVersion, supported, preferred)
351
+ : legacyAssumed;
196
352
  const ctx = { request, protocolVersion, id, method };
353
+ const cursor = params.cursor;
354
+ if (cursor !== undefined &&
355
+ (method === "tools/list" ||
356
+ method === "resources/list" ||
357
+ method === "resources/templates/list" ||
358
+ method === "prompts/list")) {
359
+ // This handler returns complete lists and never issues cursors, so any
360
+ // client-supplied cursor is unknown by definition.
361
+ return rpcError(id, INVALID_PARAMS, "Unknown pagination cursor.", undefined, 200, headers);
362
+ }
197
363
  switch (method) {
198
364
  case "initialize":
199
365
  return rpcResult(id, {
200
366
  protocolVersion,
201
367
  capabilities: {
202
368
  ...(tools.length > 0 ? { tools: {} } : {}),
203
- ...(resources.length > 0 ? { resources: {} } : {}),
369
+ ...(resources.length > 0 || resourceTemplates.length > 0 ? { resources: {} } : {}),
204
370
  ...(prompts.length > 0 ? { prompts: {} } : {}),
205
371
  },
206
372
  serverInfo: options.serverInfo,
@@ -229,23 +395,46 @@ export function createMcpHandler(options) {
229
395
  }
230
396
  case "resources/list":
231
397
  return rpcResult(id, { resources: resources.map(publicResource) }, headers);
398
+ case "resources/templates/list":
399
+ return rpcResult(id, { resourceTemplates: resourceTemplates.map(publicResourceTemplate) }, headers);
232
400
  case "resources/read": {
233
401
  const uri = typeof params.uri === "string" ? params.uri : "";
234
- const resource = resourceMap.get(uri);
235
- if (!resource) {
236
- return rpcError(id, INVALID_PARAMS, `Unknown resource: ${uri || "<missing>"}`, undefined, 200, headers);
237
- }
238
- try {
239
- const read = await resource.read(ctx);
240
- return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
241
- }
242
- catch (error) {
402
+ const readError = (error) => {
243
403
  const message = error instanceof McpToolError ? error.message : "Resource read failed.";
244
404
  const data = error instanceof McpToolError
245
405
  ? undefined
246
406
  : safeInternalErrorData(error, exposeInternalErrors);
247
407
  return rpcError(id, error instanceof McpToolError ? INVALID_PARAMS : INTERNAL_ERROR, message, data, 200, headers);
408
+ };
409
+ const resource = resourceMap.get(uri);
410
+ if (resource) {
411
+ try {
412
+ const read = await resource.read(ctx);
413
+ return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
414
+ }
415
+ catch (error) {
416
+ return readError(error);
417
+ }
418
+ }
419
+ if (uri) {
420
+ for (const compiled of compiledTemplates) {
421
+ const match = compiled.regex.exec(uri);
422
+ if (!match)
423
+ continue;
424
+ const variables = {};
425
+ compiled.variables.forEach((name, position) => {
426
+ variables[name] = match[position + 1] ?? "";
427
+ });
428
+ try {
429
+ const read = await compiled.template.read(uri, variables, ctx);
430
+ return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
431
+ }
432
+ catch (error) {
433
+ return readError(error);
434
+ }
435
+ }
248
436
  }
437
+ return rpcError(id, INVALID_PARAMS, `Unknown resource: ${uri || "<missing>"}`, undefined, 200, headers);
249
438
  }
250
439
  case "prompts/list":
251
440
  return rpcResult(id, { prompts: prompts.map(publicPrompt) }, headers);
@@ -255,8 +444,15 @@ export function createMcpHandler(options) {
255
444
  if (!prompt) {
256
445
  return rpcError(id, INVALID_PARAMS, `Unknown prompt: ${name || "<missing>"}`, undefined, 200, headers);
257
446
  }
447
+ const promptArgs = asRecord(params.arguments);
448
+ const missing = (prompt.arguments ?? [])
449
+ .filter((argument) => argument.required && promptArgs[argument.name] === undefined)
450
+ .map((argument) => argument.name);
451
+ if (missing.length > 0) {
452
+ return rpcError(id, INVALID_PARAMS, `Missing required prompt arguments: ${missing.join(", ")}`, undefined, 200, headers);
453
+ }
258
454
  try {
259
- return rpcResult(id, await prompt.get(asRecord(params.arguments), ctx), headers);
455
+ return rpcResult(id, await prompt.get(promptArgs, ctx), headers);
260
456
  }
261
457
  catch (error) {
262
458
  const message = error instanceof McpToolError ? error.message : "Prompt rendering failed.";
@@ -271,6 +467,12 @@ export function createMcpHandler(options) {
271
467
  }
272
468
  }
273
469
  return async function handleMcpRequest(request) {
470
+ // Streamable HTTP requires Origin validation on every request to defeat
471
+ // DNS rebinding; invalid browser origins are refused with 403.
472
+ const origin = request.headers.get("origin");
473
+ if (origin !== null && !isAllowedOrigin(origin, request, allowedOrigins)) {
474
+ return rpcError(null, INVALID_REQUEST, "Origin is not allowed for this MCP endpoint.", undefined, 403, headers);
475
+ }
274
476
  if (request.method === "OPTIONS") {
275
477
  return new Response(null, {
276
478
  status: 204,
@@ -284,6 +486,7 @@ export function createMcpHandler(options) {
284
486
  capabilities: {
285
487
  tools: tools.map((tool) => tool.name),
286
488
  resources: resources.map((resource) => resource.uri),
489
+ resourceTemplates: resourceTemplates.map((template) => template.uriTemplate),
287
490
  prompts: prompts.map((prompt) => prompt.name),
288
491
  },
289
492
  hint: "Send JSON-RPC 2.0 over HTTP POST to this endpoint.",
@@ -387,6 +590,7 @@ export function mcpRoutes(path, handler) {
387
590
  202: { description: "MCP notification accepted", body: MCP_JSON_RESPONSE_SCHEMA },
388
591
  204: { description: "CORS preflight accepted" },
389
592
  400: { description: "Invalid MCP request" },
593
+ 403: { description: "Origin not allowed" },
390
594
  405: { description: "Unsupported MCP transport method" },
391
595
  413: { description: "MCP request body too large" },
392
596
  };
@@ -78,16 +78,38 @@ export interface CspDirectivesOptions {
78
78
  }
79
79
  /** Options for {@link secureHeaders}. Every field can be disabled with `false`. */
80
80
  export interface SecureHeadersOptions {
81
+ /**
82
+ * `Content-Security-Policy` value: a raw header string, a
83
+ * {@link CspDirectivesOptions} object (enables nonces / Trusted Types),
84
+ * or `false` to omit. Defaults to
85
+ * `"default-src 'self'; frame-ancestors 'none'"`.
86
+ */
81
87
  contentSecurityPolicy?: string | false | CspDirectivesOptions;
88
+ /**
89
+ * `Strict-Transport-Security` settings (`maxAgeSeconds` in seconds).
90
+ * Defaults to `{ maxAgeSeconds: 31536000, includeSubDomains: true }`.
91
+ */
82
92
  hsts?: {
83
93
  maxAgeSeconds: number;
84
94
  includeSubDomains?: boolean;
85
95
  preload?: boolean;
86
96
  } | false;
97
+ /**
98
+ * `X-Frame-Options` value. Defaults to `"DENY"`. Disabling it with
99
+ * `false` while the CSP also lacks `frame-ancestors` throws at
100
+ * construction time (clickjacking guard).
101
+ */
87
102
  frameOptions?: "DENY" | "SAMEORIGIN" | false;
103
+ /** `Referrer-Policy` value. Defaults to `"no-referrer"`. */
88
104
  referrerPolicy?: string | false;
105
+ /**
106
+ * `Permissions-Policy` value. Defaults to
107
+ * `"camera=(), microphone=(), geolocation=(), clipboard-write=()"`.
108
+ */
89
109
  permissionsPolicy?: string | false;
110
+ /** `Cross-Origin-Opener-Policy` value. Defaults to `"same-origin"`. */
90
111
  crossOriginOpenerPolicy?: string | false;
112
+ /** `Cross-Origin-Resource-Policy` value. Defaults to `"same-origin"`. */
91
113
  crossOriginResourcePolicy?: string | false;
92
114
  /**
93
115
  * `Cross-Origin-Embedder-Policy` (COEP). Off by default because
@@ -101,7 +123,12 @@ export interface SecureHeadersOptions {
101
123
  * @since 0.37.0
102
124
  */
103
125
  crossOriginEmbedderPolicy?: "require-corp" | "credentialless" | "unsafe-none" | false;
126
+ /** Emit `X-Content-Type-Options: nosniff`. Defaults to `true`. */
104
127
  noSniff?: boolean;
128
+ /**
129
+ * Emit `X-XSS-Protection: 0` (disables the legacy XSS auditor, per
130
+ * modern guidance). Defaults to `false` (header omitted).
131
+ */
105
132
  xssProtection?: boolean;
106
133
  /**
107
134
  * Reporting API endpoint declarations rendered as the
@@ -165,6 +192,11 @@ export declare const SECURE_HEADERS_MARKER: unique symbol;
165
192
  * {@link SecureHeadersOptions} for the full list). Pass `false` for any
166
193
  * field to disable that header; handler-set headers always win.
167
194
  *
195
+ * @param opts - Per-header overrides. Pass `false` to disable a header.
196
+ * @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with
197
+ * {@link SECURE_HEADERS_MARKER}.
198
+ * @throws {Error} When `frameOptions: false` is combined with a CSP that has
199
+ * no `frame-ancestors` directive (would disable every clickjacking defense).
168
200
  * @since 0.1.0
169
201
  */
170
202
  export declare function secureHeaders(opts?: SecureHeadersOptions): Hooks;
@@ -286,11 +318,30 @@ export declare const CSRF_HOOK_MARKER: unique symbol;
286
318
  export type CorsOriginAllow = (origin: string) => boolean;
287
319
  /** Options for {@link cors}. */
288
320
  export interface CorsOptions {
321
+ /**
322
+ * Allowed cross-origin caller(s): a single origin string, an array of
323
+ * origins, or a predicate. `"*"` is rejected with `credentials: true`
324
+ * and refused at boot on production `secureDefaults` apps.
325
+ */
289
326
  origin: string | string[] | ((origin: string) => boolean);
327
+ /**
328
+ * Verbs advertised on preflight (`Access-Control-Allow-Methods`).
329
+ * Defaults to `["GET", "HEAD", "POST"]`. `"*"` is rejected.
330
+ */
290
331
  methods?: string[];
332
+ /**
333
+ * Request headers advertised on preflight (`Access-Control-Allow-Headers`).
334
+ * Defaults to `["content-type", "authorization"]`.
335
+ */
291
336
  allowedHeaders?: string[];
337
+ /** Response headers exposed to browser JS (`Access-Control-Expose-Headers`). None by default. */
292
338
  exposedHeaders?: string[];
339
+ /**
340
+ * Emit `Access-Control-Allow-Credentials: true`. Defaults to `false`.
341
+ * Cannot be combined with a wildcard `origin`.
342
+ */
293
343
  credentials?: boolean;
344
+ /** Preflight cache lifetime in seconds (`Access-Control-Max-Age`). Defaults to `600`. */
294
345
  maxAgeSeconds?: number;
295
346
  }
296
347
  /**
@@ -327,6 +378,11 @@ export declare function cors(opts: CorsOptions): Hooks;
327
378
  * (`@daloyjs/core/rate-limit-redis`).
328
379
  */
329
380
  export interface RateLimitStore {
381
+ /**
382
+ * Record one request for `key` in the window of `windowMs` milliseconds
383
+ * and return the updated `count` plus the window's absolute reset time
384
+ * (`resetMs`, epoch milliseconds).
385
+ */
330
386
  hit(key: string, windowMs: number): Promise<{
331
387
  count: number;
332
388
  resetMs: number;
@@ -437,6 +493,9 @@ export interface LoginThrottleOptions {
437
493
  * same `groupId`) across related routes so an attacker cannot bypass the limit
438
494
  * by rotating between password, OTP, and reset endpoints.
439
495
  *
496
+ * @param opts - Throttle tuning (see {@link LoginThrottleOptions}); every field has a safe default.
497
+ * @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`.
498
+ * @throws {Error} When `windowMs`/`max` are not positive integers or the delay options are negative.
440
499
  * @since 0.23.0
441
500
  */
442
501
  export declare function loginThrottle(opts?: LoginThrottleOptions): Hooks;
@@ -627,6 +686,11 @@ export interface CsrfOptions {
627
686
  * allowedOrigins: ["https://app.example.com"],
628
687
  * }));
629
688
  * ```
689
+ *
690
+ * @param opts - Strategy, cookie/header names, and cookie attributes (see {@link CsrfOptions}).
691
+ * @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with {@link CSRF_HOOK_MARKER}.
692
+ * @throws {Error} When `strategy` is unknown or the cookie name/attributes are invalid
693
+ * (e.g. `__Host-` prefix without `secure: true`, `path: "/"`, and no `domain`).
630
694
  */
631
695
  export declare function csrf(opts?: CsrfOptions): Hooks;
632
696
  /** Options for {@link basicAuth}. */
@@ -723,6 +787,10 @@ export declare const REQUIRE_SCOPES_HOOK_MARKER: unique symbol;
723
787
  * });
724
788
  * ```
725
789
  *
790
+ * @param scopes - Required scope strings; must be non-empty and free of RFC 6749 illegal characters.
791
+ * @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`,
792
+ * stamped with {@link REQUIRE_SCOPES_HOOK_MARKER}.
793
+ * @throws {Error} When `scopes` is empty or contains a non-string, empty, or illegal-character scope.
726
794
  * @since 0.21.0
727
795
  */
728
796
  export declare function requireScopes(scopes: readonly string[]): Hooks;
@@ -148,6 +148,11 @@ export const SECURE_HEADERS_MARKER = Symbol.for("daloyjs.middleware.secureHeader
148
148
  * {@link SecureHeadersOptions} for the full list). Pass `false` for any
149
149
  * field to disable that header; handler-set headers always win.
150
150
  *
151
+ * @param opts - Per-header overrides. Pass `false` to disable a header.
152
+ * @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with
153
+ * {@link SECURE_HEADERS_MARKER}.
154
+ * @throws {Error} When `frameOptions: false` is combined with a CSP that has
155
+ * no `frame-ancestors` directive (would disable every clickjacking defense).
151
156
  * @since 0.1.0
152
157
  */
153
158
  export function secureHeaders(opts = {}) {
@@ -706,6 +711,9 @@ function wait(ms) {
706
711
  * same `groupId`) across related routes so an attacker cannot bypass the limit
707
712
  * by rotating between password, OTP, and reset endpoints.
708
713
  *
714
+ * @param opts - Throttle tuning (see {@link LoginThrottleOptions}); every field has a safe default.
715
+ * @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`.
716
+ * @throws {Error} When `windowMs`/`max` are not positive integers or the delay options are negative.
709
717
  * @since 0.23.0
710
718
  */
711
719
  export function loginThrottle(opts = {}) {
@@ -912,6 +920,11 @@ function csrfCookieAttributes(opts) {
912
920
  * allowedOrigins: ["https://app.example.com"],
913
921
  * }));
914
922
  * ```
923
+ *
924
+ * @param opts - Strategy, cookie/header names, and cookie attributes (see {@link CsrfOptions}).
925
+ * @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with {@link CSRF_HOOK_MARKER}.
926
+ * @throws {Error} When `strategy` is unknown or the cookie name/attributes are invalid
927
+ * (e.g. `__Host-` prefix without `secure: true`, `path: "/"`, and no `domain`).
915
928
  */
916
929
  export function csrf(opts = {}) {
917
930
  const strategy = opts.strategy ?? "double-submit";
@@ -1191,6 +1204,10 @@ function readUserScopes(user) {
1191
1204
  * });
1192
1205
  * ```
1193
1206
  *
1207
+ * @param scopes - Required scope strings; must be non-empty and free of RFC 6749 illegal characters.
1208
+ * @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`,
1209
+ * stamped with {@link REQUIRE_SCOPES_HOOK_MARKER}.
1210
+ * @throws {Error} When `scopes` is empty or contains a non-string, empty, or illegal-character scope.
1194
1211
  * @since 0.21.0
1195
1212
  */
1196
1213
  export function requireScopes(scopes) {
package/dist/mtls.d.ts CHANGED
@@ -84,6 +84,9 @@ export type ClientCertificateSource = ClientCertificate | undefined | (() => Cli
84
84
  * Pass a thunk to defer the read until {@link getClientCertificate} is first
85
85
  * called; the resolved value is cached back onto the request.
86
86
  *
87
+ * @param request The incoming request to stash the certificate on.
88
+ * @param source The certificate, a lazy thunk producing one, or `undefined`
89
+ * when the connection presented no client certificate.
87
90
  * @since 0.37.0
88
91
  */
89
92
  export declare function setClientCertificate(request: Request, source: ClientCertificateSource): void;
@@ -93,6 +96,8 @@ export declare function setClientCertificate(request: Request, source: ClientCer
93
96
  * adapter does not expose TLS peer info). If a lazy thunk was stashed, it is
94
97
  * resolved once and the result cached.
95
98
  *
99
+ * @param request The request previously seen by {@link setClientCertificate}.
100
+ * @returns The attached certificate, or `undefined` when none was presented.
96
101
  * @since 0.37.0
97
102
  */
98
103
  export declare function getClientCertificate(request: Request): ClientCertificate | undefined;
@@ -104,12 +109,19 @@ export declare function getClientCertificate(request: Request): ClientCertificat
104
109
  * @since 0.37.0
105
110
  */
106
111
  export interface PeerCertificateLike {
112
+ /** Subject RDN attributes keyed by short name (e.g. `CN`, `O`). */
107
113
  subject?: Record<string, string | string[]> | null;
114
+ /** Issuer RDN attributes keyed by short name. */
108
115
  issuer?: Record<string, string | string[]> | null;
116
+ /** Validity start as a Node-formatted date string. */
109
117
  valid_from?: string;
118
+ /** Validity end as a Node-formatted date string. */
110
119
  valid_to?: string;
120
+ /** SHA-256 fingerprint, colon-delimited hex as Node emits it. */
111
121
  fingerprint256?: string;
122
+ /** Certificate serial number as a hex string. */
112
123
  serialNumber?: string;
124
+ /** Subject Alternative Names rendered as `DNS:a, IP Address:1.2.3.4, ...`. */
113
125
  subjectaltname?: string;
114
126
  }
115
127
  /**
@@ -120,6 +132,9 @@ export interface PeerCertificateLike {
120
132
  * @param raw - The structured peer-certificate object from the TLS socket.
121
133
  * @param verified - Whether the socket reported `authorized === true` (the
122
134
  * chain was verified against the configured CA).
135
+ * @returns The normalized certificate, or `undefined` when `raw` is missing
136
+ * or carries no identifying fields (no subject, issuer, fingerprint, or
137
+ * serial).
123
138
  * @since 0.37.0
124
139
  */
125
140
  export declare function normalizePeerCertificate(raw: PeerCertificateLike | null | undefined, verified: boolean): ClientCertificate | undefined;
@@ -131,8 +146,10 @@ export declare function normalizePeerCertificate(raw: PeerCertificateLike | null
131
146
  * and is the one returned. Because Envoy only emits XFCC for connections it
132
147
  * mutually authenticated, the result is marked `verified: true`.
133
148
  *
134
- * Returns `undefined` for an empty or unparseable header.
135
- *
149
+ * @param headerValue Raw XFCC header value; `null`/`undefined` are tolerated.
150
+ * @returns The certificate parsed from the first XFCC element, or `undefined`
151
+ * for an empty or unparseable header. Only trust this header behind a proxy
152
+ * you control that strips client-supplied XFCC.
136
153
  * @since 0.37.0
137
154
  */
138
155
  export declare function parseForwardedClientCert(headerValue: string | null | undefined): ClientCertificate | undefined;
package/dist/mtls.js CHANGED
@@ -36,6 +36,9 @@ const CLIENT_CERT_SYMBOL = Symbol.for("daloyjs.clientCertificate");
36
36
  * Pass a thunk to defer the read until {@link getClientCertificate} is first
37
37
  * called; the resolved value is cached back onto the request.
38
38
  *
39
+ * @param request The incoming request to stash the certificate on.
40
+ * @param source The certificate, a lazy thunk producing one, or `undefined`
41
+ * when the connection presented no client certificate.
39
42
  * @since 0.37.0
40
43
  */
41
44
  export function setClientCertificate(request, source) {
@@ -47,6 +50,8 @@ export function setClientCertificate(request, source) {
47
50
  * adapter does not expose TLS peer info). If a lazy thunk was stashed, it is
48
51
  * resolved once and the result cached.
49
52
  *
53
+ * @param request The request previously seen by {@link setClientCertificate}.
54
+ * @returns The attached certificate, or `undefined` when none was presented.
50
55
  * @since 0.37.0
51
56
  */
52
57
  export function getClientCertificate(request) {
@@ -67,6 +72,9 @@ export function getClientCertificate(request) {
67
72
  * @param raw - The structured peer-certificate object from the TLS socket.
68
73
  * @param verified - Whether the socket reported `authorized === true` (the
69
74
  * chain was verified against the configured CA).
75
+ * @returns The normalized certificate, or `undefined` when `raw` is missing
76
+ * or carries no identifying fields (no subject, issuer, fingerprint, or
77
+ * serial).
70
78
  * @since 0.37.0
71
79
  */
72
80
  export function normalizePeerCertificate(raw, verified) {
@@ -205,8 +213,10 @@ function cnFromDN(dn) {
205
213
  * and is the one returned. Because Envoy only emits XFCC for connections it
206
214
  * mutually authenticated, the result is marked `verified: true`.
207
215
  *
208
- * Returns `undefined` for an empty or unparseable header.
209
- *
216
+ * @param headerValue Raw XFCC header value; `null`/`undefined` are tolerated.
217
+ * @returns The certificate parsed from the first XFCC element, or `undefined`
218
+ * for an empty or unparseable header. Only trust this header behind a proxy
219
+ * you control that strips client-supplied XFCC.
210
220
  * @since 0.37.0
211
221
  */
212
222
  export function parseForwardedClientCert(headerValue) {