@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 +66 -0
- package/dist/guards/bearerRefreshGuard.d.ts +45 -0
- package/dist/guards/bearerRefreshGuard.d.ts.map +1 -0
- package/dist/guards/correlationIdGuard.d.ts +18 -0
- package/dist/guards/correlationIdGuard.d.ts.map +1 -0
- package/dist/guards/csrfGuard.d.ts +25 -0
- package/dist/guards/csrfGuard.d.ts.map +1 -0
- package/dist/guards/idempotencyGuard.d.ts +25 -0
- package/dist/guards/idempotencyGuard.d.ts.map +1 -0
- package/dist/index.cjs +325 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +321 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +26 -0
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/parsing/apiParserStructured.d.ts +94 -0
- package/dist/parsing/apiParserStructured.d.ts.map +1 -0
- package/package.json +1 -1
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
|
|
908
|
-
var
|
|
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[
|
|
953
|
-
const retryCount = Number(anyCtx[
|
|
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;
|