@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.
- package/README.md +21 -2
- package/bin/daloy.mjs +2 -0
- package/dist/adapters/bun.js +16 -9
- package/dist/adapters/deno.js +7 -1
- package/dist/adapters/node.d.ts +11 -0
- package/dist/adapters/node.js +24 -0
- package/dist/app.d.ts +144 -1
- package/dist/app.js +208 -1
- package/dist/asyncapi.d.ts +98 -0
- package/dist/asyncapi.js +212 -0
- package/dist/auto-ban.d.ts +205 -0
- package/dist/auto-ban.js +222 -0
- package/dist/bot-guard.d.ts +209 -0
- package/dist/bot-guard.js +291 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +88 -4
- package/dist/concurrency-limit.d.ts +135 -0
- package/dist/concurrency-limit.js +254 -0
- package/dist/docs.d.ts +57 -6
- package/dist/docs.js +34 -3
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +27 -0
- package/dist/fetch-guard.js +4 -0
- package/dist/fetch-resilience.d.ts +295 -0
- package/dist/fetch-resilience.js +485 -0
- package/dist/geo-block.d.ts +184 -0
- package/dist/geo-block.js +153 -0
- package/dist/hashing.d.ts +2 -1
- package/dist/hashing.js +12 -1
- package/dist/http-signatures.d.ts +303 -0
- package/dist/http-signatures.js +782 -0
- package/dist/idempotency.d.ts +204 -0
- package/dist/idempotency.js +341 -0
- package/dist/index.d.ts +38 -4
- package/dist/index.js +18 -1
- package/dist/ip-reputation.d.ts +198 -0
- package/dist/ip-reputation.js +253 -0
- package/dist/jwk.d.ts +15 -0
- package/dist/jwk.js +24 -2
- package/dist/load-shedding.d.ts +5 -0
- package/dist/logger.js +6 -2
- package/dist/metrics.d.ts +208 -0
- package/dist/metrics.js +452 -0
- package/dist/middleware.js +0 -10
- package/dist/mtls.d.ts +266 -0
- package/dist/mtls.js +488 -0
- package/dist/multipart.js +1 -1
- package/dist/openapi-diff.d.ts +79 -0
- package/dist/openapi-diff.js +246 -0
- package/dist/openapi.js +4 -1
- package/dist/pagination.d.ts +210 -0
- package/dist/pagination.js +353 -0
- package/dist/rate-limit-redis.d.ts +8 -0
- package/dist/rate-limit-redis.js +8 -0
- package/dist/request-decompression.d.ts +200 -0
- package/dist/request-decompression.js +363 -0
- package/dist/response-cache.d.ts +205 -0
- package/dist/response-cache.js +374 -0
- package/dist/router.d.ts +22 -0
- package/dist/router.js +64 -7
- package/dist/safe-redirect.d.ts +2 -2
- package/dist/safe-redirect.js +3 -8
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/scheduler.d.ts +315 -0
- package/dist/scheduler.js +546 -0
- package/dist/security.d.ts +27 -7
- package/dist/security.js +27 -7
- package/dist/session.js +3 -3
- package/dist/types.d.ts +33 -0
- package/dist/waf.d.ts +213 -0
- package/dist/waf.js +334 -0
- package/dist/webhook-delivery.d.ts +263 -0
- package/dist/webhook-delivery.js +311 -0
- package/dist/websocket.d.ts +52 -0
- package/dist/websocket.js +13 -0
- 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;
|