@daloyjs/core 0.36.0 → 0.38.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -3
  3. package/bin/daloy.mjs +2 -0
  4. package/dist/adapters/bun.js +16 -9
  5. package/dist/adapters/deno.js +7 -1
  6. package/dist/adapters/node.d.ts +25 -0
  7. package/dist/adapters/node.js +32 -0
  8. package/dist/app.d.ts +200 -6
  9. package/dist/app.js +235 -50
  10. package/dist/asyncapi.d.ts +98 -0
  11. package/dist/asyncapi.js +212 -0
  12. package/dist/auto-ban.d.ts +205 -0
  13. package/dist/auto-ban.js +222 -0
  14. package/dist/bot-guard.d.ts +209 -0
  15. package/dist/bot-guard.js +291 -0
  16. package/dist/cli.d.ts +8 -0
  17. package/dist/cli.js +113 -4
  18. package/dist/client.d.ts +23 -0
  19. package/dist/client.js +16 -0
  20. package/dist/concurrency-limit.d.ts +135 -0
  21. package/dist/concurrency-limit.js +254 -0
  22. package/dist/docs.d.ts +57 -6
  23. package/dist/docs.js +34 -3
  24. package/dist/errors.d.ts +43 -0
  25. package/dist/errors.js +57 -0
  26. package/dist/fetch-guard.js +4 -0
  27. package/dist/fetch-resilience.d.ts +295 -0
  28. package/dist/fetch-resilience.js +485 -0
  29. package/dist/geo-block.d.ts +184 -0
  30. package/dist/geo-block.js +153 -0
  31. package/dist/hashing.d.ts +2 -1
  32. package/dist/hashing.js +12 -1
  33. package/dist/http-signatures.d.ts +303 -0
  34. package/dist/http-signatures.js +782 -0
  35. package/dist/idempotency.d.ts +204 -0
  36. package/dist/idempotency.js +341 -0
  37. package/dist/index.d.ts +39 -5
  38. package/dist/index.js +19 -2
  39. package/dist/ip-reputation.d.ts +198 -0
  40. package/dist/ip-reputation.js +253 -0
  41. package/dist/jwk.d.ts +15 -0
  42. package/dist/jwk.js +24 -2
  43. package/dist/load-shedding.d.ts +5 -0
  44. package/dist/logger.js +6 -2
  45. package/dist/metrics.d.ts +208 -0
  46. package/dist/metrics.js +452 -0
  47. package/dist/middleware.js +0 -10
  48. package/dist/mtls.d.ts +266 -0
  49. package/dist/mtls.js +488 -0
  50. package/dist/multipart.js +1 -1
  51. package/dist/openapi-diff.d.ts +79 -0
  52. package/dist/openapi-diff.js +246 -0
  53. package/dist/openapi.js +4 -1
  54. package/dist/pagination.d.ts +210 -0
  55. package/dist/pagination.js +353 -0
  56. package/dist/rate-limit-redis.d.ts +8 -0
  57. package/dist/rate-limit-redis.js +8 -0
  58. package/dist/request-decompression.d.ts +200 -0
  59. package/dist/request-decompression.js +363 -0
  60. package/dist/response-cache.d.ts +205 -0
  61. package/dist/response-cache.js +374 -0
  62. package/dist/router.d.ts +22 -0
  63. package/dist/router.js +64 -7
  64. package/dist/safe-redirect.d.ts +2 -2
  65. package/dist/safe-redirect.js +3 -8
  66. package/dist/sbom.cdx.json +9 -9
  67. package/dist/sbom.spdx.json +5 -5
  68. package/dist/scheduler.d.ts +315 -0
  69. package/dist/scheduler.js +546 -0
  70. package/dist/security.d.ts +61 -7
  71. package/dist/security.js +75 -8
  72. package/dist/session.js +3 -3
  73. package/dist/types.d.ts +33 -0
  74. package/dist/waf.d.ts +213 -0
  75. package/dist/waf.js +334 -0
  76. package/dist/webhook-delivery.d.ts +263 -0
  77. package/dist/webhook-delivery.js +311 -0
  78. package/dist/websocket.d.ts +52 -0
  79. package/dist/websocket.js +13 -0
  80. package/package.json +79 -3
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Outbound webhook delivery — the sending counterpart to the inbound
3
+ * {@link verifyWebhookSignature} / {@link signWebhookPayload} helpers.
4
+ *
5
+ * Where the inbound helpers answer *“is this webhook I received
6
+ * authentic?”*, this module answers *“how do I reliably and
7
+ * securely deliver a webhook to someone else?”* — the three things a
8
+ * production webhook sender needs:
9
+ *
10
+ * - **Signed delivery with timestamped signatures.** Every request carries
11
+ * an HMAC signature computed over `"<timestamp>.<body>"` (the Stripe /
12
+ * Standard Webhooks convention), plus an idempotency id and a timestamp
13
+ * header, so the receiver can authenticate the payload and reject
14
+ * replays with {@link verifyWebhookSignature}.
15
+ * - **Retry with backoff.** Transient failures (network errors, timeouts,
16
+ * `408` / `429` / `5xx`) are retried with exponential backoff and full
17
+ * jitter, honouring a `Retry-After` header. The signature is computed
18
+ * **once** so every retry carries the same id and signature — the
19
+ * receiver can dedupe on the id.
20
+ * - **Dead-letter semantics.** When every attempt is exhausted (or the
21
+ * upstream returns a permanent `4xx`), the failed delivery is handed to
22
+ * a {@link WebhookDeadLetterSink} for later inspection or replay instead
23
+ * of being silently dropped.
24
+ *
25
+ * Delivery is **SSRF-hardened by default**: the transport defaults to
26
+ * {@link fetchGuard}, so a webhook URL that resolves to cloud-metadata or
27
+ * an internal address is refused before any bytes are sent. Pass your own
28
+ * `fetch` (e.g. `fetchGuard({ allowPrivate: true })` or a
29
+ * {@link resilientFetch}) to change that posture deliberately.
30
+ *
31
+ * ```ts
32
+ * import { createWebhookSender, MemoryWebhookDeadLetterSink } from "@daloyjs/core";
33
+ *
34
+ * const deadLetters = new MemoryWebhookDeadLetterSink();
35
+ * const send = createWebhookSender({ secret: process.env.WEBHOOK_SECRET!, deadLetter: deadLetters });
36
+ *
37
+ * const result = await send({
38
+ * url: "https://example.com/hooks",
39
+ * eventType: "invoice.paid",
40
+ * payload: { id: "in_123", amount: 4200 },
41
+ * });
42
+ * if (!result.ok) {
43
+ * // result.deadLettered === true; inspect deadLetters.list()
44
+ * }
45
+ * ```
46
+ *
47
+ * @module
48
+ * @since 0.37.0
49
+ */
50
+ import { signWebhookPayload } from "./security.js";
51
+ import { fetchGuard } from "./fetch-guard.js";
52
+ /**
53
+ * An in-memory, bounded {@link WebhookDeadLetterSink} suitable for tests
54
+ * and single-process apps. Holds the most recent `capacity` dead letters
55
+ * (default `1000`) in a ring buffer; older entries are evicted.
56
+ *
57
+ * @since 0.37.0
58
+ */
59
+ export class MemoryWebhookDeadLetterSink {
60
+ #capacity;
61
+ #items = [];
62
+ constructor(capacity = 1000) {
63
+ if (!Number.isInteger(capacity) || capacity < 1) {
64
+ throw new RangeError("MemoryWebhookDeadLetterSink: capacity must be a positive integer");
65
+ }
66
+ this.#capacity = capacity;
67
+ }
68
+ /** Append a dead letter, evicting the oldest if at capacity. */
69
+ add(letter) {
70
+ this.#items.push(letter);
71
+ if (this.#items.length > this.#capacity)
72
+ this.#items.shift();
73
+ }
74
+ /** A snapshot of the currently-held dead letters, oldest first. */
75
+ list() {
76
+ return [...this.#items];
77
+ }
78
+ /** Remove and return every held dead letter (e.g. for a replay sweep). */
79
+ drain() {
80
+ const out = this.#items;
81
+ this.#items = [];
82
+ return out;
83
+ }
84
+ /** The number of dead letters currently held. */
85
+ get size() {
86
+ return this.#items.length;
87
+ }
88
+ }
89
+ const DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
90
+ function defaultSleep(ms) {
91
+ if (ms <= 0)
92
+ return Promise.resolve();
93
+ return new Promise((resolve) => {
94
+ const timer = setTimeout(resolve, ms);
95
+ timer.unref?.();
96
+ });
97
+ }
98
+ function parseRetryAfter(value, now) {
99
+ if (!value)
100
+ return undefined;
101
+ const trimmed = value.trim();
102
+ if (/^\d+$/.test(trimmed))
103
+ return Number(trimmed) * 1000;
104
+ const dateMs = Date.parse(trimmed);
105
+ if (!Number.isNaN(dateMs))
106
+ return Math.max(0, dateMs - now);
107
+ return undefined;
108
+ }
109
+ /** Serialise a payload to body bytes + a default content type. */
110
+ function encodePayload(payload) {
111
+ if (payload instanceof Uint8Array) {
112
+ return { bytes: payload, contentType: "application/octet-stream" };
113
+ }
114
+ if (typeof payload === "string") {
115
+ return { bytes: new TextEncoder().encode(payload), contentType: "application/json" };
116
+ }
117
+ return {
118
+ bytes: new TextEncoder().encode(JSON.stringify(payload ?? null)),
119
+ contentType: "application/json",
120
+ };
121
+ }
122
+ function randomId() {
123
+ const c = globalThis.crypto;
124
+ if (c?.randomUUID)
125
+ return c.randomUUID();
126
+ // Web-Crypto is mandatory on every runtime Daloy supports; this is an
127
+ // unreachable last-resort guard so a missing global never throws.
128
+ throw new Error("WebCrypto unavailable: cannot generate a webhook id");
129
+ }
130
+ /**
131
+ * Build a webhook sender bound to a signing secret and delivery policy.
132
+ * The returned `send(event)` function signs, delivers, retries, and
133
+ * dead-letters a single {@link WebhookEvent}, resolving to a
134
+ * {@link WebhookDeliveryResult} (it does not throw on ordinary delivery
135
+ * failure).
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const send = createWebhookSender({ secret: process.env.WEBHOOK_SECRET! });
140
+ * const result = await send({ url, eventType: "user.created", payload: { id } });
141
+ * ```
142
+ *
143
+ * @since 0.37.0
144
+ */
145
+ export function createWebhookSender(options) {
146
+ if (options.secret === undefined || options.secret === null || options.secret === "") {
147
+ throw new Error("createWebhookSender(): a non-empty signing secret is required");
148
+ }
149
+ const algorithm = options.algorithm ?? "sha256";
150
+ const transport = options.fetch ?? fetchGuard();
151
+ const maxAttempts = options.maxAttempts ?? 5;
152
+ const retryDelayMs = options.retryDelayMs ?? 500;
153
+ const maxRetryDelayMs = options.maxRetryDelayMs ?? 30_000;
154
+ const backoffFactor = options.backoffFactor ?? 2;
155
+ const jitter = options.jitter ?? true;
156
+ const timeoutMs = options.timeoutMs ?? 10_000;
157
+ const respectRetryAfter = options.respectRetryAfter ?? true;
158
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
159
+ throw new RangeError("createWebhookSender(): maxAttempts must be a positive integer");
160
+ }
161
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
162
+ throw new RangeError("createWebhookSender(): timeoutMs must be a non-negative number");
163
+ }
164
+ const retryStatuses = new Set(options.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES);
165
+ const idHeader = options.idHeader ?? "webhook-id";
166
+ const timestampHeader = options.timestampHeader ?? "webhook-timestamp";
167
+ const signatureHeader = options.signatureHeader ?? "webhook-signature";
168
+ const eventTypeHeader = options.eventTypeHeader ?? "webhook-event-type";
169
+ const userAgent = options.userAgent ?? "DaloyJS-Webhook/1.0";
170
+ const now = options.now ?? Date.now;
171
+ const sleep = options.sleep ?? defaultSleep;
172
+ function backoffFor(attempt, response) {
173
+ if (respectRetryAfter && response) {
174
+ const fromHeader = parseRetryAfter(response.headers.get("retry-after"), now());
175
+ if (fromHeader !== undefined)
176
+ return Math.min(maxRetryDelayMs, fromHeader);
177
+ }
178
+ const exp = retryDelayMs * backoffFactor ** (attempt - 1);
179
+ const capped = Math.min(maxRetryDelayMs, exp);
180
+ // Backoff jitter spreads load; it is not a security primitive.
181
+ return jitter ? Math.random() * capped : capped; // daloy-allow-weak-random: backoff jitter is not a security primitive
182
+ }
183
+ async function attemptOnce(url, headers, body) {
184
+ const controller = new AbortController();
185
+ let timer;
186
+ if (timeoutMs > 0) {
187
+ timer = setTimeout(() => controller.abort(), timeoutMs);
188
+ timer.unref?.();
189
+ }
190
+ try {
191
+ const response = await transport(url, {
192
+ method: "POST",
193
+ headers,
194
+ body: body,
195
+ signal: controller.signal,
196
+ });
197
+ return { response };
198
+ }
199
+ catch (error) {
200
+ return { error };
201
+ }
202
+ finally {
203
+ if (timer)
204
+ clearTimeout(timer);
205
+ }
206
+ }
207
+ return async function send(event) {
208
+ const id = event.id ?? randomId();
209
+ const { bytes, contentType: defaultContentType } = encodePayload(event.payload);
210
+ const contentType = event.contentType ?? defaultContentType;
211
+ const timestamp = Math.floor(now() / 1000);
212
+ // Sign ONCE — every retry carries the same id + signature so the
213
+ // receiver can dedupe and the timestamp stays stable.
214
+ const signature = await signWebhookPayload({
215
+ payload: bytes,
216
+ secret: options.secret,
217
+ algorithm,
218
+ timestamp,
219
+ });
220
+ const baseHeaders = {
221
+ "content-type": contentType,
222
+ "user-agent": userAgent,
223
+ [idHeader]: id,
224
+ [timestampHeader]: String(timestamp),
225
+ [signatureHeader]: `${algorithm}=${signature}`,
226
+ };
227
+ if (event.eventType !== undefined)
228
+ baseHeaders[eventTypeHeader] = event.eventType;
229
+ // Caller headers are merged last but cannot clobber signature headers.
230
+ const reserved = new Set([
231
+ "content-type",
232
+ idHeader.toLowerCase(),
233
+ timestampHeader.toLowerCase(),
234
+ signatureHeader.toLowerCase(),
235
+ eventTypeHeader.toLowerCase(),
236
+ ]);
237
+ for (const [k, v] of Object.entries(event.headers ?? {})) {
238
+ if (!reserved.has(k.toLowerCase()))
239
+ baseHeaders[k] = v;
240
+ }
241
+ let lastStatus;
242
+ let lastResponse;
243
+ let lastError;
244
+ let madeAttempts = 0;
245
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
246
+ madeAttempts = attempt;
247
+ const { response, error } = await attemptOnce(event.url, baseHeaders, bytes);
248
+ if (response) {
249
+ lastResponse = response;
250
+ lastStatus = response.status;
251
+ lastError = undefined;
252
+ if (response.ok) {
253
+ options.onAttempt?.({ id, attempt, status: response.status, willRetry: false });
254
+ return { ok: true, id, eventType: event.eventType, attempts: attempt, status: response.status, response, deadLettered: false };
255
+ }
256
+ const retryable = retryStatuses.has(response.status) && attempt < maxAttempts;
257
+ const delayMs = retryable ? backoffFor(attempt, response) : undefined;
258
+ options.onAttempt?.({ id, attempt, status: response.status, willRetry: retryable, delayMs });
259
+ if (!retryable)
260
+ break;
261
+ await sleep(delayMs);
262
+ continue;
263
+ }
264
+ // Thrown error. An SSRF refusal is a permanent decision about the
265
+ // target and is never retried; a per-attempt timeout (our own abort)
266
+ // and ordinary network errors are transient and retried.
267
+ lastError = error;
268
+ lastResponse = undefined;
269
+ lastStatus = undefined;
270
+ const isSsrf = error instanceof Error && error.name === "SsrfBlockedError";
271
+ const retryable = !isSsrf && attempt < maxAttempts;
272
+ const delayMs = retryable ? backoffFor(attempt) : undefined;
273
+ options.onAttempt?.({ id, attempt, error, willRetry: retryable, delayMs });
274
+ if (!retryable)
275
+ break;
276
+ await sleep(delayMs);
277
+ }
278
+ // Exhausted / permanent failure → dead-letter.
279
+ let deadLettered = false;
280
+ if (options.deadLetter) {
281
+ await options.deadLetter.add({
282
+ id,
283
+ url: event.url,
284
+ ...(event.eventType !== undefined ? { eventType: event.eventType } : {}),
285
+ payload: bytes,
286
+ contentType,
287
+ attempts: madeAttempts,
288
+ ...(lastStatus !== undefined ? { lastStatus } : {}),
289
+ ...(lastError !== undefined ? { lastError: lastError instanceof Error ? lastError.message : String(lastError) } : {}),
290
+ timestamp,
291
+ failedAt: now(),
292
+ });
293
+ deadLettered = true;
294
+ }
295
+ const result = {
296
+ ok: false,
297
+ id,
298
+ attempts: madeAttempts,
299
+ deadLettered,
300
+ };
301
+ if (event.eventType !== undefined)
302
+ result.eventType = event.eventType;
303
+ if (lastStatus !== undefined)
304
+ result.status = lastStatus;
305
+ if (lastResponse !== undefined)
306
+ result.response = lastResponse;
307
+ if (lastError !== undefined)
308
+ result.error = lastError;
309
+ return result;
310
+ };
311
+ }
@@ -84,6 +84,40 @@ export interface WebSocketContext<P extends string = string, S = AppState> {
84
84
  /** Subprotocols offered by the client (parsed from `Sec-WebSocket-Protocol`). */
85
85
  protocols: string[];
86
86
  }
87
+ /**
88
+ * Optional contract/documentation metadata for a WebSocket route, mirroring
89
+ * the HTTP route `meta` block. Consumed by the built-in AsyncAPI generator
90
+ * ({@link generateAsyncAPI}) to describe the channel, its operations, and the
91
+ * payloads exchanged over the socket. Purely descriptive — it never changes
92
+ * runtime behavior or the RFC 6455 handshake.
93
+ *
94
+ * @since 0.37.0
95
+ */
96
+ export interface WebSocketMeta {
97
+ /** Short channel summary surfaced as the AsyncAPI channel/operation summary. */
98
+ summary?: string;
99
+ /** Longer CommonMark description for the channel. */
100
+ description?: string;
101
+ /** Tags applied to the generated AsyncAPI operations. */
102
+ tags?: string[];
103
+ /**
104
+ * Schema describing messages the **server sends to clients** (outbound).
105
+ * Surfaced as the payload of the AsyncAPI `send` operation. Falls back to no
106
+ * outbound message when omitted.
107
+ */
108
+ send?: StandardSchemaV1;
109
+ /**
110
+ * Schema describing messages the **server receives from clients** (inbound).
111
+ * Surfaced as the payload of the AsyncAPI `receive` operation. Defaults to
112
+ * {@link WebSocketHandler.request}'s `body` schema when omitted.
113
+ */
114
+ receive?: StandardSchemaV1;
115
+ /**
116
+ * Stable identifier base used to derive the AsyncAPI `operationId`s and
117
+ * channel name for this route. Defaults to a slug derived from the path.
118
+ */
119
+ operationId?: string;
120
+ }
87
121
  /**
88
122
  * User-supplied WebSocket lifecycle callbacks.
89
123
  *
@@ -98,6 +132,13 @@ export interface WebSocketHandler<P extends string = string, S = AppState, TData
98
132
  request?: {
99
133
  body?: StandardSchemaV1;
100
134
  };
135
+ /**
136
+ * Optional contract/documentation metadata consumed by the built-in
137
+ * AsyncAPI generator ({@link generateAsyncAPI}). Purely descriptive.
138
+ *
139
+ * @since 0.37.0
140
+ */
141
+ meta?: WebSocketMeta;
101
142
  /** Close the connection when queued outbound bytes exceed backpressureLimit. Default: true. */
102
143
  closeOnBackpressureLimit?: boolean;
103
144
  /** Maximum queued outbound bytes before backpressure handling triggers. Default: 1 MiB. */
@@ -224,6 +265,17 @@ export declare class WebSocketRegistry {
224
265
  private entries;
225
266
  add(path: PathString, handler: WebSocketHandler<any, any, any>, createState?: WebSocketStateFactory, options?: NormalizedWebSocketOptions): void;
226
267
  find(pathname: string): RouteMatch<WebSocketRouteEntry> | undefined;
268
+ /**
269
+ * List every registered WebSocket route entry in registration order.
270
+ *
271
+ * Returns a shallow copy so callers (the AsyncAPI generator, introspection
272
+ * tooling) cannot mutate the registry's internal array. The entry objects
273
+ * themselves are shared by reference and must be treated as read-only.
274
+ *
275
+ * @returns A new array of the registered {@link WebSocketRouteEntry} values.
276
+ * @since 0.37.0
277
+ */
278
+ list(): WebSocketRouteEntry[];
227
279
  get size(): number;
228
280
  runtimeOptions(): NormalizedWebSocketOptions;
229
281
  }
package/dist/websocket.js CHANGED
@@ -227,6 +227,19 @@ export class WebSocketRegistry {
227
227
  find(pathname) {
228
228
  return this.router.find("GET", pathname);
229
229
  }
230
+ /**
231
+ * List every registered WebSocket route entry in registration order.
232
+ *
233
+ * Returns a shallow copy so callers (the AsyncAPI generator, introspection
234
+ * tooling) cannot mutate the registry's internal array. The entry objects
235
+ * themselves are shared by reference and must be treated as read-only.
236
+ *
237
+ * @returns A new array of the registered {@link WebSocketRouteEntry} values.
238
+ * @since 0.37.0
239
+ */
240
+ list() {
241
+ return [...this.entries];
242
+ }
230
243
  get size() {
231
244
  return this._size;
232
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -85,6 +85,14 @@
85
85
  "types": "./dist/openapi.d.ts",
86
86
  "import": "./dist/openapi.js"
87
87
  },
88
+ "./openapi-diff": {
89
+ "types": "./dist/openapi-diff.d.ts",
90
+ "import": "./dist/openapi-diff.js"
91
+ },
92
+ "./asyncapi": {
93
+ "types": "./dist/asyncapi.d.ts",
94
+ "import": "./dist/asyncapi.js"
95
+ },
88
96
  "./docs": {
89
97
  "types": "./dist/docs.d.ts",
90
98
  "import": "./dist/docs.js"
@@ -121,6 +129,70 @@
121
129
  "types": "./dist/session.d.ts",
122
130
  "import": "./dist/session.js"
123
131
  },
132
+ "./idempotency": {
133
+ "types": "./dist/idempotency.d.ts",
134
+ "import": "./dist/idempotency.js"
135
+ },
136
+ "./response-cache": {
137
+ "types": "./dist/response-cache.d.ts",
138
+ "import": "./dist/response-cache.js"
139
+ },
140
+ "./pagination": {
141
+ "types": "./dist/pagination.d.ts",
142
+ "import": "./dist/pagination.js"
143
+ },
144
+ "./metrics": {
145
+ "types": "./dist/metrics.d.ts",
146
+ "import": "./dist/metrics.js"
147
+ },
148
+ "./fetch-resilience": {
149
+ "types": "./dist/fetch-resilience.d.ts",
150
+ "import": "./dist/fetch-resilience.js"
151
+ },
152
+ "./webhook-delivery": {
153
+ "types": "./dist/webhook-delivery.d.ts",
154
+ "import": "./dist/webhook-delivery.js"
155
+ },
156
+ "./scheduler": {
157
+ "types": "./dist/scheduler.d.ts",
158
+ "import": "./dist/scheduler.js"
159
+ },
160
+ "./mtls": {
161
+ "types": "./dist/mtls.d.ts",
162
+ "import": "./dist/mtls.js"
163
+ },
164
+ "./http-signatures": {
165
+ "types": "./dist/http-signatures.d.ts",
166
+ "import": "./dist/http-signatures.js"
167
+ },
168
+ "./auto-ban": {
169
+ "types": "./dist/auto-ban.d.ts",
170
+ "import": "./dist/auto-ban.js"
171
+ },
172
+ "./bot-guard": {
173
+ "types": "./dist/bot-guard.d.ts",
174
+ "import": "./dist/bot-guard.js"
175
+ },
176
+ "./ip-reputation": {
177
+ "types": "./dist/ip-reputation.d.ts",
178
+ "import": "./dist/ip-reputation.js"
179
+ },
180
+ "./geo-block": {
181
+ "types": "./dist/geo-block.d.ts",
182
+ "import": "./dist/geo-block.js"
183
+ },
184
+ "./concurrency-limit": {
185
+ "types": "./dist/concurrency-limit.d.ts",
186
+ "import": "./dist/concurrency-limit.js"
187
+ },
188
+ "./request-decompression": {
189
+ "types": "./dist/request-decompression.d.ts",
190
+ "import": "./dist/request-decompression.js"
191
+ },
192
+ "./waf": {
193
+ "types": "./dist/waf.d.ts",
194
+ "import": "./dist/waf.js"
195
+ },
124
196
  "./banner": {
125
197
  "types": "./dist/banner.d.ts",
126
198
  "import": "./dist/banner.js"
@@ -157,6 +229,7 @@
157
229
  "devDependencies": {
158
230
  "@hey-api/openapi-ts": "^0.97.1",
159
231
  "@types/node": "^25.7.0",
232
+ "fast-check": "^3.23.2",
160
233
  "prettier": "^3.8.3",
161
234
  "tsx": "^4.22.3",
162
235
  "typescript": "^6.0.3",
@@ -169,8 +242,8 @@
169
242
  "bench": "node --import tsx bench/router.bench.ts",
170
243
  "test": "node --import tsx --test tests/**/*.test.ts",
171
244
  "coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=90 --test-coverage-functions=90 tests/**/*.test.ts",
172
- "coverage:branches": "tsc -p tsconfig.coverage.json && node --test --experimental-test-coverage --test-coverage-include='dist-coverage/src/**' --test-coverage-branches=90 dist-coverage/tests/**/*.test.js",
173
- "typecheck": "tsc --noEmit",
245
+ "coverage:branches": "tsc -p tsconfig.coverage.json && node --test --experimental-test-coverage --test-coverage-include='dist-coverage/src/**' --test-coverage-branches=92 dist-coverage/tests/**/*.test.js",
246
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json",
174
247
  "format": "prettier --write .",
175
248
  "gen:openapi": "node --import tsx scripts/dump-openapi.ts",
176
249
  "gen:client": "openapi-ts",
@@ -189,6 +262,7 @@
189
262
  "verify:no-leaked-credentials": "node --import tsx scripts/verify-no-leaked-credentials.ts",
190
263
  "verify:no-leaky-agent-skills": "node --import tsx scripts/verify-no-leaky-agent-skills.ts",
191
264
  "verify:no-toxic-agent-skills": "node --import tsx scripts/verify-no-toxic-agent-skills.ts",
265
+ "verify:no-agent-config-autorun": "node --import tsx scripts/verify-no-agent-config-autorun.ts",
192
266
  "verify:no-invisible-unicode": "node --import tsx scripts/verify-no-invisible-unicode.ts",
193
267
  "verify:no-encoded-payloads": "node --import tsx scripts/verify-no-encoded-payloads.ts",
194
268
  "verify:no-registry-exfiltration": "node --import tsx scripts/verify-no-registry-exfiltration.ts",
@@ -205,6 +279,8 @@
205
279
  "verify:runtime-parity-audits": "node --import tsx scripts/verify-runtime-parity-audits.ts",
206
280
  "verify:routing-hardening-audits": "node --import tsx scripts/verify-routing-hardening-audits.ts",
207
281
  "verify:sbom": "node --import tsx scripts/verify-sbom.ts",
282
+ "verify:breaking-changes": "node --import tsx scripts/verify-breaking-changes.ts",
283
+ "verify:docs-links": "node --import tsx scripts/verify-docs-links.ts",
208
284
  "scan:staged-secrets": "node --import tsx scripts/scan-staged-secrets.ts",
209
285
  "hooks:install": "node --import tsx scripts/install-git-hooks.ts",
210
286
  "audit": "pnpm audit --prod",