@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +15 -20
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +45 -11
- package/dist/nodefony/controller/ApiKeyController.js +18 -10
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +117 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -17
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +50 -13
- package/dist/nodefony/controller/TokenAuthController.js +14 -8
- package/dist/nodefony/controller/TotpController.js +7 -8
- package/dist/nodefony/controller/WebAuthnController.js +28 -12
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +268 -65
- package/dist/nodefony/service/AdminBroker.js +4 -6
- package/dist/nodefony/service/Eta.js +1 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -6
- package/dist/nodefony/service/router.js +97 -34
- package/dist/nodefony/src/Controller.js +71 -43
- package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
- package/dist/nodefony/src/KernelAdminApi.js +82 -18
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +105 -36
- package/dist/nodefony/src/ResourceController.js +2 -3
- package/dist/nodefony/src/Route.js +54 -8
- package/dist/nodefony/src/SecurityApiController.js +23 -0
- package/dist/nodefony/src/SyslogAdminApi.js +1 -2
- package/dist/nodefony/src/Template.js +1 -2
- package/dist/nodefony/src/configMutation.js +1 -2
- package/dist/nodefony/src/docsReader.js +39 -23
- package/dist/nodefony/src/idempotency.js +14 -22
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +430 -0
- package/dist/nodefony/src/requireAuthentication.js +35 -0
- package/dist/nodefony/src/routeSchema.js +81 -0
- package/dist/nodefony/src/scopeCatalog.js +1 -2
- package/dist/nodefony/src/singletonGuard.js +1 -2
- package/dist/nodefony/src/syslogFilters.js +1 -2
- package/dist/types/index.d.ts +8 -12
- package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
- package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
- package/dist/types/nodefony/service/router.d.ts +40 -1
- package/dist/types/nodefony/src/Controller.d.ts +54 -17
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +44 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
- package/dist/types/nodefony/src/Route.d.ts +21 -1
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
- package/dist/types/nodefony/src/docsReader.d.ts +10 -6
- package/dist/types/nodefony/src/idempotency.d.ts +12 -10
- package/dist/types/nodefony/src/loginPage.d.ts +144 -0
- package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/docs/admin.md +83 -78
- package/docs/controller.md +112 -111
- package/docs/decorateurs.md +212 -163
- package/docs/idempotence.md +137 -135
- package/docs/routing.md +96 -95
- package/docs/templates.md +55 -55
- package/package.json +10 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Controller from "./Controller.js";
|
|
2
2
|
import { assertPageQuery } from "nodefony";
|
|
3
3
|
import { HttpError } from "@nodefony/http";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/ResourceController.ts
|
|
5
5
|
/**
|
|
6
6
|
* Controller de ressource **souverain** (V4.2 — POC API souveraine, Phase 2) :
|
|
7
7
|
* la logique métier est écrite UNE fois dans le service de ressource, les
|
|
@@ -22,7 +22,7 @@ import { HttpError } from "@nodefony/http";
|
|
|
22
22
|
* sur `this` doit rétrograder : `static scope = "request"`.
|
|
23
23
|
*
|
|
24
24
|
* Sécurité : aucun critère de requête n'est passé AUTOMATIQUEMENT au service
|
|
25
|
-
* (pas de `find(this.
|
|
25
|
+
* (pas de `find(this.query)` implicite) — exposer un filtrage est une
|
|
26
26
|
* décision EXPLICITE de la sous-classe (deny-by-default ; le scope de
|
|
27
27
|
* sécurité des données se branche au niveau service/criteria, P6).
|
|
28
28
|
*
|
|
@@ -144,5 +144,4 @@ var ResourceController = class extends Controller {
|
|
|
144
144
|
return Promise.resolve(resource.delete(criteria));
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
|
-
//#endregion
|
|
148
147
|
export { ResourceController as default };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { 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
|
|
5
5
|
/**
|
|
6
6
|
* Motif d'un segment variable : `{id}`, `/{slug}?`, `.{format}`, `{id}(\d+)`.
|
|
7
7
|
*
|
|
@@ -99,6 +99,32 @@ function replaceCallback(_match, slash, dot, key, capture, opt, _offset) {
|
|
|
99
99
|
}
|
|
100
100
|
throw new Error(`Bad path `);
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Exigences d'une route, l'option `method` versée dans `requirements.methods`.
|
|
104
|
+
*
|
|
105
|
+
* Seul `requirements.methods` restreint le match ; l'option `method` n'était
|
|
106
|
+
* lue que pour l'affichage, si bien qu'une route « POST » servait toutes les
|
|
107
|
+
* méthodes. Les deux déclarations s'UNISSENT, comme le font les décorateurs
|
|
108
|
+
* `@Get`/`@Post` : déclarer n'élargit jamais en silence ni n'écrase.
|
|
109
|
+
*
|
|
110
|
+
* @param requirements - exigences déclarées (jamais mutées)
|
|
111
|
+
* @param method - méthode(s) de l'option `method`
|
|
112
|
+
* @returns les exigences à poser sur la route
|
|
113
|
+
*/
|
|
114
|
+
function withDeclaredMethod(requirements, method) {
|
|
115
|
+
if (method === void 0) return requirements ?? {};
|
|
116
|
+
const own = requirements?.methods;
|
|
117
|
+
const union = new Set(Array.isArray(method) ? method : [method]);
|
|
118
|
+
if (own !== void 0) for (const m of Array.isArray(own) ? own : [own]) union.add(m);
|
|
119
|
+
return {
|
|
120
|
+
...requirements,
|
|
121
|
+
methods: [...union]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Canal chiffré : `https` (HTTP) ou `wss` (WebSocket). */
|
|
125
|
+
function isSecureScheme(scheme) {
|
|
126
|
+
return scheme === "https" || scheme === "wss";
|
|
127
|
+
}
|
|
102
128
|
var Route = class Route {
|
|
103
129
|
name;
|
|
104
130
|
path;
|
|
@@ -106,7 +132,6 @@ var Route = class Route {
|
|
|
106
132
|
classMethod;
|
|
107
133
|
prefix;
|
|
108
134
|
method;
|
|
109
|
-
schemes;
|
|
110
135
|
pattern;
|
|
111
136
|
variables = [];
|
|
112
137
|
/**
|
|
@@ -144,6 +169,8 @@ var Route = class Route {
|
|
|
144
169
|
* pas son rôle par défaut. Cf {@link RouteOptions.areaRoleExempt}.
|
|
145
170
|
*/
|
|
146
171
|
areaRoleExempt = false;
|
|
172
|
+
/** Route de repli, résolue après toutes les autres. Cf {@link RouteOptions.fallback}. */
|
|
173
|
+
fallback = false;
|
|
147
174
|
/**
|
|
148
175
|
* P2.9 — Cache mémoïsé : l'action attend-elle le **flux brut** du body
|
|
149
176
|
* (`@Body({ stream:true })`) ? `undefined` = pas encore calculé (résolu au 1er
|
|
@@ -177,9 +204,10 @@ var Route = class Route {
|
|
|
177
204
|
this.method = obj.method;
|
|
178
205
|
this.setHostname(obj.host);
|
|
179
206
|
this.setDefaults(obj.defaults);
|
|
180
|
-
this.requirements = obj.requirements
|
|
207
|
+
this.requirements = withDeclaredMethod(obj.requirements, obj.method);
|
|
181
208
|
this.bypassFirewall = obj.bypassFirewall ?? false;
|
|
182
209
|
this.areaRoleExempt = obj.areaRoleExempt ?? false;
|
|
210
|
+
this.fallback = obj.fallback ?? false;
|
|
183
211
|
this.compile();
|
|
184
212
|
}
|
|
185
213
|
this.generateId();
|
|
@@ -238,7 +266,7 @@ var Route = class Route {
|
|
|
238
266
|
const k = this.variables[i] || "wildcard";
|
|
239
267
|
param &&= decode(param);
|
|
240
268
|
const req = this.getRequirement(k);
|
|
241
|
-
let result
|
|
269
|
+
let result;
|
|
242
270
|
if (req) {
|
|
243
271
|
let compiled = this.varRegexp?.[k];
|
|
244
272
|
if (!compiled) {
|
|
@@ -366,7 +394,8 @@ var Route = class Route {
|
|
|
366
394
|
const action = this.classMethod || this.name;
|
|
367
395
|
const mod = this.module?.name ? `@${this.module.name}/` : "";
|
|
368
396
|
const auth = this.bypassFirewall ? " (no auth)" : this.areaRoleExempt ? " (hors rôle de zone)" : "";
|
|
369
|
-
|
|
397
|
+
const fallback = this.fallback ? " (repli)" : "";
|
|
398
|
+
return `${method} ${this.path} → ${mod}${ctrl}.${action}${auth}${fallback}`;
|
|
370
399
|
}
|
|
371
400
|
toObject() {
|
|
372
401
|
return {
|
|
@@ -376,10 +405,11 @@ var Route = class Route {
|
|
|
376
405
|
host: this.host,
|
|
377
406
|
controller: this.defaults.controller,
|
|
378
407
|
filePath: this.filePath,
|
|
379
|
-
schemes: this.
|
|
408
|
+
schemes: this.requirements.scheme,
|
|
380
409
|
variables: this.variables,
|
|
381
410
|
bypassFirewall: this.bypassFirewall,
|
|
382
|
-
areaRoleExempt: this.areaRoleExempt
|
|
411
|
+
areaRoleExempt: this.areaRoleExempt,
|
|
412
|
+
fallback: this.fallback
|
|
383
413
|
};
|
|
384
414
|
}
|
|
385
415
|
setDefaults(arg) {
|
|
@@ -459,6 +489,13 @@ var Route = class Route {
|
|
|
459
489
|
return Object.keys(this.requirements).length;
|
|
460
490
|
}
|
|
461
491
|
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
|
+
}
|
|
462
499
|
if (this.hasRequirements()) for (const i in this.requirements) switch (i) {
|
|
463
500
|
case "methods":
|
|
464
501
|
if (!this.methodsSet) throw new Error(`Bad config route method : ${String(this.requirements.methods)}`);
|
|
@@ -480,6 +517,16 @@ var Route = class Route {
|
|
|
480
517
|
throw error;
|
|
481
518
|
}
|
|
482
519
|
break;
|
|
520
|
+
case "scheme": {
|
|
521
|
+
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
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
483
530
|
case "domain": break;
|
|
484
531
|
case "protocol": if (context.method === "WEBSOCKET") {
|
|
485
532
|
const requirement = this.requirements[i];
|
|
@@ -502,5 +549,4 @@ var Route = class Route {
|
|
|
502
549
|
return true;
|
|
503
550
|
}
|
|
504
551
|
};
|
|
505
|
-
//#endregion
|
|
506
552
|
export { Route as default };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Controller from "./Controller.js";
|
|
2
|
+
// nodefony/src/SecurityApiController.ts
|
|
3
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
4
|
+
/**
|
|
5
|
+
* Base des contrôleurs d'authentification (`/nodefony/security/api/*`) : toute
|
|
6
|
+
* réponse JSON part en `Cache-Control: no-store`.
|
|
7
|
+
*
|
|
8
|
+
* Ces réponses portent une identité, un jeton, un secret affiché une seule
|
|
9
|
+
* fois (clé API, secret TOTP, codes de récupération) ou leur refus. Un cookie
|
|
10
|
+
* 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. La règle
|
|
12
|
+
* vit ici, une fois : une action ajoutée à un contrôleur héritier ne peut pas
|
|
13
|
+
* l'oublier. Un en-tête passé par l'action garde la priorité.
|
|
14
|
+
*/
|
|
15
|
+
var SecurityApiController = class extends Controller {
|
|
16
|
+
renderJson(obj, status, headers) {
|
|
17
|
+
return super.renderJson(obj, status, {
|
|
18
|
+
...NO_STORE,
|
|
19
|
+
...headers
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export { SecurityApiController as default };
|
|
@@ -2,7 +2,7 @@ import { SYSLOG_FILTERS, SYSLOG_SORTABLE } from "./syslogFilters.js";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Syslog, getActiveLogDriver, getLogDriver, listLogDrivers, parseFilters, parsePageQuery, redactSecrets, setActiveLogDriver } from "nodefony";
|
|
4
4
|
import fsp from "node:fs/promises";
|
|
5
|
-
|
|
5
|
+
// nodefony/src/SyslogAdminApi.ts
|
|
6
6
|
/** Plafond d'octets lus en queue de fichier (tail / fenêtre incrémentale). */
|
|
7
7
|
const MAX_TAIL_BYTES = 262144;
|
|
8
8
|
/**
|
|
@@ -462,5 +462,4 @@ function createSyslogAdminApi(syslog, options = {}) {
|
|
|
462
462
|
adminEndpoints: () => endpoints
|
|
463
463
|
};
|
|
464
464
|
}
|
|
465
|
-
//#endregion
|
|
466
465
|
export { createSyslogAdminApi };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Service } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/Template.ts
|
|
3
3
|
var Template = class extends Service {
|
|
4
4
|
engine;
|
|
5
5
|
module;
|
|
@@ -11,5 +11,4 @@ var Template = class extends Service {
|
|
|
11
11
|
this.cache = module.kernel?.environment === "prod" || module.kernel?.environment === "production";
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
|
-
//#endregion
|
|
15
14
|
export { Template as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/configMutation.ts
|
|
2
2
|
/** Narrowing défensif : `unknown` → nœud JSON Schema (ou `null` si non-objet). */
|
|
3
3
|
function asNode(value) {
|
|
4
4
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
@@ -182,5 +182,4 @@ function getResolvedPath(obj, segments) {
|
|
|
182
182
|
}
|
|
183
183
|
return cur;
|
|
184
184
|
}
|
|
185
|
-
//#endregion
|
|
186
185
|
export { getResolvedPath, navigateSchemaNode, nodeFlags, notEditableReason, recipeFor, validateLeafValue };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { basename, dirname, extname, join } from "node:path";
|
|
2
|
-
import { findProjectRoot, packageManagerCommandLines, packageManagerExecArgs, portableSpawn,
|
|
2
|
+
import { findProjectRoot, isDocVisible, packageManagerCommandLines, packageManagerExecArgs, portableSpawn, readDocScope, readSymbolsGraph, resolvePackageManager } from "nodefony";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { execFile, spawn } from "node:child_process";
|
|
8
|
-
|
|
8
|
+
// nodefony/src/docsReader.ts
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
/**
|
|
11
11
|
* Racine de l'APPLICATION servie — jamais le dossier courant du process.
|
|
@@ -97,11 +97,19 @@ async function gitLastUpdated(file) {
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
100
|
+
/**
|
|
101
|
+
* La page se sert-elle dans ce décor ? Portée lue dans `publish` par la règle
|
|
102
|
+
* du cœur (`readDocScope`/`isDocVisible`) — la même que le portail et le site.
|
|
103
|
+
*/
|
|
104
|
+
function servable(data, frameworkRepository) {
|
|
105
|
+
return isDocVisible(readDocScope(data.publish), frameworkRepository);
|
|
106
|
+
}
|
|
107
|
+
/** Construit un `DocSummary` à partir du contenu brut d'un `.md` — `null` si la page n'est pas servie ici. */
|
|
108
|
+
async function summarize(docsDir, fileName, withGit, frameworkRepository) {
|
|
102
109
|
const slug = basename(fileName, extname(fileName));
|
|
103
110
|
const full = join(docsDir, fileName);
|
|
104
111
|
const { data, body } = parseFrontmatter(await readFile(full, "utf8"));
|
|
112
|
+
if (!servable(data, frameworkRepository)) return null;
|
|
105
113
|
const title = docTitle(data, body, slug);
|
|
106
114
|
return {
|
|
107
115
|
slug,
|
|
@@ -121,7 +129,7 @@ async function summarize(docsDir, fileName, withGit) {
|
|
|
121
129
|
* @param modulePath - chemin disque du module (`Module.path`).
|
|
122
130
|
* @returns liste triée (vide si le dossier `docs/` est absent).
|
|
123
131
|
*/
|
|
124
|
-
async function listModuleDocs(modulePath, withGit = false) {
|
|
132
|
+
async function listModuleDocs(modulePath, withGit = false, frameworkRepository = false) {
|
|
125
133
|
const docsDir = join(modulePath, "docs");
|
|
126
134
|
let entries;
|
|
127
135
|
try {
|
|
@@ -130,21 +138,34 @@ async function listModuleDocs(modulePath, withGit = false) {
|
|
|
130
138
|
return [];
|
|
131
139
|
}
|
|
132
140
|
const mdFiles = entries.filter((f) => f.toLowerCase().endsWith(".md"));
|
|
133
|
-
return (await Promise.all(mdFiles.map((f) => summarize(docsDir, f, withGit)))).sort((a, b) => a.order - b.order || a.slug.localeCompare(b.slug));
|
|
141
|
+
return (await Promise.all(mdFiles.map((f) => summarize(docsDir, f, withGit, frameworkRepository)))).filter((d) => d !== null).sort((a, b) => a.order - b.order || a.slug.localeCompare(b.slug));
|
|
134
142
|
}
|
|
135
143
|
/**
|
|
136
|
-
* Comptage
|
|
137
|
-
*
|
|
144
|
+
* Comptage des docs SERVIES d'un module, sans `git` — pour les KPI/overview
|
|
145
|
+
* (`docsCount`) où seul le nombre importe. Le frontmatter est lu : le compte
|
|
146
|
+
* suit la même portée (`publish`) que la liste, sinon le KPI annonce des pages
|
|
147
|
+
* que l'onglet Docs refuse d'ouvrir.
|
|
138
148
|
*
|
|
139
149
|
* @param modulePath - chemin disque du module (`Module.path`).
|
|
140
|
-
* @
|
|
150
|
+
* @param frameworkRepository - vrai dans le dépôt du framework (pages `core` servies).
|
|
151
|
+
* @returns nombre de pages servies dans `<modulePath>/docs/` (0 si absent).
|
|
141
152
|
*/
|
|
142
|
-
async function countModuleDocs(modulePath) {
|
|
153
|
+
async function countModuleDocs(modulePath, frameworkRepository = false) {
|
|
154
|
+
const docsDir = join(modulePath, "docs");
|
|
155
|
+
let entries;
|
|
143
156
|
try {
|
|
144
|
-
|
|
157
|
+
entries = await readdir(docsDir);
|
|
145
158
|
} catch {
|
|
146
159
|
return 0;
|
|
147
160
|
}
|
|
161
|
+
return (await Promise.all(entries.filter((f) => f.toLowerCase().endsWith(".md")).map(async (f) => {
|
|
162
|
+
try {
|
|
163
|
+
const { data } = parseFrontmatter(await readFile(join(docsDir, f), "utf8"));
|
|
164
|
+
return servable(data, frameworkRepository);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}))).filter(Boolean).length;
|
|
148
169
|
}
|
|
149
170
|
/**
|
|
150
171
|
* Lit une doc module par slug : frontmatter + corps markdown brut.
|
|
@@ -154,7 +175,7 @@ async function countModuleDocs(modulePath) {
|
|
|
154
175
|
*
|
|
155
176
|
* @returns `null` si slug invalide ou fichier absent.
|
|
156
177
|
*/
|
|
157
|
-
async function readModuleDoc(modulePath, slug) {
|
|
178
|
+
async function readModuleDoc(modulePath, slug, frameworkRepository = false) {
|
|
158
179
|
if (!SLUG_RE.test(slug)) return null;
|
|
159
180
|
const full = join(modulePath, "docs", `${slug}.md`);
|
|
160
181
|
let raw;
|
|
@@ -164,6 +185,7 @@ async function readModuleDoc(modulePath, slug) {
|
|
|
164
185
|
return null;
|
|
165
186
|
}
|
|
166
187
|
const { data, body } = parseFrontmatter(raw);
|
|
188
|
+
if (!servable(data, frameworkRepository)) return null;
|
|
167
189
|
return {
|
|
168
190
|
slug,
|
|
169
191
|
frontmatter: data,
|
|
@@ -276,9 +298,10 @@ async function searchModuleDocs(targets, query, options = {}) {
|
|
|
276
298
|
} catch {
|
|
277
299
|
continue;
|
|
278
300
|
}
|
|
279
|
-
scanned += 1;
|
|
280
301
|
const slug = basename(fileName, extname(fileName));
|
|
281
302
|
const { data, body } = parseFrontmatter(raw);
|
|
303
|
+
if (!servable(data, options.frameworkRepository ?? false)) continue;
|
|
304
|
+
scanned += 1;
|
|
282
305
|
const title = docTitle(data, body, slug);
|
|
283
306
|
const foldedTitle = fold(`${title} ${typeof data.navTitle === "string" && data.navTitle.trim() ? data.navTitle.trim() : ""} ${slug}`);
|
|
284
307
|
const lines = body.split("\n");
|
|
@@ -347,15 +370,9 @@ async function searchModuleDocs(targets, query, options = {}) {
|
|
|
347
370
|
* @returns symboles exportés triés par kind puis nom (vide si fichier absent).
|
|
348
371
|
*/
|
|
349
372
|
async function listModuleSymbols(packageName) {
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
355
|
-
} catch {
|
|
356
|
-
return [];
|
|
357
|
-
}
|
|
358
|
-
const symbols = parsed.symbols ?? {};
|
|
373
|
+
const graph = readSymbolsGraph(process.cwd());
|
|
374
|
+
if (graph === null) return [];
|
|
375
|
+
const symbols = graph.symbols;
|
|
359
376
|
const out = [];
|
|
360
377
|
for (const sym of Object.values(symbols)) {
|
|
361
378
|
if (sym.module !== packageName || sym.exported !== true) continue;
|
|
@@ -935,5 +952,4 @@ async function readCoreInfo() {
|
|
|
935
952
|
dependencies
|
|
936
953
|
};
|
|
937
954
|
}
|
|
938
|
-
//#endregion
|
|
939
955
|
export { CORE_PACKAGE, checkOutdated, countModuleDocs, listModuleDocs, listModuleSymbols, listTestFiles, listTestGroups, parseFrontmatter, readCoreInfo, readCoverage, readDependencies, readModuleDoc, readSymbolDeclaration, resolveCorePath, runModuleTests, searchModuleDocs, testRunCommand };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RequestContext } from "nodefony";
|
|
2
2
|
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
6
|
* `draft-ietf-httpapi-idempotency-key-header-06`. Source unique de la sémantique
|
|
@@ -51,28 +51,21 @@ function resolveIdempotencyKey(alsKey, header) {
|
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Identité stable pour **scoper** le cache d'idempotence (anti-IDOR : un
|
|
54
|
-
* utilisateur ne doit jamais rejouer la clé d'un autre)
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* n'utilise PAS le cache (jamais de partage cross-identité).
|
|
54
|
+
* utilisateur ne doit jamais rejouer la clé d'un autre) : l'`identifier` de
|
|
55
|
+
* l'utilisateur AUTHENTIFIÉ de la requête. `null` = pas d'identité fiable →
|
|
56
|
+
* l'appelant n'utilise PAS le cache (jamais de partage cross-identité).
|
|
58
57
|
*
|
|
59
|
-
*
|
|
60
|
-
* (
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
58
|
+
* La décision « authentifié ? » est celle du cœur
|
|
59
|
+
* (`RequestContext.getAuthenticatedUser`) : l'anonyme d'une zone `anonymous`
|
|
60
|
+
* rend `null`. Lire l'`identifier` de l'utilisateur BRUT faisait partager à
|
|
61
|
+
* tous les visiteurs anonymes la même identité (`anon.`), donc le même cache.
|
|
62
|
+
*
|
|
63
|
+
* Même valeur quel que soit le transport pour un même compte : le pare-feu HTTP
|
|
64
|
+
* et le pont realtime posent tous deux le jeton et l'utilisateur dans l'ALS —
|
|
65
|
+
* une mutation tentée en WS puis rejouée en HTTP se dédoublonne.
|
|
64
66
|
*/
|
|
65
|
-
function resolveIdentity(
|
|
66
|
-
|
|
67
|
-
const o = user;
|
|
68
|
-
for (const v of [
|
|
69
|
-
o.username,
|
|
70
|
-
o.identifier,
|
|
71
|
-
o.id
|
|
72
|
-
]) if (typeof v === "string" && v) return v;
|
|
73
|
-
}
|
|
74
|
-
const uid = RequestContext.getUserId();
|
|
75
|
-
return typeof uid === "string" && uid ? uid : null;
|
|
67
|
+
function resolveIdentity() {
|
|
68
|
+
return RequestContext.getAuthenticatedUser()?.identifier ?? null;
|
|
76
69
|
}
|
|
77
70
|
/**
|
|
78
71
|
* Empreinte SHA-256 du **payload** d'une requête (parties sérialisables : nom de
|
|
@@ -133,5 +126,4 @@ async function evaluateIdempotency(opts) {
|
|
|
133
126
|
};
|
|
134
127
|
}
|
|
135
128
|
}
|
|
136
|
-
//#endregion
|
|
137
129
|
export { IDEMPOTENCY_KEY_MAX, computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GcScheduler } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/idempotencyGc.ts
|
|
3
3
|
/**
|
|
4
4
|
* Arme un {@link GcScheduler} qui purge périodiquement les entrées expirées d'un
|
|
5
5
|
* store d'idempotence — **UNIQUEMENT si le store expose `gc()`**.
|
|
@@ -28,5 +28,4 @@ function scheduleIdempotencyGc(store, opts) {
|
|
|
28
28
|
opts.log?.(armed ? `idempotency gc armed — purge every ${opts.intervalS}s (SQL store exposes gc())` : `idempotency gc disarmed — intervalS=${opts.intervalS} (delegated to cron / external)`);
|
|
29
29
|
return scheduler;
|
|
30
30
|
}
|
|
31
|
-
//#endregion
|
|
32
31
|
export { scheduleIdempotencyGc };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/idempotencyStoreRegistry.ts
|
|
2
2
|
const factories = /* @__PURE__ */ new Map();
|
|
3
3
|
/**
|
|
4
4
|
* Enregistre (ou remplace) la fabrique d'un store d'idempotence distribué.
|
|
@@ -32,5 +32,4 @@ function listIdempotencyStores() {
|
|
|
32
32
|
function listIdempotencyBackends() {
|
|
33
33
|
return ["memory", ...factories.keys()];
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { getIdempotencyStoreFactory, listIdempotencyBackends, listIdempotencyStores, registerIdempotencyStore };
|