@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.
Files changed (40) hide show
  1. package/README.md +135 -30
  2. package/dist/src/middleware/dedupe.d.ts.map +1 -1
  3. package/dist/src/middleware/dedupe.js +11 -2
  4. package/dist/src/middleware/dedupe.js.map +1 -1
  5. package/dist/src/utils/crypto.d.ts.map +1 -1
  6. package/dist/src/utils/crypto.js +2 -1
  7. package/dist/src/utils/crypto.js.map +1 -1
  8. package/dist/tests/auth-refresh.test.d.ts +2 -0
  9. package/dist/tests/auth-refresh.test.d.ts.map +1 -0
  10. package/dist/tests/auth-refresh.test.js +75 -0
  11. package/dist/tests/auth-refresh.test.js.map +1 -0
  12. package/dist/tests/chaos-heavy.test.d.ts +2 -0
  13. package/dist/tests/chaos-heavy.test.d.ts.map +1 -0
  14. package/dist/tests/chaos-heavy.test.js +164 -0
  15. package/dist/tests/chaos-heavy.test.js.map +1 -0
  16. package/dist/tests/crypto-utils.test.d.ts +2 -0
  17. package/dist/tests/crypto-utils.test.d.ts.map +1 -0
  18. package/dist/tests/crypto-utils.test.js +34 -0
  19. package/dist/tests/crypto-utils.test.js.map +1 -0
  20. package/dist/tests/fallback-validation-stale-policy.test.d.ts +2 -0
  21. package/dist/tests/fallback-validation-stale-policy.test.d.ts.map +1 -0
  22. package/dist/tests/fallback-validation-stale-policy.test.js +138 -0
  23. package/dist/tests/fallback-validation-stale-policy.test.js.map +1 -0
  24. package/dist/tests/invariant-fuzz.test.d.ts +2 -0
  25. package/dist/tests/invariant-fuzz.test.d.ts.map +1 -0
  26. package/dist/tests/invariant-fuzz.test.js +149 -0
  27. package/dist/tests/invariant-fuzz.test.js.map +1 -0
  28. package/dist/tests/resilience-shock.test.d.ts +2 -0
  29. package/dist/tests/resilience-shock.test.d.ts.map +1 -0
  30. package/dist/tests/resilience-shock.test.js +114 -0
  31. package/dist/tests/resilience-shock.test.js.map +1 -0
  32. package/dist/tests/storage-adapters.test.d.ts +2 -0
  33. package/dist/tests/storage-adapters.test.d.ts.map +1 -0
  34. package/dist/tests/storage-adapters.test.js +101 -0
  35. package/dist/tests/storage-adapters.test.js.map +1 -0
  36. package/dist/tests/traffic-load.test.d.ts +2 -0
  37. package/dist/tests/traffic-load.test.d.ts.map +1 -0
  38. package/dist/tests/traffic-load.test.js +91 -0
  39. package/dist/tests/traffic-load.test.js.map +1 -0
  40. 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 across frontend, BFF, backend, and edge runtimes.
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
- > Using this subpath ensures that Node.js native modules (like `fs` or `path`) are not accidentally bundled into your frontend builds (Webpack/Vite/Esbuild), preventing build-time errors and keeping bundle sizes small.
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
- ### The simplest case
44
+ ### A quicker start than raw fetch
44
45
 
45
46
  ```ts
46
47
  import { createClient } from "pureq";
47
48
 
48
- const client = createClient();
49
+ type User = { id: string; name: string };
49
50
 
50
- const response = await client.get("https://api.example.com/health");
51
- console.log(response.status); // 200
52
- ```
53
-
54
- ### Typed path parameters
51
+ const client = createClient({ baseURL: "https://api.example.com" });
55
52
 
56
- ```ts
57
- // TypeScript ensures you provide { id: string } for :id
58
- const response = await client.get("/users/:id", {
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
- const user = await response.json<{ id: string; name: string }>();
58
+ console.log(user.name);
63
59
  ```
64
60
 
65
- ### JSON helpers (one-liner)
61
+ ### Type-safe path parameters (Template Literal Types)
66
62
 
67
63
  ```ts
68
- // GET + status check + JSON parse in one step
69
- const user = await client.getJson<User>("/users/:id", {
70
- params: { id: "42" },
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
- ### Non-throwing Result API
77
+ ### Result API to prevent missed error handling
75
78
 
76
79
  ```ts
77
- const result = await client.getResult("/users/:id", {
78
- params: { id: "42" },
80
+ const result = await client.getJsonResult<User>("/users/:userId", {
81
+ params: { userId: "42" },
79
82
  });
80
83
 
81
84
  if (!result.ok) {
82
- // kind: human-friendly, code: machine-friendly (same error concept in two formats)
83
- console.error(result.error.kind, result.error.code, result.error.message);
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
- const response = result.data; // HttpResponse
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 → [dedupe] → [retry] → [circuitBreaker] → fetch() → Response
252
-
253
- the "onion" unwinds
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()` | | 2 | All | 5 failures / 30s cooldown | ✅ |
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:ci # unit + integration + contract + stress + typecheck
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,CAgC9D"}
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
- const key = options.keyBuilder?.(req) ??
27
- defaultKeyBuilder(req, options.includeHeaders ?? false, options.includeBody ?? false);
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,MAAM,GAAG,GACP,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC;YACzB,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK,EAAE,OAAO,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC;QAExF,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
+ {"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,CAexD;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"}
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"}
@@ -6,7 +6,8 @@
6
6
  */
7
7
  export function generateSecureId(prefix) {
8
8
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
9
- return crypto.randomUUID();
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":"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"}
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auth-refresh.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chaos-heavy.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crypto-utils.test.d.ts.map
@@ -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