@pureq/pureq 1.1.1 → 1.1.4
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 +135 -30
- package/dist/src/middleware/dedupe.d.ts.map +1 -1
- package/dist/src/middleware/dedupe.js +11 -2
- package/dist/src/middleware/dedupe.js.map +1 -1
- package/dist/src/utils/crypto.d.ts.map +1 -1
- package/dist/src/utils/crypto.js +2 -1
- package/dist/src/utils/crypto.js.map +1 -1
- package/dist/tests/auth-refresh.test.d.ts +2 -0
- package/dist/tests/auth-refresh.test.d.ts.map +1 -0
- package/dist/tests/auth-refresh.test.js +75 -0
- package/dist/tests/auth-refresh.test.js.map +1 -0
- package/dist/tests/chaos-heavy.test.d.ts +2 -0
- package/dist/tests/chaos-heavy.test.d.ts.map +1 -0
- package/dist/tests/chaos-heavy.test.js +164 -0
- package/dist/tests/chaos-heavy.test.js.map +1 -0
- package/dist/tests/crypto-utils.test.d.ts +2 -0
- package/dist/tests/crypto-utils.test.d.ts.map +1 -0
- package/dist/tests/crypto-utils.test.js +34 -0
- package/dist/tests/crypto-utils.test.js.map +1 -0
- package/dist/tests/fallback-validation-stale-policy.test.d.ts +2 -0
- package/dist/tests/fallback-validation-stale-policy.test.d.ts.map +1 -0
- package/dist/tests/fallback-validation-stale-policy.test.js +138 -0
- package/dist/tests/fallback-validation-stale-policy.test.js.map +1 -0
- package/dist/tests/invariant-fuzz.test.d.ts +2 -0
- package/dist/tests/invariant-fuzz.test.d.ts.map +1 -0
- package/dist/tests/invariant-fuzz.test.js +149 -0
- package/dist/tests/invariant-fuzz.test.js.map +1 -0
- package/dist/tests/resilience-shock.test.d.ts +2 -0
- package/dist/tests/resilience-shock.test.d.ts.map +1 -0
- package/dist/tests/resilience-shock.test.js +114 -0
- package/dist/tests/resilience-shock.test.js.map +1 -0
- package/dist/tests/storage-adapters.test.d.ts +2 -0
- package/dist/tests/storage-adapters.test.d.ts.map +1 -0
- package/dist/tests/storage-adapters.test.js +101 -0
- package/dist/tests/storage-adapters.test.js.map +1 -0
- package/dist/tests/traffic-load.test.d.ts +2 -0
- package/dist/tests/traffic-load.test.d.ts.map +1 -0
- package/dist/tests/traffic-load.test.js +91 -0
- package/dist/tests/traffic-load.test.js.map +1 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Functional, immutable, type-safe HTTP transport layer for TypeScript.
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
**pureq** is not another fetch wrapper. It's a **policy-first transport layer** that makes HTTP behavior explicit, composable, and observable
|
|
9
|
+
**pureq** is not another fetch wrapper. It's a **policy-first transport layer** that makes HTTP behavior explicit, composable, and observable across frontend, BFF, backend, and edge runtimes.
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
import { createClient, retry, circuitBreaker, dedupe } from "pureq";
|
|
@@ -19,7 +19,7 @@ const api = createClient({ baseURL: "https://api.example.com" })
|
|
|
19
19
|
const user = await api.getJson<User>("/users/:id", { params: { id: "42" } });
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Zero runtime dependencies. Works everywhere `fetch` works.
|
|
22
|
+
Zero runtime dependencies in the core package. Works everywhere `fetch` works.
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -36,55 +36,83 @@ import { FileSystemQueueStorageAdapter } from "pureq/node";
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
> [!NOTE]
|
|
39
|
-
>
|
|
39
|
+
> Core package (`pureq`) is zero-dependency and runtime-agnostic.
|
|
40
|
+
> Node-only adapters (for example `FileSystemQueueStorageAdapter`) live behind `pureq/node` so browser bundles stay clean and small.
|
|
40
41
|
|
|
41
42
|
## Quick Start
|
|
42
43
|
|
|
43
|
-
###
|
|
44
|
+
### A quicker start than raw fetch
|
|
44
45
|
|
|
45
46
|
```ts
|
|
46
47
|
import { createClient } from "pureq";
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
type User = { id: string; name: string };
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
console.log(response.status); // 200
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Typed path parameters
|
|
51
|
+
const client = createClient({ baseURL: "https://api.example.com" });
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
params: { id: "42" },
|
|
53
|
+
// GET + status check + JSON parse + typed path params
|
|
54
|
+
const user = await client.getJson<User>("/users/:userId", {
|
|
55
|
+
params: { userId: "42" },
|
|
60
56
|
});
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
console.log(user.name);
|
|
63
59
|
```
|
|
64
60
|
|
|
65
|
-
###
|
|
61
|
+
### Type-safe path parameters (Template Literal Types)
|
|
66
62
|
|
|
67
63
|
```ts
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
// Param keys are derived from the URL template at compile time.
|
|
65
|
+
|
|
66
|
+
// ✅ Compiles
|
|
67
|
+
await client.get("/users/:userId/posts/:postId", {
|
|
68
|
+
params: { userId: "1", postId: "42" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ❌ TypeScript error: missing postId
|
|
72
|
+
await client.get("/users/:userId/posts/:postId", {
|
|
73
|
+
params: { userId: "1" },
|
|
71
74
|
});
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
###
|
|
77
|
+
### Result API to prevent missed error handling
|
|
75
78
|
|
|
76
79
|
```ts
|
|
77
|
-
const result = await client.
|
|
78
|
-
params: {
|
|
80
|
+
const result = await client.getJsonResult<User>("/users/:userId", {
|
|
81
|
+
params: { userId: "42" },
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
if (!result.ok) {
|
|
82
|
-
//
|
|
83
|
-
|
|
85
|
+
// Switching on kind gives explicit, exhaustive transport handling.
|
|
86
|
+
switch (result.error.kind) {
|
|
87
|
+
case "network":
|
|
88
|
+
showOfflineBanner();
|
|
89
|
+
break;
|
|
90
|
+
case "timeout":
|
|
91
|
+
showRetryToast();
|
|
92
|
+
break;
|
|
93
|
+
case "http":
|
|
94
|
+
if (result.error.status === 401) redirectToLogin();
|
|
95
|
+
break;
|
|
96
|
+
case "circuit-open":
|
|
97
|
+
switchToFallbackMode();
|
|
98
|
+
break;
|
|
99
|
+
case "aborted":
|
|
100
|
+
case "storage-error":
|
|
101
|
+
case "auth-error":
|
|
102
|
+
case "validation-error":
|
|
103
|
+
case "unknown":
|
|
104
|
+
reportTransportError(result.error);
|
|
105
|
+
break;
|
|
106
|
+
default: {
|
|
107
|
+
const exhaustive: never = result.error.kind;
|
|
108
|
+
throw new Error(`Unhandled error kind: ${exhaustive}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
84
111
|
return;
|
|
85
112
|
}
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
// result.data is strongly typed JSON
|
|
115
|
+
renderUser(result.data);
|
|
88
116
|
```
|
|
89
117
|
|
|
90
118
|
Every request method has a `*Result` variant that never throws — transport failures become values you can pattern-match on.
|
|
@@ -243,16 +271,56 @@ const withAuth = withRetry.useRequestInterceptor((req) => ({
|
|
|
243
271
|
|
|
244
272
|
This makes it trivial to share a base client while customizing per-dependency behavior.
|
|
245
273
|
|
|
274
|
+
### The "Why" of Immutability
|
|
275
|
+
|
|
276
|
+
Unlike mutable interceptor stacks, pureq clients are branchable snapshots.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { authRefresh, createClient, dedupe, retry } from "pureq";
|
|
280
|
+
|
|
281
|
+
const base = createClient({ baseURL: "https://api.example.com" })
|
|
282
|
+
.use(retry({ maxRetries: 2, delay: 200 }));
|
|
283
|
+
|
|
284
|
+
// Public API: cache-friendly and lightweight
|
|
285
|
+
const publicApi = base.use(dedupe({ methods: ["GET", "HEAD"] }));
|
|
286
|
+
|
|
287
|
+
// Auth API: isolated auth policy without mutating publicApi
|
|
288
|
+
const authedApi = base
|
|
289
|
+
.use(authRefresh({ refresh: refreshToken }))
|
|
290
|
+
.useRequestInterceptor((req) => ({
|
|
291
|
+
...req,
|
|
292
|
+
headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
|
|
293
|
+
}));
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
This pattern is especially useful in BFFs: you keep shared defaults, then safely branch per upstream or trust boundary.
|
|
297
|
+
|
|
246
298
|
### Middleware: the Onion Model
|
|
247
299
|
|
|
248
300
|
Middleware wraps the entire request lifecycle. Each middleware can intercept the request before it happens, await the result, and post-process the response:
|
|
249
301
|
|
|
250
302
|
```text
|
|
251
|
-
Request
|
|
252
|
-
|
|
253
|
-
|
|
303
|
+
Request
|
|
304
|
+
-> [dedupe]
|
|
305
|
+
-> [retry]
|
|
306
|
+
-> [circuitBreaker]
|
|
307
|
+
-> [adapter/fetch]
|
|
308
|
+
<- [circuitBreaker]
|
|
309
|
+
<- [retry]
|
|
310
|
+
<- [dedupe]
|
|
311
|
+
Response
|
|
254
312
|
```
|
|
255
313
|
|
|
314
|
+
Think of `retry` and `circuitBreaker` as wrappers around `next()`.
|
|
315
|
+
|
|
316
|
+
- Outer wrappers see the whole execution envelope.
|
|
317
|
+
- Inner wrappers only see what reaches them.
|
|
318
|
+
|
|
319
|
+
That is why order changes behavior. Example:
|
|
320
|
+
|
|
321
|
+
- `retry` outside `circuitBreaker`: each retry attempt passes through breaker logic.
|
|
322
|
+
- `circuitBreaker` outside `retry`: breaker sees one wrapped operation that may include internal retries.
|
|
323
|
+
|
|
256
324
|
Middleware can:
|
|
257
325
|
|
|
258
326
|
- Transform the request before `next()`
|
|
@@ -333,6 +401,23 @@ const client = createClient({ baseURL: "https://api.example.com" })
|
|
|
333
401
|
.use(fallback({ value: { body: "default", status: 200 } as any })); // graceful degradation
|
|
334
402
|
```
|
|
335
403
|
|
|
404
|
+
### Standard Order (recommended)
|
|
405
|
+
|
|
406
|
+
For most production stacks, this order is a safe default:
|
|
407
|
+
|
|
408
|
+
```text
|
|
409
|
+
[dedupe] -> [httpCache] -> [retry] -> [circuitBreaker] -> [validation] -> [adapter]
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Why this order:
|
|
413
|
+
|
|
414
|
+
- `dedupe` outermost to collapse duplicate in-flight work early.
|
|
415
|
+
- `httpCache` before retry to short-circuit cache hits.
|
|
416
|
+
- `retry` before `circuitBreaker` so each attempt is still guarded by breaker state.
|
|
417
|
+
- `validation` near the inner edge to validate final payloads before returning to callers.
|
|
418
|
+
|
|
419
|
+
If order is reversed accidentally, behavior can drift (for example, retrying inside a layer you expected to run once).
|
|
420
|
+
|
|
336
421
|
### Retry
|
|
337
422
|
|
|
338
423
|
Full-featured retry with exponential backoff, Retry-After header respect, and retry budget.
|
|
@@ -496,6 +581,7 @@ import { authRefresh } from "pureq";
|
|
|
496
581
|
|
|
497
582
|
client.use(authRefresh({
|
|
498
583
|
refresh: async () => {
|
|
584
|
+
// Multiple 401s will only trigger ONE refresh call.
|
|
499
585
|
const res = await fetch("/api/refresh", { method: "POST" });
|
|
500
586
|
return (await res.json()).token;
|
|
501
587
|
},
|
|
@@ -587,7 +673,7 @@ for (const mw of backendPreset()) backend = backend.use(mw);
|
|
|
587
673
|
| `frontendPreset()` | 5s | 1 | GET/HEAD | 4 failures / 10s cooldown | ✅ |
|
|
588
674
|
| `bffPreset()` | 3s | 2 | GET/HEAD | 5 failures / 20s cooldown | ✅ body-only |
|
|
589
675
|
| `backendPreset()` | 2.5s | 3 | off | 6 failures / 30s cooldown | ✅ body-only |
|
|
590
|
-
| `resilientPreset()` |
|
|
676
|
+
| `resilientPreset()` | none (pair with `defaultTimeout()` if needed) | 2 | GET/HEAD | 5 failures / 30s cooldown | ✅ |
|
|
591
677
|
|
|
592
678
|
All presets are built from the same public middleware. You can inspect and override any parameter.
|
|
593
679
|
|
|
@@ -915,6 +1001,20 @@ More details: [Runtime compatibility matrix](./docs/runtime_compatibility_matrix
|
|
|
915
1001
|
|
|
916
1002
|
---
|
|
917
1003
|
|
|
1004
|
+
## Production Readiness Checklist
|
|
1005
|
+
|
|
1006
|
+
If you are adopting pureq in a production service, start with these defaults:
|
|
1007
|
+
|
|
1008
|
+
- Set a request time budget (`defaultTimeout()` or `deadline()`) first.
|
|
1009
|
+
- Add `retry()` with explicit status/method policy.
|
|
1010
|
+
- Add `validation()` for critical response payloads.
|
|
1011
|
+
- Add `circuitBreaker()` for unstable upstreams.
|
|
1012
|
+
- Enable diagnostics hooks (`onRequestStart/onRequestSuccess/onRequestError`).
|
|
1013
|
+
- Prefer `*Result` APIs on boundary layers to avoid unhandled transport errors.
|
|
1014
|
+
- Use `idempotencyKey()` before retrying write operations.
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
918
1018
|
## API Reference
|
|
919
1019
|
|
|
920
1020
|
### Client
|
|
@@ -1021,7 +1121,12 @@ pureq takes a defense-in-depth approach to transport layer security:
|
|
|
1021
1121
|
```bash
|
|
1022
1122
|
npm run typecheck # type checking
|
|
1023
1123
|
npm test # all tests
|
|
1024
|
-
npm run test:
|
|
1124
|
+
npm run test:unit # lightweight unit tests (heavy load tests excluded)
|
|
1125
|
+
npm run test:stress # baseline stress test
|
|
1126
|
+
npm run test:heavy # heavy load + shock + chaos + invariant-fuzz tests (nightly-recommended)
|
|
1127
|
+
npm run test:nightly # alias of test:heavy
|
|
1128
|
+
npm run test:ci # lightweight CI: unit + integration + contract + stress + typecheck
|
|
1129
|
+
npm run test:ci:full # CI + heavy tests
|
|
1025
1130
|
npm run test:browser # browser runtime smoke test
|
|
1026
1131
|
npm run test:edge # edge runtime smoke test
|
|
1027
1132
|
npm run build # production build
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAe,aAAa,EAAE,MAAM,eAAe,CAAC;AAI5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,MAAM,CAAC;CAChE;AAkBD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAe,aAAa,EAAE,MAAM,eAAe,CAAC;AAI5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,MAAM,CAAC;CAChE;AAkBD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,UAAU,CAuC9D"}
|
|
@@ -23,8 +23,17 @@ export function dedupe(options = {}) {
|
|
|
23
23
|
if (!methods.has(req.method)) {
|
|
24
24
|
return next(req);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
let key;
|
|
27
|
+
try {
|
|
28
|
+
key =
|
|
29
|
+
options.keyBuilder?.(req) ??
|
|
30
|
+
defaultKeyBuilder(req, options.includeHeaders ?? false, options.includeBody ?? false);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// If signature generation fails (e.g., circular body),
|
|
34
|
+
// safely bypass dedupe and execute request normally.
|
|
35
|
+
return next(req);
|
|
36
|
+
}
|
|
28
37
|
const existing = inflight.get(key);
|
|
29
38
|
if (existing) {
|
|
30
39
|
const sharedResponse = await existing;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AASlE,uEAAuE;AAEvE,SAAS,iBAAiB,CAAC,GAA4B,EAAE,cAAuB,EAAE,WAAoB;IACpG,MAAM,WAAW,GAAG,cAAc,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAErE,OAAO;QACL,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,GAAG;QACP,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;QACtB,WAAW;QACX,QAAQ;KACT,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,UAAyB,EAAE;IAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAC;IAE1D,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,
|
|
1
|
+
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AASlE,uEAAuE;AAEvE,SAAS,iBAAiB,CAAC,GAA4B,EAAE,cAAuB,EAAE,WAAoB;IACpG,MAAM,WAAW,GAAG,cAAc,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAErE,OAAO;QACL,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,GAAG;QACP,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;QACtB,WAAW;QACX,QAAQ;KACT,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,UAAyB,EAAE;IAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAC;IAE1D,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG;gBACD,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC;oBACzB,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK,EAAE,OAAO,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC;QAC1F,CAAC;QAAC,MAAM,CAAC;YACP,uDAAuD;YACvD,qDAAqD;YACrD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC;YACtC,OAAO,cAAc,CAAC,KAAK,EAAE,CAAC;QAChC,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;oBAAS,CAAC;gBACT,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAE3B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;QAC/B,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBxD;AAcD;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAe3E;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBpF"}
|
package/dist/src/utils/crypto.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export function generateSecureId(prefix) {
|
|
8
8
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
9
|
-
|
|
9
|
+
const value = crypto.randomUUID();
|
|
10
|
+
return prefix ? `${prefix}-${value}` : value;
|
|
10
11
|
}
|
|
11
12
|
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
12
13
|
const bytes = new Uint8Array(16);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC7E,
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-refresh.test.d.ts","sourceRoot":"","sources":["../../tests/auth-refresh.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { authRefresh } from "../src/middleware/authRefresh";
|
|
3
|
+
import { HttpResponse } from "../src/response/response";
|
|
4
|
+
describe("authRefresh middleware", () => {
|
|
5
|
+
it("refreshes token and retries once on 401", async () => {
|
|
6
|
+
const refresh = vi.fn(async () => "token-2");
|
|
7
|
+
const middleware = authRefresh({ refresh });
|
|
8
|
+
const seenAuth = [];
|
|
9
|
+
const req = {
|
|
10
|
+
method: "GET",
|
|
11
|
+
url: "/profile",
|
|
12
|
+
headers: { Authorization: "Bearer token-1" },
|
|
13
|
+
};
|
|
14
|
+
const res = await middleware(req, async (nextReq) => {
|
|
15
|
+
seenAuth.push(nextReq.headers?.Authorization ?? "");
|
|
16
|
+
if (seenAuth.length === 1) {
|
|
17
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
18
|
+
}
|
|
19
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
20
|
+
});
|
|
21
|
+
expect(res.status).toBe(200);
|
|
22
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(seenAuth).toEqual(["Bearer token-1", "Bearer token-2"]);
|
|
24
|
+
});
|
|
25
|
+
it("shares a single refresh call across concurrent 401s", async () => {
|
|
26
|
+
let refreshCount = 0;
|
|
27
|
+
const middleware = authRefresh({
|
|
28
|
+
refresh: async () => {
|
|
29
|
+
refreshCount += 1;
|
|
30
|
+
return "shared-token";
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const reqA = { method: "GET", url: "/a" };
|
|
34
|
+
const reqB = { method: "GET", url: "/b" };
|
|
35
|
+
const next = async (req) => {
|
|
36
|
+
if (req.headers?.Authorization === "Bearer shared-token") {
|
|
37
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
38
|
+
}
|
|
39
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
40
|
+
};
|
|
41
|
+
const [resA, resB] = await Promise.all([
|
|
42
|
+
middleware(reqA, next),
|
|
43
|
+
middleware(reqB, next),
|
|
44
|
+
]);
|
|
45
|
+
expect(resA.status).toBe(200);
|
|
46
|
+
expect(resB.status).toBe(200);
|
|
47
|
+
expect(refreshCount).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
it("throws wrapped error when refresh fails", async () => {
|
|
50
|
+
const rootError = new Error("refresh down");
|
|
51
|
+
const middleware = authRefresh({
|
|
52
|
+
refresh: async () => {
|
|
53
|
+
throw rootError;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await expect(middleware({ method: "GET", url: "/x" }, async () => {
|
|
57
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
58
|
+
})).rejects.toMatchObject({
|
|
59
|
+
message: "pureq: token refresh failed",
|
|
60
|
+
code: "PUREQ_AUTH_REFRESH_FAILED",
|
|
61
|
+
kind: "auth-error",
|
|
62
|
+
cause: rootError,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it("does not refresh when maxAttempts is zero", async () => {
|
|
66
|
+
const refresh = vi.fn(async () => "unused");
|
|
67
|
+
const middleware = authRefresh({ refresh, maxAttempts: 0 });
|
|
68
|
+
const res = await middleware({ method: "GET", url: "/x" }, async () => {
|
|
69
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
70
|
+
});
|
|
71
|
+
expect(res.status).toBe(401);
|
|
72
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=auth-refresh.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-refresh.test.js","sourceRoot":"","sources":["../../tests/auth-refresh.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chaos-heavy.test.d.ts","sourceRoot":"","sources":["../../tests/chaos-heavy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createCircuitBreaker, PureqCircuitOpenError } from "../src/middleware/circuitBreaker";
|
|
3
|
+
import { concurrencyLimit } from "../src/middleware/concurrencyLimit";
|
|
4
|
+
import { dedupe } from "../src/middleware/dedupe";
|
|
5
|
+
import { retry } from "../src/middleware/retry";
|
|
6
|
+
import { compose } from "../src/middleware/compose";
|
|
7
|
+
import { execute } from "../src/executor/execute";
|
|
8
|
+
import { HttpResponse } from "../src/response/response";
|
|
9
|
+
import { INTERNAL_MIDDLEWARES } from "../src/types/internal";
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
function makePrng(seed = 0x12345678) {
|
|
14
|
+
let value = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
value = (value * 1664525 + 1013904223) >>> 0;
|
|
17
|
+
return value / 0x100000000;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
describe("chaos/heavy: half-open stampede", () => {
|
|
25
|
+
it("allows only one half-open probe under concurrent burst", async () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
|
28
|
+
const breaker = createCircuitBreaker({
|
|
29
|
+
failureThreshold: 1,
|
|
30
|
+
successThreshold: 1,
|
|
31
|
+
cooldownMs: 1000,
|
|
32
|
+
keyBuilder: () => "dep:critical",
|
|
33
|
+
});
|
|
34
|
+
const req = { method: "GET", url: "/critical" };
|
|
35
|
+
await expect(breaker.middleware(req, async () => {
|
|
36
|
+
throw new Error("boom");
|
|
37
|
+
})).rejects.toThrow("boom");
|
|
38
|
+
vi.setSystemTime(new Date("2026-01-01T00:00:01.100Z"));
|
|
39
|
+
let nextCalls = 0;
|
|
40
|
+
let releaseProbe;
|
|
41
|
+
const probePromise = breaker.middleware(req, async () => {
|
|
42
|
+
nextCalls += 1;
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
releaseProbe = resolve;
|
|
45
|
+
});
|
|
46
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
47
|
+
});
|
|
48
|
+
const followers = await Promise.allSettled(Array.from({ length: 40 }, () => breaker.middleware(req, async () => {
|
|
49
|
+
nextCalls += 1;
|
|
50
|
+
return new HttpResponse(new Response("unexpected", { status: 200 }));
|
|
51
|
+
})));
|
|
52
|
+
const openErrors = followers.filter((entry) => entry.status === "rejected" &&
|
|
53
|
+
entry.reason instanceof PureqCircuitOpenError).length;
|
|
54
|
+
releaseProbe?.();
|
|
55
|
+
const probe = await probePromise;
|
|
56
|
+
expect(probe.status).toBe(200);
|
|
57
|
+
expect(nextCalls).toBe(1);
|
|
58
|
+
expect(openErrors).toBe(40);
|
|
59
|
+
vi.useRealTimers();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("chaos/heavy: mixed failure traffic", () => {
|
|
63
|
+
it("settles large mixed traffic without deadlock and respects concurrency ceiling", async () => {
|
|
64
|
+
const prng = makePrng(42);
|
|
65
|
+
let inFlight = 0;
|
|
66
|
+
let maxInFlight = 0;
|
|
67
|
+
const base = async (_req) => {
|
|
68
|
+
inFlight += 1;
|
|
69
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
70
|
+
try {
|
|
71
|
+
await sleep(Math.floor(prng() * 4));
|
|
72
|
+
const r = prng();
|
|
73
|
+
if (r < 0.12) {
|
|
74
|
+
throw new TypeError("network chaos");
|
|
75
|
+
}
|
|
76
|
+
if (r < 0.32) {
|
|
77
|
+
return new HttpResponse(new Response("temporary", { status: 503 }));
|
|
78
|
+
}
|
|
79
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
inFlight -= 1;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const pipeline = compose([
|
|
86
|
+
concurrencyLimit({ maxConcurrent: 20 }),
|
|
87
|
+
dedupe(),
|
|
88
|
+
retry({
|
|
89
|
+
maxRetries: 2,
|
|
90
|
+
delay: 0,
|
|
91
|
+
backoff: false,
|
|
92
|
+
retryOnStatus: [503],
|
|
93
|
+
retryOnNetworkError: true,
|
|
94
|
+
}),
|
|
95
|
+
], base);
|
|
96
|
+
const total = 320;
|
|
97
|
+
const tasks = Array.from({ length: total }, (_, i) => pipeline({
|
|
98
|
+
method: "GET",
|
|
99
|
+
url: `https://example.com/hot/${i % 80}`,
|
|
100
|
+
[INTERNAL_MIDDLEWARES]: [],
|
|
101
|
+
}).then(() => ({ ok: true }), () => ({ ok: false })));
|
|
102
|
+
const startedAt = Date.now();
|
|
103
|
+
const settled = await Promise.all(tasks);
|
|
104
|
+
const durationMs = Date.now() - startedAt;
|
|
105
|
+
const success = settled.filter((x) => x.ok).length;
|
|
106
|
+
const failure = settled.length - success;
|
|
107
|
+
console.info("[pureq][chaos-metrics]", {
|
|
108
|
+
scenario: "mixed-failure-traffic",
|
|
109
|
+
total,
|
|
110
|
+
success,
|
|
111
|
+
failure,
|
|
112
|
+
maxInFlight,
|
|
113
|
+
durationMs,
|
|
114
|
+
throughputRps: Number((total / Math.max(durationMs / 1000, 0.001)).toFixed(2)),
|
|
115
|
+
});
|
|
116
|
+
expect(settled).toHaveLength(total);
|
|
117
|
+
expect(maxInFlight).toBeLessThanOrEqual(20);
|
|
118
|
+
expect(success).toBeGreaterThan(0);
|
|
119
|
+
expect(failure).toBeGreaterThan(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("chaos/heavy: hostile input", () => {
|
|
123
|
+
it("handles hostile query keys without prototype pollution", async () => {
|
|
124
|
+
const query = Object.create(null);
|
|
125
|
+
query["__proto__"] = "x";
|
|
126
|
+
query["constructor"] = "y";
|
|
127
|
+
query["prototype"] = "z";
|
|
128
|
+
query["normal"] = "ok";
|
|
129
|
+
let capturedUrl = "";
|
|
130
|
+
await execute({
|
|
131
|
+
method: "GET",
|
|
132
|
+
url: "/hostile",
|
|
133
|
+
query,
|
|
134
|
+
}, {
|
|
135
|
+
adapter: async (url) => {
|
|
136
|
+
capturedUrl = url;
|
|
137
|
+
return new Response("ok", { status: 200 });
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
expect(capturedUrl.includes("__proto__=x")).toBe(true);
|
|
141
|
+
expect(capturedUrl.includes("constructor=y")).toBe(true);
|
|
142
|
+
expect(capturedUrl.includes("prototype=z")).toBe(true);
|
|
143
|
+
expect({}.polluted).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
it("bypasses dedupe safely for circular request body", async () => {
|
|
146
|
+
const middleware = dedupe({ includeBody: true, methods: ["POST"] });
|
|
147
|
+
const circular = { a: 1 };
|
|
148
|
+
circular.self = circular;
|
|
149
|
+
let upstreamCalls = 0;
|
|
150
|
+
const send = () => middleware({
|
|
151
|
+
method: "POST",
|
|
152
|
+
url: "/circular",
|
|
153
|
+
body: circular,
|
|
154
|
+
}, async () => {
|
|
155
|
+
upstreamCalls += 1;
|
|
156
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
157
|
+
});
|
|
158
|
+
const [r1, r2] = await Promise.all([send(), send()]);
|
|
159
|
+
expect(r1.status).toBe(200);
|
|
160
|
+
expect(r2.status).toBe(200);
|
|
161
|
+
expect(upstreamCalls).toBe(2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
//# sourceMappingURL=chaos-heavy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chaos-heavy.test.js","sourceRoot":"","sources":["../../tests/chaos-heavy.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.test.d.ts","sourceRoot":"","sources":["../../tests/crypto-utils.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { decrypt, encrypt, generateSecureId } from "../src/utils/crypto";
|
|
3
|
+
describe("crypto utils", () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
it("applies prefix even when randomUUID is available", () => {
|
|
8
|
+
const spy = vi
|
|
9
|
+
.spyOn(globalThis.crypto, "randomUUID")
|
|
10
|
+
.mockReturnValue("123e4567-e89b-12d3-a456-426614174000");
|
|
11
|
+
const id = generateSecureId("pureq");
|
|
12
|
+
expect(id).toBe("pureq-123e4567-e89b-12d3-a456-426614174000");
|
|
13
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
it("round-trips encrypt/decrypt with AES-GCM key", async () => {
|
|
16
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
17
|
+
"encrypt",
|
|
18
|
+
"decrypt",
|
|
19
|
+
]);
|
|
20
|
+
const input = JSON.stringify({ userId: "u1", roles: ["admin"] });
|
|
21
|
+
const encrypted = await encrypt(input, key);
|
|
22
|
+
const decrypted = await decrypt(encrypted, key);
|
|
23
|
+
expect(encrypted.includes(":")).toBe(true);
|
|
24
|
+
expect(decrypted).toBe(input);
|
|
25
|
+
});
|
|
26
|
+
it("throws helpful error for invalid encrypted format", async () => {
|
|
27
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
28
|
+
"encrypt",
|
|
29
|
+
"decrypt",
|
|
30
|
+
]);
|
|
31
|
+
await expect(decrypt("not-valid", key)).rejects.toThrow("pureq: invalid encrypted data format");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=crypto-utils.test.js.map
|