@nodefony/framework 10.0.0-alpha.3 → 10.0.0-alpha.5
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/LICENSE +201 -543
- package/README.md +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +5 -5
- package/dist/nodefony/controller/OAuth2Controller.js +13 -4
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +2 -2
- package/dist/nodefony/service/router.js +2 -2
- package/dist/nodefony/src/KernelAdminApi.js +58 -1
- package/dist/nodefony/src/Resolver.js +20 -3
- package/dist/types/index.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +2 -2
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +2 -2
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +18 -3
- package/dist/types/nodefony/src/KernelAdminApi.d.ts +32 -0
- package/dist/types/nodefony/src/Resolver.d.ts +14 -0
- package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +2 -2
- package/docs/controller.md +25 -25
- package/docs/decorateurs.md +5 -5
- package/docs/idempotence.md +1 -1
- package/docs/routing.md +2 -2
- package/docs/templates.md +9 -9
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.149.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);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.149.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import Route from "./nodefony/src/Route.js";
|
|
|
7
7
|
import Controller from "./nodefony/src/Controller.js";
|
|
8
8
|
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, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, controller, controllers, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
9
9
|
import Resolver from "./nodefony/src/Resolver.js";
|
|
10
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
11
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
10
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
|
|
11
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
|
|
12
12
|
import router_default from "./nodefony/service/router.js";
|
|
13
13
|
import ResourceController from "./nodefony/src/ResourceController.js";
|
|
14
14
|
import AdminApiController from "./nodefony/controller/AdminApiController.js";
|
|
@@ -29,7 +29,7 @@ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
|
|
|
29
29
|
import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
|
|
30
30
|
import Eta from "./nodefony/service/Eta.js";
|
|
31
31
|
import path from "node:path";
|
|
32
|
-
import { AUTO_STORE, EMPTY_INFRA, Kernel, Module, readStoreLocation, resolveAutoStore, services } from "nodefony";
|
|
32
|
+
import { AUTO_STORE, EMPTY_INFRA, Kernel, Module, durableStoreRemedy, readStoreLocation, resolveAutoStore, runNeedsExternalServices, services } from "nodefony";
|
|
33
33
|
import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
|
|
34
34
|
import { makeExecutableSchema, mergeSchemas } from "@graphql-tools/schema";
|
|
35
35
|
//#region index.ts
|
|
@@ -91,13 +91,13 @@ let Framework = class Framework extends Module {
|
|
|
91
91
|
let name = configured;
|
|
92
92
|
let reason = `store explicitement configuré ("${configured}")`;
|
|
93
93
|
if (name === AUTO_STORE) {
|
|
94
|
-
const auto = resolveAutoStore("ephemeral", this.kernel?.infra ?? EMPTY_INFRA, listIdempotencyBackends());
|
|
94
|
+
const auto = resolveAutoStore("ephemeral", this.kernel?.infra ?? EMPTY_INFRA, listIdempotencyBackends(), "memory", runNeedsExternalServices(this.kernel));
|
|
95
95
|
name = auto.store;
|
|
96
96
|
reason = auto.reason;
|
|
97
97
|
this.log(`idempotency.store "auto" → "${name}" (${auto.reason})`, "INFO");
|
|
98
98
|
}
|
|
99
99
|
if (name === "memory") {
|
|
100
|
-
if (this.kernel?.environment === "production") this.log("idempotency.store \"memory\" en PRODUCTION — déduplication per-pod uniquement : un rejeu routé vers un autre pod n'est pas dédupliqué (double-effet possible).
|
|
100
|
+
if (this.kernel?.environment === "production") this.log("idempotency.store \"memory\" en PRODUCTION — déduplication per-pod uniquement : un rejeu routé vers un autre pod n'est pas dédupliqué (double-effet possible). " + durableStoreRemedy(runNeedsExternalServices(this.kernel)), "WARNING");
|
|
101
101
|
this.kernel?.registerStoreResolution({
|
|
102
102
|
brick: "idempotency",
|
|
103
103
|
nature: "ephemeral",
|
|
@@ -30,13 +30,22 @@ var OAuth2Controller = class extends Controller {
|
|
|
30
30
|
super("OAuth2Controller", context);
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Liste PUBLIQUE des fournisseurs
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Liste PUBLIQUE des fournisseurs à proposer à l'écran de connexion.
|
|
34
|
+
*
|
|
35
|
+
* Rend `{ name, label }` : le libellé vient du serveur, seul à connaître la
|
|
36
|
+
* configuration — un écran ne doit jamais avoir à deviner comment nommer un
|
|
37
|
+
* fournisseur, ni s'autoriser à en masquer un qu'il ne reconnaît pas. Un
|
|
38
|
+
* fournisseur déclaré `hidden` n'y figure pas, mais reste autorisable :
|
|
39
|
+
* masquer n'est pas désactiver. Aucun secret n'est exposé.
|
|
36
40
|
*/
|
|
37
41
|
providers() {
|
|
38
42
|
const svc = this.#service();
|
|
39
|
-
return this.renderJson({ providers:
|
|
43
|
+
if (!svc) return this.renderJson({ providers: [] });
|
|
44
|
+
const providers = svc.listDisplayProviders ? svc.listDisplayProviders() : svc.listProviders().map((name) => ({
|
|
45
|
+
name,
|
|
46
|
+
label: name
|
|
47
|
+
}));
|
|
48
|
+
return this.renderJson({ providers });
|
|
40
49
|
}
|
|
41
50
|
/** Démarre le flux : URL d'autorisation + état anti-replay en session, 302. */
|
|
42
51
|
async authorize(provider) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
|
|
3
3
|
import router_default from "./router.js";
|
|
4
4
|
import AdminApiController from "../controller/AdminApiController.js";
|
|
5
5
|
import { ADMIN_DEFAULT_ROLE, Module, Service, injectable, resolveAdminRole } from "nodefony";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
|
|
3
3
|
import { Module, Service, assertPageQuery, injectable } from "nodefony";
|
|
4
4
|
//#region nodefony/service/IdempotencyStore.ts
|
|
5
5
|
const serviceName = "idempotencyStore";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Route from "../src/Route.js";
|
|
2
2
|
import { routeExpectsBodyStream } from "../decorators/routerDecorators.js";
|
|
3
3
|
import Resolver from "../src/Resolver.js";
|
|
4
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
|
|
5
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
|
|
6
6
|
import { Module, Service, injectable } from "nodefony";
|
|
7
7
|
import { HttpError, isDomainAllowed } from "@nodefony/http";
|
|
8
8
|
//#region nodefony/service/router.ts
|
|
@@ -2,7 +2,7 @@ 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, applyResolvedPath, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides } from "nodefony";
|
|
5
|
+
import { GitService, Syslog, aggregateProvenance, applyResolvedPath, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, readResolvedPath } from "nodefony";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
//#region nodefony/src/KernelAdminApi.ts
|
|
@@ -617,6 +617,63 @@ function createKernelAdminApi(kernel) {
|
|
|
617
617
|
summary: "Aggregated config of all modules (effective values redacted + JSON Schema + per-field provenance) for the global config page",
|
|
618
618
|
handler: async () => ({ modules: buildConfigEntries() })
|
|
619
619
|
},
|
|
620
|
+
{
|
|
621
|
+
path: "config/schema",
|
|
622
|
+
summary: "Assignable config keys of every module (dotted path, type, default, effective value, provenance, description) — the CATALOG, where `config` gives the STATE",
|
|
623
|
+
handler: (request) => {
|
|
624
|
+
const wanted = request.query.module;
|
|
625
|
+
const filter = typeof wanted === "string" ? wanted.toLowerCase() : null;
|
|
626
|
+
const rows = [];
|
|
627
|
+
const entries = buildConfigEntries();
|
|
628
|
+
const matches = (name, key) => name.toLowerCase() === filter || key.toLowerCase() === filter;
|
|
629
|
+
if (filter !== null) {
|
|
630
|
+
const entry = entries.find((e) => matches(e.name, e.key));
|
|
631
|
+
if (!entry || entry.configSchema == null) {
|
|
632
|
+
const modules = kernel.getModules();
|
|
633
|
+
const nameOf = (k) => modules[k].getModuleName?.() ?? k;
|
|
634
|
+
const loadedKey = Object.keys(modules).find((k) => matches(nameOf(k), k));
|
|
635
|
+
if (loadedKey !== void 0) {
|
|
636
|
+
const name = nameOf(loadedKey);
|
|
637
|
+
return {
|
|
638
|
+
status: 404,
|
|
639
|
+
body: {
|
|
640
|
+
error: "Schema not published",
|
|
641
|
+
module: name,
|
|
642
|
+
loaded: true,
|
|
643
|
+
hint: `Le module ${name} est chargé mais ne publie pas son schéma de configuration : ajouter dans son index.ts \`override configSchema(): unknown { return z.toJSONSchema(schema); }\` (schéma Zod de nodefony/config/config.ts). Sans lui, ses clés sont indécouvrables ici.`
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
status: 404,
|
|
649
|
+
body: {
|
|
650
|
+
error: "Unknown module",
|
|
651
|
+
module: wanted,
|
|
652
|
+
available: entries.map((e) => e.name)
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const entry of entries) {
|
|
658
|
+
if (filter !== null && entry.name.toLowerCase() !== filter && entry.key.toLowerCase() !== filter) continue;
|
|
659
|
+
for (const leaf of flattenConfigSchema(entry.configSchema)) {
|
|
660
|
+
const path = leaf.key.split(".");
|
|
661
|
+
const note = SECRET_KEY.test(path[path.length - 1]) && !leaf.note.includes("secret") ? [leaf.note, "secret"].filter(Boolean).join(", ") : leaf.note;
|
|
662
|
+
rows.push({
|
|
663
|
+
key: leaf.key,
|
|
664
|
+
module: entry.name,
|
|
665
|
+
type: leaf.type,
|
|
666
|
+
default: leaf.default,
|
|
667
|
+
effective: readResolvedPath(entry.config, path),
|
|
668
|
+
source: aggregateProvenance(entry.provenance, leaf.key),
|
|
669
|
+
note,
|
|
670
|
+
description: leaf.description
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return rows;
|
|
675
|
+
}
|
|
676
|
+
},
|
|
620
677
|
{
|
|
621
678
|
path: "stores",
|
|
622
679
|
summary: "Runtime persistence stores per brick (resolved store, provenance, available backends) + declared infra",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildParamArgs, computeActionMeta, resolveActionMeta, resolveSessionIntent } from "../decorators/routerDecorators.js";
|
|
2
2
|
import { computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity } from "./idempotency.js";
|
|
3
|
-
import { RequestContext, isArray, isPlainObject, isPromise, nodefonyError, typeOf } from "nodefony";
|
|
3
|
+
import { RequestContext, identityHint, isArray, isPlainObject, isPromise, nodefonyError, typeOf } from "nodefony";
|
|
4
4
|
import { Http2Response, HttpResponse, WebsocketResponse } from "@nodefony/http";
|
|
5
5
|
//#region nodefony/src/Resolver.ts
|
|
6
6
|
/**
|
|
@@ -292,7 +292,7 @@ var Resolver = class {
|
|
|
292
292
|
async _enforceSecurity(req) {
|
|
293
293
|
const authz = this.context.container?.get("authorization");
|
|
294
294
|
const token = RequestContext.get()?.token;
|
|
295
|
-
if (!authz || token === void 0) throw new nodefonyError("
|
|
295
|
+
if (!authz || token === void 0) throw new nodefonyError(this._accessDenied(!authz ? `aucun moteur d'autorisation n'est posé sur cette requête` : "aucune identité n'est résolue sur cette requête — la route est-elle couverte par une zone du firewall ?"), 403);
|
|
296
296
|
const clauses = req.clauses;
|
|
297
297
|
for (let i = 0; i < clauses.length; i++) {
|
|
298
298
|
const clause = clauses[i];
|
|
@@ -303,10 +303,27 @@ var Resolver = class {
|
|
|
303
303
|
ok = true;
|
|
304
304
|
break;
|
|
305
305
|
}
|
|
306
|
-
if (!ok) throw new nodefonyError("
|
|
306
|
+
if (!ok) throw new nodefonyError(this._accessDenied("l'identité de cette requête ne porte pas les droits exigés par la route (`@IsGranted`)"), 403);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
/**
|
|
310
|
+
* Le message d'un refus d'autorisation — augmenté du GESTE en développement.
|
|
311
|
+
*
|
|
312
|
+
* Refuser correctement ne suffit pas : celui qui vient d'écrire une route
|
|
313
|
+
* gardée n'a, sans cela, aucun moyen de l'essayer — mesuré, c'est le premier
|
|
314
|
+
* poste de coût d'un essai réel. La cause et le geste ne franchissent jamais
|
|
315
|
+
* la production ({@link identityHint} rend `null` hors développement) : le
|
|
316
|
+
* message y reste le `Access denied` nu qu'il a toujours été, puisque ces
|
|
317
|
+
* phrases y renseigneraient l'attaquant autant que le développeur.
|
|
318
|
+
*
|
|
319
|
+
* @param cause - ce qui a fait échouer l'autorisation, en clair.
|
|
320
|
+
* @returns le message à porter par l'erreur 403.
|
|
321
|
+
*/
|
|
322
|
+
_accessDenied(cause) {
|
|
323
|
+
const hint = identityHint(this.context.kernel);
|
|
324
|
+
return hint === null ? "Access denied" : `Access denied — ${cause}. ${hint}`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
310
327
|
* Résout un paramètre de route NOMMÉ (`@IsGranted(..., { subject: "id" })`) vers
|
|
311
328
|
* sa valeur, depuis `route.variables` (noms) + `this.variables` (valeurs déjà
|
|
312
329
|
* parsées). 0 alloc (indexOf + accès tableau).
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Kernel, Module } from "nodefony";
|
|
2
|
-
import type {
|
|
2
|
+
import type { IFrameworkConfigInput, IFrameworkConfig } from "./nodefony/config/config.js";
|
|
3
3
|
import { getIdempotencyStoreFactory, registerIdempotencyStore, listIdempotencyStores } from "./nodefony/src/idempotencyStoreRegistry.js";
|
|
4
4
|
import Router from "./nodefony/service/router.js";
|
|
5
5
|
import Route from "./nodefony/src/Route.js";
|
|
@@ -28,10 +28,10 @@ import { mergeSchemas, makeExecutableSchema } from "@graphql-tools/schema";
|
|
|
28
28
|
import { controllers, route, controller, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
29
29
|
declare module "nodefony" {
|
|
30
30
|
interface NodefonyModuleConfig {
|
|
31
|
-
"@nodefony/framework":
|
|
31
|
+
"@nodefony/framework": IFrameworkConfigInput;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
declare class Framework extends Module<
|
|
34
|
+
declare class Framework extends Module<IFrameworkConfig> {
|
|
35
35
|
#private;
|
|
36
36
|
constructor(kernel: Kernel);
|
|
37
37
|
/** JSON Schema de la config framework → data plane admin (config riche Studio). */
|
|
@@ -91,6 +91,6 @@ export type { ControllerScope } from "./nodefony/src/Controller.js";
|
|
|
91
91
|
export type { FrameworkAdminApiOptions } from "./nodefony/src/FrameworkAdminApi.js";
|
|
92
92
|
export type { PlaygroundAction, PlaygroundController, PlaygroundGuards, PlaygroundParam, } from "./nodefony/src/PlaygroundAdminApi.js";
|
|
93
93
|
export type { IResourceService, IResourceReadOptions, IResourcePageQuery, } from "./nodefony/src/ResourceController.js";
|
|
94
|
-
export type {
|
|
94
|
+
export type { IFrameworkConfig, IFrameworkConfigInput, } from "./nodefony/config/config.js";
|
|
95
95
|
export { defineFrameworkConfig, frameworkConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
|
|
96
96
|
export type { IController, IRoute, IResolver, IAdminBroker, IAdminRoute, IIdempotencyStore, IdempotencyOutcome, IdempotentResponse, } from "./nodefony/interfaces/index.js";
|
|
@@ -9,9 +9,9 @@ export declare const frameworkConfigSchema: z.ZodObject<{
|
|
|
9
9
|
}, z.core.$strict>>;
|
|
10
10
|
}, z.core.$strict>;
|
|
11
11
|
/** Type de sortie (config normalisée + défauts appliqués). */
|
|
12
|
-
export type
|
|
12
|
+
export type IFrameworkConfig = z.infer<typeof frameworkConfigSchema>;
|
|
13
13
|
/** Type d'entrée (toutes sections omissibles — défauts du schéma). */
|
|
14
|
-
export type
|
|
14
|
+
export type IFrameworkConfigInput = z.input<typeof frameworkConfigSchema>;
|
|
15
15
|
declare const _default: {
|
|
16
16
|
router?: {
|
|
17
17
|
[x: string]: unknown;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IFrameworkConfig, IFrameworkConfigInput } from "./config.js";
|
|
2
2
|
/**
|
|
3
3
|
* Builder type-safe de la configuration de `@nodefony/framework` (PUR — ne
|
|
4
4
|
* retape JAMAIS un défaut : source unique = `./config.ts`).
|
|
@@ -18,7 +18,7 @@ import type { FrameworkConfig, FrameworkConfigInput } from "./config.js";
|
|
|
18
18
|
* @throws BootConfigurationError si la config est invalide ou porte une clé
|
|
19
19
|
* inconnue — le boot s'interrompt, en dev comme en prod.
|
|
20
20
|
*/
|
|
21
|
-
export declare function defineFrameworkConfig(config?:
|
|
21
|
+
export declare function defineFrameworkConfig(config?: IFrameworkConfigInput): IFrameworkConfig;
|
|
22
22
|
/**
|
|
23
23
|
* JSON Schema introspectable de la config framework — destiné au formulaire
|
|
24
24
|
* d'édition Studio et à la documentation générée (les flags de champ posés via
|
|
@@ -8,7 +8,18 @@ import Controller from "../src/Controller.js";
|
|
|
8
8
|
*/
|
|
9
9
|
export interface IOAuth2Service {
|
|
10
10
|
isEnabled(): boolean;
|
|
11
|
+
/** Fournisseurs OPÉRATIONNELS — la garde de `/authorize`, jamais l'affichage. */
|
|
11
12
|
listProviders(): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Fournisseurs à AFFICHER, libellés compris. Optionnel à dessein : le
|
|
15
|
+
* couplage à `@nodefony/security` se fait par NOM, sans liaison de build —
|
|
16
|
+
* une version du module qui ne l'expose pas encore doit dégrader, pas
|
|
17
|
+
* casser l'écran de connexion (repli sur {@link listProviders}).
|
|
18
|
+
*/
|
|
19
|
+
listDisplayProviders?(): {
|
|
20
|
+
name: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}[];
|
|
12
23
|
getRedirects(provider?: string): {
|
|
13
24
|
success: string;
|
|
14
25
|
failure: string;
|
|
@@ -63,9 +74,13 @@ declare class OAuth2Controller extends Controller {
|
|
|
63
74
|
#private;
|
|
64
75
|
constructor(context: ContextType);
|
|
65
76
|
/**
|
|
66
|
-
* Liste PUBLIQUE des fournisseurs
|
|
67
|
-
*
|
|
68
|
-
*
|
|
77
|
+
* Liste PUBLIQUE des fournisseurs à proposer à l'écran de connexion.
|
|
78
|
+
*
|
|
79
|
+
* Rend `{ name, label }` : le libellé vient du serveur, seul à connaître la
|
|
80
|
+
* configuration — un écran ne doit jamais avoir à deviner comment nommer un
|
|
81
|
+
* fournisseur, ni s'autoriser à en masquer un qu'il ne reconnaît pas. Un
|
|
82
|
+
* fournisseur déclaré `hidden` n'y figure pas, mais reste autorisable :
|
|
83
|
+
* masquer n'est pas désactiver. Aucun secret n'est exposé.
|
|
69
84
|
*/
|
|
70
85
|
providers(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
|
|
71
86
|
/** Démarre le flux : URL d'autorisation + état anti-replay en session, 302. */
|
|
@@ -38,6 +38,38 @@ export interface IConfigEntry {
|
|
|
38
38
|
*/
|
|
39
39
|
overriddenBy: Record<string, string>;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Une clé assignable du catalogue de configuration — ce qu'on a le DROIT
|
|
43
|
+
* d'écrire, par opposition à {@link IConfigEntry} qui dit ce QUI EST écrit.
|
|
44
|
+
*
|
|
45
|
+
* Les noms de champs deviennent les en-têtes de colonnes de
|
|
46
|
+
* `nodefony inspect schema`, et les clés du JSON qu'un agent filtre.
|
|
47
|
+
*/
|
|
48
|
+
export interface ISchemaCatalogRow {
|
|
49
|
+
/**
|
|
50
|
+
* Chemin pointé de la clé (`certificates.selfSigned.hash`).
|
|
51
|
+
*
|
|
52
|
+
* EN PREMIER, et ce n'est pas cosmétique : le rendu tient la première
|
|
53
|
+
* colonne pour l'identité de la ligne et ne l'élague jamais, quand il retire
|
|
54
|
+
* les suivantes si elles valent partout pareil. `module` d'abord aurait donc
|
|
55
|
+
* fait répéter le nom du module sur les cent fiches d'un lot filtré.
|
|
56
|
+
*/
|
|
57
|
+
key: string;
|
|
58
|
+
/** Nom de paquet du module qui porte la clé, tel qu'on l'écrit dans `use()`. */
|
|
59
|
+
module: string;
|
|
60
|
+
/** Type déclaré, ou les valeurs de l'énumération quand il y en a une. */
|
|
61
|
+
type: string;
|
|
62
|
+
/** Défaut du schéma — `undefined` si le schéma n'en pose aucun. */
|
|
63
|
+
default?: unknown;
|
|
64
|
+
/** Valeur résolue au boot, secrets déjà REDACTÉS par `computeConfigEntry`. */
|
|
65
|
+
effective: unknown;
|
|
66
|
+
/** D'où vient la valeur effective : `default`, `app`, `env` ou `runtime`. */
|
|
67
|
+
source: string;
|
|
68
|
+
/** Ce que les métadonnées disent de la clé (`réservé`, `secret`…), vide sinon. */
|
|
69
|
+
note: string;
|
|
70
|
+
/** La phrase du schéma (`.describe()`) — c'est elle qu'on venait chercher. */
|
|
71
|
+
description: string;
|
|
72
|
+
}
|
|
41
73
|
/**
|
|
42
74
|
* Paquets à essayer pour une clé de module, **dans l'ordre**, et bornés au
|
|
43
75
|
* périmètre du framework.
|
|
@@ -142,6 +142,20 @@ declare class Resolver implements IResolver {
|
|
|
142
142
|
* @throws nodefonyError 403 si l'accès est refusé.
|
|
143
143
|
*/
|
|
144
144
|
private _enforceSecurity;
|
|
145
|
+
/**
|
|
146
|
+
* Le message d'un refus d'autorisation — augmenté du GESTE en développement.
|
|
147
|
+
*
|
|
148
|
+
* Refuser correctement ne suffit pas : celui qui vient d'écrire une route
|
|
149
|
+
* gardée n'a, sans cela, aucun moyen de l'essayer — mesuré, c'est le premier
|
|
150
|
+
* poste de coût d'un essai réel. La cause et le geste ne franchissent jamais
|
|
151
|
+
* la production ({@link identityHint} rend `null` hors développement) : le
|
|
152
|
+
* message y reste le `Access denied` nu qu'il a toujours été, puisque ces
|
|
153
|
+
* phrases y renseigneraient l'attaquant autant que le développeur.
|
|
154
|
+
*
|
|
155
|
+
* @param cause - ce qui a fait échouer l'autorisation, en clair.
|
|
156
|
+
* @returns le message à porter par l'erreur 403.
|
|
157
|
+
*/
|
|
158
|
+
private _accessDenied;
|
|
145
159
|
/**
|
|
146
160
|
* Résout un paramètre de route NOMMÉ (`@IsGranted(..., { subject: "id" })`) vers
|
|
147
161
|
* sa valeur, depuis `route.variables` (noms) + `this.variables` (valeurs déjà
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Module, IIdempotencyStore } from "nodefony";
|
|
2
|
-
import type {
|
|
2
|
+
import type { IFrameworkConfig } from "../config/config.js";
|
|
3
3
|
/**
|
|
4
4
|
* Registre de **fabriques de stores d'idempotence DISTRIBUÉS** — résout le nom
|
|
5
5
|
* configuré (`framework.idempotency.store`) vers une instance d'
|
|
@@ -29,7 +29,7 @@ export interface IIdempotencyStoreFactoryContext {
|
|
|
29
29
|
/** Module framework (porte `kernel.container` pour résoudre `redis`, etc.). */
|
|
30
30
|
readonly module: Module;
|
|
31
31
|
/** Config framework validée + gelée. */
|
|
32
|
-
readonly config:
|
|
32
|
+
readonly config: IFrameworkConfig;
|
|
33
33
|
}
|
|
34
34
|
/** Fabrique d'un store d'idempotence pour un nom donné. */
|
|
35
35
|
export type IdempotencyStoreFactory = (ctx: IIdempotencyStoreFactoryContext) => IIdempotencyStore;
|
package/docs/controller.md
CHANGED
|
@@ -310,7 +310,7 @@ action se tromperait d'objet.
|
|
|
310
310
|
> frame 2 — pratique pour un état de conversation, piège si tu comptais sur une instance neuve. En
|
|
311
311
|
> HTTP, l'inverse : chaque requête repart d'une instance vierge.
|
|
312
312
|
|
|
313
|
-
Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:
|
|
313
|
+
Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1702`)
|
|
314
314
|
appelle `handleFrontController()` (donc `initialize()`) **avant** `startSession()`
|
|
315
315
|
(`http-kernel.ts:1131`), avant l'acceptation de la socket, et avant le firewall
|
|
316
316
|
(`http-kernel.ts:1457`).
|
|
@@ -326,11 +326,11 @@ sont des accesseurs qui dérivent du contexte **vivant**, selon le motif `champ
|
|
|
326
326
|
| `this.route` | La route matchée | `Controller.ts:158` |
|
|
327
327
|
| `this.request` | La requête (HTTP, HTTP/2 ou WS) | `Controller.ts:162` |
|
|
328
328
|
| `this.response` | La réponse du transport | `Controller.ts:169` |
|
|
329
|
-
| `this.method` | La méthode HTTP (ou `WEBSOCKET`) | `Controller.ts:
|
|
330
|
-
| `this.queryGet` | Les paramètres de la query string | `Controller.ts:
|
|
331
|
-
| `this.queryPost` | Le corps parsé | `Controller.ts:
|
|
332
|
-
| `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:
|
|
333
|
-
| `this.queryFile` | Les fichiers uploadés | `Controller.ts:
|
|
329
|
+
| `this.method` | La méthode HTTP (ou `WEBSOCKET`) | `Controller.ts:212` |
|
|
330
|
+
| `this.queryGet` | Les paramètres de la query string | `Controller.ts:221` |
|
|
331
|
+
| `this.queryPost` | Le corps parsé | `Controller.ts:248` |
|
|
332
|
+
| `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:248` |
|
|
333
|
+
| `this.queryFile` | Les fichiers uploadés | `Controller.ts:239` |
|
|
334
334
|
| `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:229` |
|
|
335
335
|
|
|
336
336
|
Pourquoi des accesseurs plutôt que des champs recopiés au constructeur : **la fraîcheur et le coût**.
|
|
@@ -349,16 +349,16 @@ n'est créée** — donc aucun coût de stockage.
|
|
|
349
349
|
Deux corollaires :
|
|
350
350
|
|
|
351
351
|
- Dans `initialize()`, `this.session` vaut `null` (l'activation vient plus tard — étape 6 du cycle).
|
|
352
|
-
- `this.getSession()` (`Controller.ts:
|
|
352
|
+
- `this.getSession()` (`Controller.ts:496`) ne « démarre » rien : il retourne la session existante,
|
|
353
353
|
ou `undefined`.
|
|
354
354
|
|
|
355
|
-
Les messages flash s'appuient dessus : `setFlashBag()`/`addFlash()` (`Controller.ts:
|
|
356
|
-
`getFlashBag()` (`Controller.ts:
|
|
355
|
+
Les messages flash s'appuient dessus : `setFlashBag()`/`addFlash()` (`Controller.ts:530`) et
|
|
356
|
+
`getFlashBag()` (`Controller.ts:514`) journalisent une **erreur** et retournent `null` si aucune
|
|
357
357
|
session n'est active — pas de crash, mais rien n'est mémorisé.
|
|
358
358
|
|
|
359
359
|
### Contrôleur `singleton` — quand `this` n'est plus à toi
|
|
360
360
|
|
|
361
|
-
Par défaut, `Controller.scope` vaut `"request"` (`Controller.ts:
|
|
361
|
+
Par défaut, `Controller.scope` vaut `"request"` (`Controller.ts:196`). Un contrôleur **sans état**
|
|
362
362
|
peut passer en instance unique partagée :
|
|
363
363
|
|
|
364
364
|
```typescript
|
|
@@ -399,14 +399,14 @@ de ce que tu as retourné :
|
|
|
399
399
|
| Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts:734` |
|
|
400
400
|
| Un `Buffer` | Envoyé brut | `Resolver.ts:723` |
|
|
401
401
|
| Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:716` |
|
|
402
|
-
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:
|
|
402
|
+
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:855`) |
|
|
403
403
|
| `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:801` |
|
|
404
404
|
| Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:770-777` |
|
|
405
405
|
|
|
406
406
|
> [!WARNING]
|
|
407
407
|
> **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
|
|
408
408
|
> répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
|
|
409
|
-
> `NO_BODY_STATUS` (`Resolver.ts:
|
|
409
|
+
> `NO_BODY_STATUS` (`Resolver.ts:855`) contient 204, 205 et 304. Donc un `@Delete` qui fait
|
|
410
410
|
> `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
|
|
411
411
|
> laisse la requête pendue.
|
|
412
412
|
|
|
@@ -420,19 +420,19 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
|
|
|
420
420
|
| Helper | Pour… | Ancre |
|
|
421
421
|
| -------------------------------------------- | -------------------------------------------------------- | ------------------- |
|
|
422
422
|
| `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:379` |
|
|
423
|
-
| `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:
|
|
424
|
-
| `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:
|
|
425
|
-
| `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:
|
|
423
|
+
| `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:377` |
|
|
424
|
+
| `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:410` |
|
|
425
|
+
| `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:392` |
|
|
426
426
|
| `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:382` |
|
|
427
427
|
| `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:432` |
|
|
428
|
-
| `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:
|
|
428
|
+
| `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:319` |
|
|
429
429
|
|
|
430
430
|
`renderView()` mesure sa propre phase `render` et injecte automatiquement les aides frontend
|
|
431
431
|
(`frontendTags`, `frontendDocument`, `asset`) dans les variables du template
|
|
432
|
-
(`withFrontendLocals()`, `Controller.ts:
|
|
432
|
+
(`withFrontendLocals()`, `Controller.ts:448`) — tes propres valeurs restent prioritaires.
|
|
433
433
|
|
|
434
434
|
`forward()` re-résout un contrôleur sur le **même** contexte et rappelle son action
|
|
435
|
-
(`Controller.ts:
|
|
435
|
+
(`Controller.ts:534`) : c'est une délégation interne, la requête cliente reste unique.
|
|
436
436
|
|
|
437
437
|
> [!TIP]
|
|
438
438
|
> **Redirection : le code par défaut est 302** (Found), pas 301. Un statut absent ou hors de la liste
|
|
@@ -448,7 +448,7 @@ Deux besoins distincts, deux helpers.
|
|
|
448
448
|
|
|
449
449
|
`renderFileDownload(file, options?, headers?)` (`Controller.ts:473`) pose
|
|
450
450
|
`Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
|
|
451
|
-
de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:
|
|
451
|
+
de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:586`) ;
|
|
452
452
|
la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
|
|
453
453
|
et gèle le process le temps du stat.
|
|
454
454
|
|
|
@@ -465,12 +465,12 @@ plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le fl
|
|
|
465
465
|
| Plage hors fichier | **416** + `Content-Range: bytes */<taille>` (RFC 9110 §15.5.17) |
|
|
466
466
|
| Syntaxe invalide, multi-plage, unité inconnue | En-tête **ignoré** → 200 complet (jamais un 500) |
|
|
467
467
|
|
|
468
|
-
La logique est isolée dans une fonction pure exportée, `parseByteRange()` (`Controller.ts:
|
|
468
|
+
La logique est isolée dans une fonction pure exportée, `parseByteRange()` (`Controller.ts:107`) —
|
|
469
469
|
donc testable sans serveur.
|
|
470
470
|
|
|
471
471
|
### Ce que `streamFile()` garantit
|
|
472
472
|
|
|
473
|
-
`streamFile()` (`Controller.ts:
|
|
473
|
+
`streamFile()` (`Controller.ts:669`) est le moteur commun. Sa subtilité n'est pas le pipe, c'est le
|
|
474
474
|
**nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
|
|
475
475
|
téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
|
|
476
476
|
écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
|
|
@@ -578,9 +578,9 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
578
578
|
|
|
579
579
|
| Domaine | Norme | Comment le code s'y conforme |
|
|
580
580
|
| -------------------------------- | ------------------------ | -------------------------------------------------------------- |
|
|
581
|
-
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:
|
|
582
|
-
| Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts:
|
|
583
|
-
| Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:
|
|
581
|
+
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:855`) |
|
|
582
|
+
| Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts:107`) |
|
|
583
|
+
| Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:392`) |
|
|
584
584
|
| Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts:534`) |
|
|
585
585
|
| Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:760`), vérifié par le banc `auto-json` |
|
|
586
586
|
| Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts:734`) |
|
|
@@ -610,7 +610,7 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
610
610
|
| WS : l'état d'une frame « bave » sur la suivante | L'instance est partagée par toute la connexion (`Resolver.ts:262`) | Réinitialiser l'état en tête d'action, ou le porter par message |
|
|
611
611
|
| WS : l'action n'est jamais appelée | Route sans transport `WEBSOCKET` déclaré | `requirements: { methods: ["WEBSOCKET"] }` |
|
|
612
612
|
| Contrôleur `singleton` : données d'un autre utilisateur | Champ mutable per-requête sur une instance partagée | Retirer `@Scope("singleton")`, ou passer par les arguments décorés |
|
|
613
|
-
| Event loop figé sur une route de fichier | `getFile()` synchrone (`lstatSync`, `Controller.ts:
|
|
613
|
+
| Event loop figé sur une route de fichier | `getFile()` synchrone (`lstatSync`, `Controller.ts:546`) | Utiliser `getFileAsync()` (`Controller.ts:586`) |
|
|
614
614
|
|
|
615
615
|
## 🧪 Tests & couverture
|
|
616
616
|
|
package/docs/decorateurs.md
CHANGED
|
@@ -298,7 +298,7 @@ d'écriture (`routerDecorators.ts:281`) — sinon un attrape-tout masquerait les
|
|
|
298
298
|
**`@Scope("singleton")` est un contrat, pas une optimisation.** L'instance étant partagée, l'action
|
|
299
299
|
ne doit lire ni écrire **aucun** état de requête sur `this` : tout passe par les arguments décorés et
|
|
300
300
|
les accesseurs, qui retrouvent la requête courante via l'ALS. Le défaut reste `"request"` — une
|
|
301
|
-
instance par requête (`ControllerScope`, `Controller.ts:
|
|
301
|
+
instance par requête (`ControllerScope`, `Controller.ts:144`).
|
|
302
302
|
|
|
303
303
|
> [!NOTE]
|
|
304
304
|
> Le core `nodefony` exporte lui aussi un `Scope` (les portées du conteneur d'injection). Celui des
|
|
@@ -455,12 +455,12 @@ ne s'auto-promeut pas.
|
|
|
455
455
|
| `@Header("X-Foo", "bar")` | méthode | Ajoute un en-tête ; **s'empile** (plusieurs `@Header` cumulent, `routerDecorators.ts:580`) | `@Header("Cache-Control","no-store")` |
|
|
456
456
|
| `@Redirect("/url", 302)` | méthode | Redirige **si** l'action ne renvoie rien (`Redirect()`, `routerDecorators.ts:589`) | `@Redirect("/login", 302)` |
|
|
457
457
|
|
|
458
|
-
Les deux premiers sont appliqués par `Resolver._applyResponseMeta()` (`Resolver.ts:
|
|
458
|
+
Les deux premiers sont appliqués par `Resolver._applyResponseMeta()` (`Resolver.ts:687`) **avant**
|
|
459
459
|
l'appel de l'action : ton code peut donc les écraser ensuite (`this.renderJson(data, 202)` gagne).
|
|
460
460
|
|
|
461
461
|
`@Redirect` a une subtilité utile : si l'action **retourne un objet** portant `url` (et
|
|
462
462
|
éventuellement `statusCode`), cet objet **prend le dessus** sur les valeurs du décorateur
|
|
463
|
-
(`Resolver._handleRedirect()`, `Resolver.ts:
|
|
463
|
+
(`Resolver._handleRedirect()`, `Resolver.ts:703`) — la cible peut donc être calculée à l'exécution :
|
|
464
464
|
|
|
465
465
|
```typescript
|
|
466
466
|
@Get("/go")
|
|
@@ -620,7 +620,7 @@ Trois faits à retenir :
|
|
|
620
620
|
- **Les décorateurs de paramètre fonctionnent pareil.** Pour une invocation par socket, le corps de
|
|
621
621
|
la mutation voyage dans l'ALS et **prime** sur le corps HTTP (vide dans ce cas) — c'est traité dans
|
|
622
622
|
`resolveParamArg()` (`routerDecorators.ts:1283`), et `@Query` lit la query du chemin **invoqué**,
|
|
623
|
-
pas celle du handshake (`Resolver._buildParamArgs()`, `Resolver.ts:
|
|
623
|
+
pas celle du handshake (`Resolver._buildParamArgs()`, `Resolver.ts:656`).
|
|
624
624
|
- **Les gardes s'appliquent identiquement.** `@IsGranted` protège une action joignable par socket
|
|
625
625
|
exactement comme une action HTTP : la décision est prise avant l'instanciation, quel que soit le
|
|
626
626
|
transport.
|
|
@@ -708,7 +708,7 @@ Le `Resolver` consomme ce snapshot dans un ordre qui a du sens sécurité :
|
|
|
708
708
|
**garde d'abord, instanciation ensuite**. `security !== null` déclenche
|
|
709
709
|
`_enforceSecurity()` (`Resolver.ts:576`) **avant** `newController()` — un `403` n'instancie pas le
|
|
710
710
|
contrôleur et n'exécute pas son `initialize()`. Puis viennent les arguments
|
|
711
|
-
(`_buildParamArgs()`, `Resolver.ts:
|
|
711
|
+
(`_buildParamArgs()`, `Resolver.ts:656`), les métadonnées de réponse
|
|
712
712
|
(`_applyResponseMeta()`, `Resolver.ts:650`), l'action, et enfin la redirection éventuelle.
|
|
713
713
|
|
|
714
714
|
Un usage cold path mérite d'être connu : `extractActionScopes()` (`routerDecorators.ts:1476`) parcourt
|
package/docs/idempotence.md
CHANGED
|
@@ -345,7 +345,7 @@ Source unique = schéma Zod `idempotencySchema`
|
|
|
345
345
|
### Comment `store: "auto"` se résout VRAIMENT
|
|
346
346
|
|
|
347
347
|
`Framework.onKernelBoot()` (`src/packages/@nodefony/framework/index.ts:189`) délègue à
|
|
348
|
-
`resolveAutoStore("ephemeral", …)` (`src/nodefony/src/config/infra.ts:
|
|
348
|
+
`resolveAutoStore("ephemeral", …)` (`src/nodefony/src/config/infra.ts:289`), borné aux backends
|
|
349
349
|
**réellement enregistrés** (`listIdempotencyBackends()`, `idempotencyStoreRegistry.ts:81`). L'ordre
|
|
350
350
|
réel est le suivant :
|
|
351
351
|
|
package/docs/routing.md
CHANGED
|
@@ -114,7 +114,7 @@ mêmes décorateurs.
|
|
|
114
114
|
|
|
115
115
|
**Le routeur passe avant les fichiers statiques.** Une requête qui correspond à une route ne paie
|
|
116
116
|
jamais le `stat` du serveur de fichiers : le repli statique n'est tenté que si la résolution a échoué
|
|
117
|
-
(`serverStatic.handle()`, `http-kernel.ts:
|
|
117
|
+
(`serverStatic.handle()`, `http-kernel.ts:725`).
|
|
118
118
|
|
|
119
119
|
> [!NOTE]
|
|
120
120
|
> **Le routage n'a aucune option de configuration.** Le schéma Zod du module n'expose qu'un sac
|
|
@@ -569,7 +569,7 @@ alloué par requête.
|
|
|
569
569
|
| Cible identifiée par l'URI, hôte compris | RFC 9110 §7.2 | hôte vérifié avant la méthode (`Route.match()`, `Route.ts:298`) |
|
|
570
570
|
| 403 sur ressource d'un autre vhost | RFC 9110 §15.5.4 | `Route.matchHostname()` (`Route.ts:605`) |
|
|
571
571
|
| 404 quand rien ne correspond | RFC 9110 §15.5.5 | après repli statique (`http-kernel.ts:688`) |
|
|
572
|
-
| 421 sur `Host` non servi | RFC 9110 §15.5.20 | `checkValidDomain()` (`http-kernel.ts:
|
|
572
|
+
| 421 sur `Host` non servi | RFC 9110 §15.5.20 | `checkValidDomain()` (`http-kernel.ts:1740`) |
|
|
573
573
|
| Erreur de sous-protocole WS = 1002 | RFC 6455 §7.4 | `Route.matchRequirements()` (`Route.ts:649`) |
|
|
574
574
|
| Décodage pourcent des segments | RFC 3986 §2.1 | `decode()` (`Route.ts:79`) |
|
|
575
575
|
|