@nodefony/framework 10.0.0-beta.5 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js +7 -0
- package/dist/index.js +14 -10
- package/dist/nodefony/config/config.js +16 -2
- package/dist/nodefony/controller/AdminApiController.js +18 -5
- package/dist/nodefony/controller/BenchController.js +1 -1
- package/dist/nodefony/controller/CspReportController.js +70 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +3 -3
- package/dist/nodefony/controller/LoginPageController.js +5 -7
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +75 -10
- package/dist/nodefony/controller/SessionAuthController.js +6 -7
- package/dist/nodefony/controller/TokenAuthController.js +3 -3
- package/dist/nodefony/decorators/routerDecorators.js +103 -15
- package/dist/nodefony/grpc/GrpcController.js +138 -0
- package/dist/nodefony/grpc/GrpcService.js +75 -0
- package/dist/nodefony/grpc/declarations.js +33 -0
- package/dist/nodefony/grpc/index.js +3 -0
- package/dist/nodefony/service/AdminBroker.js +1 -1
- package/dist/nodefony/service/IdempotencyStore.js +1 -1
- package/dist/nodefony/service/router.js +15 -12
- package/dist/nodefony/src/Controller.js +330 -115
- package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
- package/dist/nodefony/src/Resolver.js +85 -51
- package/dist/nodefony/src/Route.js +45 -45
- package/dist/nodefony/src/SecurityApiController.js +30 -7
- package/dist/nodefony/src/conditionalRequest.js +103 -0
- package/dist/nodefony/src/docsReader.js +1 -1
- package/dist/nodefony/src/idempotency.js +1 -1
- package/dist/nodefony/src/loginPage.js +3 -2
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/viewNonce.js +58 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/nodefony/config/config.d.ts +20 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
- package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
- package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
- package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
- package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
- package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
- package/dist/types/nodefony/grpc/index.d.ts +13 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +4 -4
- package/dist/types/nodefony/service/router.d.ts +8 -6
- package/dist/types/nodefony/src/Controller.d.ts +124 -14
- package/dist/types/nodefony/src/Resolver.d.ts +35 -8
- package/dist/types/nodefony/src/Route.d.ts +35 -0
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
- package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
- package/docs/controller.md +234 -35
- package/docs/decorateurs.md +1 -1
- package/docs/grpc.md +690 -0
- package/docs/idempotence.md +13 -8
- package/docs/index.md +16 -10
- package/docs/routing.md +30 -2
- package/docs/templates.md +15 -0
- package/package.json +28 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// \0@oxc-project+runtime@0.
|
|
1
|
+
// \0@oxc-project+runtime@0.158.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
package/dist/index.js
CHANGED
|
@@ -3,17 +3,14 @@ import { defineFrameworkConfig, frameworkConfigJsonSchema } from "./nodefony/con
|
|
|
3
3
|
import { getIdempotencyStoreFactory, listIdempotencyBackends, listIdempotencyStores, registerIdempotencyStore } from "./nodefony/src/idempotencyStoreRegistry.js";
|
|
4
4
|
import { RedisIdempotencyStore } from "./nodefony/src/RedisIdempotencyStore.js";
|
|
5
5
|
import { scheduleIdempotencyGc } from "./nodefony/src/idempotencyGc.js";
|
|
6
|
-
import Route from "./nodefony/src/Route.js";
|
|
7
|
-
import { validateRouteInput } from "./nodefony/src/routeSchema.js";
|
|
8
|
-
import Controller from "./nodefony/src/Controller.js";
|
|
9
|
-
import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, buildContextParamArgs, controller, controllers, getParamArgsMeta, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
10
|
-
import Resolver from "./nodefony/src/Resolver.js";
|
|
11
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
12
6
|
import router_default, { FRAMEWORK_ROUTER, isFrameworkRouter } from "./nodefony/service/router.js";
|
|
7
|
+
import Route from "./nodefony/src/Route.js";
|
|
8
|
+
import Controller, { configureFileServing, contentDisposition } from "./nodefony/src/Controller.js";
|
|
13
9
|
import ResourceController from "./nodefony/src/ResourceController.js";
|
|
14
|
-
import
|
|
10
|
+
import Resolver from "./nodefony/src/Resolver.js";
|
|
15
11
|
import AdminBroker_default from "./nodefony/service/AdminBroker.js";
|
|
16
12
|
import IdempotencyStore_default from "./nodefony/service/IdempotencyStore.js";
|
|
13
|
+
import AdminApiController from "./nodefony/controller/AdminApiController.js";
|
|
17
14
|
import SessionAuthController, { mountSessionAuthRoutes } from "./nodefony/controller/SessionAuthController.js";
|
|
18
15
|
import LoginPageController, { mountLoginPage } from "./nodefony/controller/LoginPageController.js";
|
|
19
16
|
import TokenAuthController, { mountTokenAuthRoutes } from "./nodefony/controller/TokenAuthController.js";
|
|
@@ -21,14 +18,18 @@ import IssuerMetadataController, { mountIssuerMetadataRoutes } from "./nodefony/
|
|
|
21
18
|
import ProtectedResourceMetadataController, { collectProtectedResources, mountProtectedResourceRoutes, protectedResourceRoutePaths } from "./nodefony/controller/ProtectedResourceMetadataController.js";
|
|
22
19
|
import WebAuthnController, { mountWebAuthnRoutes } from "./nodefony/controller/WebAuthnController.js";
|
|
23
20
|
import OAuth2Controller, { mountOAuth2Routes } from "./nodefony/controller/OAuth2Controller.js";
|
|
21
|
+
import CspReportController, { mountCspReportRoute } from "./nodefony/controller/CspReportController.js";
|
|
24
22
|
import BenchController, { mountBenchRoutes } from "./nodefony/controller/BenchController.js";
|
|
25
23
|
import ApiKeyController, { mountApiKeyRoutes } from "./nodefony/controller/ApiKeyController.js";
|
|
26
24
|
import TotpController, { mountTotpRoutes } from "./nodefony/controller/TotpController.js";
|
|
27
25
|
import { createKernelAdminApi } from "./nodefony/src/KernelAdminApi.js";
|
|
28
|
-
import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
|
|
29
26
|
import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
|
|
27
|
+
import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
|
|
30
28
|
import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
|
|
31
29
|
import Eta from "./nodefony/service/Eta.js";
|
|
30
|
+
import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, buildContextParamArgs, controller, controllers, getParamArgsMeta, getRouteSchema, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
31
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
32
|
+
import { validateRouteInput } from "./nodefony/src/routeSchema.js";
|
|
32
33
|
import path from "node:path";
|
|
33
34
|
import { AUTO_STORE, EMPTY_INFRA, Module, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, services, storeConnectivity } from "nodefony";
|
|
34
35
|
// index.ts
|
|
@@ -63,7 +64,9 @@ let Framework = class Framework extends Module {
|
|
|
63
64
|
* `this.options` (matérialise les défauts, préserve `router`/`adminBroker`).
|
|
64
65
|
*/
|
|
65
66
|
async onKernelRegister() {
|
|
66
|
-
|
|
67
|
+
const validated = defineFrameworkConfig(this.options ?? {});
|
|
68
|
+
this.options = validated;
|
|
69
|
+
configureFileServing(validated.fileServing);
|
|
67
70
|
return this;
|
|
68
71
|
}
|
|
69
72
|
/**
|
|
@@ -191,6 +194,7 @@ let Framework = class Framework extends Module {
|
|
|
191
194
|
}
|
|
192
195
|
if (this.kernel?.container?.get("webauthn")) mountWebAuthnRoutes(this);
|
|
193
196
|
if (this.kernel?.container?.get("oauth2")) mountOAuth2Routes(this);
|
|
197
|
+
if (this.kernel?.container?.get("cspReports")) mountCspReportRoute(this);
|
|
194
198
|
if (process.env.NF_BENCH_ROUTE === "1") mountBenchRoutes(this);
|
|
195
199
|
if (this.kernel?.container?.get("apiKeys")) mountApiKeyRoutes(this);
|
|
196
200
|
if (this.kernel?.container?.get("totp")) mountTotpRoutes(this);
|
|
@@ -204,4 +208,4 @@ Framework = __decorate([services([
|
|
|
204
208
|
IdempotencyStore_default
|
|
205
209
|
])], Framework);
|
|
206
210
|
var framework_default = Framework;
|
|
207
|
-
export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, FRAMEWORK_ROUTER, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, LoginPageController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildContextParamArgs, buildPlaygroundSnapshot, collectProtectedResources, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, getParamArgsMeta, isFrameworkRouter, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountIssuerMetadataRoutes, mountLoginPage, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream, validateRouteInput };
|
|
211
|
+
export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CspReportController, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, FRAMEWORK_ROUTER, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, LoginPageController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildContextParamArgs, buildPlaygroundSnapshot, collectProtectedResources, contentDisposition, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, getParamArgsMeta, getRouteSchema, isFrameworkRouter, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountCspReportRoute, mountIssuerMetadataRoutes, mountLoginPage, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream, validateRouteInput };
|
|
@@ -25,6 +25,9 @@ import { z } from "zod";
|
|
|
25
25
|
* tels quels aux Services `Router` / `AdminBroker` (4ᵉ arg du `super(...)`).
|
|
26
26
|
* Aucune forme métier figée → `looseObject` + `optional` (ne RIEN stripper,
|
|
27
27
|
* absent par défaut = `undefined`, comme avant validation).
|
|
28
|
+
* - `idempotency` — store du cache d'idempotence.
|
|
29
|
+
* - `fileServing` — défaut des fichiers servis par une action (validateurs,
|
|
30
|
+
* `Cache-Control`), posé au boot dans `Controller.ts#configureFileServing`.
|
|
28
31
|
*
|
|
29
32
|
* ## Pureté
|
|
30
33
|
*
|
|
@@ -38,10 +41,21 @@ const idempotencySchema = z.strictObject({
|
|
|
38
41
|
gcIntervalS: z.number().int().min(0).default(600).describe("Intervalle de purge des clés d'idempotence expirées (s), HORS hot-path. N'a d'effet QUE pour un store SANS expiration native (`drizzle` → `DELETE WHERE expiresAt<=now`) ; `redis` (TTL `PX`) et `memory` (purge passive) l'ignorent. 0 = timer désarmé (cron/k8s)."),
|
|
39
42
|
gcJitter: z.boolean().default(true).describe("Étale le départ du gc d'idempotence par process — anti thundering-herd sur le store SQL partagé en cluster.")
|
|
40
43
|
});
|
|
44
|
+
/**
|
|
45
|
+
* Requêtes conditionnelles des fichiers servis par une action (`streamFile`,
|
|
46
|
+
* `renderFileDownload`, `renderMediaStream`). Le défaut de TOUTE
|
|
47
|
+
* l'application ; un appel le remplace par ses options (`validators`) ou ses
|
|
48
|
+
* en-têtes (`Cache-Control`, `ETag`).
|
|
49
|
+
*/
|
|
50
|
+
const fileServingSchema = z.strictObject({
|
|
51
|
+
validators: z.boolean().default(true).describe("ETag forte + Last-Modified tirés de la taille et de la date du fichier, et réponses 304/412 (RFC 9110 §13). `false` les coupe pour toute l'application — dates non fiables (archive décompressée, build reproductible : même taille et même date pour deux contenus). Un appel peut rétablir ou couper par `{ validators }`."),
|
|
52
|
+
cacheControl: z.string().trim().regex(/^[\t\x20-\x7e\x80-\xff]+$/, "valeur d'en-tête invalide (CR, LF ou caractère de contrôle)").nullable().default("private, no-cache").describe("`Cache-Control` posé quand l'action n'en fixe aucun. Défaut `private, no-cache` : `Last-Modified` rendrait sinon la réponse réutilisable par un cache PARTAGÉ sans revalidation (RFC 9111 §4.2.2), or un fichier servi par une action est souvent protégé. `null` = n'en poser aucun. Sans effet quand `validators` est coupé.")
|
|
53
|
+
});
|
|
41
54
|
const frameworkConfigSchema = z.strictObject({
|
|
42
55
|
router: serviceOptionsSchema.optional().describe("Options transmises au Service `Router` (bag d'options de Service de base : logger, timers…). Loose : non strippées. Absent (défaut) = aucune."),
|
|
43
56
|
adminBroker: serviceOptionsSchema.optional().describe("Options transmises au Service `AdminBroker` (data plane admin `/nodefony/<ns>/api/*`). Loose : non strippées. Absent (défaut) = aucune."),
|
|
44
|
-
idempotency: idempotencySchema.default(() => idempotencySchema.parse({})).describe("Idempotence des mutations (anti double-effet). Cf draft-ietf-httpapi-idempotency-key-header.")
|
|
57
|
+
idempotency: idempotencySchema.default(() => idempotencySchema.parse({})).describe("Idempotence des mutations (anti double-effet). Cf draft-ietf-httpapi-idempotency-key-header."),
|
|
58
|
+
fileServing: fileServingSchema.default(() => fileServingSchema.parse({})).describe("Fichiers servis par une action : validateurs (ETag, Last-Modified, 304/412) et Cache-Control par défaut.")
|
|
45
59
|
}).describe("Configuration de @nodefony/framework.");
|
|
46
60
|
var config_default = {
|
|
47
61
|
...frameworkConfigSchema.parse({}),
|
|
@@ -58,4 +72,4 @@ var config_default = {
|
|
|
58
72
|
}
|
|
59
73
|
} }
|
|
60
74
|
};
|
|
61
|
-
export { config_default as default, frameworkConfigSchema };
|
|
75
|
+
export { config_default as default, fileServingSchema, frameworkConfigSchema };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Controller from "../src/Controller.js";
|
|
2
|
+
import { withAuthChallenge } from "../src/SecurityApiController.js";
|
|
2
3
|
import { computeFingerprint, evaluateIdempotency, resolveIdempotencyKey, resolveIdentity } from "../src/idempotency.js";
|
|
3
|
-
import { JsonRpcServerError, RequestContext, RpcError, executeAdminEndpoint } from "nodefony";
|
|
4
|
+
import { BYTES_MARKER_HEADER, JsonRpcServerError, RequestContext, RpcError, executeAdminEndpoint } from "nodefony";
|
|
4
5
|
// nodefony/controller/AdminApiController.ts
|
|
5
6
|
/** Vrai pour une chaîne ou un tableau de chaînes — ce que porte {@link IAdminRequest.query}. */
|
|
6
7
|
function isAdminQueryValue(value) {
|
|
@@ -62,8 +63,10 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
62
63
|
* (une action, deux transports) : même exécution sur les deux transports, seul
|
|
63
64
|
* l'emballage diffère.
|
|
64
65
|
*
|
|
65
|
-
* - **HTTP** :
|
|
66
|
-
*
|
|
66
|
+
* - **HTTP** : `renderJson` + status + header `x-nodefony-instance`. Quand
|
|
67
|
+
* l'application active un codec binaire, le corps passe par le codec texte
|
|
68
|
+
* de la socket (convention `$bytes`, annoncée par `x-nodefony-bytes`) : des
|
|
69
|
+
* octets se relisent alors à l'identique sur les deux transports.
|
|
67
70
|
* - **Pont WS-RPC `api.request`** : valeur **nue** (le pont l'enveloppe
|
|
68
71
|
* `{id, result}` — snapshot ≡ GET par construction) ; statut ≥ 400 →
|
|
69
72
|
* {@link RpcError} (`data.status` + `data.body`), symétrie d'un `fetch`
|
|
@@ -85,9 +88,19 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
85
88
|
}
|
|
86
89
|
return body;
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
...headers,
|
|
91
|
+
const stamped = {
|
|
92
|
+
...status === 401 ? withAuthChallenge(headers) : headers,
|
|
90
93
|
"x-nodefony-instance": AdminApiController.instanceId
|
|
94
|
+
};
|
|
95
|
+
const codec = this.get("realtimeService")?.textFrameCodec;
|
|
96
|
+
const marker = codec?.bytesMarker;
|
|
97
|
+
if (codec === void 0 || marker === void 0) return this.renderJson(body, status, stamped);
|
|
98
|
+
const text = codec.encode(body);
|
|
99
|
+
if (typeof text !== "string") return this.renderJson(body, status, stamped);
|
|
100
|
+
this.setContextJson();
|
|
101
|
+
return this.renderResponse(text, "utf8", status, {
|
|
102
|
+
...stamped,
|
|
103
|
+
[BYTES_MARKER_HEADER]: marker
|
|
91
104
|
});
|
|
92
105
|
}
|
|
93
106
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
3
|
// nodefony/controller/BenchController.ts
|
|
4
4
|
/**
|
|
5
5
|
* Corps servi par la cible de banc — **gelé et partagé** : ce qu'on chronomètre
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import router_default from "../service/router.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
|
+
import { Body, CsrfExempt } from "../decorators/routerDecorators.js";
|
|
4
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
|
|
5
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
6
|
+
import { CSP_REPORT_MAX_BYTES, CSP_REPORT_PATH } from "nodefony";
|
|
7
|
+
import { HttpError } from "@nodefony/http";
|
|
8
|
+
// nodefony/controller/CspReportController.ts
|
|
9
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
10
|
+
let mounted = false;
|
|
11
|
+
/**
|
|
12
|
+
* Réception PUBLIQUE des rapports de violation CSP (ADR-0024 §C7) —
|
|
13
|
+
* `POST /nodefony/security/api/csp-report`, adaptateur MINCE au-dessus du
|
|
14
|
+
* service `cspReports`.
|
|
15
|
+
*
|
|
16
|
+
* Le navigateur poste sans identifiant : la route contourne le pare-feu, et la
|
|
17
|
+
* défense CSRF (`@CsrfExempt`) — elle ne change l'état de personne, et un
|
|
18
|
+
* rapport n'a aucune session à détourner. Le corps arrive en FLUX
|
|
19
|
+
* (`@Body({ stream: true })`) pour être borné PENDANT la lecture : au-delà de
|
|
20
|
+
* {@link CSP_REPORT_MAX_BYTES}, 413 sans rien garder. Tout le reste — quota,
|
|
21
|
+
* type de contenu, forme, expurgation — appartient au collecteur, et la
|
|
22
|
+
* réponse est 204 quoi qu'il en pense : l'émetteur n'apprend rien.
|
|
23
|
+
*/
|
|
24
|
+
var CspReportController = class extends Controller {
|
|
25
|
+
constructor(context) {
|
|
26
|
+
super("CspReportController", context);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Reçoit un rapport, ou un lot de rapports.
|
|
30
|
+
*
|
|
31
|
+
* @param stream - le corps brut, jamais lu par le pipeline.
|
|
32
|
+
* @throws HttpError 413 quand le corps dépasse le plafond.
|
|
33
|
+
*/
|
|
34
|
+
async report(stream) {
|
|
35
|
+
const headers = this.context?.request?.headers;
|
|
36
|
+
const declared = Number(headers?.["content-length"]);
|
|
37
|
+
if (Number.isFinite(declared) && declared > CSP_REPORT_MAX_BYTES) throw new HttpError("CSP report too large", 413, this.context);
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let seen = 0;
|
|
40
|
+
for await (const chunk of stream) {
|
|
41
|
+
seen += chunk.byteLength;
|
|
42
|
+
if (seen > CSP_REPORT_MAX_BYTES) throw new HttpError("CSP report too large", 413, this.context);
|
|
43
|
+
chunks.push(chunk);
|
|
44
|
+
}
|
|
45
|
+
const sink = this.get("cspReports");
|
|
46
|
+
if (sink) sink.receive(headers?.["content-type"], Buffer.concat(chunks, seen), this.context?.remoteAddress ?? null, headers?.["user-agent"] ?? null);
|
|
47
|
+
return this.renderResponse("", "utf8", 204, NO_STORE);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
__decorate([CsrfExempt(), __decorateParam(0, Body({ stream: true }))], CspReportController.prototype, "report", null);
|
|
51
|
+
/**
|
|
52
|
+
* Monte la route de réception — appelé par le module framework à
|
|
53
|
+
* `onKernelReady`, seulement si le service `cspReports` est présent (rapports
|
|
54
|
+
* demandés vers la route du framework).
|
|
55
|
+
*
|
|
56
|
+
* @param frameworkModule - module qui porte le contrôleur.
|
|
57
|
+
*/
|
|
58
|
+
function mountCspReportRoute(frameworkModule) {
|
|
59
|
+
if (mounted) return;
|
|
60
|
+
router_default.createRoute("security.cspReport", {
|
|
61
|
+
path: CSP_REPORT_PATH,
|
|
62
|
+
constructor: CspReportController,
|
|
63
|
+
classMethod: "report",
|
|
64
|
+
requirements: { methods: ["POST"] },
|
|
65
|
+
bypassFirewall: true
|
|
66
|
+
});
|
|
67
|
+
if (!Object.prototype.hasOwnProperty.call(CspReportController.prototype, "module")) router_default.setController(CspReportController, frameworkModule);
|
|
68
|
+
mounted = true;
|
|
69
|
+
}
|
|
70
|
+
export { CspReportController as default, mountCspReportRoute };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
3
|
import { askedAuthority, onDeclaredAuthority } from "./oauthAuthority.js";
|
|
4
4
|
import { JWKS_PATH, authorizationServerMetadataPath, buildAuthorizationServerMetadata } from "nodefony";
|
|
5
5
|
// nodefony/controller/IssuerMetadataController.ts
|
|
@@ -92,7 +92,7 @@ var IssuerMetadataController = class extends Controller {
|
|
|
92
92
|
try {
|
|
93
93
|
document = buildAuthorizationServerMetadata({ issuer });
|
|
94
94
|
} catch (error) {
|
|
95
|
-
this.log(`métadonnées d'émetteur impubliables : ${error.message}`, "CRITIC");
|
|
95
|
+
this.log(`métadonnées d'émetteur impubliables : ${error instanceof Error ? error.message : String(error)}`, "CRITIC");
|
|
96
96
|
return this.renderJson({ error: "server_error" }, 500);
|
|
97
97
|
}
|
|
98
98
|
return this.renderJson(document, 200, { "Cache-Control": "public, max-age=3600" });
|
|
@@ -110,7 +110,7 @@ var IssuerMetadataController = class extends Controller {
|
|
|
110
110
|
try {
|
|
111
111
|
keys = await publisher.getPublicJWKS();
|
|
112
112
|
} catch (error) {
|
|
113
|
-
this.log(`JWKS illisible : ${error.message}`, "CRITIC");
|
|
113
|
+
this.log(`JWKS illisible : ${error instanceof Error ? error.message : String(error)}`, "CRITIC");
|
|
114
114
|
return this.renderJson({ error: "server_error" }, 500);
|
|
115
115
|
}
|
|
116
116
|
return this.renderJson(keys, 200, { "Cache-Control": "public, max-age=300" });
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { isDevelopment } from "../src/singletonGuard.js";
|
|
2
|
-
import Controller from "../src/Controller.js";
|
|
3
|
-
import { Csp } from "../decorators/routerDecorators.js";
|
|
4
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
5
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
|
+
import { isDevelopment } from "../src/singletonGuard.js";
|
|
6
4
|
import { LOGIN_PAGE_ASSET_FILES, LOGIN_PAGE_TEMPLATE, buildLoginPageView, keepSuccess } from "../src/loginPage.js";
|
|
7
5
|
import path from "node:path";
|
|
8
6
|
import { LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_SKINS, loginPageSkinFile } from "nodefony";
|
|
@@ -53,8 +51,9 @@ const version = keepSuccess(async () => {
|
|
|
53
51
|
* feuille `nodefony/login.css`, script `nodefony/login.js`. Aucun front, aucun
|
|
54
52
|
* build côté application.
|
|
55
53
|
*
|
|
56
|
-
* Joignable sans session ; jamais mise en cache (`no-store`)
|
|
57
|
-
*
|
|
54
|
+
* Joignable sans session ; jamais mise en cache (`no-store`). L’encadrement
|
|
55
|
+
* suit `http.securityHeaders.frameOptions`, qui pose `X-Frame-Options` ET
|
|
56
|
+
* `frame-ancestors` sur toute la politique (ADR-0024 §C5).
|
|
58
57
|
*/
|
|
59
58
|
var LoginPageController = class extends Controller {
|
|
60
59
|
constructor(context) {
|
|
@@ -87,7 +86,6 @@ var LoginPageController = class extends Controller {
|
|
|
87
86
|
});
|
|
88
87
|
}
|
|
89
88
|
};
|
|
90
|
-
__decorate([Csp({ "frame-ancestors": ["'none'"] })], LoginPageController.prototype, "page", null);
|
|
91
89
|
/**
|
|
92
90
|
* Monte la page de connexion par défaut quand security la décrit : la route
|
|
93
91
|
* sur `loginPage.path`, et ses fichiers sous `LOGIN_PAGE_ASSETS_BASE`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
3
|
import { askedAuthority, onDeclaredAuthority } from "./oauthAuthority.js";
|
|
4
4
|
import { buildProtectedResourceMetadata, canonicalResourceUri, protectedResourceMetadataPath } from "nodefony";
|
|
5
5
|
// nodefony/controller/ProtectedResourceMetadataController.ts
|
|
@@ -66,9 +66,10 @@ var ProtectedResourceMetadataController = class extends Controller {
|
|
|
66
66
|
/** Chemin bien connu demandé, tel que le Router l'a matché. */
|
|
67
67
|
#askedPath() {
|
|
68
68
|
const req = this.request;
|
|
69
|
-
|
|
69
|
+
if (typeof req !== "object" || req === null) return null;
|
|
70
|
+
const p = "pathname" in req ? req.pathname : void 0;
|
|
70
71
|
if (typeof p === "string") return p;
|
|
71
|
-
const url = req
|
|
72
|
+
const url = "url" in req ? req.url : void 0;
|
|
72
73
|
if (url instanceof URL) return url.pathname;
|
|
73
74
|
if (typeof url === "string") try {
|
|
74
75
|
return new URL(url, "http://localhost").pathname;
|
|
@@ -112,7 +113,7 @@ var ProtectedResourceMetadataController = class extends Controller {
|
|
|
112
113
|
try {
|
|
113
114
|
document = buildProtectedResourceMetadata(match);
|
|
114
115
|
} catch (error) {
|
|
115
|
-
this.log(`métadonnées de ressource protégée impubliables : ${error.message}`, "CRITIC");
|
|
116
|
+
this.log(`métadonnées de ressource protégée impubliables : ${error instanceof Error ? error.message : String(error)}`, "CRITIC");
|
|
116
117
|
return this.renderJson({ error: "server_error" }, 500);
|
|
117
118
|
}
|
|
118
119
|
return this.renderJson(document, 200, { "Cache-Control": "public, max-age=3600" });
|
|
@@ -140,23 +141,87 @@ var ProtectedResourceMetadataController = class extends Controller {
|
|
|
140
141
|
* interface à implémenter formellement — le même couplage que `tokenService` ou
|
|
141
142
|
* `adminBroker`.
|
|
142
143
|
*
|
|
144
|
+
* Deux sources peuvent déclarer la MÊME ressource — une zone du pare-feu et la
|
|
145
|
+
* porte MCP qu'elle couvre, typiquement — : elles sont fusionnées
|
|
146
|
+
* ({@link mergeProtectedResources}), jamais départagées par leur ordre.
|
|
147
|
+
*
|
|
143
148
|
* @param container - conteneur de services de l'application
|
|
144
|
-
* @returns les ressources déclarées, dans l'ordre des
|
|
149
|
+
* @returns les ressources déclarées, une entrée par ressource, dans l'ordre des
|
|
150
|
+
* services
|
|
145
151
|
*/
|
|
146
152
|
function collectProtectedResources(container) {
|
|
147
153
|
const collected = [];
|
|
148
154
|
for (const name of container.keys()) {
|
|
149
155
|
const service = container.get(name);
|
|
150
|
-
if (typeof service
|
|
156
|
+
if (typeof service !== "object" || service === null || !("publishedProtectedResources" in service) || typeof service.publishedProtectedResources !== "function") continue;
|
|
151
157
|
let declared;
|
|
152
158
|
try {
|
|
153
|
-
declared = service.publishedProtectedResources
|
|
159
|
+
declared = Reflect.apply(service.publishedProtectedResources, service, []);
|
|
160
|
+
} catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(declared)) continue;
|
|
164
|
+
for (const entry of declared) if (isProtectedResourceInput(entry)) collected.push(entry);
|
|
165
|
+
}
|
|
166
|
+
return mergeProtectedResources(collected);
|
|
167
|
+
}
|
|
168
|
+
/** Une liste de chaînes, ou rien. */
|
|
169
|
+
function isStringList(value) {
|
|
170
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Une déclaration a-t-elle la forme d'une ressource publiable ? Le contrat est
|
|
174
|
+
* structurel ({@link IProtectedResourcePublisher}) : ce qu'un service rend se
|
|
175
|
+
* VÉRIFIE, il ne se suppose pas.
|
|
176
|
+
*/
|
|
177
|
+
function isProtectedResourceInput(value) {
|
|
178
|
+
if (typeof value !== "object" || value === null) return false;
|
|
179
|
+
if (!("resource" in value) || typeof value.resource !== "string") return false;
|
|
180
|
+
if (!("authorizationServers" in value) || !isStringList(value.authorizationServers)) return false;
|
|
181
|
+
return !("scopesSupported" in value) || value.scopesSupported === void 0 || isStringList(value.scopesSupported);
|
|
182
|
+
}
|
|
183
|
+
/** L'union de deux listes, dans l'ordre d'apparition. */
|
|
184
|
+
function union(first, second) {
|
|
185
|
+
return [.../* @__PURE__ */ new Set([...first, ...second])];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Une ressource, UN document : fusionne les déclarations d'une même ressource
|
|
189
|
+
* (même URI canonique) — l'union de leurs serveurs d'autorisation et de leurs
|
|
190
|
+
* scopes ; le premier nom et la première documentation déclarés.
|
|
191
|
+
*
|
|
192
|
+
* Pourquoi fusionner plutôt que garder la première : une zone du pare-feu
|
|
193
|
+
* publie sa ressource avec les émetteurs de confiance, et la porte MCP qu'elle
|
|
194
|
+
* couvre publie la MÊME ressource avec les scopes de ses outils. Garder la
|
|
195
|
+
* première jetait les scopes sans un mot — et un agent qui suit le document
|
|
196
|
+
* demandait alors un jeton sans eux, qui n'ouvrait pas l'outil.
|
|
197
|
+
*
|
|
198
|
+
* @param entries - déclarations, dans l'ordre des services
|
|
199
|
+
* @returns une entrée par ressource, à la place de la première déclaration
|
|
200
|
+
*/
|
|
201
|
+
function mergeProtectedResources(entries) {
|
|
202
|
+
const merged = [];
|
|
203
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
let key;
|
|
206
|
+
try {
|
|
207
|
+
key = canonicalResourceUri(entry.resource);
|
|
154
208
|
} catch {
|
|
209
|
+
merged.push(entry);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const seen = byResource.get(key);
|
|
213
|
+
if (seen === void 0) {
|
|
214
|
+
const first = { ...entry };
|
|
215
|
+
byResource.set(key, first);
|
|
216
|
+
merged.push(first);
|
|
155
217
|
continue;
|
|
156
218
|
}
|
|
157
|
-
|
|
219
|
+
seen.authorizationServers = union(seen.authorizationServers, entry.authorizationServers);
|
|
220
|
+
if (entry.scopesSupported !== void 0) seen.scopesSupported = union(seen.scopesSupported ?? [], entry.scopesSupported);
|
|
221
|
+
if (seen.resourceName === void 0 && entry.resourceName !== void 0) seen.resourceName = entry.resourceName;
|
|
222
|
+
if (seen.resourceDocumentation === void 0 && entry.resourceDocumentation !== void 0) seen.resourceDocumentation = entry.resourceDocumentation;
|
|
158
223
|
}
|
|
159
|
-
return
|
|
224
|
+
return merged;
|
|
160
225
|
}
|
|
161
226
|
/**
|
|
162
227
|
* Chemins bien connus à monter pour un jeu de ressources déclarées.
|
|
@@ -217,4 +282,4 @@ function mountProtectedResourceRoutes(frameworkModule, resources) {
|
|
|
217
282
|
mounted = true;
|
|
218
283
|
return paths.length;
|
|
219
284
|
}
|
|
220
|
-
export { collectProtectedResources, ProtectedResourceMetadataController as default, mountProtectedResourceRoutes, protectedResourceRoutePaths };
|
|
285
|
+
export { collectProtectedResources, ProtectedResourceMetadataController as default, mergeProtectedResources, mountProtectedResourceRoutes, protectedResourceRoutePaths };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CsrfProtect } from "../decorators/routerDecorators.js";
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
3
2
|
import router_default from "../service/router.js";
|
|
4
3
|
import SecurityApiController from "../src/SecurityApiController.js";
|
|
4
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
5
5
|
import { AUTH_API_BASE } from "nodefony";
|
|
6
6
|
// nodefony/controller/SessionAuthController.ts
|
|
7
7
|
let mounted = false;
|
|
@@ -29,12 +29,11 @@ let mounted = false;
|
|
|
29
29
|
* (Auth.js, better-auth). Le `401` y écrivait une erreur réseau dans la console
|
|
30
30
|
* de chaque visiteur anonyme, sans rien protéger : la réponse ne révèle rien.
|
|
31
31
|
*
|
|
32
|
-
* @remarks
|
|
33
|
-
*
|
|
34
|
-
* session, et un
|
|
35
|
-
* navigateur
|
|
36
|
-
*
|
|
37
|
-
* déclare un).
|
|
32
|
+
* @remarks Les 401 de `login` portent le défi `Session realm="nodefony"`
|
|
33
|
+
* (RFC 9110 §15.5.2, posé par `SecurityApiController`), celui du pare-feu :
|
|
34
|
+
* aucun schéma HTTP ne décrit un cookie de session, et un `Basic` ouvrirait la
|
|
35
|
+
* fenêtre de connexion native du navigateur — l'anti-pattern que le BFF
|
|
36
|
+
* élimine. Un schéma inconnu des navigateurs n'en ouvre aucune.
|
|
38
37
|
*/
|
|
39
38
|
var SessionAuthController = class extends SecurityApiController {
|
|
40
39
|
constructor(context) {
|
|
@@ -24,9 +24,9 @@ function parseScope(scope) {
|
|
|
24
24
|
* framework ne peut pas importer les classes d'erreur de security ; un 429
|
|
25
25
|
* reporte le `Retry-After` (RFC 6585) du throttler NIST.
|
|
26
26
|
*
|
|
27
|
-
* @remarks
|
|
28
|
-
*
|
|
29
|
-
* ressource protégée par Bearer, il DÉLIVRE le Bearer.
|
|
27
|
+
* @remarks Ces 401 portent le défi `Session realm="nodefony"` (RFC 9110
|
|
28
|
+
* §15.5.2, posé par `SecurityApiController`) et non un `Bearer` : l'endpoint
|
|
29
|
+
* d'émission n'est pas une ressource protégée par Bearer, il DÉLIVRE le Bearer.
|
|
30
30
|
*/
|
|
31
31
|
var TokenAuthController = class extends SecurityApiController {
|
|
32
32
|
constructor(context) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { assertRouteSchemaShape } from "../src/routeSchema.js";
|
|
2
|
-
import Controller from "../src/Controller.js";
|
|
3
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
|
+
import { assertRouteSchemaShape } from "../src/routeSchema.js";
|
|
4
4
|
import { BootConfigurationError, RequestContext } from "nodefony";
|
|
5
5
|
import "reflect-metadata";
|
|
6
6
|
// nodefony/decorators/routerDecorators.ts
|
|
@@ -97,6 +97,36 @@ function controllers(targets) {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
+
* Applique à une route en cours de montage ce que la classe et la méthode
|
|
101
|
+
* déclarent par décorateur : `@Domain` (hôte) et `@BypassFirewall`. Seule
|
|
102
|
+
* implémentation de ces précédences — `@controller` et `@GrpcService` montent
|
|
103
|
+
* leurs routes par elle.
|
|
104
|
+
*
|
|
105
|
+
* Précédences : `@route({ host })` > `@Domain` méthode > `@Domain` classe ;
|
|
106
|
+
* option `bypassFirewall: true` > `@BypassFirewall` méthode > classe. Lecture
|
|
107
|
+
* au montage, fail-closed : un oubli laisse la route gardée.
|
|
108
|
+
*
|
|
109
|
+
* @param ctor - la classe contrôleur.
|
|
110
|
+
* @param options - les options de la route, complétées EN PLACE.
|
|
111
|
+
* @param method - la méthode dont les décorateurs s'appliquent (celle du
|
|
112
|
+
* contrat pour une route gRPC, pas l'action de base).
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
function applyDeclaredRouteOptions(ctor, options, method) {
|
|
117
|
+
if (options.host === void 0) {
|
|
118
|
+
const methodDomain = method === void 0 ? void 0 : Reflect.getMetadata(DOMAIN_METHOD_METADATA, ctor, method);
|
|
119
|
+
const classDomain = Reflect.getMetadata(DOMAIN_CLASS_METADATA, ctor);
|
|
120
|
+
const domain = methodDomain ?? classDomain;
|
|
121
|
+
if (domain) options.host = domain;
|
|
122
|
+
}
|
|
123
|
+
if (options.bypassFirewall !== true) {
|
|
124
|
+
const methodBypass = method === void 0 ? void 0 : Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, ctor, method);
|
|
125
|
+
const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, ctor);
|
|
126
|
+
if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
100
130
|
* Declaration Controller
|
|
101
131
|
*
|
|
102
132
|
* @param prefix - prefixage du router du controller.
|
|
@@ -124,17 +154,7 @@ function controller(prefix) {
|
|
|
124
154
|
let hasMagic = false;
|
|
125
155
|
for (const [name, options] of Object.entries(metadata)) {
|
|
126
156
|
options.prefix = prefix;
|
|
127
|
-
|
|
128
|
-
const methodDomain = options.classMethod === void 0 ? void 0 : Reflect.getMetadata(DOMAIN_METHOD_METADATA, mycontroller, options.classMethod);
|
|
129
|
-
const classDomain = Reflect.getMetadata(DOMAIN_CLASS_METADATA, mycontroller);
|
|
130
|
-
const domain = methodDomain ?? classDomain;
|
|
131
|
-
if (domain) options.host = domain;
|
|
132
|
-
}
|
|
133
|
-
if (options.bypassFirewall !== true) {
|
|
134
|
-
const methodBypass = options.classMethod === void 0 ? void 0 : Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, mycontroller, options.classMethod);
|
|
135
|
-
const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
|
|
136
|
-
if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
|
|
137
|
-
}
|
|
157
|
+
applyDeclaredRouteOptions(mycontroller, options, options.classMethod);
|
|
138
158
|
if (options.path === "*") {
|
|
139
159
|
hasMagic = {
|
|
140
160
|
options,
|
|
@@ -153,6 +173,72 @@ function controller(prefix) {
|
|
|
153
173
|
function route(name, options) {
|
|
154
174
|
return defineRoute(name, options);
|
|
155
175
|
}
|
|
176
|
+
/** Plus long délai qu'un minuteur Node honore : au-delà, il tombe à 1 ms. */
|
|
177
|
+
const MAX_TIMER_MS = 2147483647;
|
|
178
|
+
/**
|
|
179
|
+
* Valide l'option de route `responseTimeout` À LA DÉCLARATION : un entier de
|
|
180
|
+
* 0 à 2³¹−1. Hors bornes, Node ramènerait le minuteur à 1 ms — chaque requête
|
|
181
|
+
* de la route expirerait aussitôt, sans que rien ne dise pourquoi.
|
|
182
|
+
*
|
|
183
|
+
* @param value - l'option telle que déclarée (absente = celle du serveur).
|
|
184
|
+
* @param className - classe du contrôleur, nommée dans l'erreur.
|
|
185
|
+
* @param method - action, nommée dans l'erreur.
|
|
186
|
+
* @returns la valeur validée, ou `undefined` si l'option est absente.
|
|
187
|
+
* @throws BootConfigurationError qui nomme l'action et la borne.
|
|
188
|
+
*/
|
|
189
|
+
function checkedResponseTimeout(value, className, method) {
|
|
190
|
+
if (value === void 0) return void 0;
|
|
191
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_TIMER_MS) throw new BootConfigurationError(`${className}.${method} : « responseTimeout » entier entre 0 et ${MAX_TIMER_MS} ms attendu (reçu ${String(value)}).`);
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
/** Les bornes d'envoi chiffrées qu'une route peut régler pour elle-même. */
|
|
195
|
+
const UPLOAD_LIMIT_KEYS = /* @__PURE__ */ new Set([
|
|
196
|
+
"maxFileSize",
|
|
197
|
+
"maxTotalFileSize",
|
|
198
|
+
"maxFiles",
|
|
199
|
+
"maxFields",
|
|
200
|
+
"maxFieldsSize"
|
|
201
|
+
]);
|
|
202
|
+
function isUploadLimitKey(key) {
|
|
203
|
+
return UPLOAD_LIMIT_KEYS.has(key);
|
|
204
|
+
}
|
|
205
|
+
/** Les empreintes que le flux d'envoi sait calculer. */
|
|
206
|
+
function isUploadHash(value) {
|
|
207
|
+
return value === false || value === "sha1" || value === "md5" || value === "sha256";
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Valide l'option de route `upload` À LA DÉCLARATION : des entiers ≥ 1 pour
|
|
211
|
+
* les bornes, une empreinte connue, aucune clé inconnue. Une faute de frappe
|
|
212
|
+
* (`maxFilesize`) laisserait sinon la route sur la borne de l'application —
|
|
213
|
+
* 500 Mo par défaut — sans que rien ne le dise.
|
|
214
|
+
*
|
|
215
|
+
* @param value - l'option telle que déclarée (absente = celles de la config).
|
|
216
|
+
* @param bodyStream - la route laisse son corps en flux : ses bornes ne
|
|
217
|
+
* s'appliqueraient jamais, la déclaration est contradictoire.
|
|
218
|
+
* @param className - classe du contrôleur, nommée dans l'erreur.
|
|
219
|
+
* @param method - action, nommée dans l'erreur.
|
|
220
|
+
* @returns les bornes validées, figées, sans clé `undefined` (la fusion sur
|
|
221
|
+
* la configuration ne doit rien écraser par du vide), ou `undefined`.
|
|
222
|
+
* @throws BootConfigurationError qui nomme l'action et la clé fautive.
|
|
223
|
+
*/
|
|
224
|
+
function checkedUpload(value, bodyStream, className, method) {
|
|
225
|
+
if (value === void 0) return void 0;
|
|
226
|
+
const where = `${className}.${method} : « upload »`;
|
|
227
|
+
if (bodyStream) throw new BootConfigurationError(`${where} et « bodyStream: true » s'excluent — un corps laissé en flux n'est jamais lu par le pipeline, ses bornes ne s'appliqueraient pas.`);
|
|
228
|
+
const checked = {};
|
|
229
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
230
|
+
if (entry === void 0) continue;
|
|
231
|
+
if (key === "hashAlgorithm") {
|
|
232
|
+
if (!isUploadHash(entry)) throw new BootConfigurationError(`${where} : « hashAlgorithm » vaut false, "sha1", "md5" ou "sha256" (reçu ${String(entry)}).`);
|
|
233
|
+
checked.hashAlgorithm = entry;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!isUploadLimitKey(key)) throw new BootConfigurationError(`${where} : clé inconnue « ${key} » — une route règle ${[...UPLOAD_LIMIT_KEYS].join(", ")} et hashAlgorithm ; le dossier et l'encodage restent ceux de la configuration.`);
|
|
237
|
+
if (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 1) throw new BootConfigurationError(`${where} : « ${key} » entier ≥ 1 attendu (reçu ${String(entry)}).`);
|
|
238
|
+
checked[key] = entry;
|
|
239
|
+
}
|
|
240
|
+
return Object.freeze(checked);
|
|
241
|
+
}
|
|
156
242
|
/** L'implémentation de {@link route}, sans les surcharges de typage. */
|
|
157
243
|
function defineRoute(name, options) {
|
|
158
244
|
return function(target, propertyKey, descriptor) {
|
|
@@ -190,7 +276,9 @@ function defineRoute(name, options) {
|
|
|
190
276
|
defaults: options.defaults,
|
|
191
277
|
requirements: options.requirements,
|
|
192
278
|
bypassFirewall: options.bypassFirewall,
|
|
193
|
-
fallback: options.fallback
|
|
279
|
+
fallback: options.fallback,
|
|
280
|
+
responseTimeout: checkedResponseTimeout(options.responseTimeout, className, propertyKey),
|
|
281
|
+
upload: checkedUpload(options.upload, options.bodyStream === true, className, propertyKey)
|
|
194
282
|
};
|
|
195
283
|
Reflect.defineMetadata(metadataKey, metadata, target.constructor);
|
|
196
284
|
return descriptor;
|
|
@@ -1176,4 +1264,4 @@ function computeAuthenticationRequirement(ctor, method) {
|
|
|
1176
1264
|
amr: Object.freeze([.../* @__PURE__ */ new Set([...onClass.amr, ...onMethod.amr])])
|
|
1177
1265
|
});
|
|
1178
1266
|
}
|
|
1179
|
-
export { All, Anonymous, BYPASS_FIREWALL_CLASS_METADATA, BYPASS_FIREWALL_METHOD_METADATA, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, DOMAIN_CLASS_METADATA, DOMAIN_METHOD_METADATA, Delete, Domain, Get, HEADERS_METADATA, HTTP_CODE_METADATA, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, PARAM_ARGS_METADATA, Param, Patch, Post, Put, Query, REDIRECT_METADATA, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, assertRouteSchemaDeclaration, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, getRouteSchema, readInvocationBody, readRouteInput, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
|
|
1267
|
+
export { All, Anonymous, BYPASS_FIREWALL_CLASS_METADATA, BYPASS_FIREWALL_METHOD_METADATA, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, DOMAIN_CLASS_METADATA, DOMAIN_METHOD_METADATA, Delete, Domain, Get, HEADERS_METADATA, HTTP_CODE_METADATA, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, PARAM_ARGS_METADATA, Param, Patch, Post, Put, Query, REDIRECT_METADATA, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, applyDeclaredRouteOptions, assertRouteSchemaDeclaration, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, getReservedActionNames, getRouteSchema, readInvocationBody, readRouteInput, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
|