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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,12 +35,78 @@ else console.error(res.errors);
35
35
  - `createApiParserLaravel()`
36
36
  - `createApiParserNest()`
37
37
  - `createApiParserGraphQL({ allowPartialData? })`
38
+ - `createApiParserStructured({ exposeMeta?, honorRetryableHint? })` — parser
39
+ pour une enveloppe structurée typée :
40
+ `{ success, code, message, issues[], i18n, meta { traceId, spanId } }`.
41
+ En cas d'erreur, chaque `AppError` porte `code`/`message` et expose dans
42
+ `details` les champs structurés (`category`, `severity`, `retryable`, `i18n`,
43
+ `traceId`, `spanId`, `envelope`).
38
44
 
39
45
  ## Auth modes
40
46
  - `bearerAuth(() => token)`
41
47
  - `cookieAuth({ credentials: "include", csrf: { headerName, getToken } })`
42
48
  - `noAuth()`
43
49
 
50
+ ## Guards
51
+
52
+ Les guards composent un middleware autour de chaque requête.
53
+
54
+ ### `bearerRefreshGuard({ refreshFn, onAuthFailed?, ... })`
55
+
56
+ Refresh JWT Bearer **single-flight** : sur 401, un seul `refreshFn()` est invoqué ;
57
+ toutes les autres requêtes 401 reçues pendant ce refresh attendent dans une queue
58
+ partagée puis sont rejouées avec le nouveau token. Si le refresh échoue,
59
+ `onAuthFailed(error)` est appelé une seule fois et toutes les requêtes en queue
60
+ voient le 401 d'origine.
61
+
62
+ ```ts
63
+ import { createHttpClient, bearerAuth, bearerRefreshGuard } from "@cedvict/http-guardian";
64
+
65
+ createHttpClient({
66
+ auth: bearerAuth(() => tokenStore.access()),
67
+ guards: [
68
+ bearerRefreshGuard({
69
+ refreshFn: async () => {
70
+ const { accessToken, refreshToken } = await api.refresh();
71
+ tokenStore.set(accessToken, refreshToken);
72
+ return { accessToken, refreshToken };
73
+ },
74
+ onAuthFailed: () => router.navigate("/login"),
75
+ skipRefreshFor: (ctx) => ctx.url.includes("/auth/"),
76
+ }),
77
+ ],
78
+ });
79
+ ```
80
+
81
+ ### `csrfGuard({ tokenFromCookie?, tokenFromStorage?, tokenFromHeader?, headerName?, applyTo? })`
82
+
83
+ Injecte un header `X-CSRF-Token` (défaut) sur les méthodes mutantes
84
+ (`POST`/`PUT`/`PATCH`/`DELETE`). **SSR-safe** : si `tokenFromCookie` est utilisé
85
+ hors browser (`typeof document === 'undefined'`), le guard skippe silencieusement.
86
+
87
+ ```ts
88
+ csrfGuard(
89
+ isPlatformBrowser(platformId)
90
+ ? { tokenFromCookie: "csrf_token" }
91
+ : { tokenFromStorage: () => null },
92
+ );
93
+ ```
94
+
95
+ ### `correlationIdGuard({ headerName?, generator?, readResponseHeader? })`
96
+
97
+ Injecte un header `X-Correlation-Id` (UUID v4 par défaut, fallback
98
+ `Math.random()`+timestamp si `crypto.randomUUID` absent). L'ID est **stable
99
+ entre retries** (mémorisé sur le ctx). Si la réponse contient le même header,
100
+ il est copié dans `ctx.meta.correlationId` pour les plugins downstream
101
+ (logger, sentry, otel).
102
+
103
+ ### `idempotencyGuard({ headerName?, applyTo?, respectExisting? })`
104
+
105
+ Injecte un header `X-Idempotency-Key` (UUID par défaut) sur `POST`/`PUT`/`PATCH`.
106
+ La clé est **stable entre retries** — sinon l'idempotence côté serveur est cassée.
107
+ Respecte une clé fournie via `RequestOptions.idempotencyKey`,
108
+ `ctx.idempotencyKey`, ou directement via header.
109
+
44
110
  ## Redirects
45
111
  - `redirects: { mode: "follow" | "manual" | "error", maxHops, onRedirect }`
46
112
 
@@ -0,0 +1,45 @@
1
+ import type { Guard } from "./types.js";
2
+ import type { RequestContext } from "../internal/context.js";
3
+ export type BearerRefreshTokens = {
4
+ accessToken: string;
5
+ refreshToken?: string;
6
+ };
7
+ export type BearerRefreshGuardOptions = {
8
+ /** Appelé sur 401 pour obtenir un nouveau token. */
9
+ refreshFn: () => Promise<BearerRefreshTokens>;
10
+ /** Appelé si le refresh échoue (logout, redirect login, etc.). */
11
+ onAuthFailed?: (error: unknown) => void | Promise<void>;
12
+ /**
13
+ * Hook optionnel : appelé avec le résultat de refreshFn() **avant** que les
14
+ * requêtes en queue ne soient rejouées. Permet à l'application de persister
15
+ * le nouveau token (localStorage, signal, etc.) de façon atomique.
16
+ *
17
+ * Si non fourni, le guard se contente de laisser `auth.getToken` retourner
18
+ * la nouvelle valeur (l'application doit donc l'avoir mise à jour ailleurs,
19
+ * typiquement dans `refreshFn` lui-même).
20
+ */
21
+ onTokenRefreshed?: (tokens: BearerRefreshTokens) => void | Promise<void>;
22
+ /**
23
+ * Ne pas tenter de refresh si la requête originale matche ce predicate.
24
+ * Ex: ne pas refresh sur `/auth/login`, `/auth/refresh`.
25
+ */
26
+ skipRefreshFor?: (ctx: RequestContext) => boolean;
27
+ /** Statuts qui déclenchent un refresh (défaut: [401]). */
28
+ triggerStatuses?: number[];
29
+ /** Délai max d'attente d'une requête en queue (défaut: 30_000 ms). */
30
+ queueTimeoutMs?: number;
31
+ };
32
+ /**
33
+ * Guard de refresh JWT Bearer avec queue / single-flight.
34
+ *
35
+ * Sur 401, un seul `refreshFn()` est invoqué ; toutes les autres requêtes 401
36
+ * reçues pendant ce refresh attendent dans une queue partagée :
37
+ * - si refresh OK → toutes sont rejouées avec le nouveau token (header
38
+ * `Authorization` ré-injecté via `auth.getToken()`).
39
+ * - si refresh KO → toutes sont rejetées et `onAuthFailed(error)` est appelé
40
+ * une seule fois.
41
+ *
42
+ * Anti-boucle : un retry n'engendre jamais un nouveau refresh.
43
+ */
44
+ export declare function bearerRefreshGuard(opts: BearerRefreshGuardOptions): Guard;
45
+ //# sourceMappingURL=bearerRefreshGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearerRefreshGuard.d.ts","sourceRoot":"","sources":["../../src/guards/bearerRefreshGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,oDAAoD;IACpD,SAAS,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAE9C,kEAAkE;IAClE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExD;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzE;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC;IAElD,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAgBF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,KAAK,CAgEzE"}
@@ -0,0 +1,18 @@
1
+ import type { Guard } from "./types.js";
2
+ export type CorrelationIdGuardOptions = {
3
+ /** Header pour l'ID corrélation (défaut: 'X-Correlation-Id'). */
4
+ headerName?: string;
5
+ /** Générateur d'ID (défaut: crypto.randomUUID si dispo, sinon fallback). */
6
+ generator?: () => string;
7
+ /** Si présent en réponse, copier dans `ctx.meta.correlationId`. */
8
+ readResponseHeader?: boolean;
9
+ };
10
+ /**
11
+ * Guard qui injecte un header `X-Correlation-Id` (UUID par défaut) sur chaque
12
+ * requête, et lit le header de réponse pour le copier dans `ctx.meta.correlationId`.
13
+ *
14
+ * L'ID est **stable entre retries** : un ID est généré une seule fois par
15
+ * requête originale (mémorisé sur le ctx), pour préserver la traçabilité.
16
+ */
17
+ export declare function correlationIdGuard(opts?: CorrelationIdGuardOptions): Guard;
18
+ //# sourceMappingURL=correlationIdGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlationIdGuard.d.ts","sourceRoot":"","sources":["../../src/guards/correlationIdGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,MAAM,yBAAyB,GAAG;IACtC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC;IAEzB,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAqBF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,yBAA8B,GAAG,KAAK,CAwC9E"}
@@ -0,0 +1,25 @@
1
+ import type { Guard } from "./types.js";
2
+ export type CsrfGuardOptions = {
3
+ /** Lit `document.cookie` (browser only). Ex: 'csrf_token'. */
4
+ tokenFromCookie?: string;
5
+ /** Source custom (storage, signal, observable, etc.). */
6
+ tokenFromStorage?: () => string | null | Promise<string | null>;
7
+ /**
8
+ * Lit le token depuis un header de réponse précédent (mémorisé en interne).
9
+ * Ex: header 'X-CSRF-Token' renvoyé par /auth/login.
10
+ */
11
+ tokenFromHeader?: string;
12
+ /** Header HTTP à injecter (défaut: 'X-CSRF-Token'). */
13
+ headerName?: string;
14
+ /** Méthodes mutantes (défaut: ['POST', 'PUT', 'PATCH', 'DELETE']). */
15
+ applyTo?: string[];
16
+ };
17
+ /**
18
+ * Guard d'injection de header CSRF sur méthodes mutantes.
19
+ *
20
+ * SSR-safety : si `tokenFromCookie` est utilisé hors browser, le guard skippe
21
+ * silencieusement (pas de header injecté). Combiner avec `tokenFromStorage`
22
+ * pour fournir un fallback côté serveur.
23
+ */
24
+ export declare function csrfGuard(opts?: CsrfGuardOptions): Guard;
25
+ //# sourceMappingURL=csrfGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrfGuard.d.ts","sourceRoot":"","sources":["../../src/guards/csrfGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChE;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAwBF;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,KAAK,CAqD5D"}
@@ -0,0 +1,25 @@
1
+ import type { Guard } from "./types.js";
2
+ export type IdempotencyGuardOptions = {
3
+ /** Header (défaut: 'X-Idempotency-Key'). */
4
+ headerName?: string;
5
+ /** Méthodes ciblées (défaut: ['POST', 'PUT', 'PATCH']). */
6
+ applyTo?: string[];
7
+ /** Générateur (défaut: crypto.randomUUID si dispo, sinon fallback). */
8
+ generator?: () => string;
9
+ /**
10
+ * Si l'utilisateur fournit déjà une key (via `RequestOptions.idempotencyKey`,
11
+ * via `ctx.idempotencyKey`, via `ctx.meta.idempotencyKey` ou directement
12
+ * via header), la respecter (défaut: true).
13
+ */
14
+ respectExisting?: boolean;
15
+ };
16
+ /**
17
+ * Guard qui injecte un header `X-Idempotency-Key` (UUID par défaut) sur les
18
+ * méthodes mutantes (POST/PUT/PATCH).
19
+ *
20
+ * **Stable entre retries** : la clé est générée une seule fois par requête
21
+ * originale (mémorisée sur le ctx) ; rejouer la même requête utilise donc la
22
+ * même clé — sinon l'idempotence côté serveur est cassée.
23
+ */
24
+ export declare function idempotencyGuard(opts?: IdempotencyGuardOptions): Guard;
25
+ //# sourceMappingURL=idempotencyGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotencyGuard.d.ts","sourceRoot":"","sources":["../../src/guards/idempotencyGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,MAAM,uBAAuB,GAAG;IACpC,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC;IAEzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAoBF;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,KAAK,CAqC1E"}
package/dist/index.cjs CHANGED
@@ -857,6 +857,250 @@ function cookieSafeRefreshGuard(opts) {
857
857
  }
858
858
  }
859
859
 
860
+ // src/guards/bearerRefreshGuard.ts
861
+ var REFRESH_FLAG2 = "__hg_bearer_refresh_in_flight__";
862
+ var RETRY_FLAG2 = "__hg_bearer_refresh_retry__";
863
+ function isReplayableBody2(body) {
864
+ if (body == null) return true;
865
+ if (typeof body === "string") return true;
866
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
867
+ if (typeof FormData !== "undefined" && body instanceof FormData) return true;
868
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return true;
869
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) return true;
870
+ if (typeof Uint8Array !== "undefined" && body instanceof Uint8Array) return true;
871
+ return false;
872
+ }
873
+ function bearerRefreshGuard(opts) {
874
+ const trigger = new Set(opts.triggerStatuses ?? [401]);
875
+ const queueTimeoutMs = opts.queueTimeoutMs ?? 3e4;
876
+ let refreshPromise = null;
877
+ return async (ctx, next) => {
878
+ const anyCtx = ctx;
879
+ if (ctx.noAuth) return next(ctx);
880
+ if (ctx.auth.mode !== "bearer") return next(ctx);
881
+ if (opts.skipRefreshFor?.(ctx)) return next(ctx);
882
+ if (anyCtx[RETRY_FLAG2]) return next(ctx);
883
+ if (anyCtx[REFRESH_FLAG2]) return next(ctx);
884
+ const res = await next(ctx);
885
+ if (!trigger.has(res.status)) return res;
886
+ if (!isReplayableBody2(ctx.body)) return res;
887
+ try {
888
+ await withTimeout(
889
+ refreshPromise ??= runRefresh(opts).finally(() => {
890
+ refreshPromise = null;
891
+ }),
892
+ queueTimeoutMs
893
+ );
894
+ } catch {
895
+ return res;
896
+ }
897
+ const retryCtx = cloneCtxForReplay(ctx);
898
+ retryCtx[RETRY_FLAG2] = true;
899
+ await applyBearerAuth(retryCtx, ctx.auth);
900
+ return next(retryCtx);
901
+ };
902
+ async function runRefresh(o) {
903
+ try {
904
+ const tokens = await o.refreshFn();
905
+ if (o.onTokenRefreshed) await o.onTokenRefreshed(tokens);
906
+ return tokens;
907
+ } catch (e) {
908
+ if (o.onAuthFailed) {
909
+ try {
910
+ await o.onAuthFailed(e);
911
+ } catch {
912
+ }
913
+ }
914
+ throw e;
915
+ }
916
+ }
917
+ }
918
+ function cloneCtxForReplay(ctx) {
919
+ return Object.assign({}, ctx, { attempt: ctx.attempt + 1 });
920
+ }
921
+ async function applyBearerAuth(ctx, auth) {
922
+ const headerName = auth.headerName ?? "Authorization";
923
+ const prefix = auth.prefix ?? "Bearer";
924
+ const token = await auth.getToken();
925
+ if (token == null || token === "") {
926
+ ctx.headers.delete(headerName);
927
+ return;
928
+ }
929
+ ctx.headers.set(headerName, prefix ? `${prefix} ${token}` : String(token));
930
+ }
931
+ function withTimeout(p, ms) {
932
+ if (!Number.isFinite(ms) || ms <= 0) return p;
933
+ return new Promise((resolve, reject) => {
934
+ const t = setTimeout(() => reject(new Error(`bearerRefreshGuard: queue timeout after ${ms}ms`)), ms);
935
+ p.then(
936
+ (v) => {
937
+ clearTimeout(t);
938
+ resolve(v);
939
+ },
940
+ (e) => {
941
+ clearTimeout(t);
942
+ reject(e);
943
+ }
944
+ );
945
+ });
946
+ }
947
+
948
+ // src/guards/csrfGuard.ts
949
+ var DEFAULT_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
950
+ function readCookie(name) {
951
+ if (typeof document === "undefined") return null;
952
+ const cookieStr = document.cookie;
953
+ if (typeof cookieStr !== "string" || cookieStr.length === 0) return null;
954
+ const target = encodeURIComponent(name);
955
+ const parts = cookieStr.split(";");
956
+ for (const part of parts) {
957
+ const eq = part.indexOf("=");
958
+ if (eq === -1) continue;
959
+ const k = part.slice(0, eq).trim();
960
+ if (k === name || k === target) {
961
+ return decodeURIComponent(part.slice(eq + 1).trim());
962
+ }
963
+ }
964
+ return null;
965
+ }
966
+ function csrfGuard(opts = {}) {
967
+ const headerName = opts.headerName ?? "X-CSRF-Token";
968
+ const methods = new Set((opts.applyTo ?? DEFAULT_METHODS).map((m) => m.toUpperCase()));
969
+ let cachedHeaderToken = null;
970
+ return async (ctx, next) => {
971
+ if (!methods.has(ctx.method)) return next(ctx);
972
+ if (ctx.headers.has(headerName)) {
973
+ const res2 = await next(ctx);
974
+ maybeReadResponseHeader(res2);
975
+ return res2;
976
+ }
977
+ const token = await resolveToken();
978
+ if (token != null && token !== "") {
979
+ ctx.headers.set(headerName, token);
980
+ }
981
+ const res = await next(ctx);
982
+ maybeReadResponseHeader(res);
983
+ return res;
984
+ };
985
+ async function resolveToken(_ctx) {
986
+ if (opts.tokenFromCookie) {
987
+ const t = readCookie(opts.tokenFromCookie);
988
+ if (t != null && t !== "") return t;
989
+ }
990
+ if (opts.tokenFromHeader && cachedHeaderToken != null) {
991
+ return cachedHeaderToken;
992
+ }
993
+ if (opts.tokenFromStorage) {
994
+ try {
995
+ const t = await opts.tokenFromStorage();
996
+ if (t != null && t !== "") return t;
997
+ } catch {
998
+ return null;
999
+ }
1000
+ }
1001
+ return null;
1002
+ }
1003
+ function maybeReadResponseHeader(res) {
1004
+ if (!opts.tokenFromHeader) return;
1005
+ try {
1006
+ const v = res.headers?.get?.(opts.tokenFromHeader);
1007
+ if (v != null && v !== "") cachedHeaderToken = v;
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ // src/guards/correlationIdGuard.ts
1014
+ var CORRELATION_FLAG = "__hg_correlation_id__";
1015
+ function defaultGenerator() {
1016
+ const c = globalThis.crypto;
1017
+ if (c && typeof c.randomUUID === "function") {
1018
+ try {
1019
+ return c.randomUUID();
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ const ts = Date.now().toString(36);
1024
+ const r1 = Math.floor(Math.random() * 4294967295).toString(36);
1025
+ const r2 = Math.floor(Math.random() * 4294967295).toString(36);
1026
+ return `${ts}-${r1}${r2}`;
1027
+ }
1028
+ function correlationIdGuard(opts = {}) {
1029
+ const headerName = opts.headerName ?? "X-Correlation-Id";
1030
+ const generator = opts.generator ?? defaultGenerator;
1031
+ const readResponseHeader = opts.readResponseHeader ?? true;
1032
+ return async (ctx, next) => {
1033
+ const anyCtx = ctx;
1034
+ let id = anyCtx[CORRELATION_FLAG] ?? void 0;
1035
+ if (id == null) {
1036
+ const existing = ctx.headers.get(headerName);
1037
+ id = existing != null && existing !== "" ? existing : generator();
1038
+ anyCtx[CORRELATION_FLAG] = id;
1039
+ }
1040
+ if (!ctx.headers.has(headerName)) {
1041
+ ctx.headers.set(headerName, id);
1042
+ }
1043
+ if (!ctx.meta) ctx.meta = {};
1044
+ if (!ctx.meta.correlationId) ctx.meta.correlationId = id;
1045
+ const res = await next(ctx);
1046
+ if (readResponseHeader) {
1047
+ try {
1048
+ const v = res.headers?.get?.(headerName);
1049
+ if (v != null && v !== "") {
1050
+ if (!ctx.meta) ctx.meta = {};
1051
+ ctx.meta.correlationId = v;
1052
+ }
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ return res;
1057
+ };
1058
+ }
1059
+
1060
+ // src/guards/idempotencyGuard.ts
1061
+ var DEFAULT_METHODS2 = ["POST", "PUT", "PATCH"];
1062
+ var IDEMPOTENCY_FLAG = "__hg_idempotency_key__";
1063
+ function defaultGenerator2() {
1064
+ const c = globalThis.crypto;
1065
+ if (c && typeof c.randomUUID === "function") {
1066
+ try {
1067
+ return c.randomUUID();
1068
+ } catch {
1069
+ }
1070
+ }
1071
+ const ts = Date.now().toString(36);
1072
+ const r1 = Math.floor(Math.random() * 4294967295).toString(36);
1073
+ const r2 = Math.floor(Math.random() * 4294967295).toString(36);
1074
+ return `${ts}-${r1}${r2}`;
1075
+ }
1076
+ function idempotencyGuard(opts = {}) {
1077
+ const headerName = opts.headerName ?? "X-Idempotency-Key";
1078
+ const methods = new Set((opts.applyTo ?? DEFAULT_METHODS2).map((m) => m.toUpperCase()));
1079
+ const generator = opts.generator ?? defaultGenerator2;
1080
+ const respectExisting = opts.respectExisting ?? true;
1081
+ return async (ctx, next) => {
1082
+ if (!methods.has(ctx.method)) return next(ctx);
1083
+ const anyCtx = ctx;
1084
+ let key = anyCtx[IDEMPOTENCY_FLAG] ?? void 0;
1085
+ if (key == null && respectExisting) {
1086
+ const fromHeader = ctx.headers.get(headerName);
1087
+ if (fromHeader != null && fromHeader !== "") {
1088
+ key = fromHeader;
1089
+ } else if (ctx.idempotencyKey != null && ctx.idempotencyKey !== "") {
1090
+ key = ctx.idempotencyKey;
1091
+ } else if (ctx.meta?.idempotencyKey != null && ctx.meta.idempotencyKey !== "") {
1092
+ key = ctx.meta.idempotencyKey;
1093
+ }
1094
+ }
1095
+ if (key == null) key = generator();
1096
+ anyCtx[IDEMPOTENCY_FLAG] = key;
1097
+ if (!ctx.headers.has(headerName)) ctx.headers.set(headerName, key);
1098
+ if (!ctx.meta) ctx.meta = {};
1099
+ if (!ctx.meta.idempotencyKey) ctx.meta.idempotencyKey = key;
1100
+ return next(ctx);
1101
+ };
1102
+ }
1103
+
860
1104
  // src/guards/unauthorizedGuard.ts
861
1105
  function normalizePath2(url) {
862
1106
  try {
@@ -904,8 +1148,8 @@ function unauthorizedGuard(opts) {
904
1148
  }
905
1149
 
906
1150
  // src/guards/instrumentGuard.ts
907
- var REFRESH_FLAG2 = "__hg_is_refresh__";
908
- var RETRY_FLAG2 = "__hg_refresh_retry__";
1151
+ var REFRESH_FLAG3 = "__hg_is_refresh__";
1152
+ var RETRY_FLAG3 = "__hg_refresh_retry__";
909
1153
  function safeUrl(u) {
910
1154
  try {
911
1155
  const url = new URL(u);
@@ -949,8 +1193,8 @@ function instrumentGuard(opts, guard) {
949
1193
  }
950
1194
  const anyCtx = ctx;
951
1195
  const attempt = ctx.attempt ?? 0;
952
- const isRefresh = Boolean(anyCtx[REFRESH_FLAG2]);
953
- const retryCount = Number(anyCtx[RETRY_FLAG2] ?? 0);
1196
+ const isRefresh = Boolean(anyCtx[REFRESH_FLAG3]);
1197
+ const retryCount = Number(anyCtx[RETRY_FLAG3] ?? 0);
954
1198
  const noAuth2 = Boolean(ctx.noAuth);
955
1199
  const baseLine = `[HG][#${id}][${opts.name}]`;
956
1200
  log(`${baseLine} \u25B6 start`, {
@@ -1186,6 +1430,78 @@ function createApiParserGraphQL(opts) {
1186
1430
  });
1187
1431
  }
1188
1432
 
1433
+ // src/parsing/apiParserStructured.ts
1434
+ function isRecord(v) {
1435
+ return !!v && typeof v === "object" && !Array.isArray(v);
1436
+ }
1437
+ function pickString(o, key) {
1438
+ const v = o[key];
1439
+ return typeof v === "string" ? v : void 0;
1440
+ }
1441
+ function createApiParserStructured(opts = {}) {
1442
+ const exposeMeta = opts.exposeMeta ?? true;
1443
+ const honorRetryableHint = opts.honorRetryableHint ?? true;
1444
+ return createShapeParser({
1445
+ isSuccess: (raw, status) => {
1446
+ if (isRecord(raw) && "success" in raw) return raw["success"] === true;
1447
+ return status >= 200 && status < 400;
1448
+ },
1449
+ getData: (raw) => {
1450
+ if (isRecord(raw) && raw["success"] === true && "data" in raw) {
1451
+ return raw["data"];
1452
+ }
1453
+ return raw;
1454
+ },
1455
+ getErrors: (raw, status) => {
1456
+ if (!isRecord(raw)) {
1457
+ return [{ message: status ? `HTTP ${status}` : "Request failed" }];
1458
+ }
1459
+ const env = raw;
1460
+ const meta = isRecord(env["meta"]) ? env["meta"] : void 0;
1461
+ const traceId = meta?.traceId;
1462
+ const spanId = meta?.spanId;
1463
+ const issues = Array.isArray(env["issues"]) ? env["issues"] : [];
1464
+ const topMessage = pickString(env, "message");
1465
+ const topCode = pickString(env, "code");
1466
+ const topI18n = isRecord(env["i18n"]) ? env["i18n"] : void 0;
1467
+ const baseDetails = (extra = {}) => ({
1468
+ ...traceId !== void 0 ? { traceId } : {},
1469
+ ...spanId !== void 0 ? { spanId } : {},
1470
+ ...exposeMeta ? { envelope: env } : {},
1471
+ ...extra
1472
+ });
1473
+ if (issues.length > 0) {
1474
+ return issues.map((iss) => {
1475
+ const message = iss.message ?? topMessage ?? (status ? `HTTP ${status}` : "Request failed");
1476
+ const code = iss.code ?? topCode;
1477
+ const issueDetails = baseDetails({
1478
+ ...iss.category !== void 0 ? { category: iss.category } : {},
1479
+ ...iss.severity !== void 0 ? { severity: iss.severity } : {},
1480
+ ...honorRetryableHint && iss.retryable !== void 0 ? { retryable: iss.retryable } : {},
1481
+ ...iss.i18n !== void 0 ? { i18n: iss.i18n } : topI18n !== void 0 ? { i18n: topI18n } : {}
1482
+ });
1483
+ return {
1484
+ message,
1485
+ ...code !== void 0 ? { code } : {},
1486
+ ...iss.field !== void 0 ? { field: iss.field } : {},
1487
+ details: issueDetails
1488
+ };
1489
+ });
1490
+ }
1491
+ const details = baseDetails({
1492
+ ...topI18n !== void 0 ? { i18n: topI18n } : {}
1493
+ });
1494
+ return [
1495
+ {
1496
+ message: topMessage ?? (status ? `HTTP ${status}` : "Request failed"),
1497
+ ...topCode !== void 0 ? { code: topCode } : {},
1498
+ details
1499
+ }
1500
+ ];
1501
+ }
1502
+ });
1503
+ }
1504
+
1189
1505
  // src/presets/presetBuilder.ts
1190
1506
  function createPresetBuilder(init) {
1191
1507
  const state = {
@@ -1385,14 +1701,17 @@ exports.alertNotifier = alertNotifier;
1385
1701
  exports.apiKeyAuth = apiKeyAuth;
1386
1702
  exports.basicAuth = basicAuth;
1387
1703
  exports.bearerAuth = bearerAuth;
1704
+ exports.bearerRefreshGuard = bearerRefreshGuard;
1388
1705
  exports.consoleNotifier = consoleNotifier;
1389
1706
  exports.cookieAuth = cookieAuth;
1390
1707
  exports.cookieSafeRefreshGuard = cookieSafeRefreshGuard;
1708
+ exports.correlationIdGuard = correlationIdGuard;
1391
1709
  exports.createApiKeyClient = createApiKeyClient;
1392
1710
  exports.createApiParserGraphQL = createApiParserGraphQL;
1393
1711
  exports.createApiParserLaravel = createApiParserLaravel;
1394
1712
  exports.createApiParserNest = createApiParserNest;
1395
1713
  exports.createApiParserRestClassic = createApiParserRestClassic;
1714
+ exports.createApiParserStructured = createApiParserStructured;
1396
1715
  exports.createBasicClient = createBasicClient;
1397
1716
  exports.createBearerClient = createBearerClient;
1398
1717
  exports.createClient = createClient;
@@ -1401,7 +1720,9 @@ exports.createHttpClient = createHttpClient;
1401
1720
  exports.createPresetBuilder = createPresetBuilder;
1402
1721
  exports.createShapeParser = createShapeParser;
1403
1722
  exports.createSimpleClient = createSimpleClient;
1723
+ exports.csrfGuard = csrfGuard;
1404
1724
  exports.fromZod = fromZod;
1725
+ exports.idempotencyGuard = idempotencyGuard;
1405
1726
  exports.instrumentGuard = instrumentGuard;
1406
1727
  exports.memoryCache = memoryCache;
1407
1728
  exports.noAuth = noAuth;