@cedvict/http-guardian 0.0.1-next.6 → 0.0.1-next.7

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 (66) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -1
  3. package/README.md +76 -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/cookieSafeRefreshGuard.d.ts +40 -0
  23. package/dist/guards/cookieSafeRefreshGuard.d.ts.map +1 -0
  24. package/dist/guards/instrumentGuard.d.ts +12 -0
  25. package/dist/guards/instrumentGuard.d.ts.map +1 -0
  26. package/dist/guards/retryGuard.d.ts +12 -0
  27. package/dist/guards/retryGuard.d.ts.map +1 -0
  28. package/dist/guards/types.d.ts +3 -0
  29. package/dist/guards/types.d.ts.map +1 -0
  30. package/dist/guards/unauthorizedGuard.d.ts +19 -0
  31. package/dist/guards/unauthorizedGuard.d.ts.map +1 -0
  32. package/dist/index.cjs +1413 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d.ts +22 -322
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +643 -63
  37. package/dist/index.js.map +1 -1
  38. package/dist/internal/authGuard.d.ts +4 -0
  39. package/dist/internal/authGuard.d.ts.map +1 -0
  40. package/dist/internal/compose.d.ts +7 -0
  41. package/dist/internal/compose.d.ts.map +1 -0
  42. package/dist/internal/context.d.ts +31 -0
  43. package/dist/internal/context.d.ts.map +1 -0
  44. package/dist/internal/headersPolyfill.d.ts +25 -0
  45. package/dist/internal/headersPolyfill.d.ts.map +1 -0
  46. package/dist/internal/stableStringify.d.ts +2 -0
  47. package/dist/internal/stableStringify.d.ts.map +1 -0
  48. package/dist/internal/url.d.ts +3 -0
  49. package/dist/internal/url.d.ts.map +1 -0
  50. package/dist/notify/defaults.d.ts +9 -0
  51. package/dist/notify/defaults.d.ts.map +1 -0
  52. package/dist/notify/types.d.ts +33 -0
  53. package/dist/notify/types.d.ts.map +1 -0
  54. package/dist/parsing/presets.d.ts +15 -0
  55. package/dist/parsing/presets.d.ts.map +1 -0
  56. package/dist/parsing/schemaAdapter.d.ts +18 -0
  57. package/dist/parsing/schemaAdapter.d.ts.map +1 -0
  58. package/dist/parsing/shapeParser.d.ts +8 -0
  59. package/dist/parsing/shapeParser.d.ts.map +1 -0
  60. package/dist/plugins/types.d.ts +22 -0
  61. package/dist/plugins/types.d.ts.map +1 -0
  62. package/dist/presets/presetBuilder.d.ts +181 -0
  63. package/dist/presets/presetBuilder.d.ts.map +1 -0
  64. package/dist/publicTypes.d.ts +12 -0
  65. package/dist/publicTypes.d.ts.map +1 -0
  66. 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));
@@ -175,21 +310,49 @@ function normalizeFetch(f) {
175
310
  const bound = f.bind ? f.bind(globalThis) : f;
176
311
  return ((input, init) => bound(input, init));
177
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
+ }
178
319
  var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
179
- function createHttpClient(opts) {
180
- const resolvedFetch = opts.fetch ? normalizeFetch(opts.fetch) : ((input, init) => globalThis.fetch(input, init));
181
- 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;
182
345
  const inFlight = new InFlight();
183
- const defaultNotifier = opts.notifier ?? consoleNotifier;
184
- const shouldNotify = opts.defaults?.shouldNotify ?? ((r) => !r.ok);
185
- 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 ?? []];
186
349
  async function terminal(ctx) {
187
350
  const controller = ctx.timeoutMs ? new AbortController() : void 0;
188
351
  const timeout = ctx.timeoutMs ? setTimeout(() => controller?.abort(new TimeoutError()), ctx.timeoutMs) : void 0;
189
352
  try {
190
353
  const init = {
191
354
  method: ctx.method,
192
- headers: ctx.headers,
355
+ headers: toFetchHeaders(ctx.headers),
193
356
  ...ctx.body !== void 0 ? { body: ctx.body } : {},
194
357
  ...ctx.signal ? { signal: controller ? anySignal([ctx.signal, controller.signal]) : ctx.signal } : controller ? { signal: controller.signal } : {},
195
358
  ...ctx.credentials !== void 0 ? { credentials: ctx.credentials } : {},
@@ -209,9 +372,9 @@ function createHttpClient(opts) {
209
372
  }
210
373
  const pipeline = composeGuards(baseGuards, terminal);
211
374
  async function request(method, path, ro) {
212
- const requestUrl = withQuery(joinUrl(opts.baseUrl, path), ro?.query);
213
- const headers = new Headers();
214
- 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);
215
378
  if (ro?.headers) {
216
379
  for (const [k, v] of Object.entries(ro.headers)) if (v !== void 0) headers.set(k, v);
217
380
  }
@@ -229,19 +392,31 @@ function createHttpClient(opts) {
229
392
  } else {
230
393
  if (!headers.has("accept")) headers.set("accept", "application/json");
231
394
  }
232
- const timeoutMs = ro?.timeoutMs ?? opts.defaults?.timeoutMs;
395
+ const timeoutMs = ro?.timeoutMs ?? draft.defaults?.timeoutMs;
233
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
+ };
234
402
  const cacheOpts = ro?.cache;
235
403
  const wantsCache = method === "GET" && !!cache && !!cacheOpts;
236
404
  const dedupe = cacheOpts?.dedupe ?? true;
237
405
  const canDedupeWrite = method !== "GET" && !!ro?.idempotencyKey;
238
- 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;
239
414
  if (inflightKey) {
240
415
  const existing = inFlight.get(inflightKey);
241
416
  if (existing) {
242
417
  try {
243
418
  const stored = await existing;
244
- return asClientResult(stored, requestUrl, opts, ro, notifier, shouldNotify);
419
+ return asClientResult(stored, requestUrl, draft, ro, notifier, shouldNotify, emit);
245
420
  } catch {
246
421
  }
247
422
  }
@@ -263,7 +438,7 @@ function createHttpClient(opts) {
263
438
  }
264
439
  }
265
440
  const res = await fetchWithRedirects({
266
- opts,
441
+ opts: draft,
267
442
  resolvedFetch,
268
443
  pipeline,
269
444
  method,
@@ -278,13 +453,17 @@ function createHttpClient(opts) {
278
453
  const envelope = { kind: "network", url: res.url || requestUrl, status: res.status, headers: res.headers, raw };
279
454
  if (wantsCache && cacheOpts && res.ok) {
280
455
  const key = inflightKey;
281
- 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
+ });
282
461
  }
283
462
  return envelope;
284
463
  async function refreshInBackground(key) {
285
464
  try {
286
465
  const res2 = await fetchWithRedirects({
287
- opts,
466
+ opts: draft,
288
467
  resolvedFetch,
289
468
  pipeline,
290
469
  method,
@@ -297,7 +476,11 @@ function createHttpClient(opts) {
297
476
  });
298
477
  if (!res2.ok) return;
299
478
  const raw2 = await decodeResponse(res2);
300
- 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
+ });
301
484
  } catch {
302
485
  }
303
486
  }
@@ -305,13 +488,13 @@ function createHttpClient(opts) {
305
488
  if (inflightKey) inFlight.set(inflightKey, promise);
306
489
  try {
307
490
  const stored = await promise;
308
- return asClientResult(stored, requestUrl, opts, ro, notifier, shouldNotify);
491
+ return asClientResult(stored, requestUrl, draft, ro, notifier, shouldNotify, emit);
309
492
  } catch (e) {
310
493
  const err = e instanceof TimeoutError ? e : new NetworkError(
311
494
  isIllegalInvocation(e) ? `Network request failed. ${ILLEGAL_INVOCATION_HINT}` : "Network request failed",
312
495
  e
313
496
  );
314
- if (shouldNotify({ ok: false, status: 0 })) notifier.notify({ type: "network-error", level: "error", title: "Network error", message: err.message, url: requestUrl });
497
+ if (shouldNotify({ ok: false, status: 0 })) emit({ type: "network-error", level: "error", title: "Network error", message: err.message, url: requestUrl });
315
498
  return { ok: false, status: 0, headers: new Headers(), errors: [{ message: err.message, details: e }], raw: null, url: requestUrl };
316
499
  }
317
500
  }
@@ -343,7 +526,19 @@ async function fetchWithRedirects(args) {
343
526
  );
344
527
  if (!REDIRECT_STATUSES.has(res.status)) return res;
345
528
  const loc = res.headers.get("location");
346
- 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
+ }
347
542
  const nextUrl = new URL(loc, currentUrl).toString();
348
543
  const decision = policy.onRedirect?.({ fromUrl: currentUrl, toUrl: nextUrl, status: res.status, hop, method });
349
544
  if (decision?.action === "deny") throw new RedirectError(decision.reason ?? "Redirect denied by policy", currentUrl, nextUrl);
@@ -363,18 +558,38 @@ async function fetchWithRedirects(args) {
363
558
  }
364
559
  function stripAuthOnCrossOriginRedirect(opts, initialUrl, toUrl, headers) {
365
560
  try {
366
- if (opts.auth.mode !== "bearer") return;
367
561
  const initialOrigin = new URL(initialUrl).origin;
368
562
  const toOrigin = new URL(toUrl).origin;
369
- if (initialOrigin !== toOrigin) {
563
+ if (initialOrigin === toOrigin) return;
564
+ if (opts.auth.mode === "bearer") {
370
565
  const headerName = opts.auth.headerName ?? "Authorization";
371
566
  headers.delete(headerName);
567
+ return;
568
+ }
569
+ if (opts.auth.mode === "basic") {
570
+ const headerName = opts.auth.headerName ?? "Authorization";
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;
372
578
  }
373
579
  } catch {
374
580
  }
375
581
  }
376
582
  function makeCtx(args, redirect) {
583
+ const baseOrigin = (() => {
584
+ try {
585
+ return new URL(args.opts.baseUrl).origin;
586
+ } catch {
587
+ return "";
588
+ }
589
+ })();
377
590
  return {
591
+ baseUrl: args.opts.baseUrl,
592
+ baseOrigin,
378
593
  method: args.method,
379
594
  url: args.url,
380
595
  headers: args.headers,
@@ -404,10 +619,10 @@ async function decodeResponse(res) {
404
619
  throw new ParseError("Failed to read response body", void 0, e);
405
620
  }
406
621
  }
407
- function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify) {
622
+ function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify, emit) {
408
623
  if (envelope?.kind === "cache-miss") {
409
624
  const errors2 = [{ message: "Cache miss" }];
410
- 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 });
411
626
  return { ok: false, status: 0, headers: new Headers(), errors: errors2, raw: envelope.raw, url: requestUrl };
412
627
  }
413
628
  const status = envelope.status ?? 0;
@@ -422,17 +637,24 @@ function asClientResult(envelope, requestUrl, opts, ro, notifier, shouldNotify)
422
637
  return { ok: true, status, headers: resHeaders, data, raw, url: finalUrl };
423
638
  } catch (e) {
424
639
  const msg2 = e instanceof Error ? e.message : "Schema parse failed";
425
- 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 });
426
641
  return { ok: false, status, headers: resHeaders, errors: [{ message: msg2, details: e }], raw, url: finalUrl };
427
642
  }
428
643
  }
429
644
  const errors = opts.parser.getErrors(raw, status);
430
645
  const msg = errors[0]?.message ?? (status ? `HTTP ${status}` : "Request failed");
431
- 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 });
432
647
  return { ok: false, status, headers: resHeaders, errors, raw, url: finalUrl };
433
648
  }
434
649
  function anySignal(signals) {
435
650
  const valid = signals.filter(Boolean);
651
+ const any = AbortSignal.any;
652
+ if (any) {
653
+ try {
654
+ return any(valid);
655
+ } catch {
656
+ }
657
+ }
436
658
  const controller = new AbortController();
437
659
  const onAbort = () => controller.abort();
438
660
  for (const s of valid) {
@@ -448,7 +670,8 @@ function bearerAuth(getToken, opts) {
448
670
  mode: "bearer",
449
671
  getToken,
450
672
  ...opts?.headerName !== void 0 ? { headerName: opts.headerName } : {},
451
- ...opts?.prefix !== void 0 ? { prefix: opts.prefix } : {}
673
+ ...opts?.prefix !== void 0 ? { prefix: opts.prefix } : {},
674
+ ...opts?.allowedOrigins !== void 0 ? { allowedOrigins: opts.allowedOrigins } : {}
452
675
  };
453
676
  }
454
677
  function cookieAuth(opts) {
@@ -458,6 +681,24 @@ function cookieAuth(opts) {
458
681
  ...opts?.csrf !== void 0 ? { csrf: opts.csrf } : {}
459
682
  };
460
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
+ }
461
702
  function noAuth() {
462
703
  return { mode: "none" };
463
704
  }
@@ -497,8 +738,7 @@ function isReplayableBody(body) {
497
738
  }
498
739
  function normalizePath(url) {
499
740
  try {
500
- const u = new URL(url);
501
- return u.pathname;
741
+ return new URL(url).pathname;
502
742
  } catch {
503
743
  const q = url.indexOf("?");
504
744
  const h = url.indexOf("#");
@@ -515,6 +755,14 @@ function parseRetryAfterMs(headers) {
515
755
  if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
516
756
  return null;
517
757
  }
758
+ function isAbsoluteUrl(u) {
759
+ try {
760
+ void new URL(u);
761
+ return true;
762
+ } catch {
763
+ return false;
764
+ }
765
+ }
518
766
  function cookieSafeRefreshGuard(opts) {
519
767
  const trigger = new Set(opts.triggerStatuses ?? [401, 419]);
520
768
  const maxRetry = opts.maxRetry ?? 1;
@@ -527,38 +775,39 @@ function cookieSafeRefreshGuard(opts) {
527
775
  const path = normalizePath(ctx.url);
528
776
  if (opts.excludePaths?.includes(path)) return next(ctx);
529
777
  if (ctx.noAuth) return next(ctx);
530
- const refreshPathNorm = normalizePath(opts.refreshPath);
778
+ const refreshPathNorm = normalizePath(resolveRefreshUrl(ctx, opts.refreshPath));
531
779
  if (path === refreshPathNorm) return next(ctx);
532
780
  if (honorRetryAfter && refreshCooldownUntil > Date.now()) {
533
781
  return next(ctx);
534
782
  }
535
783
  const res = await next(ctx);
536
784
  if (!trigger.has(res.status)) return res;
537
- if (opts.shouldRefresh && !opts.shouldRefresh(ctx, res.status)) {
538
- return res;
539
- }
785
+ if (opts.shouldRefresh && !opts.shouldRefresh(ctx, res.status)) return res;
540
786
  const alreadyRetried = Number(anyCtx[RETRY_FLAG] ?? 0);
541
787
  if (alreadyRetried >= maxRetry) return res;
542
- if (!isReplayableBody(ctx.body)) {
543
- return res;
544
- }
788
+ if (!isReplayableBody(ctx.body)) return res;
545
789
  const decision = await (refreshPromise ??= doRefresh(ctx, next).finally(() => {
546
790
  refreshPromise = null;
547
791
  }));
548
792
  if (!decision.ok) {
549
- opts.onRefreshFailed?.({ url: ctx.url, reason: decision.reason ?? "" });
793
+ opts.onRefreshFailed?.({
794
+ url: ctx.url,
795
+ ...decision.reason !== void 0 ? { reason: decision.reason } : {},
796
+ ...decision.status !== void 0 ? { status: decision.status } : {}
797
+ });
550
798
  return res;
551
799
  }
552
800
  opts.onRefreshSuccess?.({ url: ctx.url });
553
801
  const retryCtx = Object.assign({}, ctx, { attempt: ctx.attempt + 1 });
554
802
  retryCtx[RETRY_FLAG] = alreadyRetried + 1;
803
+ await applyCookieAuth(retryCtx);
555
804
  return next(retryCtx);
556
805
  };
557
806
  async function doRefresh(originalCtx, next) {
558
807
  if (honorRetryAfter && refreshCooldownUntil > Date.now()) {
559
808
  return { ok: false, reason: "Refresh cooldown active" };
560
809
  }
561
- const refreshUrl = resolveRefreshUrl(originalCtx.url, opts.refreshPath);
810
+ const refreshUrl = resolveRefreshUrl(originalCtx, opts.refreshPath);
562
811
  const refreshCtx = {
563
812
  ...originalCtx,
564
813
  url: refreshUrl,
@@ -568,36 +817,177 @@ function cookieSafeRefreshGuard(opts) {
568
817
  attempt: 0
569
818
  };
570
819
  refreshCtx[REFRESH_FLAG] = true;
820
+ refreshCtx[RETRY_FLAG] = 0;
821
+ await applyCookieAuth(refreshCtx);
571
822
  try {
572
823
  const res = await next(refreshCtx);
573
824
  if (res.status === 429 && honorRetryAfter) {
574
825
  const ms = parseRetryAfterMs(res.headers);
575
826
  refreshCooldownUntil = Date.now() + (ms ?? 3e4);
576
- return { ok: false, reason: "Refresh rate-limited (429)" };
827
+ return { ok: false, reason: "Refresh rate-limited (429)", status: 429 };
577
828
  }
578
829
  if (!res.ok) {
579
- return { ok: false, reason: `Refresh failed (HTTP ${res.status})` };
830
+ return { ok: false, reason: `Refresh failed (HTTP ${res.status})`, status: res.status };
580
831
  }
581
832
  return { ok: true };
582
833
  } catch (e) {
583
834
  return { ok: false, reason: e instanceof Error ? e.message : "Refresh exception" };
584
835
  }
585
836
  }
586
- function resolveRefreshUrl(baseRequestUrl, refreshPath) {
587
- try {
588
- const u = new URL(refreshPath);
589
- return u.toString();
590
- } catch {
591
- try {
592
- const base = new URL(baseRequestUrl);
593
- return new URL(refreshPath, base.origin).toString();
594
- } catch {
595
- return refreshPath;
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));
596
853
  }
597
854
  }
598
855
  }
599
856
  }
600
857
 
858
+ // src/guards/unauthorizedGuard.ts
859
+ function normalizePath2(url) {
860
+ try {
861
+ return new URL(url).pathname;
862
+ } catch {
863
+ const q = url.indexOf("?");
864
+ const h = url.indexOf("#");
865
+ const cut = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h);
866
+ return url.slice(0, cut);
867
+ }
868
+ }
869
+ function unauthorizedGuard(opts) {
870
+ const statuses = opts.statuses ?? [401, 419];
871
+ const ignoreNoAuth = opts.ignoreNoAuth ?? true;
872
+ const once = opts.once ?? true;
873
+ const cooldownMs = opts.cooldownMs ?? 250;
874
+ let locked = false;
875
+ let cooldownUntil = 0;
876
+ return async (ctx, next) => {
877
+ let res;
878
+ try {
879
+ res = await next(ctx);
880
+ } catch (e) {
881
+ throw e;
882
+ }
883
+ if (ignoreNoAuth && ctx.noAuth) return res;
884
+ const path = normalizePath2(ctx.url);
885
+ if (opts.excludePaths?.includes(path)) return res;
886
+ if (!statuses.includes(res.status)) return res;
887
+ if (opts.shouldHandle && !opts.shouldHandle(ctx, res.status)) return res;
888
+ const now = Date.now();
889
+ if (now < cooldownUntil) return res;
890
+ if (once) {
891
+ if (locked) return res;
892
+ locked = true;
893
+ }
894
+ try {
895
+ opts.onUnauthorized({ url: ctx.url, status: res.status });
896
+ } finally {
897
+ cooldownUntil = Date.now() + cooldownMs;
898
+ if (once) setTimeout(() => locked = false, cooldownMs);
899
+ }
900
+ return res;
901
+ };
902
+ }
903
+
904
+ // src/guards/instrumentGuard.ts
905
+ var REFRESH_FLAG2 = "__hg_is_refresh__";
906
+ var RETRY_FLAG2 = "__hg_refresh_retry__";
907
+ function safeUrl(u) {
908
+ try {
909
+ const url = new URL(u);
910
+ return url.toString();
911
+ } catch {
912
+ return u;
913
+ }
914
+ }
915
+ function nowMs() {
916
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
917
+ }
918
+ function headersToObject(h) {
919
+ if (!h) return {};
920
+ const out = {};
921
+ try {
922
+ h.forEach((v, k) => {
923
+ const key = String(k).toLowerCase();
924
+ if (key === "authorization" || key === "cookie" || key === "set-cookie" || key === "x-api-key" || key === "proxy-authorization") {
925
+ out[k] = "[REDACTED]";
926
+ } else {
927
+ out[k] = v;
928
+ }
929
+ });
930
+ } catch {
931
+ }
932
+ return out;
933
+ }
934
+ var __hg_req_seq = 0;
935
+ function instrumentGuard(opts, guard) {
936
+ const log = opts.log ?? ((line, data) => data !== void 0 ? console.log(line, data) : console.log(line));
937
+ return async (ctx, next) => {
938
+ const id = ++__hg_req_seq;
939
+ const t0 = nowMs();
940
+ const url = safeUrl(ctx.url);
941
+ const method = ctx.method ?? "GET";
942
+ if (opts.include && !opts.include.test(url)) {
943
+ return guard(ctx, next);
944
+ }
945
+ if (opts.exclude && opts.exclude.test(url)) {
946
+ return guard(ctx, next);
947
+ }
948
+ const anyCtx = ctx;
949
+ const attempt = ctx.attempt ?? 0;
950
+ const isRefresh = Boolean(anyCtx[REFRESH_FLAG2]);
951
+ const retryCount = Number(anyCtx[RETRY_FLAG2] ?? 0);
952
+ const noAuth2 = Boolean(ctx.noAuth);
953
+ const baseLine = `[HG][#${id}][${opts.name}]`;
954
+ log(`${baseLine} \u25B6 start`, {
955
+ method,
956
+ url,
957
+ attempt,
958
+ retryCount,
959
+ isRefresh,
960
+ noAuth: noAuth2,
961
+ baseUrl: ctx.baseUrl,
962
+ ...opts.logInit ? {
963
+ credentials: ctx.credentials,
964
+ redirect: ctx.redirect,
965
+ headers: headersToObject(ctx.headers)
966
+ } : {},
967
+ ...opts.stack ? { stack: new Error().stack } : {}
968
+ });
969
+ try {
970
+ const res = await guard(ctx, next);
971
+ const dt = Math.round(nowMs() - t0);
972
+ log(`${baseLine} \u25C0 end (${dt}ms)`, {
973
+ status: res.status,
974
+ ok: res.ok,
975
+ url: res.url || url,
976
+ redirected: res.redirected,
977
+ location: res.headers?.get?.("location") ?? null,
978
+ retryAfter: res.headers?.get?.("retry-after") ?? null
979
+ });
980
+ return res;
981
+ } catch (e) {
982
+ const dt = Math.round(nowMs() - t0);
983
+ log(`${baseLine} \u2716 throw (${dt}ms)`, {
984
+ error: e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : e
985
+ });
986
+ throw e;
987
+ }
988
+ };
989
+ }
990
+
601
991
  // src/cache/memoryCache.ts
602
992
  function memoryCache(opts) {
603
993
  const max = opts?.maxEntries ?? 1e3;
@@ -794,6 +1184,196 @@ function createApiParserGraphQL(opts) {
794
1184
  });
795
1185
  }
796
1186
 
797
- export { ILLEGAL_INVOCATION_HINT, NetworkError, ParseError, RedirectError, TimeoutError, alertNotifier, bearerAuth, consoleNotifier, cookieAuth, cookieSafeRefreshGuard, createApiParserGraphQL, createApiParserLaravel, createApiParserNest, createApiParserRestClassic, createHttpClient, createShapeParser, fromZod, memoryCache, noAuth, retryGuard, schema, tailwindAlertRenderer };
1187
+ // src/presets/presetBuilder.ts
1188
+ function createPresetBuilder(init) {
1189
+ const state = {
1190
+ baseUrl: init?.baseUrl ?? "",
1191
+ parser: init?.parser ?? createApiParserRestClassic(),
1192
+ auth: init?.auth ?? noAuth(),
1193
+ ...init?.fetch ? { fetch: init.fetch } : {},
1194
+ ...init?.notifier ? { notifier: init.notifier } : { notifier: consoleNotifier },
1195
+ guards: []
1196
+ };
1197
+ function ensureBaseUrl() {
1198
+ if (!state.baseUrl) throw new Error("presetBuilder: baseUrl is required");
1199
+ }
1200
+ const api = {
1201
+ /** Set / override baseUrl */
1202
+ baseUrl(baseUrl) {
1203
+ state.baseUrl = baseUrl;
1204
+ return api;
1205
+ },
1206
+ /** Set / override parser */
1207
+ parser(parser) {
1208
+ state.parser = parser;
1209
+ return api;
1210
+ },
1211
+ /** Set / override fetch */
1212
+ fetch(fetchImpl) {
1213
+ state.fetch = fetchImpl;
1214
+ return api;
1215
+ },
1216
+ /** Set / override notifier */
1217
+ notifier(notifier) {
1218
+ state.notifier = notifier;
1219
+ return api;
1220
+ },
1221
+ /** Add guard(s) (preserves order) */
1222
+ guards(...guards) {
1223
+ state.guards = [...state.guards ?? [], ...guards];
1224
+ return api;
1225
+ },
1226
+ /** Enable in-memory cache */
1227
+ memoryCache(opts) {
1228
+ state.cache = memoryCache(opts);
1229
+ return api;
1230
+ },
1231
+ /** Low-level cache assignment */
1232
+ cache(cache) {
1233
+ if (cache === void 0) {
1234
+ delete state.cache;
1235
+ } else {
1236
+ state.cache = cache;
1237
+ }
1238
+ return api;
1239
+ },
1240
+ /** Default headers / timeouts / notify policy */
1241
+ defaults(defaults) {
1242
+ state.defaults = { ...state.defaults ?? {}, ...defaults ?? {} };
1243
+ return api;
1244
+ },
1245
+ /** Redirect policy */
1246
+ redirects(redirects) {
1247
+ if (redirects === void 0) {
1248
+ delete state.redirects;
1249
+ } else {
1250
+ state.redirects = redirects;
1251
+ }
1252
+ return api;
1253
+ },
1254
+ /** Auth presets */
1255
+ auth(auth) {
1256
+ state.auth = auth;
1257
+ return api;
1258
+ },
1259
+ bearer(getToken, opts) {
1260
+ state.auth = bearerAuth(getToken, opts);
1261
+ return api;
1262
+ },
1263
+ cookie(opts) {
1264
+ state.auth = cookieAuth(opts);
1265
+ return api;
1266
+ },
1267
+ basic(getCred, opts) {
1268
+ state.auth = basicAuth(getCred, opts);
1269
+ return api;
1270
+ },
1271
+ apiKey(getKey, opts) {
1272
+ state.auth = apiKeyAuth(getKey, opts);
1273
+ return api;
1274
+ },
1275
+ none() {
1276
+ state.auth = noAuth();
1277
+ return api;
1278
+ },
1279
+ /**
1280
+ * Cookie refresh guard preset (only meaningful for cookie auth).
1281
+ * You can still add it manually via `.guards(...)`.
1282
+ */
1283
+ cookieRefresh(opts) {
1284
+ state.guards = [...state.guards ?? [], cookieSafeRefreshGuard(opts)];
1285
+ return api;
1286
+ },
1287
+ /** Unauthorized handler preset (redirect/logout). */
1288
+ unauthorized(opts) {
1289
+ state.guards = [...state.guards ?? [], unauthorizedGuard(opts)];
1290
+ return api;
1291
+ },
1292
+ /** Returns the final HttpClientOptions */
1293
+ options() {
1294
+ ensureBaseUrl();
1295
+ return { ...state, guards: [...state.guards ?? []] };
1296
+ },
1297
+ /** Creates the http client */
1298
+ create() {
1299
+ ensureBaseUrl();
1300
+ return createHttpClient(api.options());
1301
+ }
1302
+ };
1303
+ return api;
1304
+ }
1305
+ function createSimpleClient(opts) {
1306
+ return createPresetBuilder(opts).create();
1307
+ }
1308
+ function createBearerClient(opts) {
1309
+ const bearerOpts = {
1310
+ ...opts.headerName !== void 0 ? { headerName: opts.headerName } : {},
1311
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {}
1312
+ };
1313
+ return createPresetBuilder({
1314
+ baseUrl: opts.baseUrl,
1315
+ ...opts.parser ? { parser: opts.parser } : {},
1316
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1317
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1318
+ }).bearer(opts.getToken, Object.keys(bearerOpts).length ? bearerOpts : void 0).create();
1319
+ }
1320
+ function createCookieClient(opts) {
1321
+ const b = createPresetBuilder({
1322
+ baseUrl: opts.baseUrl,
1323
+ ...opts.parser ? { parser: opts.parser } : {},
1324
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1325
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1326
+ }).cookie({
1327
+ ...opts.credentials !== void 0 ? { credentials: opts.credentials } : {},
1328
+ ...opts.csrf !== void 0 ? { csrf: opts.csrf } : {}
1329
+ });
1330
+ if (opts.refresh) b.cookieRefresh(opts.refresh);
1331
+ return b.create();
1332
+ }
1333
+ function createBasicClient(opts) {
1334
+ const basicOpts = {
1335
+ ...opts.headerName !== void 0 ? { headerName: opts.headerName } : {},
1336
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {}
1337
+ };
1338
+ return createPresetBuilder({
1339
+ baseUrl: opts.baseUrl,
1340
+ ...opts.parser ? { parser: opts.parser } : {},
1341
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1342
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1343
+ }).basic(opts.getCredentials, Object.keys(basicOpts).length ? basicOpts : void 0).create();
1344
+ }
1345
+ function createApiKeyClient(opts) {
1346
+ const apiKeyOpts = {
1347
+ ...opts.headerName !== void 0 ? { headerName: opts.headerName } : {},
1348
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {}
1349
+ };
1350
+ return createPresetBuilder({
1351
+ baseUrl: opts.baseUrl,
1352
+ ...opts.parser ? { parser: opts.parser } : {},
1353
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1354
+ ...opts.notifier ? { notifier: opts.notifier } : {}
1355
+ }).apiKey(opts.getKey, Object.keys(apiKeyOpts).length ? apiKeyOpts : void 0).create();
1356
+ }
1357
+
1358
+ // src/aliases.ts
1359
+ function createClient(opts) {
1360
+ const parser = opts.parser ?? createApiParserRestClassic();
1361
+ const auth = opts.auth ?? noAuth();
1362
+ const notifier = opts.notifier ?? consoleNotifier;
1363
+ const cfg = {
1364
+ baseUrl: opts.baseUrl,
1365
+ parser,
1366
+ auth,
1367
+ notifier,
1368
+ ...opts.fetch ? { fetch: opts.fetch } : {},
1369
+ ...opts.defaults ? { defaults: opts.defaults } : {},
1370
+ ...opts.cache ? { cache: opts.cache } : {},
1371
+ ...opts.redirects ? { redirects: opts.redirects } : {},
1372
+ ...opts.guards ? { guards: opts.guards } : {}
1373
+ };
1374
+ return createHttpClient(cfg);
1375
+ }
1376
+
1377
+ export { ILLEGAL_INVOCATION_HINT, NetworkError, ParseError, RedirectError, TimeoutError, alertNotifier, apiKeyAuth, basicAuth, bearerAuth, consoleNotifier, cookieAuth, cookieSafeRefreshGuard, createApiKeyClient, createApiParserGraphQL, createApiParserLaravel, createApiParserNest, createApiParserRestClassic, createBasicClient, createBearerClient, createClient, createCookieClient, createHttpClient, createPresetBuilder, createShapeParser, createSimpleClient, fromZod, instrumentGuard, memoryCache, noAuth, retryGuard, schema, tailwindAlertRenderer, unauthorizedGuard };
798
1378
  //# sourceMappingURL=index.js.map
799
1379
  //# sourceMappingURL=index.js.map