@daloyjs/core 1.0.0-beta.6 → 1.0.0-beta.7

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 +7 -1
  14. package/dist/adapters/vercel.d.ts +13 -1
  15. package/dist/adapters/vercel.js +12 -1
  16. package/dist/app.d.ts +67 -17
  17. package/dist/app.js +97 -31
  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/session.js CHANGED
@@ -161,6 +161,7 @@ function makeSessionContext(id, data, internal, regenerate) {
161
161
  */
162
162
  export class MemorySessionStore {
163
163
  map = new Map();
164
+ /** Load a record; expired records are deleted and reported as `null`. */
164
165
  get(sid) {
165
166
  const rec = this.map.get(sid);
166
167
  if (!rec)
@@ -171,12 +172,15 @@ export class MemorySessionStore {
171
172
  }
172
173
  return rec;
173
174
  }
175
+ /** Store (or overwrite) a record for a session id. */
174
176
  set(sid, record) {
175
177
  this.map.set(sid, record);
176
178
  }
179
+ /** Remove the record for a session id, if present. */
177
180
  destroy(sid) {
178
181
  this.map.delete(sid);
179
182
  }
183
+ /** Extend an existing record's expiry (ms since epoch) without rewriting data. */
180
184
  touch(sid, expiresAt) {
181
185
  const rec = this.map.get(sid);
182
186
  if (rec)
@@ -210,6 +214,14 @@ export class MemorySessionStore {
210
214
  * const app = new App();
211
215
  * app.use(session({ secret: process.env.SESSION_SECRET! }));
212
216
  * ```
217
+ *
218
+ * @param opts Secrets, cookie attributes, store, TTL, and rolling behavior;
219
+ * see {@link SessionOptions}. Cookies default to `__Host-` prefixed,
220
+ * `HttpOnly`, `Secure`, `SameSite=Lax`.
221
+ * @returns A {@link Hooks} object that loads/verifies the session before the
222
+ * handler and persists mutations plus the `Set-Cookie` header afterwards.
223
+ * @throws Error at setup time on missing/short secrets, invalid cookie
224
+ * attribute combinations, or a non-positive `ttlSeconds`.
213
225
  */
214
226
  export function session(opts) {
215
227
  const cookieName = opts.cookieName ?? DEFAULT_COOKIE_NAME;
@@ -432,6 +444,12 @@ function stableSnapshot(value) {
432
444
  * session key-rotation arrays: old cookies verify with any configured secret
433
445
  * and rotated cookies are re-signed with the first/current secret.
434
446
  *
447
+ * @param opts Which session keys (or computed value) to watch, and whether
448
+ * payload data survives rotation (`keepData`, default `true`); see
449
+ * {@link RotateSessionOptions}.
450
+ * @returns A {@link Hooks} object that snapshots the watched value before the
451
+ * handler and regenerates the session id when it changed afterwards
452
+ * (session-fixation defense).
435
453
  * @since 0.23.0
436
454
  */
437
455
  export function rotateSession(opts = {}) {
@@ -462,9 +480,13 @@ export function rotateSession(opts = {}) {
462
480
  }
463
481
  // ---------- Low-level signing helpers (re-exported for advanced use) ----------
464
482
  /**
465
- * Sign an arbitrary string with HMAC-SHA256. Returns `${value}.${sig}` where
466
- * `sig` is URL-safe base64. Useful for building custom signed cookies or
467
- * tokens that do not need a session store.
483
+ * Sign an arbitrary string with HMAC-SHA256. Useful for building custom
484
+ * signed cookies or tokens that do not need a session store.
485
+ *
486
+ * @param value String to sign; must not contain `.` (the separator).
487
+ * @param secret HMAC key, at least 16 characters.
488
+ * @returns `${value}.${sig}` where `sig` is URL-safe base64.
489
+ * @throws Error when `value` contains `.` or the secret is too short.
468
490
  */
469
491
  export async function signValue(value, secret) {
470
492
  if (value.includes(".")) {
@@ -475,8 +497,12 @@ export async function signValue(value, secret) {
475
497
  return `${value}.${sig}`;
476
498
  }
477
499
  /**
478
- * Verify a `signValue()`-produced string. Returns the original value when the
479
- * signature checks out, otherwise `null`. Constant-time on the signature.
500
+ * Verify a `signValue()`-produced string. Constant-time on the signature
501
+ * comparison.
502
+ *
503
+ * @param signed The `${value}.${sig}` string to verify.
504
+ * @param secret HMAC key(s); an array lets rotated old secrets still verify.
505
+ * @returns The original value when any secret's signature matches, else `null`.
480
506
  */
481
507
  export async function verifySignedValue(signed, secret) {
482
508
  const dot = signed.lastIndexOf(".");
@@ -36,8 +36,11 @@
36
36
  * and reconnection control without sending an event payload.
37
37
  */
38
38
  export interface SSEMessage {
39
+ /** Event payload. Strings are sent verbatim; other values are `JSON.stringify`-ed. */
39
40
  data?: unknown;
41
+ /** Event name (`event:` field). Newlines are replaced with spaces to prevent frame injection. */
40
42
  event?: string;
43
+ /** Last-event ID (`id:` field). Newlines are replaced with spaces to prevent frame injection. */
41
44
  id?: string;
42
45
  /** Reconnection delay in milliseconds. */
43
46
  retry?: number;
@@ -76,11 +79,19 @@ type IterableSource<T> = AsyncIterable<T> | Iterable<T> | (() => AsyncIterable<T
76
79
  * Build a backpressure-safe `ReadableStream` from an async iterable of SSE
77
80
  * messages. The iterator is only advanced when the consumer pulls the next
78
81
  * chunk, so a slow client cannot cause unbounded buffering.
82
+ *
83
+ * @param source Async/sync iterable (or factory) yielding {@link SSEMessage}s or plain strings.
84
+ * @param opts Abort signal and optional keep-alive interval; see {@link SSEStreamOptions}.
85
+ * @returns A `ReadableStream<Uint8Array>` of encoded `text/event-stream` frames.
79
86
  */
80
87
  export declare function sseStream(source: IterableSource<SSEMessage | string>, opts?: SSEStreamOptions): ReadableStream<Uint8Array>;
81
88
  /**
82
89
  * Wrap `sseStream` in a `Response` with the proper SSE headers
83
90
  * (`text/event-stream`, no caching, keep-alive). Caller-supplied headers win.
91
+ *
92
+ * @param source Async/sync iterable (or factory) yielding {@link SSEMessage}s or plain strings.
93
+ * @param opts Status, headers, abort signal, and keep-alive; see {@link SSEResponseOptions}.
94
+ * @returns A streaming `Response` (default status `200`) with SSE headers applied.
84
95
  */
85
96
  export declare function sseResponse(source: IterableSource<SSEMessage | string>, opts?: SSEResponseOptions): Response;
86
97
  /**
@@ -88,11 +99,19 @@ export declare function sseResponse(source: IterableSource<SSEMessage | string>,
88
99
  * JSON) records from an async iterable. Each yielded value is encoded with
89
100
  * `JSON.stringify` and terminated with `\n`. Values that stringify to
90
101
  * `undefined` throw because they cannot be represented as valid NDJSON.
102
+ *
103
+ * @param source Async/sync iterable (or factory) yielding JSON-serializable values.
104
+ * @param opts Optional abort signal; see {@link StreamOptions}.
105
+ * @returns A `ReadableStream<Uint8Array>` of newline-terminated JSON records.
91
106
  */
92
107
  export declare function ndjsonStream<T>(source: IterableSource<T>, opts?: StreamOptions): ReadableStream<Uint8Array>;
93
108
  /**
94
109
  * Wrap `ndjsonStream` in a `Response` with `application/x-ndjson` and
95
110
  * cache-busting headers. Caller-supplied headers win.
111
+ *
112
+ * @param source Async/sync iterable (or factory) yielding JSON-serializable values.
113
+ * @param opts Status, headers, and abort signal; see {@link NDJSONResponseOptions}.
114
+ * @returns A streaming `Response` (default status `200`) with NDJSON headers applied.
96
115
  */
97
116
  export declare function ndjsonResponse<T>(source: IterableSource<T>, opts?: NDJSONResponseOptions): Response;
98
117
  export {};
package/dist/streaming.js CHANGED
@@ -87,6 +87,10 @@ function encodeNDJSON(value) {
87
87
  * Build a backpressure-safe `ReadableStream` from an async iterable of SSE
88
88
  * messages. The iterator is only advanced when the consumer pulls the next
89
89
  * chunk, so a slow client cannot cause unbounded buffering.
90
+ *
91
+ * @param source Async/sync iterable (or factory) yielding {@link SSEMessage}s or plain strings.
92
+ * @param opts Abort signal and optional keep-alive interval; see {@link SSEStreamOptions}.
93
+ * @returns A `ReadableStream<Uint8Array>` of encoded `text/event-stream` frames.
90
94
  */
91
95
  export function sseStream(source, opts = {}) {
92
96
  const iterator = getAsyncIterator(source);
@@ -165,6 +169,10 @@ export function sseStream(source, opts = {}) {
165
169
  /**
166
170
  * Wrap `sseStream` in a `Response` with the proper SSE headers
167
171
  * (`text/event-stream`, no caching, keep-alive). Caller-supplied headers win.
172
+ *
173
+ * @param source Async/sync iterable (or factory) yielding {@link SSEMessage}s or plain strings.
174
+ * @param opts Status, headers, abort signal, and keep-alive; see {@link SSEResponseOptions}.
175
+ * @returns A streaming `Response` (default status `200`) with SSE headers applied.
168
176
  */
169
177
  export function sseResponse(source, opts = {}) {
170
178
  const stream = sseStream(source, opts);
@@ -185,6 +193,10 @@ export function sseResponse(source, opts = {}) {
185
193
  * JSON) records from an async iterable. Each yielded value is encoded with
186
194
  * `JSON.stringify` and terminated with `\n`. Values that stringify to
187
195
  * `undefined` throw because they cannot be represented as valid NDJSON.
196
+ *
197
+ * @param source Async/sync iterable (or factory) yielding JSON-serializable values.
198
+ * @param opts Optional abort signal; see {@link StreamOptions}.
199
+ * @returns A `ReadableStream<Uint8Array>` of newline-terminated JSON records.
188
200
  */
189
201
  export function ndjsonStream(source, opts = {}) {
190
202
  const iterator = getAsyncIterator(source);
@@ -246,6 +258,10 @@ export function ndjsonStream(source, opts = {}) {
246
258
  /**
247
259
  * Wrap `ndjsonStream` in a `Response` with `application/x-ndjson` and
248
260
  * cache-busting headers. Caller-supplied headers win.
261
+ *
262
+ * @param source Async/sync iterable (or factory) yielding JSON-serializable values.
263
+ * @param opts Status, headers, and abort signal; see {@link NDJSONResponseOptions}.
264
+ * @returns A streaming `Response` (default status `200`) with NDJSON headers applied.
249
265
  */
250
266
  export function ndjsonResponse(source, opts = {}) {
251
267
  const stream = ndjsonStream(source, opts);
@@ -92,6 +92,10 @@ export interface SubdomainsResult {
92
92
  * // => { baseDomain: "bar.s3.amazonaws.com", subdomain: "foo", labels: ["foo"] }
93
93
  * ```
94
94
  *
95
+ * @param hostname - Hostname to split (lowercased; a trailing FQDN dot is stripped).
96
+ * @param opts - Optional pinned `baseDomain`, `extraSuffixes`, and the `production` staleness gate.
97
+ * @returns The registrable {@link SubdomainsResult} (`baseDomain`, `subdomain`, `labels`).
98
+ * @throws {Error} On an empty hostname, a host outside a declared `baseDomain`, or a stale PSL snapshot in production.
95
99
  * @since 0.24.0
96
100
  */
97
101
  export declare function subdomains(hostname: string, opts?: SubdomainsOptions): SubdomainsResult;
@@ -93,6 +93,10 @@ export const MAX_SNAPSHOT_AGE_DAYS = 90;
93
93
  * // => { baseDomain: "bar.s3.amazonaws.com", subdomain: "foo", labels: ["foo"] }
94
94
  * ```
95
95
  *
96
+ * @param hostname - Hostname to split (lowercased; a trailing FQDN dot is stripped).
97
+ * @param opts - Optional pinned `baseDomain`, `extraSuffixes`, and the `production` staleness gate.
98
+ * @returns The registrable {@link SubdomainsResult} (`baseDomain`, `subdomain`, `labels`).
99
+ * @throws {Error} On an empty hostname, a host outside a declared `baseDomain`, or a stale PSL snapshot in production.
96
100
  * @since 0.24.0
97
101
  */
98
102
  export function subdomains(hostname, opts = {}) {
@@ -26,10 +26,17 @@
26
26
  * @since 0.27.0
27
27
  */
28
28
  export declare class TemporalClaimError extends Error {
29
+ /** Machine-readable failure code identifying which claim check failed. */
29
30
  readonly code: TemporalClaimErrorCode;
30
31
  constructor(code: TemporalClaimErrorCode, message: string);
31
32
  }
32
- /** @since 0.27.0 */
33
+ /**
34
+ * Failure codes for {@link TemporalClaimError}. `invalid_*` codes mean the
35
+ * claim (or option) was present but not a finite number; the remaining codes
36
+ * mean a well-formed claim failed its RFC 7519 time comparison.
37
+ *
38
+ * @since 0.27.0
39
+ */
33
40
  export type TemporalClaimErrorCode = "invalid_exp" | "token_expired" | "invalid_nbf" | "token_not_yet_valid" | "invalid_iat" | "iat_in_future" | "invalid_clock_skew";
34
41
  /**
35
42
  * Subset of a JWT-style payload that participates in temporal validation.
@@ -39,11 +46,19 @@ export type TemporalClaimErrorCode = "invalid_exp" | "token_expired" | "invalid_
39
46
  * @since 0.27.0
40
47
  */
41
48
  export interface TemporalClaims {
49
+ /** Expiration time (RFC 7519 `exp`), unix seconds. Validated when present. */
42
50
  readonly exp?: unknown;
51
+ /** Not-before time (RFC 7519 `nbf`), unix seconds. Validated when present. */
43
52
  readonly nbf?: unknown;
53
+ /** Issued-at time (RFC 7519 `iat`), unix seconds. Rejected if in the future. */
44
54
  readonly iat?: unknown;
45
55
  }
46
- /** @since 0.27.0 */
56
+ /**
57
+ * Options for {@link assertTemporalClaims}: the reference clock and the
58
+ * symmetric clock-skew tolerance.
59
+ *
60
+ * @since 0.27.0
61
+ */
47
62
  export interface AssertTemporalClaimsOptions {
48
63
  /** Current unix-seconds timestamp. Injectable for tests. */
49
64
  readonly now: number;
@@ -65,6 +80,11 @@ export interface AssertTemporalClaimsOptions {
65
80
  * - `iat` rejected when `iat - skew > now` (issued in the future — the
66
81
  * issuer's clock is wrong, or someone pre-issued a token).
67
82
  *
83
+ * @param claims Decoded payload; only the claims that are present are checked.
84
+ * @param opts Reference clock (`now`, unix seconds) and optional
85
+ * `clockSkewSeconds` tolerance (defaults to `0`).
86
+ * @throws TemporalClaimError on the first failing check, including malformed
87
+ * (non-finite) claim values or an invalid `now` / negative skew.
68
88
  * @since 0.27.0
69
89
  */
70
90
  export declare function assertTemporalClaims(claims: TemporalClaims, opts: AssertTemporalClaimsOptions): void;
@@ -26,6 +26,7 @@
26
26
  * @since 0.27.0
27
27
  */
28
28
  export class TemporalClaimError extends Error {
29
+ /** Machine-readable failure code identifying which claim check failed. */
29
30
  code;
30
31
  constructor(code, message) {
31
32
  super(message);
@@ -48,6 +49,11 @@ function isFiniteNumber(v) {
48
49
  * - `iat` rejected when `iat - skew > now` (issued in the future — the
49
50
  * issuer's clock is wrong, or someone pre-issued a token).
50
51
  *
52
+ * @param claims Decoded payload; only the claims that are present are checked.
53
+ * @param opts Reference clock (`now`, unix seconds) and optional
54
+ * `clockSkewSeconds` tolerance (defaults to `0`).
55
+ * @throws TemporalClaimError on the first failing check, including malformed
56
+ * (non-finite) claim values or an invalid `now` / negative skew.
51
57
  * @since 0.27.0
52
58
  */
53
59
  export function assertTemporalClaims(claims, opts) {
package/dist/tracing.d.ts CHANGED
@@ -47,13 +47,18 @@ export type TracingAttributes = Record<string, TracingAttributeValue>;
47
47
  * `Span` (extra OTel methods are ignored).
48
48
  */
49
49
  export interface TracingSpan {
50
+ /** Set a single attribute on the span. */
50
51
  setAttribute(key: string, value: TracingAttributeValue): void;
52
+ /** Optional bulk attribute setter; DaloyJS falls back to `setAttribute` per key. */
51
53
  setAttributes?(attrs: TracingAttributes): void;
54
+ /** Set the span status (e.g. {@link TRACING_SPAN_STATUS_ERROR} on failure). */
52
55
  setStatus(status: {
53
56
  code: number;
54
57
  message?: string;
55
58
  }): void;
59
+ /** Optional exception recorder; called with the thrown error on failures. */
56
60
  recordException?(err: unknown): void;
61
+ /** End the span. DaloyJS calls this exactly once per request. */
57
62
  end(endTime?: number): void;
58
63
  }
59
64
  /** Options passed to {@link TracingTracer.startSpan}. */
@@ -65,6 +70,7 @@ export interface TracingStartSpanOptions {
65
70
  }
66
71
  /** Minimum tracer surface DaloyJS needs. Compatible with `@opentelemetry/api`'s `Tracer`. */
67
72
  export interface TracingTracer {
73
+ /** Create a span; `context` carries the extracted upstream parent context, if any. */
68
74
  startSpan(name: string, options?: TracingStartSpanOptions, context?: unknown): TracingSpan;
69
75
  }
70
76
  /** Options for {@link otelTracing}. */
@@ -101,5 +107,11 @@ export interface OtelTracingOptions {
101
107
  * Middleware that wraps every request in an OpenTelemetry-compatible span.
102
108
  * Pass any tracer that matches the {@link TracingTracer} surface; DaloyJS
103
109
  * does not import `@opentelemetry/api` itself.
110
+ *
111
+ * @param opts Tracer plus optional span naming, attribute, and context
112
+ * extraction hooks; see {@link OtelTracingOptions}.
113
+ * @returns A {@link Hooks} object that starts a SERVER span per request,
114
+ * exposes it on `ctx.state[stateKey]`, records exceptions, marks 5xx
115
+ * responses as errors, and ends the span exactly once on send.
104
116
  */
105
117
  export declare function otelTracing(opts: OtelTracingOptions): Hooks;
package/dist/tracing.js CHANGED
@@ -72,6 +72,12 @@ function endOnce(entry, attrs) {
72
72
  * Middleware that wraps every request in an OpenTelemetry-compatible span.
73
73
  * Pass any tracer that matches the {@link TracingTracer} surface; DaloyJS
74
74
  * does not import `@opentelemetry/api` itself.
75
+ *
76
+ * @param opts Tracer plus optional span naming, attribute, and context
77
+ * extraction hooks; see {@link OtelTracingOptions}.
78
+ * @returns A {@link Hooks} object that starts a SERVER span per request,
79
+ * exposes it on `ctx.state[stateKey]`, records exceptions, marks 5xx
80
+ * responses as errors, and ends the span exactly once on send.
75
81
  */
76
82
  export function otelTracing(opts) {
77
83
  const stateKey = opts.stateKey ?? "otelSpan";
package/dist/types.d.ts CHANGED
@@ -63,9 +63,13 @@ export type PathParams<P extends string> = {
63
63
  * @since 0.1.0
64
64
  */
65
65
  export interface RequestSchemas {
66
+ /** Validator for path parameters. Without it, `ctx.params` is raw {@link PathParams} strings. */
66
67
  params?: StandardSchemaV1;
68
+ /** Validator for the parsed query string. Without it, `ctx.query` is a raw string record. */
67
69
  query?: StandardSchemaV1;
70
+ /** Validator for request headers. Without it, `ctx.headers` is a raw string record. */
68
71
  headers?: StandardSchemaV1;
72
+ /** Validator for the parsed request body. Without it, `ctx.body` is `unknown`. Prefer `.strict()` object schemas so unexpected keys are rejected. */
69
73
  body?: StandardSchemaV1;
70
74
  }
71
75
  /** Infer the validated output of a Standard Schema validator, or `undefined` when no schema is present. */
@@ -110,12 +114,16 @@ export type InferRequest<R extends RequestSchemas | undefined, P extends string>
110
114
  * @since 0.1.0
111
115
  */
112
116
  export interface ResponseSpec {
117
+ /** Human-readable description emitted into the OpenAPI response object. Required. */
113
118
  description: string;
119
+ /** Response-body validator; handler return values are checked against it when `AppOptions.validateResponses` is on. */
114
120
  body?: StandardSchemaV1;
121
+ /** Documented response headers keyed by header name; surfaced in the OpenAPI document. */
115
122
  headers?: Record<string, {
116
123
  description?: string;
117
124
  schema?: StandardSchemaV1;
118
125
  }>;
126
+ /** Named example payloads emitted into OpenAPI and served when `AppOptions.mockMode` is enabled. */
119
127
  examples?: Record<string, unknown>;
120
128
  }
121
129
  /**
@@ -216,9 +224,18 @@ export interface AppState {
216
224
  * @since 0.24.0
217
225
  */
218
226
  export type AuthScheme = "bearer" | "basic" | "jwt" | "jwk" | "webhook" | "session" | "apiKey";
219
- /** @since 0.24.0 */
227
+ /**
228
+ * Verified-identity envelope written to `ctx.state.auth` by the first-party
229
+ * auth helpers. The `scheme` discriminant keeps per-scheme logic (revocation
230
+ * lists, audit logs) from being applied to credentials issued by a different
231
+ * scheme (see {@link AuthScheme}).
232
+ *
233
+ * @since 0.24.0
234
+ */
220
235
  export interface AuthContext<TCredentials = unknown> {
236
+ /** Discriminant naming the auth helper that verified the request (e.g. `"jwt"`, `"session"`). */
221
237
  readonly scheme: AuthScheme;
238
+ /** The verified credential payload (decoded JWT claims, session record, ...); shape depends on the scheme. */
222
239
  readonly credentials: TCredentials;
223
240
  }
224
241
  /**
@@ -235,11 +252,15 @@ export interface AuthContext<TCredentials = unknown> {
235
252
  * @since 0.1.0
236
253
  */
237
254
  export interface BaseContext<P extends string, R extends RequestSchemas | undefined> {
255
+ /** The original web-standard `Request`. Its body stream may already be consumed when a `body` schema triggered parsing. */
238
256
  request: Request;
239
257
  /** Validated request data (or raw fallbacks if no schema). */
240
258
  params: InferRequest<R, P>["params"];
259
+ /** Validated query params; raw `Record<string, string | string[] | undefined>` without a schema. */
241
260
  query: InferRequest<R, P>["query"];
261
+ /** Validated request headers; raw `Record<string, string | undefined>` without a schema. */
242
262
  headers: InferRequest<R, P>["headers"];
263
+ /** Validated request body; `unknown` without a schema. Parsed prototype-pollution-safe (forbidden keys rejected). */
243
264
  body: InferRequest<R, P>["body"];
244
265
  /** Mutable per-request state. Plugin-augmented context lives here. */
245
266
  state: AppState & Record<string, unknown>;
@@ -269,9 +290,13 @@ export interface BaseContext<P extends string, R extends RequestSchemas | undefi
269
290
  * @since 0.1.0
270
291
  */
271
292
  export interface Hooks {
293
+ /** Runs first, before validation or context building. Receives the raw web-standard `Request`. */
272
294
  onRequest?: (req: Request) => void | Promise<void>;
295
+ /** Runs with the validated {@link BaseContext} before the handler. Returning a `Response` short-circuits the handler entirely (useful for auth guards). */
273
296
  beforeHandle?: (ctx: BaseContext<any, any>) => void | Response | Promise<void | Response>;
297
+ /** Runs after the handler with its raw return value. Return a non-`undefined` value to replace the result before serialization and response-schema validation. */
274
298
  afterHandle?: (ctx: BaseContext<any, any>, result: unknown) => void | unknown | Promise<void | unknown>;
299
+ /** Runs on the error path before serialization. `ctx` is `undefined` if the error occurred before context was built. Return a `Response` to replace the default RFC 9457 problem+json error response. */
275
300
  onError?: (err: unknown, ctx: BaseContext<any, any> | undefined) => void | Response | Promise<void | Response>;
276
301
  /**
277
302
  * Symmetric to `beforeHandle`, but for outgoing responses. Runs after the Response
@@ -281,6 +306,7 @@ export interface Hooks {
281
306
  * the existing response. Multiple `onSend` hooks compose pipeline-style.
282
307
  */
283
308
  onSend?: (res: Response, ctx: BaseContext<any, any> | undefined) => void | Response | Promise<void | Response>;
309
+ /** Fire-and-forget observer of the final outgoing `Response` (logging, metrics). Runs last; cannot alter the response. */
284
310
  onResponse?: (res: Response) => void | Promise<void>;
285
311
  }
286
312
  /**
@@ -312,14 +338,35 @@ export interface Hooks {
312
338
  * @since 0.1.0
313
339
  */
314
340
  export interface RouteDefinition<P extends PathString = PathString, M extends HttpMethod = HttpMethod, Req extends RequestSchemas | undefined = undefined, Res extends ResponsesMap = ResponsesMap> {
341
+ /** HTTP method to match (uppercase, e.g. `"GET"`). See {@link HttpMethod}. */
315
342
  method: M;
343
+ /** URL path pattern starting with `/`; `:name` segments become typed path params (e.g. `"/books/:id"`). */
316
344
  path: P;
345
+ /** Stable unique operation id for OpenAPI and the generated client (drives SDK method names). Omitted from the spec when unset. */
317
346
  operationId?: string;
347
+ /** One-line summary shown in OpenAPI docs UIs. */
318
348
  summary?: string;
349
+ /** Longer free-form description for the OpenAPI operation (CommonMark allowed). */
319
350
  description?: string;
351
+ /** OpenAPI tags used to group the operation in docs UIs; merged with {@link RouteMeta.tags}. */
320
352
  tags?: string[];
353
+ /** Emits `deprecated: true` on the OpenAPI operation. Set implicitly when {@link RouteDefinition.sunset} is present. */
321
354
  deprecated?: boolean;
355
+ /** Optional per-route API version label. Informational metadata only; not emitted into the OpenAPI document. */
322
356
  version?: string;
357
+ /**
358
+ * Acknowledge that this route's `2xx` responses intentionally carry no
359
+ * response body schema — an opaque, framework-controlled, or non-JSON body
360
+ * (a raw `Response`, an HTML page, a spec document, a proxied payload).
361
+ *
362
+ * Setting this suppresses the `security.response.bodySchemaMissing` boot
363
+ * warning and the `audit.response.bodySchema` `daloy doctor` finding for
364
+ * this route only. It documents intent; it does not add protection —
365
+ * response field-level stripping (OWASP API3) still does not run for a
366
+ * `2xx` response without a body schema, so never set this on a route whose
367
+ * handler builds JSON from domain objects.
368
+ */
369
+ acknowledgeNoResponseBodySchema?: boolean;
323
370
  /**
324
371
  * Mark the endpoint as scheduled for removal at a specific date (RFC 8594
325
372
  * "The Sunset HTTP Header Field"). Accepts an ISO-8601 string, any string
@@ -353,8 +400,11 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
353
400
  * @since 0.37.0
354
401
  */
355
402
  sunset?: string | Date;
403
+ /** Standard Schemas ({@link RequestSchemas}) validating `params`/`query`/`headers`/`body`. Parts without a schema arrive untyped; validation failures return 400/422 before the handler runs. */
356
404
  request?: Req;
405
+ /** Map of status code to {@link ResponseSpec}. Drives response-body validation, the handler's allowed return types, OpenAPI responses, and the typed client. */
357
406
  responses: Res;
407
+ /** Declarative auth requirement ({@link AuthSpec}); surfaces as the OpenAPI `security` requirement for this operation. */
358
408
  auth?: AuthSpec;
359
409
  /**
360
410
  * Per-route Content-Type allowlist. When the route declares a `body`
@@ -421,6 +471,7 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
421
471
  * @since 0.14.0
422
472
  */
423
473
  meta?: RouteMeta;
474
+ /** Per-route lifecycle {@link Hooks}. Run after global and group hooks in the pipeline. */
424
475
  hooks?: Hooks;
425
476
  /**
426
477
  * The route handler. Receives the typed, validated {@link BaseContext} and
@@ -455,14 +506,23 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
455
506
  * `handler` (no execution path on the producer side).
456
507
  */
457
508
  export interface CallbackOperation {
509
+ /** HTTP method the producer uses when invoking the callback URL. */
458
510
  method: HttpMethod;
511
+ /** Stable unique operation id for the callback operation in the OpenAPI document. */
459
512
  operationId?: string;
513
+ /** One-line summary shown in OpenAPI docs UIs. */
460
514
  summary?: string;
515
+ /** Longer free-form description for the callback operation. */
461
516
  description?: string;
517
+ /** OpenAPI tags grouping the callback operation in docs UIs. */
462
518
  tags?: string[];
519
+ /** Emits `deprecated: true` on the callback operation. */
463
520
  deprecated?: boolean;
521
+ /** Schemas describing the outgoing callback request (documentation only; never executed by the framework). */
464
522
  request?: RequestSchemas;
523
+ /** Responses the producer expects back from the consumer, keyed by status code. */
465
524
  responses: ResponsesMap;
525
+ /** Auth requirement the callback request is documented to carry ({@link AuthSpec}). */
466
526
  auth?: AuthSpec;
467
527
  }
468
528
  /**
@@ -495,14 +555,18 @@ export interface CallbackMap {
495
555
  * @since 0.14.0
496
556
  */
497
557
  export interface RouteExample {
558
+ /** One-line label for the example, surfaced as the OpenAPI example `summary`. */
498
559
  summary?: string;
560
+ /** Longer explanation of what the example demonstrates. */
499
561
  description?: string;
562
+ /** Sample inbound request. `body` is validated against the route's request body schema by `runContractTests()`. */
500
563
  request?: {
501
564
  params?: Record<string, string>;
502
565
  query?: Record<string, unknown>;
503
566
  headers?: Record<string, string>;
504
567
  body?: unknown;
505
568
  };
569
+ /** Sample response. `status` must be declared in the route's `responses`; `body` is validated against that status's schema. */
506
570
  response?: {
507
571
  status: number;
508
572
  body?: unknown;
@@ -526,9 +590,14 @@ export interface RouteExample {
526
590
  * @since 0.14.0
527
591
  */
528
592
  export interface RouteMeta {
593
+ /** Fallback operation summary; the route-level `summary` wins when both are set. */
529
594
  summary?: string;
595
+ /** Fallback operation description; the route-level `description` wins when both are set. */
530
596
  description?: string;
597
+ /** Extra OpenAPI tags, merged (deduplicated) with the route-level `tags`. */
531
598
  tags?: string[];
599
+ /** Named request/response examples ({@link RouteExample}); validated against the route's schemas by `runContractTests()`. */
532
600
  examples?: Record<string, RouteExample>;
601
+ /** Free-form vendor extensions emitted as `x-<key>` on the OpenAPI operation; keys are auto-prefixed with `x-` when missing. */
533
602
  extensions?: Record<string, unknown>;
534
603
  }
package/dist/waf.js CHANGED
@@ -80,7 +80,12 @@ const XSS_SIGNATURES = Object.freeze([
80
80
  /<script[\s\S]{0,40}?>/i,
81
81
  /<\/script\s*>/i,
82
82
  /javascript:\s*\S/i,
83
- /\bon(?:error|load|click|mouseover|focus|submit|toggle|animationstart)\s*=/i,
83
+ // Inline event-handler attributes. Explicit alternation (not `on\w+`) to
84
+ // avoid false-positives on benign params like `online=`/`once=`, but broadened
85
+ // well beyond the classic four to cover the paren-less handlers commonly used
86
+ // to evade keyword blocklists (pointer/focus/touch/wheel/toggle events — see
87
+ // the ES6-for-pentesters technique in the cure53-web-frontend-offense skill).
88
+ /\bon(?:error|load|click|dblclick|aux(?:click)?|contextmenu|mouse(?:over|enter|move|down|up|out|leave)|pointer(?:over|enter|down|up|move|rawupdate|leave)|touch(?:start|move|end)|focus(?:in|out)?|blur|input|change|submit|reset|toggle|beforetoggle|scroll|wheel|drag|drop|copy|cut|paste|play|playing|canplay|show|hashchange|popstate|pageshow|pagehide|message|animation(?:start|end|iteration)|transitionend|key(?:down|up|press)|load(?:start|end)|progress)\s*=/i,
84
89
  /<iframe[\s>]/i,
85
90
  /<img[\s\S]{0,80}?\bonerror\s*=/i,
86
91
  /<svg[\s\S]{0,40}?\bonload\s*=/i,
@@ -268,11 +273,26 @@ export function waf(opts = {}) {
268
273
  if (inspectQuery && url.search.length > 1) {
269
274
  // Scan both the raw query string and a best-effort decoded form so an
270
275
  // encoded payload (`%27%20OR%201=1`) is caught after normalization.
276
+ // This is a SINGLE decode on purpose: the framework's request path also
277
+ // decodes the query exactly once, so the WAF sees the same bytes the
278
+ // handler will. Recursive decoding is deliberately avoided — it would
279
+ // false-positive on values that legitimately contain percent-encoded
280
+ // text, and a double-encoded payload stays inert (`%3Cscript%3E`) all
281
+ // the way to the handler. See red-team-attacks-6 "DOCUMENTED LIMITATION".
271
282
  const raw = url.search.slice(1);
272
283
  scanValue(raw, "query", rules, scored);
273
284
  const decoded = safeDecode(raw);
274
285
  if (decoded !== raw)
275
286
  scanValue(decoded, "query", rules, scored);
287
+ // Additionally inspect each key/value the way the app's OWN query parser
288
+ // (`URLSearchParams`) decodes them: notably `+` becomes a space, which a
289
+ // plain `decodeURIComponent` does NOT do. Without this, `1+OR+1=1` slipped
290
+ // past the WAF while the handler still received `1 OR 1=1` (a parser
291
+ // differential — the WAF must inspect the bytes the app actually parses).
292
+ for (const [k, v] of url.searchParams) {
293
+ scanValue(k, "query", rules, scored);
294
+ scanValue(v, "query", rules, scored);
295
+ }
276
296
  }
277
297
  if (headerAllowlist.length > 0) {
278
298
  for (const name of headerAllowlist) {
@@ -122,6 +122,7 @@ export interface WebhookDeadLetter {
122
122
  * @since 0.37.0
123
123
  */
124
124
  export interface WebhookDeadLetterSink {
125
+ /** Persist a permanently-failed delivery. May be async; the sender awaits it. */
125
126
  add(letter: WebhookDeadLetter): void | Promise<void>;
126
127
  }
127
128
  /**
@@ -258,6 +259,11 @@ export interface WebhookSenderOptions {
258
259
  * const result = await send({ url, eventType: "user.created", payload: { id } });
259
260
  * ```
260
261
  *
262
+ * @param options - Signing secret plus delivery policy; see
263
+ * {@link WebhookSenderOptions} for the per-field defaults.
264
+ * @returns A `send(event)` function that resolves to a
265
+ * {@link WebhookDeliveryResult}.
266
+ * @throws Error at construction when `secret` is missing or empty.
261
267
  * @since 0.37.0
262
268
  */
263
269
  export declare function createWebhookSender(options: WebhookSenderOptions): (event: WebhookEvent) => Promise<WebhookDeliveryResult>;
@@ -140,6 +140,11 @@ function randomId() {
140
140
  * const result = await send({ url, eventType: "user.created", payload: { id } });
141
141
  * ```
142
142
  *
143
+ * @param options - Signing secret plus delivery policy; see
144
+ * {@link WebhookSenderOptions} for the per-field defaults.
145
+ * @returns A `send(event)` function that resolves to a
146
+ * {@link WebhookDeliveryResult}.
147
+ * @throws Error at construction when `secret` is missing or empty.
143
148
  * @since 0.37.0
144
149
  */
145
150
  export function createWebhookSender(options) {