@nodefony/framework 10.0.0-alpha.9 → 10.0.0-beta.2
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 +5 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +4 -4
- package/dist/nodefony/controller/AdminApiController.js +4 -3
- package/dist/nodefony/controller/ApiKeyController.js +3 -2
- package/dist/nodefony/controller/OAuth2Controller.js +11 -6
- package/dist/nodefony/controller/SessionAuthController.js +3 -3
- package/dist/nodefony/controller/TokenAuthController.js +4 -4
- package/dist/nodefony/controller/TotpController.js +7 -5
- package/dist/nodefony/controller/WebAuthnController.js +4 -4
- package/dist/nodefony/decorators/routerDecorators.js +41 -35
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -5
- package/dist/nodefony/service/router.js +78 -26
- package/dist/nodefony/src/Controller.js +184 -78
- package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
- package/dist/nodefony/src/KernelAdminApi.js +49 -49
- package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
- package/dist/nodefony/src/Resolver.js +221 -65
- package/dist/nodefony/src/Route.js +62 -39
- package/dist/nodefony/src/SyslogAdminApi.js +4 -4
- package/dist/nodefony/src/docsReader.js +49 -39
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/singletonGuard.js +76 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -1
- package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +2 -2
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -1
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
- package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
- package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
- package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
- package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
- package/dist/types/nodefony/service/router.d.ts +33 -7
- package/dist/types/nodefony/src/Controller.d.ts +97 -24
- package/dist/types/nodefony/src/Resolver.d.ts +155 -16
- package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
- package/dist/types/nodefony/src/Route.d.ts +39 -23
- package/dist/types/nodefony/src/docsReader.d.ts +19 -9
- package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
- package/docs/admin.md +1 -1
- package/docs/controller.md +139 -121
- package/docs/decorateurs.md +102 -98
- package/docs/idempotence.md +23 -21
- package/docs/index.md +1 -1
- package/docs/routing.md +60 -53
- package/docs/templates.md +14 -14
- package/package.json +10 -11
package/README.md
CHANGED
|
@@ -6,13 +6,16 @@ sert **HTTP et WebSocket dans le même contexte**, nativement.
|
|
|
6
6
|
|
|
7
7
|
> Docs IA (dans le dépôt) : [`CLAUDE.md`](https://github.com/nodefony/nodefony-core/blob/main/src/packages/@nodefony/framework/CLAUDE.md) (instructions session) · [`MEMORY.md`](https://github.com/nodefony/nodefony-core/blob/main/src/packages/@nodefony/framework/MEMORY.md)
|
|
8
8
|
> (internals concis) · [`docs/`](./docs) (concepts). `@nodefony/http` ne peut PAS importer
|
|
9
|
-
> ce module (dépendance circulaire) — l'accès se fait via `
|
|
9
|
+
> ce module (dépendance circulaire) — l'accès se fait via `context.resolver`, typé par le contrat `IRouteResolver` (défini par http, implémenté par framework).
|
|
10
10
|
|
|
11
11
|
## Rôle
|
|
12
12
|
|
|
13
13
|
- **Router** : matching de routes (path + méthode + domaine), `Route.match()`.
|
|
14
14
|
- **Resolver** : résout route → controller → action, injecte les paramètres décorés.
|
|
15
|
-
- **Controller** : classe de base ; cycle `initialize()` → action ; HTTP + WS.
|
|
15
|
+
- **Controller** : classe de base ; cycle `initialize()` → action ; HTTP + WS. Singleton par défaut :
|
|
16
|
+
une instance partagée par toutes les requêtes, sans état sur `this`, qui ne peut donc pas injecter
|
|
17
|
+
un service `@injectable({ scope: "request" })` — refusé au démarrage. `@Scope("request")` rend une
|
|
18
|
+
instance par requête (par connexion en WebSocket). `RealtimeController` est par connexion.
|
|
16
19
|
- **Décorateurs** : `@controller`, `@route`, `@Get/@Post/...`, `@Param/@Body/@Query/@Headers`,
|
|
17
20
|
`@Domain`, etc.
|
|
18
21
|
- **Data plane admin** : `AdminApiController` + `IAdminBroker` (surface `/nodefony/<module>/api/*`).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.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.152.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.152.0/helpers/esm/decorateMetadata.js";
|
|
11
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.152.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";
|
|
@@ -87,7 +87,7 @@ let Framework = class Framework extends Module {
|
|
|
87
87
|
* silencieux (le WARNING annonce la dégradation).
|
|
88
88
|
*/
|
|
89
89
|
async onKernelBoot() {
|
|
90
|
-
const configured = this.options
|
|
90
|
+
const configured = this.options.idempotency.store;
|
|
91
91
|
let name = configured;
|
|
92
92
|
let reason = `store explicitement configuré ("${configured}")`;
|
|
93
93
|
if (name === AUTO_STORE) {
|
|
@@ -167,7 +167,7 @@ let Framework = class Framework extends Module {
|
|
|
167
167
|
if (!broker.has("kernel")) broker.register(createKernelAdminApi(this.kernel));
|
|
168
168
|
if (!broker.has("framework")) broker.register(createFrameworkAdminApi(broker, { playground: this.kernel.environment === "development" || Boolean(this.kernel.debug) }));
|
|
169
169
|
if (this.kernel.syslog && !broker.has("syslog")) {
|
|
170
|
-
const logDir = path.resolve(process.cwd(), this.kernel.options
|
|
170
|
+
const logDir = path.resolve(process.cwd(), this.kernel.options.log?.dir ?? "logs");
|
|
171
171
|
broker.register(createSyslogAdminApi(this.kernel.syslog, {
|
|
172
172
|
logDir,
|
|
173
173
|
enableFiles: this.kernel.environment !== "production",
|
|
@@ -43,7 +43,7 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
43
43
|
*/
|
|
44
44
|
async dispatch(...args) {
|
|
45
45
|
const { status, headers, body } = await this.runAdmin(args);
|
|
46
|
-
if (this.context?.type
|
|
46
|
+
if (this.context?.type.startsWith("websocket")) {
|
|
47
47
|
if (status >= 400) {
|
|
48
48
|
const message = body?.error ?? "admin error";
|
|
49
49
|
throw new RpcError(message, -32e3, {
|
|
@@ -140,14 +140,15 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
140
140
|
}
|
|
141
141
|
const als = RequestContext.get();
|
|
142
142
|
const user = als?.user ?? null;
|
|
143
|
+
const post = this.queryPost;
|
|
143
144
|
return {
|
|
144
145
|
params,
|
|
145
146
|
query: this.query ?? {},
|
|
146
|
-
body: als?.body !== void 0 ? als.body :
|
|
147
|
+
body: als?.body !== void 0 ? als.body : post ?? null,
|
|
147
148
|
user,
|
|
148
149
|
roles: this.extractRoles(user),
|
|
149
150
|
requestId: als?.requestId,
|
|
150
|
-
idempotencyKey: resolveIdempotencyKey(als?.idempotencyKey, this.context?.request?.headers
|
|
151
|
+
idempotencyKey: resolveIdempotencyKey(als?.idempotencyKey, this.context?.request?.headers["idempotency-key"])
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
/**
|
|
@@ -18,7 +18,8 @@ let mounted = false;
|
|
|
18
18
|
* zone data plane `^/nodefony/[^/]+/api(/|$)` → **session BFF requise**. Le porteur
|
|
19
19
|
* est TOUJOURS l'utilisateur courant (`authFlow.me`), jamais un paramètre — on ne
|
|
20
20
|
* crée/révoque jamais une clé pour autrui. Montés seulement si le service `apiKeys`
|
|
21
|
-
* existe (security chargé
|
|
21
|
+
* existe (security chargé) — sans security, 404 et zéro surface ; clés
|
|
22
|
+
* désactivées, les routes répondent 503.
|
|
22
23
|
*
|
|
23
24
|
* Erreurs mappées par DUCK-TYPING sur `code` (400/409/503) — framework ne peut pas
|
|
24
25
|
* importer les classes d'erreur de security.
|
|
@@ -40,7 +41,7 @@ var ApiKeyController = class extends Controller {
|
|
|
40
41
|
scopes: body.scopes,
|
|
41
42
|
expiresInDays: body.expiresInDays
|
|
42
43
|
});
|
|
43
|
-
return this.renderJson(created, 201);
|
|
44
|
+
return await this.renderJson(created, 201);
|
|
44
45
|
} catch (e) {
|
|
45
46
|
return this.#renderApiKeyError(e);
|
|
46
47
|
}
|
|
@@ -66,7 +66,7 @@ var OAuth2Controller = class extends Controller {
|
|
|
66
66
|
session.set(VERIFIER_KEY, auth.codeVerifier);
|
|
67
67
|
session.set(PROVIDER_KEY, provider);
|
|
68
68
|
await session.save();
|
|
69
|
-
|
|
69
|
+
this.redirect(auth.url, 302);
|
|
70
70
|
}
|
|
71
71
|
/** Valide `state`, échange le `code`, ouvre la session BFF (302). */
|
|
72
72
|
async callback(provider) {
|
|
@@ -85,26 +85,31 @@ var OAuth2Controller = class extends Controller {
|
|
|
85
85
|
const code = this.#queryString("code");
|
|
86
86
|
const returnedState = this.#queryString("state");
|
|
87
87
|
const returnedIss = this.#queryString("iss");
|
|
88
|
-
if (code === null || returnedState === null || typeof expectedState !== "string" || returnedState !== expectedState || expectedProvider !== provider)
|
|
88
|
+
if (code === null || returnedState === null || typeof expectedState !== "string" || returnedState !== expectedState || expectedProvider !== provider) {
|
|
89
|
+
this.redirect(failure, 302);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
89
92
|
try {
|
|
90
93
|
const { identifier } = await svc.exchangeAndProvision(provider, code, typeof storedVerifier === "string" ? storedVerifier : null, returnedIss);
|
|
91
94
|
await flow.establishSessionFor(this.context, identifier, "oauth");
|
|
92
|
-
|
|
95
|
+
this.redirect(success, 302);
|
|
96
|
+
return;
|
|
93
97
|
} catch (error) {
|
|
94
98
|
this.log(`oauth2 callback "${provider}" : ${error.message.slice(0, 200)}`, "WARNING");
|
|
95
|
-
|
|
99
|
+
this.redirect(failure, 302);
|
|
100
|
+
return;
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
#service() {
|
|
99
104
|
const svc = this.get("oauth2");
|
|
100
|
-
return svc
|
|
105
|
+
return svc?.isEnabled() ? svc : null;
|
|
101
106
|
}
|
|
102
107
|
#flow() {
|
|
103
108
|
return this.get("authFlow") ?? null;
|
|
104
109
|
}
|
|
105
110
|
/** Lit un paramètre de query string (GET), ou `null`. */
|
|
106
111
|
#queryString(key) {
|
|
107
|
-
const v =
|
|
112
|
+
const v = this.queryGet?.[key];
|
|
108
113
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
109
114
|
}
|
|
110
115
|
};
|
|
@@ -37,11 +37,11 @@ var SessionAuthController = class extends Controller {
|
|
|
37
37
|
const body = this.queryPost ?? {};
|
|
38
38
|
try {
|
|
39
39
|
const outcome = await flow.login(this.context, body.username, body.password);
|
|
40
|
-
if (outcome.status === "mfa_required") return this.renderJson({
|
|
40
|
+
if (outcome.status === "mfa_required") return await this.renderJson({
|
|
41
41
|
mfaRequired: true,
|
|
42
42
|
methods: outcome.methods
|
|
43
43
|
}, 202);
|
|
44
|
-
return this.renderJson({ user: outcome.user });
|
|
44
|
+
return await this.renderJson({ user: outcome.user });
|
|
45
45
|
} catch (e) {
|
|
46
46
|
return this.#renderAuthError(e);
|
|
47
47
|
}
|
|
@@ -57,7 +57,7 @@ var SessionAuthController = class extends Controller {
|
|
|
57
57
|
const body = this.queryPost ?? {};
|
|
58
58
|
try {
|
|
59
59
|
const user = await flow.completeMfaLogin(this.context, body.code);
|
|
60
|
-
return this.renderJson({ user });
|
|
60
|
+
return await this.renderJson({ user });
|
|
61
61
|
} catch (e) {
|
|
62
62
|
return this.#renderAuthError(e);
|
|
63
63
|
}
|
|
@@ -16,8 +16,8 @@ function parseScope(scope) {
|
|
|
16
16
|
* - `POST /nodefony/security/api/token/refresh` — body `{refresh_token}` → rotation
|
|
17
17
|
*
|
|
18
18
|
* Montés par le module framework UNIQUEMENT si le service `tokenService` existe
|
|
19
|
-
* (module security chargé
|
|
20
|
-
*
|
|
19
|
+
* (module security chargé) — sans lui, les routes n'existent pas (404, zéro
|
|
20
|
+
* surface). JWT désactivé ou store indisponible : les routes répondent 503.
|
|
21
21
|
*
|
|
22
22
|
* Le JWT part en **réponse JSON** (Bearer), JAMAIS en cookie ni en URL (anti
|
|
23
23
|
* fuite / non-révocable). Erreurs mappées par DUCK-TYPING sur `code` (401/429) —
|
|
@@ -39,7 +39,7 @@ var TokenAuthController = class extends Controller {
|
|
|
39
39
|
const body = this.queryPost ?? {};
|
|
40
40
|
try {
|
|
41
41
|
const tokens = await svc.issueForCredentials(body.username, body.password, parseScope(body.scope), body.resource);
|
|
42
|
-
return this.renderJson(tokens);
|
|
42
|
+
return await this.renderJson(tokens);
|
|
43
43
|
} catch (e) {
|
|
44
44
|
return this.#renderAuthError(e);
|
|
45
45
|
}
|
|
@@ -51,7 +51,7 @@ var TokenAuthController = class extends Controller {
|
|
|
51
51
|
const body = this.queryPost ?? {};
|
|
52
52
|
try {
|
|
53
53
|
const tokens = await svc.refresh(body.refresh_token, body.resource);
|
|
54
|
-
return this.renderJson(tokens);
|
|
54
|
+
return await this.renderJson(tokens);
|
|
55
55
|
} catch (e) {
|
|
56
56
|
return this.#renderAuthError(e);
|
|
57
57
|
}
|
|
@@ -17,7 +17,8 @@ let mounted = false;
|
|
|
17
17
|
* plane `^/nodefony/[^/]+/api(/|$)` → **session BFF requise**. Le sujet est
|
|
18
18
|
* TOUJOURS l'utilisateur courant (`authFlow.me`), jamais un paramètre — on n'active
|
|
19
19
|
* /ne désactive jamais le 2FA d'autrui (anti-IDOR). Montés seulement si le service
|
|
20
|
-
* `totp` existe (security chargé
|
|
20
|
+
* `totp` existe (security chargé) — sans security, 404 et zéro surface ; 2FA
|
|
21
|
+
* désactivé, les routes répondent 503.
|
|
21
22
|
*/
|
|
22
23
|
var TotpController = class extends Controller {
|
|
23
24
|
constructor(context) {
|
|
@@ -38,14 +39,15 @@ var TotpController = class extends Controller {
|
|
|
38
39
|
if (!svc) return this.renderJson({ error: "2FA unavailable" }, 503);
|
|
39
40
|
const subject = await this.#currentSubject();
|
|
40
41
|
if (subject === null) return this.renderJson({ error: "Unauthorized" }, 401);
|
|
41
|
-
const code =
|
|
42
|
+
const code = this.queryPost?.code;
|
|
42
43
|
if (typeof code !== "string" || code.length === 0) return this.renderJson({ error: "Invalid code" }, 400);
|
|
44
|
+
let activation;
|
|
43
45
|
try {
|
|
44
|
-
|
|
45
|
-
return this.renderJson(activation);
|
|
46
|
+
activation = await svc.confirmEnrollment(subject, code);
|
|
46
47
|
} catch {
|
|
47
48
|
return this.renderJson({ error: "Invalid or expired code" }, 400);
|
|
48
49
|
}
|
|
50
|
+
return this.renderJson(activation);
|
|
49
51
|
}
|
|
50
52
|
/** Désactive le 2FA du porteur courant (retire secret + codes). */
|
|
51
53
|
async disable() {
|
|
@@ -66,7 +68,7 @@ var TotpController = class extends Controller {
|
|
|
66
68
|
}
|
|
67
69
|
#service() {
|
|
68
70
|
const svc = this.get("totp");
|
|
69
|
-
return svc
|
|
71
|
+
return svc?.isEnabled() ? svc : null;
|
|
70
72
|
}
|
|
71
73
|
#flow() {
|
|
72
74
|
return this.get("authFlow") ?? null;
|
|
@@ -61,7 +61,7 @@ var WebAuthnController = class extends Controller {
|
|
|
61
61
|
if (!challenge) return this.renderJson({ error: "No challenge" }, 400);
|
|
62
62
|
try {
|
|
63
63
|
const credential = await svc.verifyRegistration(this.#body().response, challenge, me.username, this.#origin());
|
|
64
|
-
return this.renderJson({
|
|
64
|
+
return await this.renderJson({
|
|
65
65
|
verified: true,
|
|
66
66
|
credentialId: credential.id
|
|
67
67
|
});
|
|
@@ -106,7 +106,7 @@ var WebAuthnController = class extends Controller {
|
|
|
106
106
|
try {
|
|
107
107
|
const { userId } = await svc.verifyAuthentication(this.#body().response, challenge, this.#origin());
|
|
108
108
|
const user = await flow.establishSessionFor(this.context, userId, "webauthn");
|
|
109
|
-
return this.renderJson({
|
|
109
|
+
return await this.renderJson({
|
|
110
110
|
verified: true,
|
|
111
111
|
user
|
|
112
112
|
});
|
|
@@ -143,7 +143,7 @@ var WebAuthnController = class extends Controller {
|
|
|
143
143
|
}
|
|
144
144
|
#service() {
|
|
145
145
|
const svc = this.get("webauthn");
|
|
146
|
-
return svc
|
|
146
|
+
return svc?.isEnabled() ? svc : null;
|
|
147
147
|
}
|
|
148
148
|
#flow() {
|
|
149
149
|
return this.get("authFlow") ?? null;
|
|
@@ -158,7 +158,7 @@ var WebAuthnController = class extends Controller {
|
|
|
158
158
|
}
|
|
159
159
|
/** Origine HTTP de la requête (validée par le service contre le rpID). */
|
|
160
160
|
#origin() {
|
|
161
|
-
const origin = this.context.request?.headers
|
|
161
|
+
const origin = this.context.request?.headers.origin;
|
|
162
162
|
return typeof origin === "string" ? origin : void 0;
|
|
163
163
|
}
|
|
164
164
|
/** Lit le challenge en session puis l'INVALIDE (usage unique, anti-replay). */
|
|
@@ -118,20 +118,19 @@ function controllers(targets) {
|
|
|
118
118
|
function controller(prefix) {
|
|
119
119
|
return function(mycontroller) {
|
|
120
120
|
mycontroller.prefix = prefix;
|
|
121
|
-
const metadata = Reflect.getMetadata(metadataKey, mycontroller)
|
|
122
|
-
if (
|
|
121
|
+
const metadata = Reflect.getMetadata(metadataKey, mycontroller) ?? {};
|
|
122
|
+
if (Object.keys(metadata).length !== 0) {
|
|
123
123
|
let hasMagic = false;
|
|
124
|
-
for (const name
|
|
125
|
-
const options = metadata[name];
|
|
124
|
+
for (const [name, options] of Object.entries(metadata)) {
|
|
126
125
|
options.prefix = prefix;
|
|
127
126
|
if (options.host === void 0) {
|
|
128
|
-
const methodDomain = Reflect.getMetadata(DOMAIN_METHOD_METADATA, mycontroller, options.classMethod);
|
|
127
|
+
const methodDomain = options.classMethod === void 0 ? void 0 : Reflect.getMetadata(DOMAIN_METHOD_METADATA, mycontroller, options.classMethod);
|
|
129
128
|
const classDomain = Reflect.getMetadata(DOMAIN_CLASS_METADATA, mycontroller);
|
|
130
129
|
const domain = methodDomain ?? classDomain;
|
|
131
130
|
if (domain) options.host = domain;
|
|
132
131
|
}
|
|
133
132
|
if (options.bypassFirewall !== true) {
|
|
134
|
-
const methodBypass = Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, mycontroller, options.classMethod);
|
|
133
|
+
const methodBypass = options.classMethod === void 0 ? void 0 : Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, mycontroller, options.classMethod);
|
|
135
134
|
const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
|
|
136
135
|
if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
|
|
137
136
|
}
|
|
@@ -184,7 +183,7 @@ function route(name, options) {
|
|
|
184
183
|
} catch (error) {
|
|
185
184
|
filePath = error;
|
|
186
185
|
}
|
|
187
|
-
const metadata = Reflect.getMetadata(metadataKey, target.constructor)
|
|
186
|
+
const metadata = Reflect.getMetadata(metadataKey, target.constructor) ?? {};
|
|
188
187
|
metadata[name] = {
|
|
189
188
|
path,
|
|
190
189
|
filePath,
|
|
@@ -205,7 +204,7 @@ function route(name, options) {
|
|
|
205
204
|
function extractControllerFilePath(stackTrace) {
|
|
206
205
|
for (const line of stackTrace) {
|
|
207
206
|
const match = line.match(/\s+at file:\/\/(.*\/controllers?\/.*\.js)/);
|
|
208
|
-
if (match
|
|
207
|
+
if (match?.[1]) return match[1];
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
const HTTP_CODE_METADATA = "route:httpCode";
|
|
@@ -313,7 +312,7 @@ function HttpCode(statusCode) {
|
|
|
313
312
|
*/
|
|
314
313
|
function Header(key, value) {
|
|
315
314
|
return function(target, propertyKey, descriptor) {
|
|
316
|
-
const existing = Reflect.getMetadata("route:responseHeaders", target, propertyKey)
|
|
315
|
+
const existing = Reflect.getMetadata("route:responseHeaders", target, propertyKey) ?? {};
|
|
317
316
|
existing[key] = value;
|
|
318
317
|
Reflect.defineMetadata(HEADERS_METADATA, existing, target, propertyKey);
|
|
319
318
|
return descriptor;
|
|
@@ -420,25 +419,31 @@ function BypassFirewall(target, propertyKey, descriptor) {
|
|
|
420
419
|
return descriptor;
|
|
421
420
|
}
|
|
422
421
|
/**
|
|
423
|
-
* Déclare
|
|
424
|
-
*
|
|
425
|
-
*
|
|
422
|
+
* Déclare la portée d'instanciation d'un contrôleur — pose le statique `scope`
|
|
423
|
+
* de la classe (hérité de `Controller`, défaut `"singleton"`). Lu par le
|
|
424
|
+
* constructeur de `Controller` (`new.target`) et par le `Resolver` : 0 Reflect.
|
|
426
425
|
*
|
|
427
|
-
* `@Scope("
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
426
|
+
* `@Scope("request")` : une instance par requête — par CONNEXION en
|
|
427
|
+
* WebSocket. À déclarer quand le contrôleur injecte un service de portée
|
|
428
|
+
* `request` à son constructeur (un singleton qui le tente est refusé au
|
|
429
|
+
* démarrage), prépare chaque requête dans `initialize()`, ou garde un état
|
|
430
|
+
* sur `this`.
|
|
431
|
+
*
|
|
432
|
+
* `@Scope("singleton")` redit le défaut : UNE instance partagée par toutes les
|
|
433
|
+
* requêtes (cache du Router, `initialize()` appelé une fois, à la création).
|
|
434
|
+
* **Contrat sans état** : l'action ne lit ni n'écrit aucun état de requête sur
|
|
435
|
+
* `this` — tout passe par les arguments décorés (`@Param`/`@Body`…) et les
|
|
436
|
+
* helpers, qui retrouvent la requête courante dans l'ALS. Les accesseurs
|
|
437
|
+
* d'état refusent l'écriture, et le développement refuse toute écriture sur
|
|
438
|
+
* un champ.
|
|
434
439
|
*
|
|
435
440
|
* ⚠️ Homonyme : le core `nodefony` exporte aussi `Scope` (le scope DI du
|
|
436
441
|
* `Container`) — celui-ci s'importe depuis `@nodefony/framework`.
|
|
437
442
|
*
|
|
438
443
|
* @example
|
|
439
|
-
* \@Scope("
|
|
440
|
-
* \@controller("/
|
|
441
|
-
* class
|
|
444
|
+
* \@Scope("request")
|
|
445
|
+
* \@controller("/me")
|
|
446
|
+
* class MeController extends Controller { ... }
|
|
442
447
|
*/
|
|
443
448
|
function Scope(scope) {
|
|
444
449
|
return function(target) {
|
|
@@ -517,12 +522,12 @@ function IsGranted(attribute, options) {
|
|
|
517
522
|
};
|
|
518
523
|
return function(target, propertyKey, descriptor) {
|
|
519
524
|
if (propertyKey === void 0) {
|
|
520
|
-
const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target)
|
|
525
|
+
const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) ?? [];
|
|
521
526
|
existing.push(clause);
|
|
522
527
|
Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target);
|
|
523
528
|
return target;
|
|
524
529
|
}
|
|
525
|
-
const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey)
|
|
530
|
+
const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey) ?? [];
|
|
526
531
|
existing.push(clause);
|
|
527
532
|
Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target, propertyKey);
|
|
528
533
|
return descriptor;
|
|
@@ -573,12 +578,12 @@ function RequireScope(scope) {
|
|
|
573
578
|
const clause = { anyOf: Array.isArray(scope) ? [...scope] : [scope] };
|
|
574
579
|
return function(target, propertyKey, descriptor) {
|
|
575
580
|
if (propertyKey === void 0) {
|
|
576
|
-
const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target)
|
|
581
|
+
const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) ?? [];
|
|
577
582
|
existing.push(clause);
|
|
578
583
|
Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target);
|
|
579
584
|
return target;
|
|
580
585
|
}
|
|
581
|
-
const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey)
|
|
586
|
+
const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey) ?? [];
|
|
582
587
|
existing.push(clause);
|
|
583
588
|
Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target, propertyKey);
|
|
584
589
|
return descriptor;
|
|
@@ -593,9 +598,9 @@ function mergeCspDirectives(a, b) {
|
|
|
593
598
|
const out = {};
|
|
594
599
|
for (const src of [a, b]) {
|
|
595
600
|
if (!src) continue;
|
|
596
|
-
for (const name
|
|
601
|
+
for (const [name, values] of Object.entries(src)) {
|
|
597
602
|
const list = out[name] ??= [];
|
|
598
|
-
for (const v of
|
|
603
|
+
for (const v of values) if (!list.includes(v)) list.push(v);
|
|
599
604
|
}
|
|
600
605
|
}
|
|
601
606
|
return out;
|
|
@@ -695,7 +700,7 @@ function Idempotent(options) {
|
|
|
695
700
|
function paramDecoratorFactory(source) {
|
|
696
701
|
return function(key) {
|
|
697
702
|
return function(target, propertyKey, parameterIndex) {
|
|
698
|
-
const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey)
|
|
703
|
+
const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey) ?? [];
|
|
699
704
|
existing.push({
|
|
700
705
|
source,
|
|
701
706
|
key,
|
|
@@ -746,11 +751,12 @@ const Query = paramDecoratorFactory("query");
|
|
|
746
751
|
* ```
|
|
747
752
|
*/
|
|
748
753
|
function Body(keyOrOptions) {
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
const
|
|
754
|
+
const raw = keyOrOptions;
|
|
755
|
+
const isOptions = typeof raw === "object" && raw !== null;
|
|
756
|
+
const key = isOptions ? void 0 : raw ?? void 0;
|
|
757
|
+
const stream = isOptions ? raw.stream === true : false;
|
|
752
758
|
return function(target, propertyKey, parameterIndex) {
|
|
753
|
-
const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey)
|
|
759
|
+
const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey) ?? [];
|
|
754
760
|
const meta = {
|
|
755
761
|
source: "body",
|
|
756
762
|
key,
|
|
@@ -834,7 +840,7 @@ function routeExpectsBodyStream(routeDef) {
|
|
|
834
840
|
let flag = false;
|
|
835
841
|
const ctor = routeDef.controller;
|
|
836
842
|
const method = routeDef.classMethod;
|
|
837
|
-
if (ctor && method) flag = (Reflect.getMetadata("route:paramArgs", ctor.prototype, method)
|
|
843
|
+
if (ctor && method) flag = (Reflect.getMetadata("route:paramArgs", ctor.prototype, method) ?? []).some((m) => m.source === "body" && m.stream === true);
|
|
838
844
|
routeDef.bodyStream = flag;
|
|
839
845
|
}
|
|
840
846
|
return routeDef.bodyStream;
|
|
@@ -963,7 +969,7 @@ function computeActionMeta(ctor, method) {
|
|
|
963
969
|
* (pas d'import `Route` → 0 cycle).
|
|
964
970
|
*/
|
|
965
971
|
function resolveActionMeta(routeDef) {
|
|
966
|
-
|
|
972
|
+
routeDef.actionMeta ??= computeActionMeta(routeDef.controller, routeDef.classMethod);
|
|
967
973
|
return routeDef.actionMeta;
|
|
968
974
|
}
|
|
969
975
|
//#endregion
|
|
@@ -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.152.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.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.152.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.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";
|
|
@@ -96,7 +96,7 @@ let MemoryIdempotencyStore = class MemoryIdempotencyStore extends Service {
|
|
|
96
96
|
const entries = this.entries;
|
|
97
97
|
if (entries === null) return;
|
|
98
98
|
const existing = entries.get(key);
|
|
99
|
-
if (existing
|
|
99
|
+
if (existing?.kind !== "in-flight") return;
|
|
100
100
|
entries.set(key, {
|
|
101
101
|
kind: "done",
|
|
102
102
|
fingerprint: existing.fingerprint,
|
|
@@ -108,8 +108,7 @@ let MemoryIdempotencyStore = class MemoryIdempotencyStore extends Service {
|
|
|
108
108
|
abort(key) {
|
|
109
109
|
const entries = this.entries;
|
|
110
110
|
if (entries === null) return;
|
|
111
|
-
|
|
112
|
-
if (existing !== void 0 && existing.kind === "in-flight") entries.delete(key);
|
|
111
|
+
if (entries.get(key)?.kind === "in-flight") entries.delete(key);
|
|
113
112
|
}
|
|
114
113
|
/**
|
|
115
114
|
* Borne la taille : purge passive des expirées rencontrées, puis éviction FIFO
|