@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +15 -20
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +45 -11
- package/dist/nodefony/controller/ApiKeyController.js +18 -10
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +117 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -17
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +50 -13
- package/dist/nodefony/controller/TokenAuthController.js +14 -8
- package/dist/nodefony/controller/TotpController.js +7 -8
- package/dist/nodefony/controller/WebAuthnController.js +28 -12
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +268 -65
- package/dist/nodefony/service/AdminBroker.js +4 -6
- package/dist/nodefony/service/Eta.js +1 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -6
- package/dist/nodefony/service/router.js +97 -34
- package/dist/nodefony/src/Controller.js +71 -43
- package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
- package/dist/nodefony/src/KernelAdminApi.js +82 -18
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +105 -36
- package/dist/nodefony/src/ResourceController.js +2 -3
- package/dist/nodefony/src/Route.js +54 -8
- package/dist/nodefony/src/SecurityApiController.js +23 -0
- package/dist/nodefony/src/SyslogAdminApi.js +1 -2
- package/dist/nodefony/src/Template.js +1 -2
- package/dist/nodefony/src/configMutation.js +1 -2
- package/dist/nodefony/src/docsReader.js +39 -23
- package/dist/nodefony/src/idempotency.js +14 -22
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +430 -0
- package/dist/nodefony/src/requireAuthentication.js +35 -0
- package/dist/nodefony/src/routeSchema.js +81 -0
- package/dist/nodefony/src/scopeCatalog.js +1 -2
- package/dist/nodefony/src/singletonGuard.js +1 -2
- package/dist/nodefony/src/syslogFilters.js +1 -2
- package/dist/types/index.d.ts +8 -12
- package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
- package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
- package/dist/types/nodefony/service/router.d.ts +40 -1
- package/dist/types/nodefony/src/Controller.d.ts +54 -17
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +44 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
- package/dist/types/nodefony/src/Route.d.ts +21 -1
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
- package/dist/types/nodefony/src/docsReader.d.ts +10 -6
- package/dist/types/nodefony/src/idempotency.d.ts +12 -10
- package/dist/types/nodefony/src/loginPage.d.ts +144 -0
- package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/docs/admin.md +83 -78
- package/docs/controller.md +112 -111
- package/docs/decorateurs.md +212 -163
- package/docs/idempotence.md +137 -135
- package/docs/routing.md +96 -95
- package/docs/templates.md +55 -55
- package/package.json +10 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -2,10 +2,10 @@ import { CORE_PACKAGE, checkOutdated, countModuleDocs, listModuleDocs, listModul
|
|
|
2
2
|
import { getResolvedPath, navigateSchemaNode, nodeFlags, notEditableReason, recipeFor, validateLeafValue } from "./configMutation.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { GitService, Syslog, aggregateProvenance, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath, withResolvedPath } from "nodefony";
|
|
5
|
+
import { GitService, Syslog, aggregateProvenance, collectDevStatus, collectEnvShortcuts, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, isFrameworkRepository, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath, redactUrlCredentials, resumeSupervisor, runAppContributions, suspendSupervisor, withResolvedPath } from "nodefony";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
-
|
|
8
|
+
// nodefony/src/KernelAdminApi.ts
|
|
9
9
|
/** Clé du pseudo-module core dans Studio (cf carte "Core" / `resolveCorePath`). */
|
|
10
10
|
const CORE_KEY = "core";
|
|
11
11
|
/** Racine projet — pour ne PAS exposer de chemin absolu (sécu). */
|
|
@@ -109,7 +109,7 @@ const SECRET_KEY = /(?:^|[a-z0-9])(secret|password|passwd|passphrase|credential|
|
|
|
109
109
|
function safeConfig(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
110
110
|
if (typeof value === "function") return "[Function]";
|
|
111
111
|
if (typeof value === "bigint") return value.toString();
|
|
112
|
-
if (typeof value === "string") return stripAbs(value);
|
|
112
|
+
if (typeof value === "string") return stripAbs(redactUrlCredentials(value));
|
|
113
113
|
if (value === null || typeof value !== "object") return value;
|
|
114
114
|
if (depth > 5) return "[depth limit]";
|
|
115
115
|
if (seen.has(value)) return "[Circular]";
|
|
@@ -204,13 +204,23 @@ function computeConfigEntry(key, mod, runtimePaths) {
|
|
|
204
204
|
const isApp = mod.isApp ?? false;
|
|
205
205
|
const seg = computeSeg(pkg, isApp);
|
|
206
206
|
const envKeys = {};
|
|
207
|
+
const shortcutPaths = [];
|
|
208
|
+
if (!isApp) for (const s of collectEnvShortcuts(schema)) {
|
|
209
|
+
const raw = process.env[s.env];
|
|
210
|
+
const file = process.env[`${s.env}_FILE`];
|
|
211
|
+
const fromFile = file !== void 0 && file !== "";
|
|
212
|
+
if ((raw === void 0 || raw === "") && !fromFile) continue;
|
|
213
|
+
const dotted = s.path.join(".").toLowerCase();
|
|
214
|
+
envKeys[dotted] = fromFile ? `${s.env}_FILE` : s.env;
|
|
215
|
+
shortcutPaths.push(dotted);
|
|
216
|
+
}
|
|
207
217
|
for (const o of parseNfEnvOverrides(process.env)) if (o.moduleSeg === seg) envKeys[o.path.join(".")] = o.envKey;
|
|
208
218
|
let provenance = null;
|
|
209
219
|
if (isApp) {
|
|
210
220
|
const envPaths = new Set(parseNfEnvOverrides(process.env).filter((o) => o.moduleSeg === "app").map((o) => o.path.join(".")));
|
|
211
221
|
provenance = computeConfigProvenance(opts, defaultAppConfig, envPaths);
|
|
212
222
|
} else if (schema) {
|
|
213
|
-
const envPaths = new Set(parseNfEnvOverrides(process.env).filter((o) => o.moduleSeg === seg).map((o) => o.path.join(".")));
|
|
223
|
+
const envPaths = /* @__PURE__ */ new Set([...shortcutPaths, ...parseNfEnvOverrides(process.env).filter((o) => o.moduleSeg === seg).map((o) => o.path.join("."))]);
|
|
214
224
|
provenance = computeConfigProvenance(opts, extractJsonSchemaDefaults(schema), envPaths);
|
|
215
225
|
}
|
|
216
226
|
if (provenance && runtimePaths?.size) {
|
|
@@ -235,7 +245,7 @@ function actorLabel(user) {
|
|
|
235
245
|
if (typeof u.getUserIdentifier === "function") try {
|
|
236
246
|
return u.getUserIdentifier();
|
|
237
247
|
} catch {}
|
|
238
|
-
return u.
|
|
248
|
+
return u.identifier ?? u.email ?? (u.id != null ? String(u.id) : null);
|
|
239
249
|
}
|
|
240
250
|
/**
|
|
241
251
|
* Journalise une mutation de config (catégorie `config`) **si** le service d'audit
|
|
@@ -337,6 +347,33 @@ function nodefonyPackageCandidates(name) {
|
|
|
337
347
|
const inScope = (pkg) => pkg === "nodefony" || pkg === "@nodefony/core" || pkg.startsWith("@nodefony/");
|
|
338
348
|
return (name.includes("/") ? [name] : [`@nodefony/${name}`, name]).filter(inScope);
|
|
339
349
|
}
|
|
350
|
+
/** Refus de `scaffold` hors développement. */
|
|
351
|
+
const scaffoldRefused = () => ({
|
|
352
|
+
status: 403,
|
|
353
|
+
body: { error: "scaffold:sync is disabled outside development" }
|
|
354
|
+
});
|
|
355
|
+
/**
|
|
356
|
+
* La réponse de `scaffold` : les rapports tels quels, et les totaux qu'un écran
|
|
357
|
+
* affiche sans les recompter.
|
|
358
|
+
*
|
|
359
|
+
* @param reports - le rendu de `runAppContributions`
|
|
360
|
+
* @param dryRun - `true` pour un plan
|
|
361
|
+
* @returns le corps de la réponse
|
|
362
|
+
*/
|
|
363
|
+
function scaffoldPayload(reports, dryRun) {
|
|
364
|
+
const sum = (pick) => reports.reduce((n, r) => n + pick(r), 0);
|
|
365
|
+
return {
|
|
366
|
+
dryRun,
|
|
367
|
+
reports,
|
|
368
|
+
totals: {
|
|
369
|
+
written: sum((r) => r.written.length),
|
|
370
|
+
updated: sum((r) => r.updated.length),
|
|
371
|
+
outdated: sum((r) => r.outdated.length),
|
|
372
|
+
modified: sum((r) => r.modified.length),
|
|
373
|
+
failed: sum((r) => r.error === null ? 0 : 1)
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
340
377
|
function createKernelAdminApi(kernel) {
|
|
341
378
|
const descriptor = {
|
|
342
379
|
label: "Kernel",
|
|
@@ -376,6 +413,8 @@ function createKernelAdminApi(kernel) {
|
|
|
376
413
|
* @param key - clé courte (`http`) ou nom de paquet (`@nodefony/redis`).
|
|
377
414
|
* @returns le dossier à lire, ou `null`.
|
|
378
415
|
*/
|
|
416
|
+
let frameworkRepository = null;
|
|
417
|
+
const inFrameworkRepository = () => frameworkRepository ??= isFrameworkRepository(kernel.path);
|
|
379
418
|
const resolveDocDir = (key) => resolveTarget(key)?.path ?? resolvePackageDir(key);
|
|
380
419
|
/**
|
|
381
420
|
* Ce qu'on peut proposer à qui s'est trompé de nom — les clés qui répondent.
|
|
@@ -402,7 +441,6 @@ function createKernelAdminApi(kernel) {
|
|
|
402
441
|
const devGuard = () => kernel.environment === "development" || Boolean(kernel.debug);
|
|
403
442
|
const repoRoot = process.cwd();
|
|
404
443
|
const relPath = (p) => p?.startsWith(repoRoot) ? p.slice(repoRoot.length).replace(/^[/\\]+/, "") || "." : p ?? null;
|
|
405
|
-
const redactInfraUrl = (url) => url.replace(/\/\/[^/@]*@/, "//***@");
|
|
406
444
|
const buildConfigEntries = () => {
|
|
407
445
|
const modules = kernel.getModules();
|
|
408
446
|
const entries = [];
|
|
@@ -538,6 +576,30 @@ function createKernelAdminApi(kernel) {
|
|
|
538
576
|
git: GitService.read()
|
|
539
577
|
})
|
|
540
578
|
},
|
|
579
|
+
{
|
|
580
|
+
path: "scaffold",
|
|
581
|
+
summary: "Files delivered by installed packages (dev only) — what `scaffold:sync --update` would do",
|
|
582
|
+
handler: async () => {
|
|
583
|
+
if (!devGuard()) return scaffoldRefused();
|
|
584
|
+
return scaffoldPayload(await runAppContributions(REPO_ROOT, true, true), true);
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
path: "scaffold/sync",
|
|
589
|
+
method: "POST",
|
|
590
|
+
summary: "Run `scaffold:sync` (dev only) — body `{ update?: boolean }`",
|
|
591
|
+
handler: async (request) => {
|
|
592
|
+
if (!devGuard()) return scaffoldRefused();
|
|
593
|
+
const body = request.body;
|
|
594
|
+
const update = typeof body === "object" && body !== null && "update" in body && body.update === true;
|
|
595
|
+
suspendSupervisor(REPO_ROOT, "synchronisation des fichiers livrés", update ? "scaffold:sync --update" : "scaffold:sync");
|
|
596
|
+
try {
|
|
597
|
+
return scaffoldPayload(await runAppContributions(REPO_ROOT, false, update), false);
|
|
598
|
+
} finally {
|
|
599
|
+
resumeSupervisor(REPO_ROOT);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
},
|
|
541
603
|
{
|
|
542
604
|
path: "processes",
|
|
543
605
|
summary: "Dev process topology (supervisor → server → Vite) + server ports — `nodefony status` over the data plane",
|
|
@@ -673,8 +735,8 @@ function createKernelAdminApi(kernel) {
|
|
|
673
735
|
handler: () => {
|
|
674
736
|
const infra = kernel.infra;
|
|
675
737
|
const entriesBySeg = new Map(buildConfigEntries().map((e) => [e.seg, e]));
|
|
676
|
-
const dbUrl = infra.database ?
|
|
677
|
-
const cacheUrl = infra.cache ?
|
|
738
|
+
const dbUrl = infra.database ? redactUrlCredentials(infra.database.url) : null;
|
|
739
|
+
const cacheUrl = infra.cache ? redactUrlCredentials(infra.cache.url) : null;
|
|
678
740
|
const storeEndpoint = (res) => {
|
|
679
741
|
if (res.location) return void 0;
|
|
680
742
|
if (res.resolved === "redis") return cacheUrl ?? void 0;
|
|
@@ -713,8 +775,8 @@ function createKernelAdminApi(kernel) {
|
|
|
713
775
|
} : null,
|
|
714
776
|
cache: infra.cache ? { url: cacheUrl } : null,
|
|
715
777
|
logs: infra.logs ? {
|
|
716
|
-
lokiUrl: infra.logs.lokiUrl ?
|
|
717
|
-
opensearchUrl: infra.logs.opensearchUrl ?
|
|
778
|
+
lokiUrl: infra.logs.lokiUrl ? redactUrlCredentials(infra.logs.lokiUrl) : null,
|
|
779
|
+
opensearchUrl: infra.logs.opensearchUrl ? redactUrlCredentials(infra.logs.opensearchUrl) : null
|
|
718
780
|
} : null
|
|
719
781
|
},
|
|
720
782
|
stores: kernel.storeResolutions.map((res) => {
|
|
@@ -927,7 +989,7 @@ function createKernelAdminApi(kernel) {
|
|
|
927
989
|
services: [],
|
|
928
990
|
config: {},
|
|
929
991
|
configSchema: null,
|
|
930
|
-
docsCount: await countModuleDocs(core.path),
|
|
992
|
+
docsCount: await countModuleDocs(core.path, inFrameworkRepository()),
|
|
931
993
|
symbolsCount: (await listModuleSymbols(CORE_PACKAGE)).length,
|
|
932
994
|
coverageLines: (await readCoverage(core.path)).total?.lines ?? null
|
|
933
995
|
};
|
|
@@ -959,7 +1021,7 @@ function createKernelAdminApi(kernel) {
|
|
|
959
1021
|
provenance: cfg.provenance,
|
|
960
1022
|
seg: cfg.seg,
|
|
961
1023
|
envKeys: cfg.envKeys,
|
|
962
|
-
docsCount: await countModuleDocs(mod.path),
|
|
1024
|
+
docsCount: await countModuleDocs(mod.path, inFrameworkRepository()),
|
|
963
1025
|
symbolsCount: (await listModuleSymbols(cfg.name)).length,
|
|
964
1026
|
coverageLines: (await readCoverage(mod.path)).total?.lines ?? null
|
|
965
1027
|
};
|
|
@@ -1020,7 +1082,7 @@ function createKernelAdminApi(kernel) {
|
|
|
1020
1082
|
};
|
|
1021
1083
|
return {
|
|
1022
1084
|
key,
|
|
1023
|
-
docs: await listModuleDocs(dir)
|
|
1085
|
+
docs: await listModuleDocs(dir, false, inFrameworkRepository())
|
|
1024
1086
|
};
|
|
1025
1087
|
}
|
|
1026
1088
|
},
|
|
@@ -1038,14 +1100,14 @@ function createKernelAdminApi(kernel) {
|
|
|
1038
1100
|
available: docKeys()
|
|
1039
1101
|
}
|
|
1040
1102
|
};
|
|
1041
|
-
const doc = await readModuleDoc(dir, request.params.slug ?? "");
|
|
1103
|
+
const doc = await readModuleDoc(dir, request.params.slug ?? "", inFrameworkRepository());
|
|
1042
1104
|
if (!doc) return {
|
|
1043
1105
|
status: 404,
|
|
1044
1106
|
body: {
|
|
1045
1107
|
error: "Doc not found",
|
|
1046
1108
|
key,
|
|
1047
1109
|
slug: request.params.slug,
|
|
1048
|
-
available: (await listModuleDocs(dir)).map((d) => d.slug)
|
|
1110
|
+
available: (await listModuleDocs(dir, false, inFrameworkRepository())).map((d) => d.slug)
|
|
1049
1111
|
}
|
|
1050
1112
|
};
|
|
1051
1113
|
const wanted = typeof request.query.section === "string" ? request.query.section : "";
|
|
@@ -1085,7 +1147,7 @@ function createKernelAdminApi(kernel) {
|
|
|
1085
1147
|
handler: async () => {
|
|
1086
1148
|
const modules = [];
|
|
1087
1149
|
for (const target of docTargets()) {
|
|
1088
|
-
const docs = await listModuleDocs(target.path);
|
|
1150
|
+
const docs = await listModuleDocs(target.path, false, inFrameworkRepository());
|
|
1089
1151
|
if (docs.length > 0) modules.push({
|
|
1090
1152
|
key: target.key,
|
|
1091
1153
|
docs
|
|
@@ -1111,7 +1173,10 @@ function createKernelAdminApi(kernel) {
|
|
|
1111
1173
|
}
|
|
1112
1174
|
};
|
|
1113
1175
|
const limit = Number.parseInt(String(request.query.limit ?? ""), 10);
|
|
1114
|
-
return searchModuleDocs(docTargets(), q, {
|
|
1176
|
+
return searchModuleDocs(docTargets(), q, {
|
|
1177
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : void 0,
|
|
1178
|
+
frameworkRepository: inFrameworkRepository()
|
|
1179
|
+
});
|
|
1115
1180
|
}
|
|
1116
1181
|
},
|
|
1117
1182
|
{
|
|
@@ -1301,5 +1366,4 @@ function createKernelAdminApi(kernel) {
|
|
|
1301
1366
|
adminEndpoints: () => endpoints
|
|
1302
1367
|
};
|
|
1303
1368
|
}
|
|
1304
|
-
//#endregion
|
|
1305
1369
|
export { createKernelAdminApi, nodefonyPackageCandidates, safeConfig };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { extractActionScopes, resolveActionMeta } from "../decorators/routerDecorators.js";
|
|
2
2
|
import router_default from "../service/router.js";
|
|
3
3
|
import AdminApiController from "../controller/AdminApiController.js";
|
|
4
|
-
|
|
4
|
+
import { standardJsonSchemaOf } from "nodefony";
|
|
5
|
+
// nodefony/src/PlaygroundAdminApi.ts
|
|
5
6
|
/** Normalise `requirements.methods` (string | string[]) → tableau majuscule. */
|
|
6
7
|
function methodsOf(route) {
|
|
7
8
|
const m = route.requirements.methods ?? route.method;
|
|
@@ -15,11 +16,79 @@ function publicDefaults(route) {
|
|
|
15
16
|
for (const [k, v] of Object.entries(route.defaults)) if (k !== "controller") out[k] = v;
|
|
16
17
|
return out;
|
|
17
18
|
}
|
|
19
|
+
/** Source du formulaire pour chaque partie d'un schéma de route. */
|
|
20
|
+
const PART_SOURCE = {
|
|
21
|
+
params: "param",
|
|
22
|
+
query: "query",
|
|
23
|
+
body: "body",
|
|
24
|
+
headers: "headers"
|
|
25
|
+
};
|
|
26
|
+
/** JSON Schema d'entrée d'un schéma, ou `null` s'il ne sait pas se décrire. */
|
|
27
|
+
function describeSchema(schema) {
|
|
28
|
+
const converter = standardJsonSchemaOf(schema);
|
|
29
|
+
if (converter === null) return null;
|
|
30
|
+
try {
|
|
31
|
+
return converter.input({ target: "draft-2020-12" });
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Les champs de premier niveau d'un JSON Schema objet. */
|
|
37
|
+
function propertiesOf(json) {
|
|
38
|
+
const properties = json?.properties;
|
|
39
|
+
return typeof properties === "object" && properties !== null ? Object.keys(properties) : [];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Les entrées d'une action à schéma, au format des paramètres décorés, et le
|
|
43
|
+
* contrat décrit partie par partie.
|
|
44
|
+
*/
|
|
45
|
+
function schemaParams(schema, bodyStream) {
|
|
46
|
+
const params = [];
|
|
47
|
+
const described = {};
|
|
48
|
+
for (const part of [
|
|
49
|
+
"params",
|
|
50
|
+
"query",
|
|
51
|
+
"body",
|
|
52
|
+
"headers"
|
|
53
|
+
]) {
|
|
54
|
+
const partSchema = schema[part];
|
|
55
|
+
if (partSchema === void 0) continue;
|
|
56
|
+
const json = describeSchema(partSchema);
|
|
57
|
+
described[part] = json;
|
|
58
|
+
const source = PART_SOURCE[part];
|
|
59
|
+
if (part === "body") {
|
|
60
|
+
params.push({
|
|
61
|
+
source,
|
|
62
|
+
key: null,
|
|
63
|
+
index: 0,
|
|
64
|
+
stream: false
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const key of propertiesOf(json)) params.push({
|
|
69
|
+
source,
|
|
70
|
+
key,
|
|
71
|
+
index: 0,
|
|
72
|
+
stream: false
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (bodyStream) params.push({
|
|
76
|
+
source: "body",
|
|
77
|
+
key: null,
|
|
78
|
+
index: 0,
|
|
79
|
+
stream: true
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
params,
|
|
83
|
+
described
|
|
84
|
+
};
|
|
85
|
+
}
|
|
18
86
|
/** Sérialise une route en action playground (métadonnées figées, JSON-safe). */
|
|
19
87
|
function serializeAction(route) {
|
|
20
88
|
const meta = resolveActionMeta(route);
|
|
21
89
|
const methods = methodsOf(route);
|
|
22
90
|
const scopes = route.controller && route.classMethod ? extractActionScopes(route.controller, route.classMethod) : [];
|
|
91
|
+
const fromSchema = meta.schema !== null ? schemaParams(meta.schema, meta.bodyStream) : null;
|
|
23
92
|
return {
|
|
24
93
|
route: route.name,
|
|
25
94
|
path: route.path ?? null,
|
|
@@ -28,12 +97,13 @@ function serializeAction(route) {
|
|
|
28
97
|
action: route.classMethod ?? null,
|
|
29
98
|
variables: [...route.variables],
|
|
30
99
|
defaults: publicDefaults(route),
|
|
31
|
-
params: meta.paramsMeta?.map((p) => ({
|
|
100
|
+
params: fromSchema?.params ?? meta.paramsMeta?.map((p) => ({
|
|
32
101
|
source: p.source,
|
|
33
102
|
key: p.key ?? null,
|
|
34
103
|
index: p.index,
|
|
35
104
|
stream: p.stream === true
|
|
36
105
|
})).sort((a, b) => a.index - b.index) ?? [],
|
|
106
|
+
schema: fromSchema?.described ?? null,
|
|
37
107
|
guards: {
|
|
38
108
|
security: meta.security ? { clauses: meta.security.clauses.map((c) => ({
|
|
39
109
|
anyOf: [...c.anyOf],
|
|
@@ -93,5 +163,4 @@ function createPlaygroundEndpoints() {
|
|
|
93
163
|
handler: () => buildPlaygroundSnapshot()
|
|
94
164
|
}];
|
|
95
165
|
}
|
|
96
|
-
//#endregion
|
|
97
166
|
export { buildPlaygroundSnapshot, createPlaygroundEndpoints };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertPageQuery } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/RedisIdempotencyStore.ts
|
|
3
3
|
/** Préfixe namespacé des clés d'idempotence dans Redis. */
|
|
4
4
|
/**
|
|
5
5
|
* Préfixe HISTORIQUE des clés d'idempotence. Utilisé tel quel par une application
|
|
@@ -42,6 +42,50 @@ function decodeCursor(cursor) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
+
* Reconstruit une entrée relue du store, ou `null` si sa forme est inattendue.
|
|
46
|
+
* La valeur vient de Redis, pas de ce processus : une entrée « terminée » sans
|
|
47
|
+
* réponse rejouerait `undefined` comme une réponse HTTP.
|
|
48
|
+
*/
|
|
49
|
+
function toEntry(v) {
|
|
50
|
+
if (typeof v !== "object" || v === null || !("s" in v) || !("f" in v)) return null;
|
|
51
|
+
const f = v.f;
|
|
52
|
+
if (typeof f !== "string") return null;
|
|
53
|
+
if (v.s === "if") return {
|
|
54
|
+
s: "if",
|
|
55
|
+
f
|
|
56
|
+
};
|
|
57
|
+
if (v.s !== "d" || !("r" in v)) return null;
|
|
58
|
+
const r = toResponse(v.r);
|
|
59
|
+
return r === null ? null : {
|
|
60
|
+
s: "d",
|
|
61
|
+
f,
|
|
62
|
+
r
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Reconstruit une réponse mémorisée, ou `null` si sa forme est inattendue. */
|
|
66
|
+
function toResponse(v) {
|
|
67
|
+
if (typeof v !== "object" || v === null || !("status" in v)) return null;
|
|
68
|
+
const status = v.status;
|
|
69
|
+
if (typeof status !== "number") return null;
|
|
70
|
+
const body = "body" in v ? v.body : void 0;
|
|
71
|
+
if (!("headers" in v) || v.headers === void 0) return {
|
|
72
|
+
status,
|
|
73
|
+
body
|
|
74
|
+
};
|
|
75
|
+
const headers = v.headers;
|
|
76
|
+
if (typeof headers !== "object" || headers === null) return null;
|
|
77
|
+
const copy = {};
|
|
78
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
79
|
+
if (typeof value !== "string") return null;
|
|
80
|
+
copy[name] = value;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
headers: copy,
|
|
85
|
+
body
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
45
89
|
* Store d'idempotence **Redis** (node-redis v6) — implémentation distribuée
|
|
46
90
|
* d'{@link IIdempotencyStore} pour le cluster (dédup des mutations rejouées
|
|
47
91
|
* PARTAGÉE cross-pod, là où l'impl mémoire par défaut reste affine à un pod).
|
|
@@ -183,8 +227,7 @@ var RedisIdempotencyStore = class {
|
|
|
183
227
|
#parse(raw) {
|
|
184
228
|
if (raw === null) return null;
|
|
185
229
|
try {
|
|
186
|
-
|
|
187
|
-
return v && (v.s === "if" || v.s === "d") ? v : null;
|
|
230
|
+
return toEntry(JSON.parse(raw));
|
|
188
231
|
} catch {
|
|
189
232
|
return null;
|
|
190
233
|
}
|
|
@@ -250,5 +293,4 @@ var RedisIdempotencyStore = class {
|
|
|
250
293
|
if (this.#pending > 0) this.#pending--;
|
|
251
294
|
}
|
|
252
295
|
};
|
|
253
|
-
//#endregion
|
|
254
296
|
export { RedisIdempotencyStore };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { guardSingletonState, isDevelopment } from "./singletonGuard.js";
|
|
2
|
-
import {
|
|
2
|
+
import { validateRouteInput } from "./routeSchema.js";
|
|
3
|
+
import { buildParamArgs, computeActionMeta, readInvocationBody, readRouteInput, resolveActionMeta, resolveSessionIntent } from "../decorators/routerDecorators.js";
|
|
3
4
|
import { computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity } from "./idempotency.js";
|
|
5
|
+
import { authenticationShortfall, stepUpChallenge } from "./requireAuthentication.js";
|
|
4
6
|
import { RequestContext, finallyMaybe, identityHint, isArray, isPlainObject, isPromise, nodefonyError, thenMaybe, typeOf } from "nodefony";
|
|
5
7
|
import { Http2Response, HttpResponse, WebsocketResponse } from "@nodefony/http";
|
|
6
|
-
|
|
8
|
+
// nodefony/src/Resolver.ts
|
|
7
9
|
/**
|
|
8
10
|
* Résout une route vers son couple controller/action et exécute l'action —
|
|
9
11
|
* UN Resolver est alloué par requête HTTP (et par connexion WS, réutilisé
|
|
@@ -46,7 +48,7 @@ var Resolver = class {
|
|
|
46
48
|
/**
|
|
47
49
|
* Query d'une invocation **par message** (pont WS-RPC `api.request`) — pendant
|
|
48
50
|
* de `cleanPathOverride` pour le `?…` du path invoqué. Le contexte WS étant
|
|
49
|
-
* PARTAGÉ par la connexion (sa `
|
|
51
|
+
* PARTAGÉ par la connexion (sa `query` = celle du handshake), la query
|
|
50
52
|
* per-invocation vit ici (le Resolver est per-invocation → zéro bleed entre
|
|
51
53
|
* requêtes concurrentes d'une même socket). `null` (hot path HTTP) = ignoré.
|
|
52
54
|
* Consommé par `@Query` (`_buildParamArgs`) et copié sur le controller
|
|
@@ -74,6 +76,11 @@ var Resolver = class {
|
|
|
74
76
|
* administrateurs.
|
|
75
77
|
*/
|
|
76
78
|
messageInvocation = false;
|
|
79
|
+
/**
|
|
80
|
+
* Les gardes de l'action ont été jugées favorables par {@link authorizeAction}
|
|
81
|
+
* et ne se rejugent pas à la prochaine exécution — consommé par elle.
|
|
82
|
+
*/
|
|
83
|
+
preAuthorized = false;
|
|
77
84
|
constructor(context) {
|
|
78
85
|
this.context = context;
|
|
79
86
|
this.injector = context.container?.get("injector") ?? null;
|
|
@@ -83,7 +90,7 @@ var Resolver = class {
|
|
|
83
90
|
if (match) {
|
|
84
91
|
this.variables = match;
|
|
85
92
|
this.route = route;
|
|
86
|
-
this.controller = route.controller;
|
|
93
|
+
this.controller = route.controller ?? null;
|
|
87
94
|
this.actionName = route.classMethod;
|
|
88
95
|
this.resolve = true;
|
|
89
96
|
this.bypassFirewall = this.route.bypassFirewall;
|
|
@@ -180,10 +187,20 @@ var Resolver = class {
|
|
|
180
187
|
* `instanceof` au message suivant — le hub était RÉINSTANCIÉ (DI +
|
|
181
188
|
* `initialize()`) à chaque frame qui suivait un `api.request`.
|
|
182
189
|
*
|
|
190
|
+
* Ni pour un `forward` sur une connexion WebSocket : son résolveur est neuf,
|
|
191
|
+
* pas une invocation par message, et réécrivait le pointeur que la poignée
|
|
192
|
+
* de main a posé — même réinstanciation à la frame suivante (#574). Une
|
|
193
|
+
* connexion garde le contrôleur qui l'a ouverte. Chemin froid : seulement
|
|
194
|
+
* quand un contrôleur est créé.
|
|
195
|
+
*
|
|
183
196
|
* @returns le controller reçu, pour enchaîner.
|
|
184
197
|
*/
|
|
185
198
|
_pinController(controller) {
|
|
186
|
-
if (
|
|
199
|
+
if (this.messageInvocation) return controller;
|
|
200
|
+
const container = this.context.container;
|
|
201
|
+
if (!container) return controller;
|
|
202
|
+
if (this.context.method === "WEBSOCKET" && container.get("controller") !== null) return controller;
|
|
203
|
+
container.set("controller", controller);
|
|
187
204
|
return controller;
|
|
188
205
|
}
|
|
189
206
|
/**
|
|
@@ -220,7 +237,7 @@ var Resolver = class {
|
|
|
220
237
|
* @returns le controller reçu, pour enchaîner.
|
|
221
238
|
*/
|
|
222
239
|
_guardSingleton(controller, ctx) {
|
|
223
|
-
if (this.controller
|
|
240
|
+
if (this.controller?.scope === "singleton" && isDevelopment(ctx.kernel?.environment)) guardSingletonState(controller, controller.constructor.name);
|
|
224
241
|
return controller;
|
|
225
242
|
}
|
|
226
243
|
/**
|
|
@@ -245,9 +262,46 @@ var Resolver = class {
|
|
|
245
262
|
*/
|
|
246
263
|
executeAction(data, reload = false, metaArg) {
|
|
247
264
|
const meta = metaArg ?? (this.route ? resolveActionMeta(this.route) : computeActionMeta(this.controller, this.actionName));
|
|
265
|
+
let guarded;
|
|
266
|
+
if (this.preAuthorized) this.preAuthorized = false;
|
|
267
|
+
else guarded = this._enforceActionSecurity(meta);
|
|
268
|
+
const schema = meta.schema;
|
|
269
|
+
if (schema !== null) {
|
|
270
|
+
if (guarded !== void 0) return guarded.then(() => this._validateAndInvoke(schema, meta, data, reload));
|
|
271
|
+
return this._validateAndInvoke(schema, meta, data, reload);
|
|
272
|
+
}
|
|
273
|
+
if (guarded !== void 0) return guarded.then(() => this._acquireAndInvoke(meta, data, reload, null));
|
|
274
|
+
return this._acquireAndInvoke(meta, data, reload, null);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Valide l'entrée d'une action à schéma, puis l'exécute avec cette entrée
|
|
278
|
+
* pour argument unique. Synchrone tant que le validateur l'est.
|
|
279
|
+
*
|
|
280
|
+
* @throws SchemaValidationError (422) quand l'entrée viole le schéma.
|
|
281
|
+
*/
|
|
282
|
+
_validateAndInvoke(schema, meta, data, reload) {
|
|
283
|
+
const validated = validateRouteInput(schema, readRouteInput(this._zipRouteParams(), this.context.request, this.queryOverride ?? void 0, meta.bodyStream));
|
|
284
|
+
if (validated instanceof Promise) return validated.then((input) => this._acquireAndInvoke(meta, data, reload, input));
|
|
285
|
+
return this._acquireAndInvoke(meta, data, reload, validated);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Juge les gardes de l'action résolue SANS l'exécuter — appelé par le
|
|
289
|
+
* pipeline WebSocket AVANT d'accepter le handshake (#596), pour qu'un
|
|
290
|
+
* connecté sans le rôle reçoive son 403 en statut HTTP au lieu d'une
|
|
291
|
+
* ouverture suivie d'une fermeture. Un verdict favorable vaut pour la
|
|
292
|
+
* PROCHAINE exécution (l'action de handshake), qui ne le rejuge pas ; les
|
|
293
|
+
* messages suivants, eux, sont jugés chacun.
|
|
294
|
+
*
|
|
295
|
+
* @returns `undefined` quand l'action n'est pas gardée ; sinon la promesse du
|
|
296
|
+
* verdict, rompue par un refus
|
|
297
|
+
*/
|
|
298
|
+
authorizeAction() {
|
|
299
|
+
const meta = this.route ? resolveActionMeta(this.route) : computeActionMeta(this.controller, this.actionName);
|
|
248
300
|
const guarded = this._enforceActionSecurity(meta);
|
|
249
|
-
if (guarded
|
|
250
|
-
return
|
|
301
|
+
if (guarded === void 0) return void 0;
|
|
302
|
+
return guarded.then(() => {
|
|
303
|
+
this.preAuthorized = true;
|
|
304
|
+
});
|
|
251
305
|
}
|
|
252
306
|
/**
|
|
253
307
|
* Les gardes d'autorisation d'une action : `@IsGranted`/`@RequireScope`, puis
|
|
@@ -263,6 +317,7 @@ var Resolver = class {
|
|
|
263
317
|
* lookup, ni attente) ; sinon la promesse du verdict, rompue par un refus.
|
|
264
318
|
*/
|
|
265
319
|
_enforceActionSecurity(meta) {
|
|
320
|
+
if (meta.authentication !== null) return this._enforceStepUp(meta);
|
|
266
321
|
if (meta.security === null) return this._enforceAreaSecurity();
|
|
267
322
|
const granted = this._enforceSecurity(meta.security);
|
|
268
323
|
return meta.security.hasRoleClause ? granted : granted.then(() => this._enforceAreaSecurity());
|
|
@@ -284,10 +339,10 @@ var Resolver = class {
|
|
|
284
339
|
* c'est la bonne classe, sinon une création —, puis l'action elle-même.
|
|
285
340
|
* N'attend que la création, et seulement quand elle attend.
|
|
286
341
|
*/
|
|
287
|
-
_acquireAndInvoke(meta, data, reload) {
|
|
342
|
+
_acquireAndInvoke(meta, data, reload, input) {
|
|
288
343
|
const pinned = this.context.container?.get("controller");
|
|
289
|
-
if (!pinned || reload || this.controller && !(pinned instanceof this.controller)) return thenMaybe(this.newController(), (controller) => this._invokeAction(controller, meta, data));
|
|
290
|
-
return this._invokeAction(pinned, meta, data);
|
|
344
|
+
if (!pinned || reload || this.controller && !(pinned instanceof this.controller)) return thenMaybe(this.newController(), (controller) => this._invokeAction(controller, meta, data, input));
|
|
345
|
+
return this._invokeAction(pinned, meta, data, input);
|
|
291
346
|
}
|
|
292
347
|
/**
|
|
293
348
|
* Appelle l'action sur l'instance : état de requête (portée `request`
|
|
@@ -296,17 +351,15 @@ var Resolver = class {
|
|
|
296
351
|
*
|
|
297
352
|
* @throws de façon SYNCHRONE ce que lève l'action, ou l'absence d'action.
|
|
298
353
|
*/
|
|
299
|
-
_invokeAction(controller, meta, data) {
|
|
354
|
+
_invokeAction(controller, meta, data, input) {
|
|
300
355
|
if (this.controller?.scope !== "singleton") {
|
|
301
356
|
controller.setRoute(this.route);
|
|
302
|
-
if (this.queryOverride !== null)
|
|
303
|
-
controller.queryGet = this.queryOverride;
|
|
304
|
-
controller.query = this.queryOverride;
|
|
305
|
-
}
|
|
357
|
+
if (this.queryOverride !== null) controller.query = this.queryOverride;
|
|
306
358
|
}
|
|
307
359
|
const methodKey = this.actionName;
|
|
308
360
|
let args;
|
|
309
|
-
if (
|
|
361
|
+
if (input !== null) args = [input];
|
|
362
|
+
else if (meta.paramsMeta) args = this._buildParamArgs(meta.paramsMeta);
|
|
310
363
|
else if (data) args = [...this.variables, ...data];
|
|
311
364
|
else args = [...this.variables];
|
|
312
365
|
this._applyResponseMeta(meta);
|
|
@@ -381,18 +434,18 @@ var Resolver = class {
|
|
|
381
434
|
const als = RequestContext.get();
|
|
382
435
|
const httpReq = context.request;
|
|
383
436
|
const params = this._zipRouteParams();
|
|
384
|
-
const body =
|
|
437
|
+
const body = readInvocationBody(httpReq) ?? null;
|
|
385
438
|
const store = context.container?.get("idempotencyStore");
|
|
386
439
|
const verdict = await evaluateIdempotency({
|
|
387
440
|
store,
|
|
388
|
-
identity: resolveIdentity(
|
|
441
|
+
identity: resolveIdentity(),
|
|
389
442
|
clientKey: resolveIdempotencyKey(als?.idempotencyKey, httpReq?.headers?.["idempotency-key"]),
|
|
390
443
|
fingerprint: computeFingerprint([
|
|
391
444
|
this.route?.name ?? this.actionName,
|
|
392
445
|
params,
|
|
393
446
|
body
|
|
394
447
|
]),
|
|
395
|
-
isWs:
|
|
448
|
+
isWs: context.type === "websocket",
|
|
396
449
|
required: meta.idempotent?.required ?? true
|
|
397
450
|
});
|
|
398
451
|
if (verdict.kind === "reject") throw new nodefonyError(verdict.message, verdict.status);
|
|
@@ -526,16 +579,14 @@ var Resolver = class {
|
|
|
526
579
|
return idx === -1 ? void 0 : this.variables[idx];
|
|
527
580
|
}
|
|
528
581
|
_buildParamArgs(metas) {
|
|
529
|
-
const
|
|
530
|
-
const paramsMap = this._zipRouteParams();
|
|
531
|
-
const ctx = this.context;
|
|
582
|
+
const context = this.context;
|
|
532
583
|
return buildParamArgs(metas, {
|
|
533
|
-
paramsMap,
|
|
534
|
-
request:
|
|
535
|
-
response:
|
|
536
|
-
session:
|
|
584
|
+
paramsMap: this._zipRouteParams(),
|
|
585
|
+
request: context.request,
|
|
586
|
+
response: context.response,
|
|
587
|
+
session: context.session,
|
|
537
588
|
queryOverride: this.queryOverride ?? void 0,
|
|
538
|
-
getRequestCookies: (name) =>
|
|
589
|
+
getRequestCookies: (name) => context.getRequestCookies(name)
|
|
539
590
|
});
|
|
540
591
|
}
|
|
541
592
|
/**
|
|
@@ -617,17 +668,11 @@ var Resolver = class {
|
|
|
617
668
|
return this.context.render(result);
|
|
618
669
|
}
|
|
619
670
|
switch (this.context.type) {
|
|
620
|
-
case "http":
|
|
621
|
-
case "http2":
|
|
622
|
-
case "http3":
|
|
623
|
-
case "https": this.context.waitAsync = true;
|
|
671
|
+
case "http": this.context.waitAsync = true;
|
|
624
672
|
}
|
|
625
673
|
return;
|
|
626
674
|
default: switch (this.context.type) {
|
|
627
675
|
case "http":
|
|
628
|
-
case "http2":
|
|
629
|
-
case "http3":
|
|
630
|
-
case "https":
|
|
631
676
|
if (this.context.sended) return;
|
|
632
677
|
if (this.context.isRedirect) return this.context.send();
|
|
633
678
|
if (NO_BODY_STATUS.has(this.getResponseStatus())) return this.context.send();
|
|
@@ -639,6 +684,31 @@ var Resolver = class {
|
|
|
639
684
|
getResponseStatus() {
|
|
640
685
|
return this.context.response?.statusCode ?? 0;
|
|
641
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* `@RequireAuthentication` — les gardes d'autorisation d'abord (un rôle
|
|
689
|
+
* absent reste un 403 définitif : inutile de faire redemander une connexion à
|
|
690
|
+
* qui n'aura pas le droit), PUIS l'âge et les moyens de la connexion.
|
|
691
|
+
*
|
|
692
|
+
* Le refus est un 401 « step-up » (RFC 9470) : le `WWW-Authenticate` porte
|
|
693
|
+
* `error="insufficient_user_authentication"` et, quand la route fixe un âge,
|
|
694
|
+
* `max_age` — le client redemande une connexion au lieu de conclure à un
|
|
695
|
+
* accès fermé. Cold path : seules les routes décorées y passent.
|
|
696
|
+
*
|
|
697
|
+
* @throws nodefonyError 401 si la connexion est trop ancienne ou trop faible.
|
|
698
|
+
*/
|
|
699
|
+
async _enforceStepUp(meta) {
|
|
700
|
+
const requirement = meta.authentication;
|
|
701
|
+
if (requirement === null) return;
|
|
702
|
+
await this._enforceActionSecurity({
|
|
703
|
+
...meta,
|
|
704
|
+
authentication: null
|
|
705
|
+
});
|
|
706
|
+
const cause = authenticationShortfall(this.context.session ?? null, requirement, Date.now());
|
|
707
|
+
if (cause === null) return;
|
|
708
|
+
const response = this.context.response;
|
|
709
|
+
if (response !== null && "setHeader" in response) response.setHeader("WWW-Authenticate", stepUpChallenge(requirement));
|
|
710
|
+
throw new nodefonyError(identityHint(this.context.kernel) === null ? "Authentication required" : `Authentication required — ${cause}. Reconnectez-vous (\`@RequireAuthentication\` de la route).`, 401);
|
|
711
|
+
}
|
|
642
712
|
};
|
|
643
713
|
/** Statuts dont la réponse n'a, par définition, pas de corps (RFC 9110). */
|
|
644
714
|
const NO_BODY_STATUS = /* @__PURE__ */ new Set([
|
|
@@ -646,5 +716,4 @@ const NO_BODY_STATUS = /* @__PURE__ */ new Set([
|
|
|
646
716
|
205,
|
|
647
717
|
304
|
|
648
718
|
]);
|
|
649
|
-
//#endregion
|
|
650
719
|
export { Resolver as default };
|