@cedvict/http-guardian 0.0.1-next.0 → 0.0.1-next.10

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 (76) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -1
  3. package/README.md +223 -0
  4. package/dist/aliases.d.ts +35 -0
  5. package/dist/aliases.d.ts.map +1 -0
  6. package/dist/auth/authConfig.d.ts +91 -0
  7. package/dist/auth/authConfig.d.ts.map +1 -0
  8. package/dist/cache/inFlight.d.ts +10 -0
  9. package/dist/cache/inFlight.d.ts.map +1 -0
  10. package/dist/cache/key.d.ts +10 -0
  11. package/dist/cache/key.d.ts.map +1 -0
  12. package/dist/cache/memoryCache.d.ts +16 -0
  13. package/dist/cache/memoryCache.d.ts.map +1 -0
  14. package/dist/cache/types.d.ts +13 -0
  15. package/dist/cache/types.d.ts.map +1 -0
  16. package/dist/client/errors.d.ts +29 -0
  17. package/dist/client/errors.d.ts.map +1 -0
  18. package/dist/client/httpClient.d.ts +13 -0
  19. package/dist/client/httpClient.d.ts.map +1 -0
  20. package/dist/client/types.d.ts +84 -0
  21. package/dist/client/types.d.ts.map +1 -0
  22. package/dist/guards/bearerRefreshGuard.d.ts +45 -0
  23. package/dist/guards/bearerRefreshGuard.d.ts.map +1 -0
  24. package/dist/guards/cookieSafeRefreshGuard.d.ts +40 -0
  25. package/dist/guards/cookieSafeRefreshGuard.d.ts.map +1 -0
  26. package/dist/guards/correlationIdGuard.d.ts +18 -0
  27. package/dist/guards/correlationIdGuard.d.ts.map +1 -0
  28. package/dist/guards/csrfGuard.d.ts +25 -0
  29. package/dist/guards/csrfGuard.d.ts.map +1 -0
  30. package/dist/guards/idempotencyGuard.d.ts +25 -0
  31. package/dist/guards/idempotencyGuard.d.ts.map +1 -0
  32. package/dist/guards/instrumentGuard.d.ts +12 -0
  33. package/dist/guards/instrumentGuard.d.ts.map +1 -0
  34. package/dist/guards/retryGuard.d.ts +12 -0
  35. package/dist/guards/retryGuard.d.ts.map +1 -0
  36. package/dist/guards/types.d.ts +3 -0
  37. package/dist/guards/types.d.ts.map +1 -0
  38. package/dist/guards/unauthorizedGuard.d.ts +19 -0
  39. package/dist/guards/unauthorizedGuard.d.ts.map +1 -0
  40. package/dist/index.cjs +1734 -0
  41. package/dist/index.cjs.map +1 -0
  42. package/dist/index.d.ts +27 -266
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +1077 -42
  45. package/dist/index.js.map +1 -1
  46. package/dist/internal/authGuard.d.ts +4 -0
  47. package/dist/internal/authGuard.d.ts.map +1 -0
  48. package/dist/internal/compose.d.ts +7 -0
  49. package/dist/internal/compose.d.ts.map +1 -0
  50. package/dist/internal/context.d.ts +57 -0
  51. package/dist/internal/context.d.ts.map +1 -0
  52. package/dist/internal/headersPolyfill.d.ts +25 -0
  53. package/dist/internal/headersPolyfill.d.ts.map +1 -0
  54. package/dist/internal/stableStringify.d.ts +2 -0
  55. package/dist/internal/stableStringify.d.ts.map +1 -0
  56. package/dist/internal/url.d.ts +3 -0
  57. package/dist/internal/url.d.ts.map +1 -0
  58. package/dist/notify/defaults.d.ts +9 -0
  59. package/dist/notify/defaults.d.ts.map +1 -0
  60. package/dist/notify/types.d.ts +33 -0
  61. package/dist/notify/types.d.ts.map +1 -0
  62. package/dist/parsing/apiParserStructured.d.ts +94 -0
  63. package/dist/parsing/apiParserStructured.d.ts.map +1 -0
  64. package/dist/parsing/presets.d.ts +15 -0
  65. package/dist/parsing/presets.d.ts.map +1 -0
  66. package/dist/parsing/schemaAdapter.d.ts +18 -0
  67. package/dist/parsing/schemaAdapter.d.ts.map +1 -0
  68. package/dist/parsing/shapeParser.d.ts +8 -0
  69. package/dist/parsing/shapeParser.d.ts.map +1 -0
  70. package/dist/plugins/types.d.ts +22 -0
  71. package/dist/plugins/types.d.ts.map +1 -0
  72. package/dist/presets/presetBuilder.d.ts +181 -0
  73. package/dist/presets/presetBuilder.d.ts.map +1 -0
  74. package/dist/publicTypes.d.ts +12 -0
  75. package/dist/publicTypes.d.ts.map +1 -0
  76. package/package.json +10 -5
package/dist/index.js CHANGED
@@ -1,16 +1,14 @@
1
1
  // src/internal/compose.ts
2
2
  function composeGuards(guards, terminal) {
3
- return (ctx) => {
4
- let idx = -1;
5
- const dispatch = (i, c) => {
6
- if (i <= idx) return Promise.reject(new Error("composeGuards: next() called multiple times"));
7
- idx = i;
8
- const guard = guards[i];
9
- if (!guard) return terminal(c);
10
- return guard(c, (nextCtx) => dispatch(i + 1, nextCtx));
11
- };
12
- return dispatch(0, ctx);
13
- };
3
+ if (!guards.length) return terminal;
4
+ return (ctx) => dispatch(0, ctx);
5
+ function dispatch(i, c) {
6
+ const guard = guards[i];
7
+ if (!guard) return terminal(c);
8
+ return Promise.resolve(
9
+ guard(c, (nextCtx) => dispatch(i + 1, nextCtx ?? c))
10
+ );
11
+ }
14
12
  }
15
13
 
16
14
  // src/internal/url.ts
@@ -36,6 +34,34 @@ function sanitizeHeaderValue(v) {
36
34
  if (/[\r\n]/.test(v)) return null;
37
35
  return v;
38
36
  }
37
+ function base64Encode(input) {
38
+ if (typeof globalThis.btoa === "function") return globalThis.btoa(input);
39
+ const B = globalThis.Buffer;
40
+ if (typeof B?.from === "function") return B.from(input, "utf8").toString("base64");
41
+ throw new Error("No base64 encoder available in this runtime");
42
+ }
43
+ function isUnsafeMethod(m) {
44
+ const u = m.toUpperCase();
45
+ return u !== "GET" && u !== "HEAD" && u !== "OPTIONS";
46
+ }
47
+ function sameOrigin(url, baseOrigin) {
48
+ if (!baseOrigin) return true;
49
+ try {
50
+ return new URL(url).origin === baseOrigin;
51
+ } catch {
52
+ return true;
53
+ }
54
+ }
55
+ function isAllowedOrigin(url, baseOrigin, allowed) {
56
+ if (!allowed) return sameOrigin(url, baseOrigin);
57
+ if (typeof allowed === "function") return !!allowed(url);
58
+ try {
59
+ const origin = new URL(url).origin;
60
+ return allowed.includes(origin);
61
+ } catch {
62
+ return true;
63
+ }
64
+ }
39
65
  function authGuard(auth) {
40
66
  return async (ctx, next) => {
41
67
  if (ctx.noAuth) return next(ctx);
@@ -43,6 +69,9 @@ function authGuard(auth) {
43
69
  if (auth.mode === "bearer") {
44
70
  const token = await auth.getToken();
45
71
  if (token) {
72
+ if (!isAllowedOrigin(ctx.url, ctx.baseOrigin, auth.allowedOrigins)) {
73
+ return next(ctx);
74
+ }
46
75
  const headerName = auth.headerName ?? "Authorization";
47
76
  const prefix = auth.prefix ?? "Bearer";
48
77
  const safe = sanitizeHeaderValue(`${prefix} ${token}`);
@@ -50,8 +79,36 @@ function authGuard(auth) {
50
79
  }
51
80
  return next(ctx);
52
81
  }
82
+ if (auth.mode === "basic") {
83
+ const cred = await auth.getCredentials();
84
+ if (cred) {
85
+ if (!isAllowedOrigin(ctx.url, ctx.baseOrigin, auth.allowedOrigins)) {
86
+ return next(ctx);
87
+ }
88
+ const headerName = auth.headerName ?? "Authorization";
89
+ const prefix = auth.prefix ?? "Basic";
90
+ const raw = typeof cred === "string" ? cred : `${cred.username}:${cred.password}`;
91
+ const safe = sanitizeHeaderValue(`${prefix} ${base64Encode(raw)}`);
92
+ if (safe) ctx.headers.set(headerName, safe);
93
+ }
94
+ return next(ctx);
95
+ }
96
+ if (auth.mode === "apiKey") {
97
+ const key = await auth.getKey();
98
+ if (key) {
99
+ if (!isAllowedOrigin(ctx.url, ctx.baseOrigin, auth.allowedOrigins)) {
100
+ return next(ctx);
101
+ }
102
+ const headerName = auth.headerName ?? "X-API-Key";
103
+ const prefix = auth.prefix ?? "";
104
+ const value = prefix ? `${prefix} ${key}` : key;
105
+ const safe = sanitizeHeaderValue(value);
106
+ if (safe) ctx.headers.set(headerName, safe);
107
+ }
108
+ return next(ctx);
109
+ }
53
110
  ctx.credentials = auth.credentials ?? "include";
54
- if (auth.csrf) {
111
+ if (auth.csrf && isUnsafeMethod(ctx.method)) {
55
112
  const csrf = await auth.csrf.getToken();
56
113
  if (csrf) ctx.headers.set(auth.csrf.headerName, csrf);
57
114
  }
@@ -59,6 +116,84 @@ function authGuard(auth) {
59
116
  };
60
117
  }
61
118
 
119
+ // src/internal/headersPolyfill.ts
120
+ function norm(name) {
121
+ return String(name).toLowerCase();
122
+ }
123
+ var HeadersPolyfill = class {
124
+ map = /* @__PURE__ */ new Map();
125
+ constructor(init) {
126
+ if (!init) return;
127
+ if (typeof init.forEach === "function" && typeof init.get === "function") {
128
+ init.forEach((v, k) => this.set(k, v));
129
+ return;
130
+ }
131
+ if (Symbol.iterator in Object(init)) {
132
+ for (const [k, v] of init) this.append(k, v);
133
+ return;
134
+ }
135
+ for (const [k, v] of Object.entries(init)) {
136
+ this.set(k, String(v));
137
+ }
138
+ }
139
+ get(name) {
140
+ const hit = this.map.get(norm(name));
141
+ return hit ? hit.value : null;
142
+ }
143
+ has(name) {
144
+ return this.map.has(norm(name));
145
+ }
146
+ set(name, value) {
147
+ this.map.set(norm(name), { name, value: String(value) });
148
+ }
149
+ append(name, value) {
150
+ const key = norm(name);
151
+ const prev = this.map.get(key);
152
+ if (!prev) {
153
+ this.map.set(key, { name, value: String(value) });
154
+ return;
155
+ }
156
+ this.map.set(key, { name: prev.name, value: `${prev.value}, ${String(value)}` });
157
+ }
158
+ delete(name) {
159
+ this.map.delete(norm(name));
160
+ }
161
+ forEach(cb) {
162
+ for (const { name, value } of this.map.values()) cb(value, name);
163
+ }
164
+ entries() {
165
+ const it = this.map.values();
166
+ return {
167
+ [Symbol.iterator]() {
168
+ return this;
169
+ },
170
+ next() {
171
+ const n = it.next();
172
+ if (n.done) return { done: true, value: void 0 };
173
+ return { done: false, value: [n.value.name, n.value.value] };
174
+ }
175
+ };
176
+ }
177
+ /** Convert to HeadersInit that any fetch can consume. */
178
+ toObject() {
179
+ const out = {};
180
+ for (const { name, value } of this.map.values()) out[name] = value;
181
+ return out;
182
+ }
183
+ };
184
+ function createHeaders(init) {
185
+ if (typeof globalThis.Headers === "function") {
186
+ return new globalThis.Headers(init);
187
+ }
188
+ return new HeadersPolyfill(init);
189
+ }
190
+ function toFetchHeaders(headers) {
191
+ if (typeof globalThis.Headers === "function" && headers instanceof globalThis.Headers) {
192
+ return headers;
193
+ }
194
+ return headers.toObject();
195
+ }
196
+
62
197
  // src/internal/stableStringify.ts
63
198
  function stableStringify(value) {
64
199
  return JSON.stringify(sortValue(value));
@@ -164,37 +299,82 @@ var ParseError = class extends Error {
164
299
  }
165
300
  name = "ParseError";
166
301
  };
302
+ var ILLEGAL_INVOCATION_HINT = `Detected a browser fetch invocation error ("Illegal invocation"). This usually happens when passing a detached reference to window.fetch. Fix by configuring the client with fetch: (input, init) => window.fetch(input, init) or fetch: window.fetch.bind(window).`;
167
303
 
168
304
  // src/client/httpClient.ts
305
+ function isIllegalInvocation(err) {
306
+ const msg = String(err?.message ?? err);
307
+ return /Illegal invocation/i.test(msg);
308
+ }
309
+ function normalizeFetch(f) {
310
+ const bound = f.bind ? f.bind(globalThis) : f;
311
+ return ((input, init) => bound(input, init));
312
+ }
313
+ function getDefaultFetch() {
314
+ if (typeof globalThis.fetch === "function") return globalThis.fetch;
315
+ throw new Error(
316
+ "No global fetch detected. Provide a fetch implementation via createHttpClient({ fetch }) or run in an environment that supports fetch (Node >= 18 / modern browsers)."
317
+ );
318
+ }
169
319
  var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
170
- function createHttpClient(opts) {
171
- const cache = opts.cache;
320
+ function createHttpClient(options) {
321
+ const draft = { ...options };
322
+ if (draft.plugins?.length) {
323
+ const seen = /* @__PURE__ */ new Set();
324
+ for (const p of draft.plugins) {
325
+ if (!p || !p.name || typeof p.apply !== "function") continue;
326
+ if (seen.has(p.name)) continue;
327
+ seen.add(p.name);
328
+ p.apply(draft);
329
+ }
330
+ }
331
+ const resolvedFetch = (() => {
332
+ if (draft.fetch) return normalizeFetch(draft.fetch);
333
+ let lastRef = null;
334
+ let lastBound = null;
335
+ return ((input, init) => {
336
+ const cur = getDefaultFetch();
337
+ if (cur !== lastRef || !lastBound) {
338
+ lastRef = cur;
339
+ lastBound = normalizeFetch(cur);
340
+ }
341
+ return lastBound(input, init);
342
+ });
343
+ })();
344
+ const cache = draft.cache;
172
345
  const inFlight = new InFlight();
173
- const defaultNotifier = opts.notifier ?? consoleNotifier;
174
- const shouldNotify = opts.defaults?.shouldNotify ?? ((r) => !r.ok);
175
- const baseGuards = [authGuard(opts.auth), ...opts.guards ?? []];
346
+ const defaultNotifier = draft.notifier ?? consoleNotifier;
347
+ const shouldNotify = draft.defaults?.shouldNotify ?? ((r) => !r.ok);
348
+ const baseGuards = [authGuard(draft.auth), ...draft.guards ?? []];
176
349
  async function terminal(ctx) {
177
350
  const controller = ctx.timeoutMs ? new AbortController() : void 0;
178
351
  const timeout = ctx.timeoutMs ? setTimeout(() => controller?.abort(new TimeoutError()), ctx.timeoutMs) : void 0;
179
352
  try {
180
353
  const init = {
181
354
  method: ctx.method,
182
- headers: ctx.headers,
355
+ headers: toFetchHeaders(ctx.headers),
183
356
  ...ctx.body !== void 0 ? { body: ctx.body } : {},
184
357
  ...ctx.signal ? { signal: controller ? anySignal([ctx.signal, controller.signal]) : ctx.signal } : controller ? { signal: controller.signal } : {},
185
358
  ...ctx.credentials !== void 0 ? { credentials: ctx.credentials } : {},
186
359
  ...ctx.redirect !== void 0 ? { redirect: ctx.redirect } : {}
187
360
  };
188
- return await ctx._fetch(ctx.url, init);
361
+ try {
362
+ return await ctx._fetch(ctx.url, init);
363
+ } catch (err) {
364
+ if (isIllegalInvocation(err) && typeof globalThis.fetch === "function") {
365
+ return await globalThis.fetch(ctx.url, init);
366
+ }
367
+ throw err;
368
+ }
189
369
  } finally {
190
370
  if (timeout) clearTimeout(timeout);
191
371
  }
192
372
  }
193
373
  const pipeline = composeGuards(baseGuards, terminal);
194
374
  async function request(method, path, ro) {
195
- const requestUrl = withQuery(joinUrl(opts.baseUrl, path), ro?.query);
196
- const headers = new Headers();
197
- if (opts.defaults?.headers) for (const [k, v] of Object.entries(opts.defaults.headers)) headers.set(k, v);
375
+ const requestUrl = withQuery(joinUrl(draft.baseUrl, path), ro?.query);
376
+ const headers = createHeaders();
377
+ if (draft.defaults?.headers) for (const [k, v] of Object.entries(draft.defaults.headers)) headers.set(k, v);
198
378
  if (ro?.headers) {
199
379
  for (const [k, v] of Object.entries(ro.headers)) if (v !== void 0) headers.set(k, v);
200
380
  }
@@ -212,19 +392,31 @@ function createHttpClient(opts) {
212
392
  } else {
213
393
  if (!headers.has("accept")) headers.set("accept", "application/json");
214
394
  }
215
- const timeoutMs = ro?.timeoutMs ?? opts.defaults?.timeoutMs;
395
+ const timeoutMs = ro?.timeoutMs ?? draft.defaults?.timeoutMs;
216
396
  const notifier = ro?.notifier ?? defaultNotifier;
397
+ const emit = (event) => {
398
+ const redact = notifier.redact;
399
+ const n = notifier.notify?.bind(notifier) ?? defaultNotifier.notify.bind(defaultNotifier);
400
+ return n(redact ? redact(event) : event);
401
+ };
217
402
  const cacheOpts = ro?.cache;
218
403
  const wantsCache = method === "GET" && !!cache && !!cacheOpts;
219
404
  const dedupe = cacheOpts?.dedupe ?? true;
220
405
  const canDedupeWrite = method !== "GET" && !!ro?.idempotencyKey;
221
- const inflightKey = (wantsCache || canDedupeWrite) && dedupe ? makeCacheKey({ method, url: requestUrl, headers, ...method === "GET" ? {} : { body: bodyForKey }, ...ro?.idempotencyKey !== void 0 ? { idempotencyKey: ro.idempotencyKey } : {}, ...cacheOpts?.key !== void 0 ? { keyOverride: cacheOpts.key } : {} }) : void 0;
406
+ const inflightKey = (wantsCache || canDedupeWrite) && dedupe ? makeCacheKey({
407
+ method,
408
+ url: requestUrl,
409
+ headers,
410
+ ...method === "GET" ? {} : { body: bodyForKey },
411
+ ...ro?.idempotencyKey !== void 0 ? { idempotencyKey: ro.idempotencyKey } : {},
412
+ ...cacheOpts?.key !== void 0 ? { keyOverride: cacheOpts.key } : {}
413
+ }) : void 0;
222
414
  if (inflightKey) {
223
415
  const existing = inFlight.get(inflightKey);
224
416
  if (existing) {
225
417
  try {
226
418
  const stored = await existing;
227
- return asClientResult(stored, requestUrl, opts, ro, notifier, shouldNotify);
419
+ return asClientResult(stored, requestUrl, draft, ro, notifier, shouldNotify, emit);
228
420
  } catch {
229
421
  }
230
422
  }
@@ -246,7 +438,8 @@ function createHttpClient(opts) {
246
438
  }
247
439
  }
248
440
  const res = await fetchWithRedirects({
249
- opts,
441
+ opts: draft,
442
+ resolvedFetch,
250
443
  pipeline,
251
444
  method,
252
445
  url: requestUrl,
@@ -260,13 +453,18 @@ function createHttpClient(opts) {
260
453
  const envelope = { kind: "network", url: res.url || requestUrl, status: res.status, headers: res.headers, raw };
261
454
  if (wantsCache && cacheOpts && res.ok) {
262
455
  const key = inflightKey;
263
- cache.set(key, { expiresAt: Date.now() + cacheOpts.ttlMs, value: raw, ...cacheOpts.tags ? { tags: cacheOpts.tags } : {} });
456
+ cache.set(key, {
457
+ expiresAt: Date.now() + cacheOpts.ttlMs,
458
+ value: raw,
459
+ ...cacheOpts.tags ? { tags: cacheOpts.tags } : {}
460
+ });
264
461
  }
265
462
  return envelope;
266
463
  async function refreshInBackground(key) {
267
464
  try {
268
465
  const res2 = await fetchWithRedirects({
269
- opts,
466
+ opts: draft,
467
+ resolvedFetch,
270
468
  pipeline,
271
469
  method,
272
470
  url: requestUrl,
@@ -278,7 +476,11 @@ function createHttpClient(opts) {
278
476
  });
279
477
  if (!res2.ok) return;
280
478
  const raw2 = await decodeResponse(res2);
281
- cache.set(key, { expiresAt: Date.now() + cacheOpts.ttlMs, value: raw2, ...cacheOpts.tags ? { tags: cacheOpts.tags } : {} });
479
+ cache.set(key, {
480
+ expiresAt: Date.now() + cacheOpts.ttlMs,
481
+ value: raw2,
482
+ ...cacheOpts.tags ? { tags: cacheOpts.tags } : {}
483
+ });
282
484
  } catch {
283
485
  }
284
486
  }
@@ -286,10 +488,13 @@ function createHttpClient(opts) {
286
488
  if (inflightKey) inFlight.set(inflightKey, promise);
287
489
  try {
288
490
  const stored = await promise;
289
- return asClientResult(stored, requestUrl, opts, ro, notifier, shouldNotify);
491
+ return asClientResult(stored, requestUrl, draft, ro, notifier, shouldNotify, emit);
290
492
  } catch (e) {
291
- const err = e instanceof TimeoutError ? e : new NetworkError("Network request failed", e);
292
- if (shouldNotify({ ok: false, status: 0 })) notifier.notify({ type: "network-error", level: "error", title: "Network error", message: err.message, url: requestUrl });
493
+ const err = e instanceof TimeoutError ? e : new NetworkError(
494
+ isIllegalInvocation(e) ? `Network request failed. ${ILLEGAL_INVOCATION_HINT}` : "Network request failed",
495
+ e
496
+ );
497
+ if (shouldNotify({ ok: false, status: 0 })) emit({ type: "network-error", level: "error", title: "Network error", message: err.message, url: requestUrl });
293
498
  return { ok: false, status: 0, headers: new Headers(), errors: [{ message: err.message, details: e }], raw: null, url: requestUrl };
294
499
  }
295
500
  }
@@ -321,7 +526,19 @@ async function fetchWithRedirects(args) {
321
526
  );
322
527
  if (!REDIRECT_STATUSES.has(res.status)) return res;
323
528
  const loc = res.headers.get("location");
324
- if (!loc) throw new RedirectError("Redirect without Location header", currentUrl);
529
+ if (!loc) {
530
+ return args.pipeline(
531
+ makeCtx(
532
+ {
533
+ ...args,
534
+ url: currentUrl,
535
+ method,
536
+ ...body !== void 0 ? { body } : {}
537
+ },
538
+ "follow"
539
+ )
540
+ );
541
+ }
325
542
  const nextUrl = new URL(loc, currentUrl).toString();
326
543
  const decision = policy.onRedirect?.({ fromUrl: currentUrl, toUrl: nextUrl, status: res.status, hop, method });
327
544
  if (decision?.action === "deny") throw new RedirectError(decision.reason ?? "Redirect denied by policy", currentUrl, nextUrl);
@@ -341,18 +558,38 @@ async function fetchWithRedirects(args) {
341
558
  }
342
559
  function stripAuthOnCrossOriginRedirect(opts, initialUrl, toUrl, headers) {
343
560
  try {
344
- if (opts.auth.mode !== "bearer") return;
345
561
  const initialOrigin = new URL(initialUrl).origin;
346
562
  const toOrigin = new URL(toUrl).origin;
347
- if (initialOrigin !== toOrigin) {
563
+ if (initialOrigin === toOrigin) return;
564
+ if (opts.auth.mode === "bearer") {
565
+ const headerName = opts.auth.headerName ?? "Authorization";
566
+ headers.delete(headerName);
567
+ return;
568
+ }
569
+ if (opts.auth.mode === "basic") {
348
570
  const headerName = opts.auth.headerName ?? "Authorization";
349
571
  headers.delete(headerName);
572
+ return;
573
+ }
574
+ if (opts.auth.mode === "apiKey") {
575
+ const headerName = opts.auth.headerName ?? "X-API-Key";
576
+ headers.delete(headerName);
577
+ return;
350
578
  }
351
579
  } catch {
352
580
  }
353
581
  }
354
582
  function makeCtx(args, redirect) {
583
+ const baseOrigin = (() => {
584
+ try {
585
+ return new URL(args.opts.baseUrl).origin;
586
+ } catch {
587
+ return "";
588
+ }
589
+ })();
355
590
  return {
591
+ baseUrl: args.opts.baseUrl,
592
+ baseOrigin,
356
593
  method: args.method,
357
594
  url: args.url,
358
595
  headers: args.headers,
@@ -363,10 +600,11 @@ function makeCtx(args, redirect) {
363
600
  attempt: 0,
364
601
  auth: args.opts.auth,
365
602
  redirect,
366
- _fetch: args.opts.fetch ?? fetch
603
+ _fetch: args.resolvedFetch
367
604
  };
368
605
  }
369
606
  async function decodeResponse(res) {
607
+ if (res.status === 204) return null;
370
608
  const ct = res.headers.get("content-type") ?? "";
371
609
  if (ct.includes("application/json")) {
372
610
  try {
@@ -381,10 +619,10 @@ async function decodeResponse(res) {
381
619
  throw new ParseError("Failed to read response body", void 0, e);
382
620
  }
383
621
  }
384
- function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify) {
622
+ function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify, emit) {
385
623
  if (envelope?.kind === "cache-miss") {
386
624
  const errors2 = [{ message: "Cache miss" }];
387
- if (shouldNotify({ ok: false, status: 0 })) notifier.notify({ type: "http-error", level: "warning", title: "Cache", message: "Cache miss", status: 0, errors: errors2, url: requestUrl });
625
+ if (shouldNotify({ ok: false, status: 0 })) emit({ type: "http-error", level: "warning", title: "Cache", message: "Cache miss", status: 0, errors: errors2, url: requestUrl });
388
626
  return { ok: false, status: 0, headers: new Headers(), errors: errors2, raw: envelope.raw, url: requestUrl };
389
627
  }
390
628
  const status = envelope.status ?? 0;
@@ -399,17 +637,24 @@ function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify)
399
637
  return { ok: true, status, headers: resHeaders, data, raw, url: finalUrl };
400
638
  } catch (e) {
401
639
  const msg2 = e instanceof Error ? e.message : "Schema parse failed";
402
- if (shouldNotify({ ok: false, status })) notifier.notify({ type: "parse-error", level: "error", title: "Parse error", message: msg2, url: finalUrl });
640
+ if (shouldNotify({ ok: false, status })) emit({ type: "parse-error", level: "error", title: "Parse error", message: msg2, url: finalUrl });
403
641
  return { ok: false, status, headers: resHeaders, errors: [{ message: msg2, details: e }], raw, url: finalUrl };
404
642
  }
405
643
  }
406
644
  const errors = opts.parser.getErrors(raw, status);
407
645
  const msg = errors[0]?.message ?? (status ? `HTTP ${status}` : "Request failed");
408
- if (shouldNotify({ ok: false, status })) notifier.notify({ type: "http-error", level: status >= 500 ? "error" : "warning", title: "Request failed", message: msg, status, errors, url: finalUrl });
646
+ if (shouldNotify({ ok: false, status })) emit({ type: "http-error", level: status >= 500 ? "error" : "warning", title: "Request failed", message: msg, status, errors, url: finalUrl });
409
647
  return { ok: false, status, headers: resHeaders, errors, raw, url: finalUrl };
410
648
  }
411
649
  function anySignal(signals) {
412
650
  const valid = signals.filter(Boolean);
651
+ const any = AbortSignal.any;
652
+ if (any) {
653
+ try {
654
+ return any(valid);
655
+ } catch {
656
+ }
657
+ }
413
658
  const controller = new AbortController();
414
659
  const onAbort = () => controller.abort();
415
660
  for (const s of valid) {
@@ -425,7 +670,8 @@ function bearerAuth(getToken, opts) {
425
670
  mode: "bearer",
426
671
  getToken,
427
672
  ...opts?.headerName !== void 0 ? { headerName: opts.headerName } : {},
428
- ...opts?.prefix !== void 0 ? { prefix: opts.prefix } : {}
673
+ ...opts?.prefix !== void 0 ? { prefix: opts.prefix } : {},
674
+ ...opts?.allowedOrigins !== void 0 ? { allowedOrigins: opts.allowedOrigins } : {}
429
675
  };
430
676
  }
431
677
  function cookieAuth(opts) {
@@ -435,6 +681,24 @@ function cookieAuth(opts) {
435
681
  ...opts?.csrf !== void 0 ? { csrf: opts.csrf } : {}
436
682
  };
437
683
  }
684
+ function basicAuth(getCredentials, opts) {
685
+ return {
686
+ mode: "basic",
687
+ getCredentials,
688
+ ...opts?.headerName !== void 0 ? { headerName: opts.headerName } : {},
689
+ ...opts?.prefix !== void 0 ? { prefix: opts.prefix } : {},
690
+ ...opts?.allowedOrigins !== void 0 ? { allowedOrigins: opts.allowedOrigins } : {}
691
+ };
692
+ }
693
+ function apiKeyAuth(getKey, opts) {
694
+ return {
695
+ mode: "apiKey",
696
+ getKey,
697
+ ...opts?.headerName !== void 0 ? { headerName: opts.headerName } : {},
698
+ ...opts?.prefix !== void 0 ? { prefix: opts.prefix } : {},
699
+ ...opts?.allowedOrigins !== void 0 ? { allowedOrigins: opts.allowedOrigins } : {}
700
+ };
701
+ }
438
702
  function noAuth() {
439
703
  return { mode: "none" };
440
704
  }
@@ -459,6 +723,515 @@ function retryGuard(opts) {
459
723
  };
460
724
  }
461
725
 
726
+ // src/guards/cookieSafeRefreshGuard.ts
727
+ var REFRESH_FLAG = "__hg_is_refresh__";
728
+ var RETRY_FLAG = "__hg_refresh_retry__";
729
+ function isReplayableBody(body) {
730
+ if (body == null) return true;
731
+ if (typeof body === "string") return true;
732
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
733
+ if (typeof FormData !== "undefined" && body instanceof FormData) return true;
734
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return true;
735
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) return true;
736
+ if (typeof Uint8Array !== "undefined" && body instanceof Uint8Array) return true;
737
+ return false;
738
+ }
739
+ function normalizePath(url) {
740
+ try {
741
+ return new URL(url).pathname;
742
+ } catch {
743
+ const q = url.indexOf("?");
744
+ const h = url.indexOf("#");
745
+ const cut = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h);
746
+ return url.slice(0, cut);
747
+ }
748
+ }
749
+ function parseRetryAfterMs(headers) {
750
+ const ra = headers.get("retry-after");
751
+ if (!ra) return null;
752
+ const seconds = Number(ra);
753
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds * 1e3;
754
+ const date = Date.parse(ra);
755
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
756
+ return null;
757
+ }
758
+ function isAbsoluteUrl(u) {
759
+ try {
760
+ void new URL(u);
761
+ return true;
762
+ } catch {
763
+ return false;
764
+ }
765
+ }
766
+ function cookieSafeRefreshGuard(opts) {
767
+ const trigger = new Set(opts.triggerStatuses ?? [401, 419]);
768
+ const maxRetry = opts.maxRetry ?? 1;
769
+ const honorRetryAfter = opts.honorRetryAfter ?? true;
770
+ let refreshPromise = null;
771
+ let refreshCooldownUntil = 0;
772
+ return async (ctx, next) => {
773
+ const anyCtx = ctx;
774
+ if (anyCtx[REFRESH_FLAG]) return next(ctx);
775
+ const path = normalizePath(ctx.url);
776
+ if (opts.excludePaths?.includes(path)) return next(ctx);
777
+ if (ctx.noAuth) return next(ctx);
778
+ const refreshPathNorm = normalizePath(resolveRefreshUrl(ctx, opts.refreshPath));
779
+ if (path === refreshPathNorm) return next(ctx);
780
+ if (honorRetryAfter && refreshCooldownUntil > Date.now()) {
781
+ return next(ctx);
782
+ }
783
+ const res = await next(ctx);
784
+ if (!trigger.has(res.status)) return res;
785
+ if (opts.shouldRefresh && !opts.shouldRefresh(ctx, res.status)) return res;
786
+ const alreadyRetried = Number(anyCtx[RETRY_FLAG] ?? 0);
787
+ if (alreadyRetried >= maxRetry) return res;
788
+ if (!isReplayableBody(ctx.body)) return res;
789
+ const decision = await (refreshPromise ??= doRefresh(ctx, next).finally(() => {
790
+ refreshPromise = null;
791
+ }));
792
+ if (!decision.ok) {
793
+ opts.onRefreshFailed?.({
794
+ url: ctx.url,
795
+ ...decision.reason !== void 0 ? { reason: decision.reason } : {},
796
+ ...decision.status !== void 0 ? { status: decision.status } : {}
797
+ });
798
+ return res;
799
+ }
800
+ opts.onRefreshSuccess?.({ url: ctx.url });
801
+ const retryCtx = Object.assign({}, ctx, { attempt: ctx.attempt + 1 });
802
+ retryCtx[RETRY_FLAG] = alreadyRetried + 1;
803
+ await applyCookieAuth(retryCtx);
804
+ return next(retryCtx);
805
+ };
806
+ async function doRefresh(originalCtx, next) {
807
+ if (honorRetryAfter && refreshCooldownUntil > Date.now()) {
808
+ return { ok: false, reason: "Refresh cooldown active" };
809
+ }
810
+ const refreshUrl = resolveRefreshUrl(originalCtx, opts.refreshPath);
811
+ const refreshCtx = {
812
+ ...originalCtx,
813
+ url: refreshUrl,
814
+ method: opts.refreshMethod ?? "POST",
815
+ // Un refresh n’a en général pas besoin du body original
816
+ ...Object.prototype.hasOwnProperty.call(originalCtx, "body") ? { body: void 0 } : {},
817
+ attempt: 0
818
+ };
819
+ refreshCtx[REFRESH_FLAG] = true;
820
+ refreshCtx[RETRY_FLAG] = 0;
821
+ await applyCookieAuth(refreshCtx);
822
+ try {
823
+ const res = await next(refreshCtx);
824
+ if (res.status === 429 && honorRetryAfter) {
825
+ const ms = parseRetryAfterMs(res.headers);
826
+ refreshCooldownUntil = Date.now() + (ms ?? 3e4);
827
+ return { ok: false, reason: "Refresh rate-limited (429)", status: 429 };
828
+ }
829
+ if (!res.ok) {
830
+ return { ok: false, reason: `Refresh failed (HTTP ${res.status})`, status: res.status };
831
+ }
832
+ return { ok: true };
833
+ } catch (e) {
834
+ return { ok: false, reason: e instanceof Error ? e.message : "Refresh exception" };
835
+ }
836
+ }
837
+ function resolveRefreshUrl(ctx, refreshPath) {
838
+ if (isAbsoluteUrl(refreshPath)) return refreshPath;
839
+ return joinUrl(ctx.baseUrl, refreshPath);
840
+ }
841
+ async function applyCookieAuth(ctx) {
842
+ if (ctx.noAuth) return;
843
+ const auth = ctx.auth;
844
+ if (!auth || auth.mode !== "cookie") return;
845
+ if (ctx.credentials === void 0) {
846
+ ctx.credentials = auth.credentials ?? "include";
847
+ }
848
+ if (auth.csrf) {
849
+ const hn = auth.csrf.headerName;
850
+ if (!ctx.headers.has(hn)) {
851
+ const t = await auth.csrf.getToken();
852
+ if (t != null && String(t) !== "") ctx.headers.set(hn, String(t));
853
+ }
854
+ }
855
+ }
856
+ }
857
+
858
+ // src/guards/bearerRefreshGuard.ts
859
+ var REFRESH_FLAG2 = "__hg_bearer_refresh_in_flight__";
860
+ var RETRY_FLAG2 = "__hg_bearer_refresh_retry__";
861
+ function isReplayableBody2(body) {
862
+ if (body == null) return true;
863
+ if (typeof body === "string") return true;
864
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
865
+ if (typeof FormData !== "undefined" && body instanceof FormData) return true;
866
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return true;
867
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) return true;
868
+ if (typeof Uint8Array !== "undefined" && body instanceof Uint8Array) return true;
869
+ return false;
870
+ }
871
+ function bearerRefreshGuard(opts) {
872
+ const trigger = new Set(opts.triggerStatuses ?? [401]);
873
+ const queueTimeoutMs = opts.queueTimeoutMs ?? 3e4;
874
+ let refreshPromise = null;
875
+ return async (ctx, next) => {
876
+ const anyCtx = ctx;
877
+ if (ctx.noAuth) return next(ctx);
878
+ if (ctx.auth.mode !== "bearer") return next(ctx);
879
+ if (opts.skipRefreshFor?.(ctx)) return next(ctx);
880
+ if (anyCtx[RETRY_FLAG2]) return next(ctx);
881
+ if (anyCtx[REFRESH_FLAG2]) return next(ctx);
882
+ const res = await next(ctx);
883
+ if (!trigger.has(res.status)) return res;
884
+ if (!isReplayableBody2(ctx.body)) return res;
885
+ try {
886
+ await withTimeout(
887
+ refreshPromise ??= runRefresh(opts).finally(() => {
888
+ refreshPromise = null;
889
+ }),
890
+ queueTimeoutMs
891
+ );
892
+ } catch {
893
+ return res;
894
+ }
895
+ const retryCtx = cloneCtxForReplay(ctx);
896
+ retryCtx[RETRY_FLAG2] = true;
897
+ await applyBearerAuth(retryCtx, ctx.auth);
898
+ return next(retryCtx);
899
+ };
900
+ async function runRefresh(o) {
901
+ try {
902
+ const tokens = await o.refreshFn();
903
+ if (o.onTokenRefreshed) await o.onTokenRefreshed(tokens);
904
+ return tokens;
905
+ } catch (e) {
906
+ if (o.onAuthFailed) {
907
+ try {
908
+ await o.onAuthFailed(e);
909
+ } catch {
910
+ }
911
+ }
912
+ throw e;
913
+ }
914
+ }
915
+ }
916
+ function cloneCtxForReplay(ctx) {
917
+ return Object.assign({}, ctx, { attempt: ctx.attempt + 1 });
918
+ }
919
+ async function applyBearerAuth(ctx, auth) {
920
+ const headerName = auth.headerName ?? "Authorization";
921
+ const prefix = auth.prefix ?? "Bearer";
922
+ const token = await auth.getToken();
923
+ if (token == null || token === "") {
924
+ ctx.headers.delete(headerName);
925
+ return;
926
+ }
927
+ ctx.headers.set(headerName, prefix ? `${prefix} ${token}` : String(token));
928
+ }
929
+ function withTimeout(p, ms) {
930
+ if (!Number.isFinite(ms) || ms <= 0) return p;
931
+ return new Promise((resolve, reject) => {
932
+ const t = setTimeout(() => reject(new Error(`bearerRefreshGuard: queue timeout after ${ms}ms`)), ms);
933
+ p.then(
934
+ (v) => {
935
+ clearTimeout(t);
936
+ resolve(v);
937
+ },
938
+ (e) => {
939
+ clearTimeout(t);
940
+ reject(e);
941
+ }
942
+ );
943
+ });
944
+ }
945
+
946
+ // src/guards/csrfGuard.ts
947
+ var DEFAULT_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
948
+ function readCookie(name) {
949
+ if (typeof document === "undefined") return null;
950
+ const cookieStr = document.cookie;
951
+ if (typeof cookieStr !== "string" || cookieStr.length === 0) return null;
952
+ const target = encodeURIComponent(name);
953
+ const parts = cookieStr.split(";");
954
+ for (const part of parts) {
955
+ const eq = part.indexOf("=");
956
+ if (eq === -1) continue;
957
+ const k = part.slice(0, eq).trim();
958
+ if (k === name || k === target) {
959
+ return decodeURIComponent(part.slice(eq + 1).trim());
960
+ }
961
+ }
962
+ return null;
963
+ }
964
+ function csrfGuard(opts = {}) {
965
+ const headerName = opts.headerName ?? "X-CSRF-Token";
966
+ const methods = new Set((opts.applyTo ?? DEFAULT_METHODS).map((m) => m.toUpperCase()));
967
+ let cachedHeaderToken = null;
968
+ return async (ctx, next) => {
969
+ if (!methods.has(ctx.method)) return next(ctx);
970
+ if (ctx.headers.has(headerName)) {
971
+ const res2 = await next(ctx);
972
+ maybeReadResponseHeader(res2);
973
+ return res2;
974
+ }
975
+ const token = await resolveToken();
976
+ if (token != null && token !== "") {
977
+ ctx.headers.set(headerName, token);
978
+ }
979
+ const res = await next(ctx);
980
+ maybeReadResponseHeader(res);
981
+ return res;
982
+ };
983
+ async function resolveToken(_ctx) {
984
+ if (opts.tokenFromCookie) {
985
+ const t = readCookie(opts.tokenFromCookie);
986
+ if (t != null && t !== "") return t;
987
+ }
988
+ if (opts.tokenFromHeader && cachedHeaderToken != null) {
989
+ return cachedHeaderToken;
990
+ }
991
+ if (opts.tokenFromStorage) {
992
+ try {
993
+ const t = await opts.tokenFromStorage();
994
+ if (t != null && t !== "") return t;
995
+ } catch {
996
+ return null;
997
+ }
998
+ }
999
+ return null;
1000
+ }
1001
+ function maybeReadResponseHeader(res) {
1002
+ if (!opts.tokenFromHeader) return;
1003
+ try {
1004
+ const v = res.headers?.get?.(opts.tokenFromHeader);
1005
+ if (v != null && v !== "") cachedHeaderToken = v;
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ // src/guards/correlationIdGuard.ts
1012
+ var CORRELATION_FLAG = "__hg_correlation_id__";
1013
+ function defaultGenerator() {
1014
+ const c = globalThis.crypto;
1015
+ if (c && typeof c.randomUUID === "function") {
1016
+ try {
1017
+ return c.randomUUID();
1018
+ } catch {
1019
+ }
1020
+ }
1021
+ const ts = Date.now().toString(36);
1022
+ const r1 = Math.floor(Math.random() * 4294967295).toString(36);
1023
+ const r2 = Math.floor(Math.random() * 4294967295).toString(36);
1024
+ return `${ts}-${r1}${r2}`;
1025
+ }
1026
+ function correlationIdGuard(opts = {}) {
1027
+ const headerName = opts.headerName ?? "X-Correlation-Id";
1028
+ const generator = opts.generator ?? defaultGenerator;
1029
+ const readResponseHeader = opts.readResponseHeader ?? true;
1030
+ return async (ctx, next) => {
1031
+ const anyCtx = ctx;
1032
+ let id = anyCtx[CORRELATION_FLAG] ?? void 0;
1033
+ if (id == null) {
1034
+ const existing = ctx.headers.get(headerName);
1035
+ id = existing != null && existing !== "" ? existing : generator();
1036
+ anyCtx[CORRELATION_FLAG] = id;
1037
+ }
1038
+ if (!ctx.headers.has(headerName)) {
1039
+ ctx.headers.set(headerName, id);
1040
+ }
1041
+ if (!ctx.meta) ctx.meta = {};
1042
+ if (!ctx.meta.correlationId) ctx.meta.correlationId = id;
1043
+ const res = await next(ctx);
1044
+ if (readResponseHeader) {
1045
+ try {
1046
+ const v = res.headers?.get?.(headerName);
1047
+ if (v != null && v !== "") {
1048
+ if (!ctx.meta) ctx.meta = {};
1049
+ ctx.meta.correlationId = v;
1050
+ }
1051
+ } catch {
1052
+ }
1053
+ }
1054
+ return res;
1055
+ };
1056
+ }
1057
+
1058
+ // src/guards/idempotencyGuard.ts
1059
+ var DEFAULT_METHODS2 = ["POST", "PUT", "PATCH"];
1060
+ var IDEMPOTENCY_FLAG = "__hg_idempotency_key__";
1061
+ function defaultGenerator2() {
1062
+ const c = globalThis.crypto;
1063
+ if (c && typeof c.randomUUID === "function") {
1064
+ try {
1065
+ return c.randomUUID();
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ const ts = Date.now().toString(36);
1070
+ const r1 = Math.floor(Math.random() * 4294967295).toString(36);
1071
+ const r2 = Math.floor(Math.random() * 4294967295).toString(36);
1072
+ return `${ts}-${r1}${r2}`;
1073
+ }
1074
+ function idempotencyGuard(opts = {}) {
1075
+ const headerName = opts.headerName ?? "X-Idempotency-Key";
1076
+ const methods = new Set((opts.applyTo ?? DEFAULT_METHODS2).map((m) => m.toUpperCase()));
1077
+ const generator = opts.generator ?? defaultGenerator2;
1078
+ const respectExisting = opts.respectExisting ?? true;
1079
+ return async (ctx, next) => {
1080
+ if (!methods.has(ctx.method)) return next(ctx);
1081
+ const anyCtx = ctx;
1082
+ let key = anyCtx[IDEMPOTENCY_FLAG] ?? void 0;
1083
+ if (key == null && respectExisting) {
1084
+ const fromHeader = ctx.headers.get(headerName);
1085
+ if (fromHeader != null && fromHeader !== "") {
1086
+ key = fromHeader;
1087
+ } else if (ctx.idempotencyKey != null && ctx.idempotencyKey !== "") {
1088
+ key = ctx.idempotencyKey;
1089
+ } else if (ctx.meta?.idempotencyKey != null && ctx.meta.idempotencyKey !== "") {
1090
+ key = ctx.meta.idempotencyKey;
1091
+ }
1092
+ }
1093
+ if (key == null) key = generator();
1094
+ anyCtx[IDEMPOTENCY_FLAG] = key;
1095
+ if (!ctx.headers.has(headerName)) ctx.headers.set(headerName, key);
1096
+ if (!ctx.meta) ctx.meta = {};
1097
+ if (!ctx.meta.idempotencyKey) ctx.meta.idempotencyKey = key;
1098
+ return next(ctx);
1099
+ };
1100
+ }
1101
+
1102
+ // src/guards/unauthorizedGuard.ts
1103
+ function normalizePath2(url) {
1104
+ try {
1105
+ return new URL(url).pathname;
1106
+ } catch {
1107
+ const q = url.indexOf("?");
1108
+ const h = url.indexOf("#");
1109
+ const cut = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h);
1110
+ return url.slice(0, cut);
1111
+ }
1112
+ }
1113
+ function unauthorizedGuard(opts) {
1114
+ const statuses = opts.statuses ?? [401, 419];
1115
+ const ignoreNoAuth = opts.ignoreNoAuth ?? true;
1116
+ const once = opts.once ?? true;
1117
+ const cooldownMs = opts.cooldownMs ?? 250;
1118
+ let locked = false;
1119
+ let cooldownUntil = 0;
1120
+ return async (ctx, next) => {
1121
+ let res;
1122
+ try {
1123
+ res = await next(ctx);
1124
+ } catch (e) {
1125
+ throw e;
1126
+ }
1127
+ if (ignoreNoAuth && ctx.noAuth) return res;
1128
+ const path = normalizePath2(ctx.url);
1129
+ if (opts.excludePaths?.includes(path)) return res;
1130
+ if (!statuses.includes(res.status)) return res;
1131
+ if (opts.shouldHandle && !opts.shouldHandle(ctx, res.status)) return res;
1132
+ const now = Date.now();
1133
+ if (now < cooldownUntil) return res;
1134
+ if (once) {
1135
+ if (locked) return res;
1136
+ locked = true;
1137
+ }
1138
+ try {
1139
+ opts.onUnauthorized({ url: ctx.url, status: res.status });
1140
+ } finally {
1141
+ cooldownUntil = Date.now() + cooldownMs;
1142
+ if (once) setTimeout(() => locked = false, cooldownMs);
1143
+ }
1144
+ return res;
1145
+ };
1146
+ }
1147
+
1148
+ // src/guards/instrumentGuard.ts
1149
+ var REFRESH_FLAG3 = "__hg_is_refresh__";
1150
+ var RETRY_FLAG3 = "__hg_refresh_retry__";
1151
+ function safeUrl(u) {
1152
+ try {
1153
+ const url = new URL(u);
1154
+ return url.toString();
1155
+ } catch {
1156
+ return u;
1157
+ }
1158
+ }
1159
+ function nowMs() {
1160
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1161
+ }
1162
+ function headersToObject(h) {
1163
+ if (!h) return {};
1164
+ const out = {};
1165
+ try {
1166
+ h.forEach((v, k) => {
1167
+ const key = String(k).toLowerCase();
1168
+ if (key === "authorization" || key === "cookie" || key === "set-cookie" || key === "x-api-key" || key === "proxy-authorization") {
1169
+ out[k] = "[REDACTED]";
1170
+ } else {
1171
+ out[k] = v;
1172
+ }
1173
+ });
1174
+ } catch {
1175
+ }
1176
+ return out;
1177
+ }
1178
+ var __hg_req_seq = 0;
1179
+ function instrumentGuard(opts, guard) {
1180
+ const log = opts.log ?? ((line, data) => data !== void 0 ? console.log(line, data) : console.log(line));
1181
+ return async (ctx, next) => {
1182
+ const id = ++__hg_req_seq;
1183
+ const t0 = nowMs();
1184
+ const url = safeUrl(ctx.url);
1185
+ const method = ctx.method ?? "GET";
1186
+ if (opts.include && !opts.include.test(url)) {
1187
+ return guard(ctx, next);
1188
+ }
1189
+ if (opts.exclude && opts.exclude.test(url)) {
1190
+ return guard(ctx, next);
1191
+ }
1192
+ const anyCtx = ctx;
1193
+ const attempt = ctx.attempt ?? 0;
1194
+ const isRefresh = Boolean(anyCtx[REFRESH_FLAG3]);
1195
+ const retryCount = Number(anyCtx[RETRY_FLAG3] ?? 0);
1196
+ const noAuth2 = Boolean(ctx.noAuth);
1197
+ const baseLine = `[HG][#${id}][${opts.name}]`;
1198
+ log(`${baseLine} \u25B6 start`, {
1199
+ method,
1200
+ url,
1201
+ attempt,
1202
+ retryCount,
1203
+ isRefresh,
1204
+ noAuth: noAuth2,
1205
+ baseUrl: ctx.baseUrl,
1206
+ ...opts.logInit ? {
1207
+ credentials: ctx.credentials,
1208
+ redirect: ctx.redirect,
1209
+ headers: headersToObject(ctx.headers)
1210
+ } : {},
1211
+ ...opts.stack ? { stack: new Error().stack } : {}
1212
+ });
1213
+ try {
1214
+ const res = await guard(ctx, next);
1215
+ const dt = Math.round(nowMs() - t0);
1216
+ log(`${baseLine} \u25C0 end (${dt}ms)`, {
1217
+ status: res.status,
1218
+ ok: res.ok,
1219
+ url: res.url || url,
1220
+ redirected: res.redirected,
1221
+ location: res.headers?.get?.("location") ?? null,
1222
+ retryAfter: res.headers?.get?.("retry-after") ?? null
1223
+ });
1224
+ return res;
1225
+ } catch (e) {
1226
+ const dt = Math.round(nowMs() - t0);
1227
+ log(`${baseLine} \u2716 throw (${dt}ms)`, {
1228
+ error: e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : e
1229
+ });
1230
+ throw e;
1231
+ }
1232
+ };
1233
+ }
1234
+
462
1235
  // src/cache/memoryCache.ts
463
1236
  function memoryCache(opts) {
464
1237
  const max = opts?.maxEntries ?? 1e3;
@@ -655,6 +1428,268 @@ function createApiParserGraphQL(opts) {
655
1428
  });
656
1429
  }
657
1430
 
658
- export { NetworkError, ParseError, RedirectError, TimeoutError, alertNotifier, bearerAuth, consoleNotifier, cookieAuth, createApiParserGraphQL, createApiParserLaravel, createApiParserNest, createApiParserRestClassic, createHttpClient, createShapeParser, fromZod, memoryCache, noAuth, retryGuard, schema, tailwindAlertRenderer };
1431
+ // src/parsing/apiParserStructured.ts
1432
+ function isRecord(v) {
1433
+ return !!v && typeof v === "object" && !Array.isArray(v);
1434
+ }
1435
+ function pickString(o, key) {
1436
+ const v = o[key];
1437
+ return typeof v === "string" ? v : void 0;
1438
+ }
1439
+ function createApiParserStructured(opts = {}) {
1440
+ const exposeMeta = opts.exposeMeta ?? true;
1441
+ const honorRetryableHint = opts.honorRetryableHint ?? true;
1442
+ return createShapeParser({
1443
+ isSuccess: (raw, status) => {
1444
+ if (isRecord(raw) && "success" in raw) return raw["success"] === true;
1445
+ return status >= 200 && status < 400;
1446
+ },
1447
+ getData: (raw) => {
1448
+ if (isRecord(raw) && raw["success"] === true && "data" in raw) {
1449
+ return raw["data"];
1450
+ }
1451
+ return raw;
1452
+ },
1453
+ getErrors: (raw, status) => {
1454
+ if (!isRecord(raw)) {
1455
+ return [{ message: status ? `HTTP ${status}` : "Request failed" }];
1456
+ }
1457
+ const env = raw;
1458
+ const meta = isRecord(env["meta"]) ? env["meta"] : void 0;
1459
+ const traceId = meta?.traceId;
1460
+ const spanId = meta?.spanId;
1461
+ const issues = Array.isArray(env["issues"]) ? env["issues"] : [];
1462
+ const topMessage = pickString(env, "message");
1463
+ const topCode = pickString(env, "code");
1464
+ const topI18n = isRecord(env["i18n"]) ? env["i18n"] : void 0;
1465
+ const baseDetails = (extra = {}) => ({
1466
+ ...traceId !== void 0 ? { traceId } : {},
1467
+ ...spanId !== void 0 ? { spanId } : {},
1468
+ ...exposeMeta ? { envelope: env } : {},
1469
+ ...extra
1470
+ });
1471
+ if (issues.length > 0) {
1472
+ return issues.map((iss) => {
1473
+ const message = iss.message ?? topMessage ?? (status ? `HTTP ${status}` : "Request failed");
1474
+ const code = iss.code ?? topCode;
1475
+ const issueDetails = baseDetails({
1476
+ ...iss.category !== void 0 ? { category: iss.category } : {},
1477
+ ...iss.severity !== void 0 ? { severity: iss.severity } : {},
1478
+ ...honorRetryableHint && iss.retryable !== void 0 ? { retryable: iss.retryable } : {},
1479
+ ...iss.i18n !== void 0 ? { i18n: iss.i18n } : topI18n !== void 0 ? { i18n: topI18n } : {}
1480
+ });
1481
+ return {
1482
+ message,
1483
+ ...code !== void 0 ? { code } : {},
1484
+ ...iss.field !== void 0 ? { field: iss.field } : {},
1485
+ details: issueDetails
1486
+ };
1487
+ });
1488
+ }
1489
+ const details = baseDetails({
1490
+ ...topI18n !== void 0 ? { i18n: topI18n } : {}
1491
+ });
1492
+ return [
1493
+ {
1494
+ message: topMessage ?? (status ? `HTTP ${status}` : "Request failed"),
1495
+ ...topCode !== void 0 ? { code: topCode } : {},
1496
+ details
1497
+ }
1498
+ ];
1499
+ }
1500
+ });
1501
+ }
1502
+
1503
+ // src/presets/presetBuilder.ts
1504
+ function createPresetBuilder(init) {
1505
+ const state = {
1506
+ baseUrl: init?.baseUrl ?? "",
1507
+ parser: init?.parser ?? createApiParserRestClassic(),
1508
+ auth: init?.auth ?? noAuth(),
1509
+ ...init?.fetch ? { fetch: init.fetch } : {},
1510
+ ...init?.notifier ? { notifier: init.notifier } : { notifier: consoleNotifier },
1511
+ guards: []
1512
+ };
1513
+ function ensureBaseUrl() {
1514
+ if (!state.baseUrl) throw new Error("presetBuilder: baseUrl is required");
1515
+ }
1516
+ const api = {
1517
+ /** Set / override baseUrl */
1518
+ baseUrl(baseUrl) {
1519
+ state.baseUrl = baseUrl;
1520
+ return api;
1521
+ },
1522
+ /** Set / override parser */
1523
+ parser(parser) {
1524
+ state.parser = parser;
1525
+ return api;
1526
+ },
1527
+ /** Set / override fetch */
1528
+ fetch(fetchImpl) {
1529
+ state.fetch = fetchImpl;
1530
+ return api;
1531
+ },
1532
+ /** Set / override notifier */
1533
+ notifier(notifier) {
1534
+ state.notifier = notifier;
1535
+ return api;
1536
+ },
1537
+ /** Add guard(s) (preserves order) */
1538
+ guards(...guards) {
1539
+ state.guards = [...state.guards ?? [], ...guards];
1540
+ return api;
1541
+ },
1542
+ /** Enable in-memory cache */
1543
+ memoryCache(opts) {
1544
+ state.cache = memoryCache(opts);
1545
+ return api;
1546
+ },
1547
+ /** Low-level cache assignment */
1548
+ cache(cache) {
1549
+ if (cache === void 0) {
1550
+ delete state.cache;
1551
+ } else {
1552
+ state.cache = cache;
1553
+ }
1554
+ return api;
1555
+ },
1556
+ /** Default headers / timeouts / notify policy */
1557
+ defaults(defaults) {
1558
+ state.defaults = { ...state.defaults ?? {}, ...defaults ?? {} };
1559
+ return api;
1560
+ },
1561
+ /** Redirect policy */
1562
+ redirects(redirects) {
1563
+ if (redirects === void 0) {
1564
+ delete state.redirects;
1565
+ } else {
1566
+ state.redirects = redirects;
1567
+ }
1568
+ return api;
1569
+ },
1570
+ /** Auth presets */
1571
+ auth(auth) {
1572
+ state.auth = auth;
1573
+ return api;
1574
+ },
1575
+ bearer(getToken, opts) {
1576
+ state.auth = bearerAuth(getToken, opts);
1577
+ return api;
1578
+ },
1579
+ cookie(opts) {
1580
+ state.auth = cookieAuth(opts);
1581
+ return api;
1582
+ },
1583
+ basic(getCred, opts) {
1584
+ state.auth = basicAuth(getCred, opts);
1585
+ return api;
1586
+ },
1587
+ apiKey(getKey, opts) {
1588
+ state.auth = apiKeyAuth(getKey, opts);
1589
+ return api;
1590
+ },
1591
+ none() {
1592
+ state.auth = noAuth();
1593
+ return api;
1594
+ },
1595
+ /**
1596
+ * Cookie refresh guard preset (only meaningful for cookie auth).
1597
+ * You can still add it manually via `.guards(...)`.
1598
+ */
1599
+ cookieRefresh(opts) {
1600
+ state.guards = [...state.guards ?? [], cookieSafeRefreshGuard(opts)];
1601
+ return api;
1602
+ },
1603
+ /** Unauthorized handler preset (redirect/logout). */
1604
+ unauthorized(opts) {
1605
+ state.guards = [...state.guards ?? [], unauthorizedGuard(opts)];
1606
+ return api;
1607
+ },
1608
+ /** Returns the final HttpClientOptions */
1609
+ options() {
1610
+ ensureBaseUrl();
1611
+ return { ...state, guards: [...state.guards ?? []] };
1612
+ },
1613
+ /** Creates the http client */
1614
+ create() {
1615
+ ensureBaseUrl();
1616
+ return createHttpClient(api.options());
1617
+ }
1618
+ };
1619
+ return api;
1620
+ }
1621
+ function createSimpleClient(opts) {
1622
+ return createPresetBuilder(opts).create();
1623
+ }
1624
+ function createBearerClient(opts) {
1625
+ const bearerOpts = {
1626
+ ...opts.headerName !== void 0 ? { headerName: opts.headerName } : {},
1627
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {}
1628
+ };
1629
+ return createPresetBuilder({
1630
+ baseUrl: opts.baseUrl,
1631
+ ...opts.parser ? { parser: opts.parser } : {},
1632
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1633
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1634
+ }).bearer(opts.getToken, Object.keys(bearerOpts).length ? bearerOpts : void 0).create();
1635
+ }
1636
+ function createCookieClient(opts) {
1637
+ const b = createPresetBuilder({
1638
+ baseUrl: opts.baseUrl,
1639
+ ...opts.parser ? { parser: opts.parser } : {},
1640
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1641
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1642
+ }).cookie({
1643
+ ...opts.credentials !== void 0 ? { credentials: opts.credentials } : {},
1644
+ ...opts.csrf !== void 0 ? { csrf: opts.csrf } : {}
1645
+ });
1646
+ if (opts.refresh) b.cookieRefresh(opts.refresh);
1647
+ return b.create();
1648
+ }
1649
+ function createBasicClient(opts) {
1650
+ const basicOpts = {
1651
+ ...opts.headerName !== void 0 ? { headerName: opts.headerName } : {},
1652
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {}
1653
+ };
1654
+ return createPresetBuilder({
1655
+ baseUrl: opts.baseUrl,
1656
+ ...opts.parser ? { parser: opts.parser } : {},
1657
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1658
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1659
+ }).basic(opts.getCredentials, Object.keys(basicOpts).length ? basicOpts : void 0).create();
1660
+ }
1661
+ function createApiKeyClient(opts) {
1662
+ const apiKeyOpts = {
1663
+ ...opts.headerName !== void 0 ? { headerName: opts.headerName } : {},
1664
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {}
1665
+ };
1666
+ return createPresetBuilder({
1667
+ baseUrl: opts.baseUrl,
1668
+ ...opts.parser ? { parser: opts.parser } : {},
1669
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1670
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1671
+ }).apiKey(opts.getKey, Object.keys(apiKeyOpts).length ? apiKeyOpts : void 0).create();
1672
+ }
1673
+
1674
+ // src/aliases.ts
1675
+ function createClient(opts) {
1676
+ const parser = opts.parser ?? createApiParserRestClassic();
1677
+ const auth = opts.auth ?? noAuth();
1678
+ const notifier = opts.notifier ?? consoleNotifier;
1679
+ const cfg = {
1680
+ baseUrl: opts.baseUrl,
1681
+ parser,
1682
+ auth,
1683
+ notifier,
1684
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1685
+ ...opts.defaults ? { defaults: opts.defaults } : {},
1686
+ ...opts.cache ? { cache: opts.cache } : {},
1687
+ ...opts.redirects ? { redirects: opts.redirects } : {},
1688
+ ...opts.guards ? { guards: opts.guards } : {}
1689
+ };
1690
+ return createHttpClient(cfg);
1691
+ }
1692
+
1693
+ export { ILLEGAL_INVOCATION_HINT, NetworkError, ParseError, RedirectError, TimeoutError, alertNotifier, apiKeyAuth, basicAuth, bearerAuth, bearerRefreshGuard, consoleNotifier, cookieAuth, cookieSafeRefreshGuard, correlationIdGuard, createApiKeyClient, createApiParserGraphQL, createApiParserLaravel, createApiParserNest, createApiParserRestClassic, createApiParserStructured, createBasicClient, createBearerClient, createClient, createCookieClient, createHttpClient, createPresetBuilder, createShapeParser, createSimpleClient, csrfGuard, fromZod, idempotencyGuard, instrumentGuard, memoryCache, noAuth, retryGuard, schema, tailwindAlertRenderer, unauthorizedGuard };
659
1694
  //# sourceMappingURL=index.js.map
660
1695
  //# sourceMappingURL=index.js.map