@daloyjs/core 0.36.0 → 0.37.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 (77) hide show
  1. package/README.md +21 -2
  2. package/bin/daloy.mjs +2 -0
  3. package/dist/adapters/bun.js +16 -9
  4. package/dist/adapters/deno.js +7 -1
  5. package/dist/adapters/node.d.ts +11 -0
  6. package/dist/adapters/node.js +24 -0
  7. package/dist/app.d.ts +144 -1
  8. package/dist/app.js +208 -1
  9. package/dist/asyncapi.d.ts +98 -0
  10. package/dist/asyncapi.js +212 -0
  11. package/dist/auto-ban.d.ts +205 -0
  12. package/dist/auto-ban.js +222 -0
  13. package/dist/bot-guard.d.ts +209 -0
  14. package/dist/bot-guard.js +291 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.js +88 -4
  17. package/dist/concurrency-limit.d.ts +135 -0
  18. package/dist/concurrency-limit.js +254 -0
  19. package/dist/docs.d.ts +57 -6
  20. package/dist/docs.js +34 -3
  21. package/dist/errors.d.ts +20 -0
  22. package/dist/errors.js +27 -0
  23. package/dist/fetch-guard.js +4 -0
  24. package/dist/fetch-resilience.d.ts +295 -0
  25. package/dist/fetch-resilience.js +485 -0
  26. package/dist/geo-block.d.ts +184 -0
  27. package/dist/geo-block.js +153 -0
  28. package/dist/hashing.d.ts +2 -1
  29. package/dist/hashing.js +12 -1
  30. package/dist/http-signatures.d.ts +303 -0
  31. package/dist/http-signatures.js +782 -0
  32. package/dist/idempotency.d.ts +204 -0
  33. package/dist/idempotency.js +341 -0
  34. package/dist/index.d.ts +38 -4
  35. package/dist/index.js +18 -1
  36. package/dist/ip-reputation.d.ts +198 -0
  37. package/dist/ip-reputation.js +253 -0
  38. package/dist/jwk.d.ts +15 -0
  39. package/dist/jwk.js +24 -2
  40. package/dist/load-shedding.d.ts +5 -0
  41. package/dist/logger.js +6 -2
  42. package/dist/metrics.d.ts +208 -0
  43. package/dist/metrics.js +452 -0
  44. package/dist/middleware.js +0 -10
  45. package/dist/mtls.d.ts +266 -0
  46. package/dist/mtls.js +488 -0
  47. package/dist/multipart.js +1 -1
  48. package/dist/openapi-diff.d.ts +79 -0
  49. package/dist/openapi-diff.js +246 -0
  50. package/dist/openapi.js +4 -1
  51. package/dist/pagination.d.ts +210 -0
  52. package/dist/pagination.js +353 -0
  53. package/dist/rate-limit-redis.d.ts +8 -0
  54. package/dist/rate-limit-redis.js +8 -0
  55. package/dist/request-decompression.d.ts +200 -0
  56. package/dist/request-decompression.js +363 -0
  57. package/dist/response-cache.d.ts +205 -0
  58. package/dist/response-cache.js +374 -0
  59. package/dist/router.d.ts +22 -0
  60. package/dist/router.js +64 -7
  61. package/dist/safe-redirect.d.ts +2 -2
  62. package/dist/safe-redirect.js +3 -8
  63. package/dist/sbom.cdx.json +9 -9
  64. package/dist/sbom.spdx.json +5 -5
  65. package/dist/scheduler.d.ts +315 -0
  66. package/dist/scheduler.js +546 -0
  67. package/dist/security.d.ts +27 -7
  68. package/dist/security.js +27 -7
  69. package/dist/session.js +3 -3
  70. package/dist/types.d.ts +33 -0
  71. package/dist/waf.d.ts +213 -0
  72. package/dist/waf.js +334 -0
  73. package/dist/webhook-delivery.d.ts +263 -0
  74. package/dist/webhook-delivery.js +311 -0
  75. package/dist/websocket.d.ts +52 -0
  76. package/dist/websocket.js +13 -0
  77. package/package.json +76 -2
@@ -0,0 +1,295 @@
1
+ /**
2
+ * `resilientFetch()` — circuit breaker, retry-with-backoff, and per-call
3
+ * timeout for outbound `fetch`, designed to layer **on top of**
4
+ * {@link fetchGuard} (which only covers SSRF on egress).
5
+ *
6
+ * `fetchGuard()` answers "is this outbound address safe?". This module
7
+ * answers "is this upstream healthy, and how do we behave when it is
8
+ * not?" — the operational half of a mature outbound HTTP client
9
+ * (timeouts that prevent a hung upstream from exhausting your event
10
+ * loop, bounded retries that ride out a blip without amplifying an
11
+ * outage, and a circuit breaker that fails fast when an upstream is
12
+ * clearly down). The two compose: wrap an SSRF-guarded fetch in a
13
+ * resilient one and you get both safety and resilience with zero runtime
14
+ * dependencies.
15
+ *
16
+ * ```ts
17
+ * import { fetchGuard, resilientFetch } from "@daloyjs/core";
18
+ *
19
+ * const safeFetch = resilientFetch({
20
+ * fetch: fetchGuard(), // SSRF floor underneath
21
+ * timeoutMs: 2_000, // abort any call that stalls past 2s
22
+ * retries: 2, // up to 2 retries on transient failures
23
+ * circuitBreaker: { failureThreshold: 5, resetTimeoutMs: 30_000 },
24
+ * });
25
+ *
26
+ * const res = await safeFetch("https://api.example.com/things");
27
+ * ```
28
+ *
29
+ * ## Design notes
30
+ *
31
+ * - **Per-call timeout** is enforced with an `AbortController` that is
32
+ * combined with any caller-supplied `signal`, so cancellation works in
33
+ * both directions. A timeout surfaces as {@link FetchTimeoutError}; a
34
+ * caller-initiated abort surfaces as the caller's own `AbortError` and
35
+ * is **never** retried or counted as an upstream failure.
36
+ * - **Retry-with-backoff** only retries idempotent methods
37
+ * (`GET`/`HEAD`/`OPTIONS`/`PUT`/`DELETE`) and a conservative set of
38
+ * transient statuses (`408`, `429`, `500`, `502`, `503`, `504`) plus
39
+ * network errors and timeouts. Backoff is exponential with full
40
+ * jitter and honours a `Retry-After` header when present. A
41
+ * {@link SsrfBlockedError} is treated as a hard refusal — never
42
+ * retried, never trips the breaker.
43
+ * - **Circuit breaker** is a classic three-state machine
44
+ * (`closed → open → half-open`). Consecutive failures past the
45
+ * threshold open the circuit; while open every call fails fast with
46
+ * {@link CircuitOpenError} until `resetTimeoutMs` elapses, after which
47
+ * a limited number of trial requests probe the upstream. The breaker
48
+ * is shared across every call made through the returned function, so a
49
+ * single hot upstream is protected process-wide.
50
+ *
51
+ * @module
52
+ * @since 0.37.0
53
+ */
54
+ /**
55
+ * The three states of a {@link CircuitBreaker}.
56
+ *
57
+ * - `closed` — normal operation; calls pass through and failures are
58
+ * counted.
59
+ * - `open` — the upstream is considered down; calls fail fast with
60
+ * {@link CircuitOpenError} without touching the network.
61
+ * - `half-open` — a recovery probe window; a limited number of trial
62
+ * calls are allowed through to test whether the upstream has healed.
63
+ *
64
+ * @since 0.37.0
65
+ */
66
+ export type CircuitState = "closed" | "open" | "half-open";
67
+ /**
68
+ * Thrown by {@link resilientFetch} (and {@link CircuitBreaker.execute})
69
+ * when the circuit is open and the call is refused without hitting the
70
+ * network. Distinct from a network failure so callers can render a
71
+ * dedicated "service temporarily unavailable" path.
72
+ *
73
+ * @since 0.37.0
74
+ */
75
+ export declare class CircuitOpenError extends Error {
76
+ /** Milliseconds until the breaker will next allow a trial request. */
77
+ readonly retryAfterMs: number;
78
+ constructor(retryAfterMs: number);
79
+ }
80
+ /**
81
+ * Thrown by {@link resilientFetch} when a single attempt exceeds the
82
+ * configured `timeoutMs`. A caller-initiated abort (via a `signal`
83
+ * passed in the request init) surfaces as the caller's own `AbortError`
84
+ * instead and is never retried.
85
+ *
86
+ * @since 0.37.0
87
+ */
88
+ export declare class FetchTimeoutError extends Error {
89
+ /** The timeout that was exceeded, in milliseconds. */
90
+ readonly timeoutMs: number;
91
+ constructor(timeoutMs: number);
92
+ }
93
+ /**
94
+ * Tuning for the {@link CircuitBreaker}. All fields are optional and
95
+ * default to a conservative posture suited to a single upstream.
96
+ *
97
+ * @since 0.37.0
98
+ */
99
+ export interface CircuitBreakerOptions {
100
+ /**
101
+ * Number of consecutive failures that trips the breaker from `closed`
102
+ * to `open`. Default `5`.
103
+ */
104
+ failureThreshold?: number;
105
+ /**
106
+ * Time the breaker stays `open` before allowing trial requests
107
+ * (transition to `half-open`), in milliseconds. Default `30_000`.
108
+ */
109
+ resetTimeoutMs?: number;
110
+ /**
111
+ * Number of concurrent trial requests permitted while `half-open`.
112
+ * Extra calls during the probe window fail fast with
113
+ * {@link CircuitOpenError}. Default `1`.
114
+ */
115
+ halfOpenMaxAttempts?: number;
116
+ /**
117
+ * Number of consecutive trial successes required to close the breaker
118
+ * again. Default `1`.
119
+ */
120
+ successThreshold?: number;
121
+ /**
122
+ * Observe state transitions (e.g. to emit a metric or log). Called
123
+ * synchronously with the previous and next state.
124
+ */
125
+ onStateChange?: (next: CircuitState, previous: CircuitState) => void;
126
+ /**
127
+ * Monotonic clock, primarily for deterministic tests. Defaults to
128
+ * `Date.now`.
129
+ */
130
+ now?: () => number;
131
+ }
132
+ /**
133
+ * A standalone three-state circuit breaker. {@link resilientFetch}
134
+ * builds on this, but it is exported for callers who want to protect a
135
+ * non-`fetch` dependency (a database driver, a gRPC client, …) with the
136
+ * same semantics.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * const breaker = new CircuitBreaker({ failureThreshold: 3 });
141
+ * const rows = await breaker.execute(() => db.query("SELECT 1"));
142
+ * ```
143
+ *
144
+ * @since 0.37.0
145
+ */
146
+ export declare class CircuitBreaker {
147
+ #private;
148
+ constructor(options?: CircuitBreakerOptions);
149
+ /** The breaker's current state, after applying any pending timeout. */
150
+ get state(): CircuitState;
151
+ /** Milliseconds until the breaker will next admit a trial request. */
152
+ get retryAfterMs(): number;
153
+ /**
154
+ * Run `fn` under breaker supervision. Throws {@link CircuitOpenError}
155
+ * immediately when the circuit is open. A thrown error (other than
156
+ * `CircuitOpenError`) counts as a failure; a returned value counts as
157
+ * a success. Use {@link recordOutcome} from {@link resilientFetch}
158
+ * when an HTTP *response* (not a thrown error) should count as a
159
+ * failure.
160
+ */
161
+ execute<T>(fn: () => Promise<T>): Promise<T>;
162
+ /**
163
+ * Record an externally-determined outcome. Lets a caller treat a
164
+ * non-throwing result (e.g. an HTTP 503 response) as a failure while
165
+ * still flowing the value back. Returns nothing; pair with an explicit
166
+ * {@link admit}/{@link release} when you need full manual control.
167
+ */
168
+ recordOutcome(success: boolean): void;
169
+ /**
170
+ * Reserve a breaker slot for a manually-supervised call. Throws
171
+ * {@link CircuitOpenError} if the circuit will not admit the call.
172
+ * Must be paired with exactly one {@link recordOutcome} or
173
+ * {@link release}.
174
+ */
175
+ admit(): void;
176
+ /**
177
+ * Release a slot reserved by {@link admit} without recording a success
178
+ * or failure. Use for outcomes that are not an upstream health signal
179
+ * (a caller-initiated abort, an SSRF refusal). Counts and state are
180
+ * left untouched aside from freeing a half-open probe slot.
181
+ */
182
+ release(): void;
183
+ }
184
+ /**
185
+ * Context passed to the {@link ResilientFetchOptions.onRetry} hook and
186
+ * the {@link ResilientFetchOptions.isRetryable} predicate.
187
+ *
188
+ * @since 0.37.0
189
+ */
190
+ export interface RetryContext {
191
+ /** 1-based attempt number that just failed. */
192
+ readonly attempt: number;
193
+ /** The request that was attempted. */
194
+ readonly request: Request;
195
+ /** The response received, when the failure was a retryable status. */
196
+ readonly response?: Response;
197
+ /** The error thrown, when the failure was a network error or timeout. */
198
+ readonly error?: unknown;
199
+ }
200
+ /**
201
+ * Options for {@link resilientFetch}. Every field is optional; the
202
+ * defaults bias toward safe, low-amplification behaviour.
203
+ *
204
+ * @since 0.37.0
205
+ */
206
+ export interface ResilientFetchOptions {
207
+ /**
208
+ * Underlying fetch implementation. Defaults to `globalThis.fetch`.
209
+ * Pass a {@link fetchGuard} result to keep the SSRF floor underneath
210
+ * the resilience layer.
211
+ */
212
+ fetch?: typeof fetch;
213
+ /**
214
+ * Per-attempt timeout in milliseconds. Each retry gets a fresh
215
+ * timeout. Set `0` to disable. Default `10_000`.
216
+ */
217
+ timeoutMs?: number;
218
+ /**
219
+ * Maximum number of retries **after** the first attempt. `0` disables
220
+ * retrying. Default `2` (so up to 3 total attempts).
221
+ */
222
+ retries?: number;
223
+ /**
224
+ * Base backoff delay in milliseconds for the first retry. Default
225
+ * `100`.
226
+ */
227
+ retryDelayMs?: number;
228
+ /** Upper bound on any single backoff delay. Default `2_000`. */
229
+ maxRetryDelayMs?: number;
230
+ /** Exponential backoff multiplier. Default `2`. */
231
+ backoffFactor?: number;
232
+ /**
233
+ * Apply full jitter (`delay * random()`) to backoff to avoid
234
+ * thundering-herd retries. Default `true`.
235
+ */
236
+ jitter?: boolean;
237
+ /**
238
+ * HTTP methods that are safe to retry. Default the idempotent set:
239
+ * `GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`. Non-idempotent methods
240
+ * (`POST`, `PATCH`) are never retried unless added here.
241
+ */
242
+ retryableMethods?: readonly string[];
243
+ /**
244
+ * Response statuses that should be retried. Default
245
+ * `[408, 429, 500, 502, 503, 504]`.
246
+ */
247
+ retryableStatuses?: readonly number[];
248
+ /**
249
+ * Honour a `Retry-After` header on a retryable response (seconds or
250
+ * HTTP-date), capped by `maxRetryDelayMs`. Default `true`.
251
+ */
252
+ respectRetryAfter?: boolean;
253
+ /**
254
+ * Override the retry decision entirely. Return `true` to retry the
255
+ * given outcome. When provided, replaces the method/status defaults.
256
+ */
257
+ isRetryable?: (context: RetryContext) => boolean;
258
+ /**
259
+ * Observe each retry, e.g. to emit a metric. Called with the failed
260
+ * attempt's context and the delay before the next attempt.
261
+ */
262
+ onRetry?: (context: RetryContext, delayMs: number) => void;
263
+ /**
264
+ * Circuit breaker configuration, an existing {@link CircuitBreaker}
265
+ * instance to share across clients, or `false` to disable. Default
266
+ * enabled with {@link CircuitBreakerOptions} defaults.
267
+ */
268
+ circuitBreaker?: CircuitBreakerOptions | CircuitBreaker | false;
269
+ /**
270
+ * Response statuses that count as an upstream failure for the circuit
271
+ * breaker. Default `[500, 502, 503, 504]`. A failing status still
272
+ * flows back to the caller after retries are exhausted.
273
+ */
274
+ circuitBreakerFailureStatuses?: readonly number[];
275
+ /**
276
+ * Sleep implementation, primarily for deterministic tests. Receives
277
+ * the delay and an `AbortSignal` that fires if the caller cancels.
278
+ * Defaults to a `setTimeout`-based abortable sleep.
279
+ */
280
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
281
+ }
282
+ /**
283
+ * Wrap a `fetch` with per-call timeout, retry-with-backoff, and a shared
284
+ * circuit breaker. The returned function has the same call signature as
285
+ * the global `fetch`.
286
+ *
287
+ * Layer it over {@link fetchGuard} to keep SSRF protection underneath:
288
+ *
289
+ * ```ts
290
+ * const safeFetch = resilientFetch({ fetch: fetchGuard(), timeoutMs: 2_000 });
291
+ * ```
292
+ *
293
+ * @since 0.37.0
294
+ */
295
+ export declare function resilientFetch(options?: ResilientFetchOptions): typeof fetch;