@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
|
@@ -3,9 +3,13 @@ import { validateRouteInput } from "./routeSchema.js";
|
|
|
3
3
|
import { buildParamArgs, computeActionMeta, readInvocationBody, readRouteInput, resolveActionMeta, resolveSessionIntent } from "../decorators/routerDecorators.js";
|
|
4
4
|
import { computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity } from "./idempotency.js";
|
|
5
5
|
import { authenticationShortfall, stepUpChallenge } from "./requireAuthentication.js";
|
|
6
|
-
import { RequestContext, finallyMaybe, identityHint, isArray, isPlainObject, isPromise, nodefonyError, thenMaybe
|
|
7
|
-
import { Http2Response, HttpResponse, WebsocketResponse } from "@nodefony/http";
|
|
6
|
+
import { RequestContext, finallyMaybe, identityHint, isArray, isPlainObject, isPromise, nodefonyError, thenMaybe } from "nodefony";
|
|
7
|
+
import { Http2Response, HttpResponse, WebsocketResponse, isContentlessStatus } from "@nodefony/http";
|
|
8
|
+
import { Buffer } from "node:buffer";
|
|
9
|
+
import { Readable } from "node:stream";
|
|
8
10
|
// nodefony/src/Resolver.ts
|
|
11
|
+
/** Une annulation qui échoue (flux déjà fermé) n'a plus personne à prévenir. */
|
|
12
|
+
const IGNORE_CANCEL_ERROR = () => void 0;
|
|
9
13
|
/**
|
|
10
14
|
* Résout une route vers son couple controller/action et exécute l'action —
|
|
11
15
|
* UN Resolver est alloué par requête HTTP (et par connexion WS, réutilisé
|
|
@@ -56,12 +60,12 @@ var Resolver = class {
|
|
|
56
60
|
*/
|
|
57
61
|
queryOverride = null;
|
|
58
62
|
/**
|
|
59
|
-
* Méthode HTTP **logique** d'une invocation par le pont WS-RPC `api.request
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* sur `context.method`.
|
|
63
|
+
* Méthode HTTP **logique** d'une invocation par le pont WS-RPC `api.request`,
|
|
64
|
+
* GET compris. Posée par `Router.resolve(ctx, cleanPath, methodOverride)` →
|
|
65
|
+
* consommée par `match()` pour désambiguïser, sur le transport WEBSOCKET
|
|
66
|
+
* unique, la route logique visée (règle `bridgeServesVerb` du cœur, cf
|
|
67
|
+
* `Route.matchRequirements`). `null` (requête HTTP, ouverture de socket) =
|
|
68
|
+
* match sur `context.method`.
|
|
65
69
|
*/
|
|
66
70
|
methodOverride = null;
|
|
67
71
|
/**
|
|
@@ -396,7 +400,7 @@ var Resolver = class {
|
|
|
396
400
|
* (`returnController`) — le chemin du **pont `api.request`** (WS) : la valeur
|
|
397
401
|
* nue est enveloppée `{id, result}` par le peer, jamais rendue sur le
|
|
398
402
|
* transport. La méthode HTTP logique d'une mutation du pont voyage dans
|
|
399
|
-
* {@link methodOverride} ; sans porte ici, un rejeu `socket.
|
|
403
|
+
* {@link methodOverride} ; sans porte ici, un rejeu `socket.post` (socket
|
|
400
404
|
* qui reconnecte) ré-exécuterait la mutation (doublon — vécu au banc duplex).
|
|
401
405
|
*
|
|
402
406
|
* @returns `{ result }` — la valeur retournée par l'action (ou la réponse
|
|
@@ -431,6 +435,8 @@ var Resolver = class {
|
|
|
431
435
|
async _callWithIdempotency(meta, data, reload = false) {
|
|
432
436
|
const context = this.context;
|
|
433
437
|
if (!isMutationMethod(this.methodOverride ?? context.method)) return this.executeAction(data, reload, meta);
|
|
438
|
+
if (this.preAuthorized) this.preAuthorized = false;
|
|
439
|
+
else await this._enforceActionSecurity(meta);
|
|
434
440
|
const als = RequestContext.get();
|
|
435
441
|
const httpReq = context.request;
|
|
436
442
|
const params = this._zipRouteParams();
|
|
@@ -463,6 +469,7 @@ var Resolver = class {
|
|
|
463
469
|
redirectMeta: void 0
|
|
464
470
|
};
|
|
465
471
|
}
|
|
472
|
+
this.preAuthorized = true;
|
|
466
473
|
if (verdict.kind === "execute") return this.executeAction(data, reload, meta);
|
|
467
474
|
try {
|
|
468
475
|
const { result, redirectMeta } = await this.executeAction(data, reload, meta);
|
|
@@ -631,7 +638,15 @@ var Resolver = class {
|
|
|
631
638
|
/**
|
|
632
639
|
* Rend la valeur d'une action sur le transport, selon sa nature : promesse
|
|
633
640
|
* (démêlée puis re-rendue), texte, Buffer, réponse déjà construite, scalaire
|
|
634
|
-
* ou objet (auto-JSON), rien (l'action envoie elle-même)
|
|
641
|
+
* ou objet (auto-JSON), rien (l'action envoie elle-même) — et, en HTTP, une
|
|
642
|
+
* `Response` Web standard (`fetch()`, `new Response(…)`, `Response.json(…)`)
|
|
643
|
+
* ou un flux d'octets nu (`Readable` Node, `ReadableStream` Web), envoyés EN
|
|
644
|
+
* FLUX par `HttpContext.openWebResponse` / `HttpContext.openStream`.
|
|
645
|
+
*
|
|
646
|
+
* Ces deux-là sont reconnus APRÈS l'objet simple et le tableau : l'auto-JSON,
|
|
647
|
+
* le cas courant, ne paie aucun test de plus. Une réponse déjà partie
|
|
648
|
+
* (`sended`) n'est jamais réécrite ; une `Response` renvoyée en plus voit
|
|
649
|
+
* alors son corps annulé, pour ne pas retenir l'amont.
|
|
635
650
|
*
|
|
636
651
|
* Pas `async` : l'envelopper dans une promesse de plus ne changeait rien au
|
|
637
652
|
* rendu, et coûtait une suspension à chaque requête (#505). La valeur n'est
|
|
@@ -643,45 +658,70 @@ var Resolver = class {
|
|
|
643
658
|
* @throws de façon SYNCHRONE ce que lève un envoi synchrone.
|
|
644
659
|
*/
|
|
645
660
|
returnController(result) {
|
|
646
|
-
|
|
647
|
-
switch (
|
|
648
|
-
case
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
case
|
|
652
|
-
case
|
|
653
|
-
case
|
|
654
|
-
case result instanceof WebsocketResponse: return result;
|
|
655
|
-
case type === "buffer":
|
|
656
|
-
if (this.context.sended) return;
|
|
657
|
-
return this.context.send(result);
|
|
658
|
-
case type === "number":
|
|
659
|
-
case type === "boolean":
|
|
661
|
+
if (isPromise(result)) return result.then((myresult) => this.returnController(myresult));
|
|
662
|
+
switch (typeof result) {
|
|
663
|
+
case "object":
|
|
664
|
+
if (result === null) break;
|
|
665
|
+
return this.returnObject(result);
|
|
666
|
+
case "string": return this.context.send(result);
|
|
667
|
+
case "number":
|
|
668
|
+
case "boolean":
|
|
660
669
|
if (this.context.sended) return;
|
|
661
670
|
this.context.setContextJson();
|
|
662
671
|
return this.context.render(result);
|
|
663
|
-
case type === "array":
|
|
664
|
-
case type === "object":
|
|
665
|
-
if (this.context.sended) return;
|
|
666
|
-
if (isPlainObject(result) || isArray(result)) {
|
|
667
|
-
this.context.setContextJson();
|
|
668
|
-
return this.context.render(result);
|
|
669
|
-
}
|
|
670
|
-
switch (this.context.type) {
|
|
671
|
-
case "http":
|
|
672
|
-
this.context.waitAsync = true;
|
|
673
|
-
this.unrendered = `une instance de ${(typeof result === "object" && result !== null ? result.constructor.name : void 0) ?? "classe"}`;
|
|
674
|
-
}
|
|
675
|
-
return;
|
|
676
|
-
default: switch (this.context.type) {
|
|
677
|
-
case "http":
|
|
678
|
-
if (this.context.sended) return;
|
|
679
|
-
if (this.context.isRedirect) return this.context.send();
|
|
680
|
-
if (NO_BODY_STATUS.has(this.getResponseStatus())) return this.context.send();
|
|
681
|
-
this.context.waitAsync = true;
|
|
682
|
-
this.unrendered = result === null ? "null" : typeof result;
|
|
683
|
-
}
|
|
684
672
|
}
|
|
673
|
+
return this.returnNothing(result);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Aiguille une valeur OBJET rendue par l'action — dans l'ordre de fréquence :
|
|
677
|
+
* le tableau et l'objet simple (auto-JSON) d'abord, les instances rares
|
|
678
|
+
* ensuite. Partie de {@link Resolver.returnController}.
|
|
679
|
+
*
|
|
680
|
+
* @param result - un objet non nul.
|
|
681
|
+
* @returns ce que rend l'envoi, ou `undefined` quand il n'y a rien à envoyer.
|
|
682
|
+
*/
|
|
683
|
+
returnObject(result) {
|
|
684
|
+
const context = this.context;
|
|
685
|
+
if (isArray(result) || isPlainObject(result)) {
|
|
686
|
+
if (context.sended) return;
|
|
687
|
+
context.setContextJson();
|
|
688
|
+
return context.render(result);
|
|
689
|
+
}
|
|
690
|
+
if (Buffer.isBuffer(result)) {
|
|
691
|
+
if (context.sended) return;
|
|
692
|
+
return context.send(result);
|
|
693
|
+
}
|
|
694
|
+
if (result instanceof String) return context.send(result.valueOf());
|
|
695
|
+
if (result instanceof HttpResponse || result instanceof Http2Response || result instanceof WebsocketResponse) return result;
|
|
696
|
+
if (context.sended) {
|
|
697
|
+
if (result instanceof Response) result.body?.cancel().catch(IGNORE_CANCEL_ERROR);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (result instanceof Date || result instanceof RegExp || result instanceof Error) return this.returnNothing(result);
|
|
701
|
+
if (context.type === "http") {
|
|
702
|
+
if (result instanceof Response) return context.openWebResponse(result);
|
|
703
|
+
if (result instanceof Readable || result instanceof ReadableStream) return context.openStream(result);
|
|
704
|
+
context.waitAsync = true;
|
|
705
|
+
this.unrendered = `une instance de ${result.constructor.name || "classe"}`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Ce qui arrive quand l'action ne rend RIEN d'envoyable (`undefined`,
|
|
710
|
+
* `null`, une fonction…) : en HTTP, la redirection posée ou le statut sans
|
|
711
|
+
* corps part ; sinon la requête est marquée non rendue (`waitAsync`). Partie
|
|
712
|
+
* de {@link Resolver.returnController}.
|
|
713
|
+
*
|
|
714
|
+
* @param result - la valeur rendue par l'action.
|
|
715
|
+
* @returns l'envoi d'une redirection ou d'un statut sans corps, sinon `undefined`.
|
|
716
|
+
*/
|
|
717
|
+
returnNothing(result) {
|
|
718
|
+
if (this.context.type !== "http") return;
|
|
719
|
+
const context = this.context;
|
|
720
|
+
if (context.sended) return;
|
|
721
|
+
if (context.isRedirect) return context.send();
|
|
722
|
+
if (isContentlessStatus(this.getResponseStatus())) return context.send();
|
|
723
|
+
context.waitAsync = true;
|
|
724
|
+
this.unrendered = result === null ? "null" : typeof result;
|
|
685
725
|
}
|
|
686
726
|
/** Statut courant de la réponse (0 si le transport n'en porte pas). */
|
|
687
727
|
getResponseStatus() {
|
|
@@ -713,10 +753,4 @@ var Resolver = class {
|
|
|
713
753
|
throw new nodefonyError(identityHint(this.context.kernel) === null ? "Authentication required" : `Authentication required — ${cause}. Reconnectez-vous (\`@RequireAuthentication\` de la route).`, 401);
|
|
714
754
|
}
|
|
715
755
|
};
|
|
716
|
-
/** Statuts dont la réponse n'a, par définition, pas de corps (RFC 9110). */
|
|
717
|
-
const NO_BODY_STATUS = /* @__PURE__ */ new Set([
|
|
718
|
-
204,
|
|
719
|
-
205,
|
|
720
|
-
304
|
|
721
|
-
]);
|
|
722
756
|
export { Resolver as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeRegExp, stripTrailingSlashes, typeOf } from "nodefony";
|
|
1
|
+
import { bridgeServesVerb, escapeRegExp, stripTrailingSlashes, typeOf } from "nodefony";
|
|
2
2
|
import { HttpError, compileDomainPatterns, isDomainAllowed } from "@nodefony/http";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
// nodefony/src/Route.ts
|
|
@@ -125,6 +125,24 @@ function withDeclaredMethod(requirements, method) {
|
|
|
125
125
|
function isSecureScheme(scheme) {
|
|
126
126
|
return scheme === "https" || scheme === "wss";
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Un refus de route (méthode, domaine, schéma, protocole) SANS pile : c'est un
|
|
130
|
+
* verdict que le routeur RATTRAPE pour essayer la route suivante — deux routes
|
|
131
|
+
* sur un même chemin (`GET` et `POST /x`) le lèvent à chaque requête qui
|
|
132
|
+
* croise la voisine. La pile, toujours la même, coûterait ~3,6 µs par refus
|
|
133
|
+
* (`micro-error-path.mjs`) pour ne rien dire de la requête.
|
|
134
|
+
*
|
|
135
|
+
* @param message - le motif lisible.
|
|
136
|
+
* @param code - statut HTTP (ou code de fermeture WebSocket).
|
|
137
|
+
* @param type - la famille du refus (`method`, `domain`, `scheme`, `protocol`).
|
|
138
|
+
* @param allow - les méthodes servies, pour l'en-tête `Allow` d'un 405.
|
|
139
|
+
*/
|
|
140
|
+
function routeRefusal(message, code, type, allow) {
|
|
141
|
+
const error = HttpError.withoutStack(message, code);
|
|
142
|
+
error.type = type;
|
|
143
|
+
if (allow !== void 0) error.allow = allow;
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
128
146
|
var Route = class Route {
|
|
129
147
|
name;
|
|
130
148
|
path;
|
|
@@ -172,6 +190,13 @@ var Route = class Route {
|
|
|
172
190
|
/** Route de repli, résolue après toutes les autres. Cf {@link RouteOptions.fallback}. */
|
|
173
191
|
fallback = false;
|
|
174
192
|
/**
|
|
193
|
+
* Délai de réponse PROPRE à la route (ms), `null` = celui du serveur.
|
|
194
|
+
* Cf {@link RouteOptions.responseTimeout}.
|
|
195
|
+
*/
|
|
196
|
+
responseTimeout = null;
|
|
197
|
+
/** Bornes d'envoi propres à la route. Cf {@link RouteOptions.upload}. */
|
|
198
|
+
upload = null;
|
|
199
|
+
/**
|
|
175
200
|
* P2.9 — Cache mémoïsé : l'action attend-elle le **flux brut** du body
|
|
176
201
|
* (`@Body({ stream:true })`) ? `undefined` = pas encore calculé (résolu au 1er
|
|
177
202
|
* `routeExpectsBodyStream(route)` via lecture `Reflect` des `ParamMeta`, O(1)
|
|
@@ -179,6 +204,13 @@ var Route = class Route {
|
|
|
179
204
|
*/
|
|
180
205
|
bodyStream;
|
|
181
206
|
/**
|
|
207
|
+
* Route servie par un point d'entrée gRPC — posé au démarrage par
|
|
208
|
+
* `markGrpcRoute` (`@nodefony/http/grpc`), jamais à la main.
|
|
209
|
+
*
|
|
210
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
211
|
+
*/
|
|
212
|
+
grpc;
|
|
213
|
+
/**
|
|
182
214
|
* P5 — Metadata d'action figées (`@HttpCode`/`@Header`/`@Redirect`/params/
|
|
183
215
|
* session), memo au 1er hit comme {@link bodyStream} : `undefined` = pas
|
|
184
216
|
* encore résolu (via `resolveActionMeta`, 1 lecture Reflect par route, O(1)
|
|
@@ -208,6 +240,8 @@ var Route = class Route {
|
|
|
208
240
|
this.bypassFirewall = obj.bypassFirewall ?? false;
|
|
209
241
|
this.areaRoleExempt = obj.areaRoleExempt ?? false;
|
|
210
242
|
this.fallback = obj.fallback ?? false;
|
|
243
|
+
this.responseTimeout = obj.responseTimeout ?? null;
|
|
244
|
+
this.upload = obj.upload ?? null;
|
|
211
245
|
this.compile();
|
|
212
246
|
}
|
|
213
247
|
this.generateId();
|
|
@@ -409,7 +443,9 @@ var Route = class Route {
|
|
|
409
443
|
variables: this.variables,
|
|
410
444
|
bypassFirewall: this.bypassFirewall,
|
|
411
445
|
areaRoleExempt: this.areaRoleExempt,
|
|
412
|
-
fallback: this.fallback
|
|
446
|
+
fallback: this.fallback,
|
|
447
|
+
responseTimeout: this.responseTimeout,
|
|
448
|
+
upload: this.upload
|
|
413
449
|
};
|
|
414
450
|
}
|
|
415
451
|
setDefaults(arg) {
|
|
@@ -449,10 +485,7 @@ var Route = class Route {
|
|
|
449
485
|
matchHostname(context) {
|
|
450
486
|
if (this.hostRegexp) {
|
|
451
487
|
if (isDomainAllowed(this.hostRegexp, context.domain)) return true;
|
|
452
|
-
|
|
453
|
-
error.code = 403;
|
|
454
|
-
error.type = "domain";
|
|
455
|
-
throw error;
|
|
488
|
+
throw routeRefusal(`Domain ${context.domain} Forbidden for this route`, 403, "domain");
|
|
456
489
|
}
|
|
457
490
|
return true;
|
|
458
491
|
}
|
|
@@ -489,42 +522,19 @@ var Route = class Route {
|
|
|
489
522
|
return Object.keys(this.requirements).length;
|
|
490
523
|
}
|
|
491
524
|
matchRequirements(context, methodOverride) {
|
|
492
|
-
if (this.requirements.methods === void 0 && (methodOverride !== void 0 || context.method === "WEBSOCKET")) {
|
|
493
|
-
const error = new HttpError(`Method ${methodOverride ?? "WEBSOCKET"} Not Allowed`);
|
|
494
|
-
error.code = 405;
|
|
495
|
-
error.type = "method";
|
|
496
|
-
error.allow = "";
|
|
497
|
-
throw error;
|
|
498
|
-
}
|
|
525
|
+
if (this.requirements.methods === void 0 && (methodOverride !== void 0 || context.method === "WEBSOCKET")) throw routeRefusal(`Method ${methodOverride ?? "WEBSOCKET"} Not Allowed`, 405, "method", "");
|
|
499
526
|
if (this.hasRequirements()) for (const i in this.requirements) switch (i) {
|
|
500
527
|
case "methods":
|
|
501
528
|
if (!this.methodsSet) throw new Error(`Bad config route method : ${String(this.requirements.methods)}`);
|
|
502
529
|
if (methodOverride !== void 0) {
|
|
503
|
-
if (!this.methodsSet
|
|
504
|
-
const error = new HttpError(`Method ${methodOverride} Not Allowed`);
|
|
505
|
-
error.code = 405;
|
|
506
|
-
error.type = "method";
|
|
507
|
-
error.allow = this.methodsAllow ?? "";
|
|
508
|
-
throw error;
|
|
509
|
-
}
|
|
530
|
+
if (!bridgeServesVerb(this.methodsSet, methodOverride)) throw routeRefusal(`Method ${methodOverride} Not Allowed`, 405, "method", this.methodsAllow ?? "");
|
|
510
531
|
break;
|
|
511
532
|
}
|
|
512
|
-
if (!this.methodsSet.has(context.method)) {
|
|
513
|
-
const error = new HttpError(`Method ${context.method} Not Allowed`);
|
|
514
|
-
error.code = 405;
|
|
515
|
-
error.type = "method";
|
|
516
|
-
error.allow = this.methodsAllow ?? "";
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
533
|
+
if (!this.methodsSet.has(context.method)) throw routeRefusal(`Method ${context.method} Not Allowed`, 405, "method", this.methodsAllow ?? "");
|
|
519
534
|
break;
|
|
520
535
|
case "scheme": {
|
|
521
536
|
const required = this.requirements.scheme;
|
|
522
|
-
if (required !== void 0 && isSecureScheme(required) !== isSecureScheme(context.scheme)) {
|
|
523
|
-
const error = new HttpError(`Scheme ${context.scheme} Forbidden for this route : ${required} required`);
|
|
524
|
-
error.code = 403;
|
|
525
|
-
error.type = "scheme";
|
|
526
|
-
throw error;
|
|
527
|
-
}
|
|
537
|
+
if (required !== void 0 && isSecureScheme(required) !== isSecureScheme(context.scheme)) throw routeRefusal(`Scheme ${context.scheme} Forbidden for this route : ${required} required`, 403, "scheme");
|
|
528
538
|
break;
|
|
529
539
|
}
|
|
530
540
|
case "domain": break;
|
|
@@ -532,18 +542,8 @@ var Route = class Route {
|
|
|
532
542
|
const requirement = this.requirements[i];
|
|
533
543
|
if (!requirement) return true;
|
|
534
544
|
if (typeof requirement === "string") {
|
|
535
|
-
if (context.acceptedProtocol !== requirement) {
|
|
536
|
-
|
|
537
|
-
error.code = 1002;
|
|
538
|
-
error.type = "protocol";
|
|
539
|
-
throw error;
|
|
540
|
-
}
|
|
541
|
-
} else {
|
|
542
|
-
const error = new HttpError(`Protocol ${context.acceptedProtocol} Not Supported`);
|
|
543
|
-
error.code = 1002;
|
|
544
|
-
error.type = "protocol";
|
|
545
|
-
throw error;
|
|
546
|
-
}
|
|
545
|
+
if (context.acceptedProtocol !== requirement) throw routeRefusal(`Protocol ${context.acceptedProtocol} Not Supported`, 1002, "protocol");
|
|
546
|
+
} else throw routeRefusal(`Protocol ${context.acceptedProtocol} Not Supported`, 1002, "protocol");
|
|
547
547
|
}
|
|
548
548
|
}
|
|
549
549
|
return true;
|
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
import Controller from "./Controller.js";
|
|
2
|
+
import { SESSION_AUTH_CHALLENGE } from "nodefony";
|
|
2
3
|
// nodefony/src/SecurityApiController.ts
|
|
3
4
|
const NO_STORE = { "cache-control": "no-store" };
|
|
4
5
|
/**
|
|
6
|
+
* En-têtes d'un 401 : ceux reçus, plus le défi par défaut
|
|
7
|
+
* ({@link SESSION_AUTH_CHALLENGE}) s'ils n'en portent aucun — RFC 9110 §15.5.2
|
|
8
|
+
* exige un `WWW-Authenticate` sur TOUT 401. Un défi posé par l'appelant (un
|
|
9
|
+
* `Bearer` de ressource protégée, par exemple) garde la main.
|
|
10
|
+
*
|
|
11
|
+
* Chemin froid : n'est appelé que pour un 401.
|
|
12
|
+
*
|
|
13
|
+
* @param headers - les en-têtes déjà prévus pour la réponse.
|
|
14
|
+
* @returns les mêmes en-têtes, défi compris.
|
|
15
|
+
*/
|
|
16
|
+
function withAuthChallenge(headers) {
|
|
17
|
+
if (headers !== void 0) {
|
|
18
|
+
for (const name in headers) if (name.toLowerCase() === "www-authenticate") return { ...headers };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...headers,
|
|
22
|
+
"www-authenticate": SESSION_AUTH_CHALLENGE
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
5
26
|
* Base des contrôleurs d'authentification (`/nodefony/security/api/*`) : toute
|
|
6
|
-
* réponse JSON part en `Cache-Control: no-store
|
|
27
|
+
* réponse JSON part en `Cache-Control: no-store`, et tout 401 porte un défi.
|
|
7
28
|
*
|
|
8
29
|
* Ces réponses portent une identité, un jeton, un secret affiché une seule
|
|
9
30
|
* fois (clé API, secret TOTP, codes de récupération) ou leur refus. Un cookie
|
|
10
31
|
* ou un en-tête dans la requête n'empêche pas un cache partagé de STOCKER la
|
|
11
|
-
* réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
32
|
+
* réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton. Un 401
|
|
33
|
+
* reçoit le défi par défaut ({@link withAuthChallenge}, RFC 9110 §15.5.2). Ces
|
|
34
|
+
* règles vivent ici, une fois : une action ajoutée à un contrôleur héritier ne
|
|
35
|
+
* peut pas les oublier. Un en-tête passé par l'action garde la priorité.
|
|
14
36
|
*/
|
|
15
37
|
var SecurityApiController = class extends Controller {
|
|
16
38
|
renderJson(obj, status, headers) {
|
|
17
|
-
|
|
39
|
+
const merged = {
|
|
18
40
|
...NO_STORE,
|
|
19
41
|
...headers
|
|
20
|
-
}
|
|
42
|
+
};
|
|
43
|
+
return super.renderJson(obj, status, Number(status) === 401 ? withAuthChallenge(merged) : merged);
|
|
21
44
|
}
|
|
22
45
|
};
|
|
23
|
-
export { SecurityApiController as default };
|
|
46
|
+
export { SecurityApiController as default, withAuthChallenge };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// nodefony/src/conditionalRequest.ts
|
|
2
|
+
/** Une `entity-tag` (RFC 9110 §8.8.3) : `W/` facultatif, puis une chaîne entre guillemets. */
|
|
3
|
+
const ENTITY_TAG = /(?:W\/)?"[^"]*"/g;
|
|
4
|
+
/**
|
|
5
|
+
* Validateurs d'un fichier, tirés de ses métadonnées sans le lire.
|
|
6
|
+
*
|
|
7
|
+
* L'`ETag` est FORTE : taille et date de modification à la microseconde, deux
|
|
8
|
+
* grandeurs qu'une écriture change. C'est la forme que posent les serveurs de
|
|
9
|
+
* fichiers courants, et la seule qui autorise une reprise (§13.1.5).
|
|
10
|
+
*
|
|
11
|
+
* @param stats - taille et date de modification du fichier
|
|
12
|
+
* @param now - instant de la réponse (ms)
|
|
13
|
+
* @returns les validateurs du fichier
|
|
14
|
+
*/
|
|
15
|
+
function fileValidators(stats, now) {
|
|
16
|
+
const micros = Math.floor(stats.mtimeMs * 1e3);
|
|
17
|
+
const seconds = Math.floor(Math.min(stats.mtimeMs, now) / 1e3) * 1e3;
|
|
18
|
+
return {
|
|
19
|
+
etag: `"${stats.size.toString(16)}-${micros.toString(16)}"`,
|
|
20
|
+
lastModified: seconds,
|
|
21
|
+
lastModifiedStrong: stats.mtimeMs + 1e3 <= now
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Date HTTP (RFC 9110 §5.6.7) lue en ms, ou `null` si la valeur n'en est pas
|
|
26
|
+
* une — une condition sur une date invalide DOIT être ignorée (§13.1.3, §13.1.4).
|
|
27
|
+
*
|
|
28
|
+
* @param value - valeur brute de l'en-tête
|
|
29
|
+
*/
|
|
30
|
+
function parseHttpDate(value) {
|
|
31
|
+
if (value === void 0) return null;
|
|
32
|
+
const ms = Date.parse(value);
|
|
33
|
+
return Number.isNaN(ms) ? null : ms;
|
|
34
|
+
}
|
|
35
|
+
/** Les `entity-tag` d'une liste, ou `"*"`. */
|
|
36
|
+
function entityTags(value) {
|
|
37
|
+
if (value.trim() === "*") return "*";
|
|
38
|
+
return value.match(ENTITY_TAG) ?? [];
|
|
39
|
+
}
|
|
40
|
+
/** Partie opaque d'une `entity-tag` (sans `W/`). */
|
|
41
|
+
function opaque(tag) {
|
|
42
|
+
return tag.startsWith("W/") ? tag.slice(2) : tag;
|
|
43
|
+
}
|
|
44
|
+
/** Comparaison FORTE (§8.8.3.2) : aucune des deux n'est faible, et elles sont identiques. */
|
|
45
|
+
function strongMatch(a, b) {
|
|
46
|
+
return !a.startsWith("W/") && !b.startsWith("W/") && a === b;
|
|
47
|
+
}
|
|
48
|
+
/** Comparaison FAIBLE (§8.8.3.2) : parties opaques identiques. */
|
|
49
|
+
function weakMatch(a, b) {
|
|
50
|
+
return opaque(a) === opaque(b);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Évalue les préconditions d'une requête sur une représentation qui EXISTE,
|
|
54
|
+
* dans l'ordre de RFC 9110 §13.2.2 — `If-Match`, sinon `If-Unmodified-Since` ;
|
|
55
|
+
* puis `If-None-Match`, sinon `If-Modified-Since`. `Range` et `If-Range`
|
|
56
|
+
* viennent après, s'il faut servir (voir {@link ifRangeHolds}).
|
|
57
|
+
*
|
|
58
|
+
* @param method - méthode de la requête
|
|
59
|
+
* @param headers - en-têtes de la requête
|
|
60
|
+
* @param validators - validateurs de la représentation
|
|
61
|
+
* @returns 200 pour servir, 304 si rien n'a changé (GET et HEAD seulement),
|
|
62
|
+
* 412 si une condition est fausse
|
|
63
|
+
*/
|
|
64
|
+
function evaluatePreconditions(method, headers, validators) {
|
|
65
|
+
const read = method === "GET" || method === "HEAD";
|
|
66
|
+
const ifMatch = headers["if-match"];
|
|
67
|
+
if (ifMatch !== void 0) {
|
|
68
|
+
const tags = entityTags(ifMatch);
|
|
69
|
+
const { etag } = validators;
|
|
70
|
+
if (!(tags === "*" || etag !== null && tags.some((tag) => strongMatch(tag, etag)))) return 412;
|
|
71
|
+
} else {
|
|
72
|
+
const since = parseHttpDate(headers["if-unmodified-since"]);
|
|
73
|
+
if (since !== null && validators.lastModified !== null && validators.lastModified > since) return 412;
|
|
74
|
+
}
|
|
75
|
+
const ifNoneMatch = headers["if-none-match"];
|
|
76
|
+
if (ifNoneMatch !== void 0) {
|
|
77
|
+
const tags = entityTags(ifNoneMatch);
|
|
78
|
+
const { etag } = validators;
|
|
79
|
+
if (tags === "*" || etag !== null && tags.some((tag) => weakMatch(tag, etag))) return read ? 304 : 412;
|
|
80
|
+
} else if (read) {
|
|
81
|
+
const since = parseHttpDate(headers["if-modified-since"]);
|
|
82
|
+
if (since !== null && validators.lastModified !== null && validators.lastModified <= since) return 304;
|
|
83
|
+
}
|
|
84
|
+
return 200;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Une condition `If-Range` tient-elle (RFC 9110 §13.1.5) ? Vraie, la plage
|
|
88
|
+
* demandée est servie (206) ; fausse, `Range` est ignoré et la représentation
|
|
89
|
+
* part entière (200) — un client ne recolle jamais deux versions.
|
|
90
|
+
*
|
|
91
|
+
* Une étiquette tient si elle est FORTEMENT égale ; une date, si elle égale
|
|
92
|
+
* exactement `Last-Modified` ET que celle-ci est un validateur fort.
|
|
93
|
+
*
|
|
94
|
+
* @param value - valeur brute de `If-Range`
|
|
95
|
+
* @param validators - validateurs de la représentation
|
|
96
|
+
*/
|
|
97
|
+
function ifRangeHolds(value, validators) {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (trimmed.startsWith("\"") || trimmed.startsWith("W/")) return validators.etag !== null && strongMatch(trimmed, validators.etag);
|
|
100
|
+
const date = parseHttpDate(trimmed);
|
|
101
|
+
return date !== null && validators.lastModifiedStrong && validators.lastModified === date;
|
|
102
|
+
}
|
|
103
|
+
export { evaluatePreconditions, fileValidators, ifRangeHolds, parseHttpDate };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { basename, dirname, extname, join } from "node:path";
|
|
2
2
|
import { findProjectRoot, isDocVisible, packageManagerCommandLines, packageManagerExecArgs, portableSpawn, readDocScope, readSymbolsGraph, resolvePackageManager } from "nodefony";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
4
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
8
|
// nodefony/src/docsReader.ts
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
/**
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
// nodefony/src/idempotency.ts
|
|
4
4
|
/**
|
|
5
5
|
* Logique **pure et partagée** de la porte d'idempotence des mutations, conforme à
|
|
6
|
-
* `draft-ietf-httpapi-idempotency-key-header-
|
|
6
|
+
* `draft-ietf-httpapi-idempotency-key-header-08`. Source unique de la sémantique
|
|
7
7
|
* (statuts normatifs, scope de clé, fingerprint, bornage) consommée par les DEUX
|
|
8
8
|
* call-sites qui mutent :
|
|
9
9
|
* - le **data plane admin** (`AdminApiController` — réponse `{status,headers,body}`) ;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_LAYOUTS, LOGIN_PAGE_SKINS, LOGIN_PAGE_SKIN_SPECS, LOGIN_PAGE_THEME_KEY, OAUTH2_CANCELLED_REASON, isLoginPageSkin, loginPageSkinFile, oauth2AuthorizePath, safeRedirectPath } from "nodefony";
|
|
1
|
+
import { CSP_NONCE_META_PROPERTY, LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_LAYOUTS, LOGIN_PAGE_SKINS, LOGIN_PAGE_SKIN_SPECS, LOGIN_PAGE_THEME_KEY, OAUTH2_CANCELLED_REASON, isLoginPageSkin, loginPageSkinFile, oauth2AuthorizePath, safeRedirectPath } from "nodefony";
|
|
2
2
|
// nodefony/src/loginPage.ts
|
|
3
3
|
/**
|
|
4
4
|
* Mémoïse une ressource chargée de façon asynchrone : le SUCCÈS est gardé, un
|
|
@@ -158,6 +158,7 @@ const LOGIN_PAGE_TEMPLATE = `<!doctype html>
|
|
|
158
158
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
159
159
|
<meta name="color-scheme" content="dark light" />
|
|
160
160
|
<meta name="robots" content="noindex" />
|
|
161
|
+
<% if (it.nonce) { %><meta property="${CSP_NONCE_META_PROPERTY}" nonce="<%= it.nonce %>"><% } %>
|
|
161
162
|
<title><%= it.title %> — <%= it.brand %></title>
|
|
162
163
|
<link rel="icon" href="<%= it.logoHref %>" type="image/svg+xml" />
|
|
163
164
|
<% if (!it.theme) { %>
|
|
@@ -210,7 +211,7 @@ const LOGIN_PAGE_TEMPLATE = `<!doctype html>
|
|
|
210
211
|
</p>
|
|
211
212
|
<ul class="badges">
|
|
212
213
|
<li class="agentic">Agentic ready</li>
|
|
213
|
-
<li>Node.js
|
|
214
|
+
<li>Node.js 24.16+ ou 26+</li>
|
|
214
215
|
<li>TypeScript strict</li>
|
|
215
216
|
<li>ESM only</li>
|
|
216
217
|
<li>Licence Apache 2.0</li>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { extractActionScopes } from "../decorators/routerDecorators.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import { extractActionScopes } from "../decorators/routerDecorators.js";
|
|
3
3
|
// nodefony/src/scopeCatalog.ts
|
|
4
4
|
/**
|
|
5
5
|
* P6.8 — **Découverte au boot** : scanne TOUTES les routes montées (`Router.routes`)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// nodefony/src/viewNonce.ts
|
|
2
|
+
/**
|
|
3
|
+
* Signature CSP des vues : chaque `<script>` et `<style>` ÉCRIT dans un
|
|
4
|
+
* gabarit reçoit le nonce de la requête, sans geste de l'auteur.
|
|
5
|
+
*
|
|
6
|
+
* La politique par défaut exige un nonce sur les scripts (`'strict-dynamic'`)
|
|
7
|
+
* et sur les styles (ADR-0024 §C1, §C2). Une page écrite vite, par quelqu'un
|
|
8
|
+
* qui ignore tout de la CSP, ne doit pas être bloquée (§C0) : la signature se
|
|
9
|
+
* pose donc au rendu, par le framework.
|
|
10
|
+
*
|
|
11
|
+
* Elle porte sur le TEXTE DU GABARIT, avant que les données y soient insérées :
|
|
12
|
+
* une balise que les données apporteraient (une valeur insérée brute par
|
|
13
|
+
* `<%~ %>`) n'est jamais signée — c'est ce qui garde au nonce sa valeur contre
|
|
14
|
+
* l'injection. Même principe que l'`index.html` d'un front bâti
|
|
15
|
+
* (`prebuiltUi.ts#splitAroundNonce`).
|
|
16
|
+
*/
|
|
17
|
+
/** Variable de vue qui porte le nonce CSP de la requête (`<%= cspNonce %>`). */
|
|
18
|
+
const VIEW_NONCE_LOCAL = "cspNonce";
|
|
19
|
+
/** Ouverture d'une balise `<script` ou `<style` qui ne porte pas de `nonce`. */
|
|
20
|
+
const UNSIGNED_TAG_OPEN = /<(script|style)\b(?![^>]*\bnonce\s*=)/giu;
|
|
21
|
+
/** Une balise Eta (`<% %>`, `<%= %>`, `<%~ %>`) : du code, jamais du balisage. */
|
|
22
|
+
const ETA_TAG = /<%[\s\S]*?%>/gu;
|
|
23
|
+
/** Attribut inséré : le nonce, lu dans les variables de la vue à chaque rendu. */
|
|
24
|
+
const NONCE_ATTRIBUTE = ` nonce="<%= it.${VIEW_NONCE_LOCAL} %>"`;
|
|
25
|
+
/**
|
|
26
|
+
* Ajoute le nonce de la requête à chaque `<script>` et `<style>` littéral du
|
|
27
|
+
* gabarit qui n'en porte pas.
|
|
28
|
+
*
|
|
29
|
+
* Une balise déjà signée par l'auteur est laissée telle quelle ; un `<script`
|
|
30
|
+
* écrit À L'INTÉRIEUR d'une balise Eta (une chaîne de code) n'est jamais
|
|
31
|
+
* touché. Un `<script>` dont un attribut vient des données
|
|
32
|
+
* (`src="<%= it.url %>"`) n'est pas signé non plus : le signer exécuterait
|
|
33
|
+
* l'URL qu'elles apportent. L'auteur qui la tient pour sûre signe lui-même
|
|
34
|
+
* (`nonce="<%= it.cspNonce %>"`). Le rendu doit fournir la variable
|
|
35
|
+
* {@link VIEW_NONCE_LOCAL}.
|
|
36
|
+
*
|
|
37
|
+
* @param source - texte du gabarit Eta, tel que lu sur le disque.
|
|
38
|
+
* @returns le gabarit signé — la MÊME chaîne quand il n'y a rien à signer.
|
|
39
|
+
*/
|
|
40
|
+
function signTemplateTags(source) {
|
|
41
|
+
UNSIGNED_TAG_OPEN.lastIndex = 0;
|
|
42
|
+
if (!UNSIGNED_TAG_OPEN.test(source)) return source;
|
|
43
|
+
let etaRanges = null;
|
|
44
|
+
ETA_TAG.lastIndex = 0;
|
|
45
|
+
for (let m = ETA_TAG.exec(source); m !== null; m = ETA_TAG.exec(source)) (etaRanges ??= []).push(m.index, m.index + m[0].length);
|
|
46
|
+
return source.replace(UNSIGNED_TAG_OPEN, (tag, name, offset) => {
|
|
47
|
+
if (etaRanges !== null) for (let i = 0; i < etaRanges.length; i += 2) {
|
|
48
|
+
const start = etaRanges[i] ?? 0;
|
|
49
|
+
if (start > offset) {
|
|
50
|
+
if (name.toLowerCase() === "script" && start < source.indexOf(">", offset)) return tag;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (offset < (etaRanges[i + 1] ?? 0)) return tag;
|
|
54
|
+
}
|
|
55
|
+
return tag + NONCE_ATTRIBUTE;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export { VIEW_NONCE_LOCAL, signTemplateTags };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import IssuerMetadataController, { mountIssuerMetadataRoutes } from "./nodefony/
|
|
|
16
16
|
import ProtectedResourceMetadataController, { mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources } from "./nodefony/controller/ProtectedResourceMetadataController.js";
|
|
17
17
|
import WebAuthnController, { mountWebAuthnRoutes } from "./nodefony/controller/WebAuthnController.js";
|
|
18
18
|
import OAuth2Controller, { mountOAuth2Routes } from "./nodefony/controller/OAuth2Controller.js";
|
|
19
|
+
import CspReportController, { mountCspReportRoute } from "./nodefony/controller/CspReportController.js";
|
|
19
20
|
import BenchController, { mountBenchRoutes } from "./nodefony/controller/BenchController.js";
|
|
20
21
|
import ApiKeyController, { mountApiKeyRoutes } from "./nodefony/controller/ApiKeyController.js";
|
|
21
22
|
import TotpController, { mountTotpRoutes } from "./nodefony/controller/TotpController.js";
|
|
@@ -24,9 +25,10 @@ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
|
|
|
24
25
|
import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
|
|
25
26
|
import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
|
|
26
27
|
import Eta from "./nodefony/service/Eta.js";
|
|
27
|
-
import { controllers, route, controller, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta } from "./nodefony/decorators/routerDecorators.js";
|
|
28
|
+
import { controllers, route, controller, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta, getRouteSchema } from "./nodefony/decorators/routerDecorators.js";
|
|
28
29
|
export type { IHttpMethodDecorator } from "./nodefony/decorators/routerDecorators.js";
|
|
29
30
|
export { validateRouteInput } from "./nodefony/src/routeSchema.js";
|
|
31
|
+
export { contentDisposition } from "./nodefony/src/Controller.js";
|
|
30
32
|
export type { IRouteSchema, RouteInput, RoutePart, RawRouteInput, RawRouteParams, RawRouteQuery, RawRouteHeaders, } from "./nodefony/src/routeSchema.js";
|
|
31
33
|
declare module "nodefony" {
|
|
32
34
|
interface NodefonyModuleConfig {
|
|
@@ -79,7 +81,7 @@ declare class Framework extends Module<IFrameworkConfig> {
|
|
|
79
81
|
onKernelReady(): Promise<this>;
|
|
80
82
|
}
|
|
81
83
|
export default Framework;
|
|
82
|
-
export { Controller, ResourceController, Route, Router, FRAMEWORK_ROUTER, isFrameworkRouter, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, LoginPageController, mountLoginPage, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta, registerIdempotencyStore, getIdempotencyStoreFactory, listIdempotencyStores, };
|
|
84
|
+
export { Controller, ResourceController, Route, Router, FRAMEWORK_ROUTER, isFrameworkRouter, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, LoginPageController, mountLoginPage, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, CspReportController, mountCspReportRoute, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta, getRouteSchema, registerIdempotencyStore, getIdempotencyStoreFactory, listIdempotencyStores, };
|
|
83
85
|
export { frameworkConfigSchema } from "./nodefony/config/config.js";
|
|
84
86
|
export type { IdempotencyStoreFactory, IIdempotencyStoreFactoryContext, } from "./nodefony/src/idempotencyStoreRegistry.js";
|
|
85
87
|
export type { SecurityClause, SecurityRequirement, IAuthenticationRequirement, IRequireAuthenticationOptions, CspDirectives, IParamArgSource, ParamMeta, } from "./nodefony/decorators/routerDecorators.js";
|