@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
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ sert **HTTP et WebSocket dans le même contexte**, nativement.
|
|
|
18
18
|
instance par requête (par connexion en WebSocket). `RealtimeController` est par connexion.
|
|
19
19
|
- **Décorateurs** : `@controller`, `@route`, `@Get/@Post/...`, `@Param/@Body/@Query/@Headers`,
|
|
20
20
|
`@Domain`, etc.
|
|
21
|
+
- **Routes à schéma** : `@Post("/posts", { schema: { body } })` — l'entrée est validée avant
|
|
22
|
+
l'action (422 qui nomme chaque champ, sur HTTP comme par la socket) et reçue typée en un seul
|
|
23
|
+
argument, `input: RouteInput<S>`. Zod, Valibot, ArkType : tout Standard Schema. Détail :
|
|
24
|
+
[`docs/decorateurs.md`](docs/decorateurs.md).
|
|
21
25
|
- **Data plane admin** : `AdminApiController` + `IAdminBroker` (surface `/nodefony/<module>/api/*`).
|
|
22
26
|
|
|
23
27
|
## Exemple
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// \0@oxc-project+runtime@0.155.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);
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
}
|
|
8
|
-
//#endregion
|
|
9
8
|
export { __decorate as default };
|
package/dist/index.js
CHANGED
|
@@ -4,17 +4,18 @@ import { getIdempotencyStoreFactory, listIdempotencyBackends, listIdempotencySto
|
|
|
4
4
|
import { RedisIdempotencyStore } from "./nodefony/src/RedisIdempotencyStore.js";
|
|
5
5
|
import { scheduleIdempotencyGc } from "./nodefony/src/idempotencyGc.js";
|
|
6
6
|
import Route from "./nodefony/src/Route.js";
|
|
7
|
+
import { validateRouteInput } from "./nodefony/src/routeSchema.js";
|
|
7
8
|
import Controller from "./nodefony/src/Controller.js";
|
|
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
|
+
import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, buildContextParamArgs, controller, controllers, getParamArgsMeta, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
9
10
|
import Resolver from "./nodefony/src/Resolver.js";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import router_default from "./nodefony/service/router.js";
|
|
11
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
12
|
+
import router_default, { FRAMEWORK_ROUTER, isFrameworkRouter } from "./nodefony/service/router.js";
|
|
13
13
|
import ResourceController from "./nodefony/src/ResourceController.js";
|
|
14
14
|
import AdminApiController from "./nodefony/controller/AdminApiController.js";
|
|
15
15
|
import AdminBroker_default from "./nodefony/service/AdminBroker.js";
|
|
16
16
|
import IdempotencyStore_default from "./nodefony/service/IdempotencyStore.js";
|
|
17
17
|
import SessionAuthController, { mountSessionAuthRoutes } from "./nodefony/controller/SessionAuthController.js";
|
|
18
|
+
import LoginPageController, { mountLoginPage } from "./nodefony/controller/LoginPageController.js";
|
|
18
19
|
import TokenAuthController, { mountTokenAuthRoutes } from "./nodefony/controller/TokenAuthController.js";
|
|
19
20
|
import IssuerMetadataController, { mountIssuerMetadataRoutes } from "./nodefony/controller/IssuerMetadataController.js";
|
|
20
21
|
import ProtectedResourceMetadataController, { collectProtectedResources, mountProtectedResourceRoutes, protectedResourceRoutePaths } from "./nodefony/controller/ProtectedResourceMetadataController.js";
|
|
@@ -29,10 +30,8 @@ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
|
|
|
29
30
|
import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
|
|
30
31
|
import Eta from "./nodefony/service/Eta.js";
|
|
31
32
|
import path from "node:path";
|
|
32
|
-
import { AUTO_STORE, EMPTY_INFRA,
|
|
33
|
-
|
|
34
|
-
import { makeExecutableSchema, mergeSchemas } from "@graphql-tools/schema";
|
|
35
|
-
//#region index.ts
|
|
33
|
+
import { AUTO_STORE, EMPTY_INFRA, Module, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, services, storeConnectivity } from "nodefony";
|
|
34
|
+
// index.ts
|
|
36
35
|
registerIdempotencyStore("redis", (ctx) => {
|
|
37
36
|
const redis = ctx.module.kernel?.container?.get("redis");
|
|
38
37
|
if (!redis) throw new Error("the @nodefony/redis module is not loaded (add use(\"@nodefony/redis\") in nodefony.config.ts)");
|
|
@@ -91,13 +90,13 @@ let Framework = class Framework extends Module {
|
|
|
91
90
|
let name = configured;
|
|
92
91
|
let reason = `store explicitement configuré ("${configured}")`;
|
|
93
92
|
if (name === AUTO_STORE) {
|
|
94
|
-
const auto = resolveAutoStore("ephemeral", this.kernel?.infra ?? EMPTY_INFRA, listIdempotencyBackends(), "memory",
|
|
93
|
+
const auto = resolveAutoStore("ephemeral", this.kernel?.infra ?? EMPTY_INFRA, listIdempotencyBackends(), "memory", storeConnectivity(this.kernel));
|
|
95
94
|
name = auto.store;
|
|
96
95
|
reason = auto.reason;
|
|
97
96
|
this.log(`idempotency.store "auto" → "${name}" (${auto.reason})`, "INFO");
|
|
98
97
|
}
|
|
99
98
|
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). " + durableStoreRemedy(
|
|
99
|
+
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(storeConnectivity(this.kernel)), "WARNING");
|
|
101
100
|
this.kernel?.registerStoreResolution({
|
|
102
101
|
brick: "idempotency",
|
|
103
102
|
nature: "ephemeral",
|
|
@@ -176,7 +175,10 @@ let Framework = class Framework extends Module {
|
|
|
176
175
|
}
|
|
177
176
|
broker.mountAll();
|
|
178
177
|
}
|
|
179
|
-
if (this.kernel?.container?.get("authFlow"))
|
|
178
|
+
if (this.kernel?.container?.get("authFlow")) {
|
|
179
|
+
mountSessionAuthRoutes(this);
|
|
180
|
+
mountLoginPage(this);
|
|
181
|
+
}
|
|
180
182
|
if (this.kernel?.container?.get("tokenService")) {
|
|
181
183
|
mountTokenAuthRoutes(this);
|
|
182
184
|
const issuer = this.kernel.container.get("tokenService")?.publishedIssuer();
|
|
@@ -200,13 +202,6 @@ Framework = __decorate([services([
|
|
|
200
202
|
Eta,
|
|
201
203
|
AdminBroker_default,
|
|
202
204
|
IdempotencyStore_default
|
|
203
|
-
])
|
|
204
|
-
const graphql = {
|
|
205
|
-
mergeSchemas,
|
|
206
|
-
makeExecutableSchema,
|
|
207
|
-
mergeResolvers,
|
|
208
|
-
mergeTypeDefs
|
|
209
|
-
};
|
|
205
|
+
])], Framework);
|
|
210
206
|
var framework_default = Framework;
|
|
211
|
-
|
|
212
|
-
export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildPlaygroundSnapshot, collectProtectedResources, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, graphql, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountIssuerMetadataRoutes, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream };
|
|
207
|
+
export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, FRAMEWORK_ROUTER, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, LoginPageController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildContextParamArgs, buildPlaygroundSnapshot, collectProtectedResources, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, getParamArgsMeta, isFrameworkRouter, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountIssuerMetadataRoutes, mountLoginPage, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream, validateRouteInput };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
// nodefony/config/config.ts
|
|
3
3
|
/**
|
|
4
4
|
* @nodefony/framework — CONFIGURATION DU MODULE (schéma Zod = source unique).
|
|
5
5
|
*
|
|
@@ -58,5 +58,4 @@ var config_default = {
|
|
|
58
58
|
}
|
|
59
59
|
} }
|
|
60
60
|
};
|
|
61
|
-
//#endregion
|
|
62
61
|
export { config_default as default, frameworkConfigSchema };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { frameworkConfigSchema } from "./config.js";
|
|
2
2
|
import { parseModuleConfig } from "nodefony";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
|
|
4
|
+
// nodefony/config/defineModuleConfig.ts
|
|
5
5
|
/**
|
|
6
6
|
* Builder type-safe de la configuration de `@nodefony/framework` (PUR — ne
|
|
7
7
|
* retape JAMAIS un défaut : source unique = `./config.ts`).
|
|
@@ -32,5 +32,4 @@ function defineFrameworkConfig(config = {}) {
|
|
|
32
32
|
function frameworkConfigJsonSchema() {
|
|
33
33
|
return z.toJSONSchema(frameworkConfigSchema);
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { defineFrameworkConfig, frameworkConfigJsonSchema };
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import Controller from "../src/Controller.js";
|
|
2
2
|
import { computeFingerprint, evaluateIdempotency, resolveIdempotencyKey, resolveIdentity } from "../src/idempotency.js";
|
|
3
|
-
import { RequestContext, RpcError, executeAdminEndpoint } from "nodefony";
|
|
4
|
-
|
|
3
|
+
import { JsonRpcServerError, RequestContext, RpcError, executeAdminEndpoint } from "nodefony";
|
|
4
|
+
// nodefony/controller/AdminApiController.ts
|
|
5
|
+
/** Vrai pour une chaîne ou un tableau de chaînes — ce que porte {@link IAdminRequest.query}. */
|
|
6
|
+
function isAdminQueryValue(value) {
|
|
7
|
+
if (typeof value === "string") return true;
|
|
8
|
+
if (!Array.isArray(value)) return false;
|
|
9
|
+
for (const item of value) if (typeof item !== "string") return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Ramène les paramètres d'URL au contrat {@link IAdminRequest.query} : seules
|
|
14
|
+
* les chaînes et les tableaux de chaînes passent. `qs` produit des objets pour
|
|
15
|
+
* `a[b]=1` ; les laisser passer sous un type qui les nie tromperait chaque
|
|
16
|
+
* producteur du plan d'administration.
|
|
17
|
+
*
|
|
18
|
+
* @param source - paramètres d'URL de la requête (`Controller.query`)
|
|
19
|
+
* @returns `source` lui-même quand il respecte déjà le contrat (aucune
|
|
20
|
+
* allocation), sinon une copie sans les valeurs imbriquées
|
|
21
|
+
*/
|
|
22
|
+
function toAdminQuery(source) {
|
|
23
|
+
if (source === void 0) return {};
|
|
24
|
+
let conforms = true;
|
|
25
|
+
for (const key of Object.keys(source)) if (!isAdminQueryValue(source[key])) {
|
|
26
|
+
conforms = false;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (conforms) return source;
|
|
30
|
+
const kept = [];
|
|
31
|
+
for (const key of Object.keys(source)) {
|
|
32
|
+
const value = source[key];
|
|
33
|
+
if (isAdminQueryValue(value)) kept.push([key, value]);
|
|
34
|
+
}
|
|
35
|
+
return Object.fromEntries(kept);
|
|
36
|
+
}
|
|
5
37
|
/**
|
|
6
38
|
* Controller pont unique du data plane admin (Studio).
|
|
7
39
|
*
|
|
@@ -43,10 +75,10 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
43
75
|
*/
|
|
44
76
|
async dispatch(...args) {
|
|
45
77
|
const { status, headers, body } = await this.runAdmin(args);
|
|
46
|
-
if (this.context?.type
|
|
78
|
+
if (this.context?.type === "websocket") {
|
|
47
79
|
if (status >= 400) {
|
|
48
80
|
const message = body?.error ?? "admin error";
|
|
49
|
-
throw new RpcError(message,
|
|
81
|
+
throw new RpcError(message, JsonRpcServerError.DEFAULT, {
|
|
50
82
|
status,
|
|
51
83
|
body
|
|
52
84
|
});
|
|
@@ -84,7 +116,10 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
84
116
|
request: this.buildRequest(args),
|
|
85
117
|
requiredRole: adminRoute.role,
|
|
86
118
|
gate: (request) => this.idempotencyGate(adminRoute, request),
|
|
87
|
-
onServerError: (error) => this.log(error, "ERROR")
|
|
119
|
+
onServerError: (error) => this.log(error, "ERROR"),
|
|
120
|
+
assertCredentialTransport: () => {
|
|
121
|
+
this.get("credentialTransport")?.assert(this.context ?? {}, `admin ${name}`);
|
|
122
|
+
}
|
|
88
123
|
});
|
|
89
124
|
}
|
|
90
125
|
/**
|
|
@@ -104,14 +139,14 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
104
139
|
const store = this.get("idempotencyStore");
|
|
105
140
|
const verdict = await evaluateIdempotency({
|
|
106
141
|
store,
|
|
107
|
-
identity: resolveIdentity(
|
|
142
|
+
identity: resolveIdentity(),
|
|
108
143
|
clientKey: request.idempotencyKey,
|
|
109
144
|
fingerprint: computeFingerprint([
|
|
110
145
|
adminRoute.name,
|
|
111
146
|
request.params,
|
|
112
147
|
request.body ?? null
|
|
113
148
|
]),
|
|
114
|
-
isWs:
|
|
149
|
+
isWs: this.context?.type === "websocket",
|
|
115
150
|
required: false
|
|
116
151
|
});
|
|
117
152
|
switch (verdict.kind) {
|
|
@@ -140,10 +175,10 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
140
175
|
}
|
|
141
176
|
const als = RequestContext.get();
|
|
142
177
|
const user = als?.user ?? null;
|
|
143
|
-
const post = this.
|
|
178
|
+
const post = this.body;
|
|
144
179
|
return {
|
|
145
180
|
params,
|
|
146
|
-
query: this.query
|
|
181
|
+
query: toAdminQuery(this.query),
|
|
147
182
|
body: als?.body !== void 0 ? als.body : post ?? null,
|
|
148
183
|
user,
|
|
149
184
|
roles: this.extractRoles(user),
|
|
@@ -180,5 +215,4 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
180
215
|
return [...walker.reachableRoles(roles)];
|
|
181
216
|
}
|
|
182
217
|
};
|
|
183
|
-
|
|
184
|
-
export { AdminApiController as default };
|
|
218
|
+
export { AdminApiController as default, toAdminQuery };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import SecurityApiController from "../src/SecurityApiController.js";
|
|
3
3
|
import { collectDeclaredApiScopes } from "../src/scopeCatalog.js";
|
|
4
|
-
|
|
4
|
+
// nodefony/controller/ApiKeyController.ts
|
|
5
5
|
let mounted = false;
|
|
6
6
|
/**
|
|
7
7
|
* Endpoints HTTP de **gestion des clés API personnelles (PAT, P6.12)** — console
|
|
@@ -24,7 +24,7 @@ let mounted = false;
|
|
|
24
24
|
* Erreurs mappées par DUCK-TYPING sur `code` (400/409/503) — framework ne peut pas
|
|
25
25
|
* importer les classes d'erreur de security.
|
|
26
26
|
*/
|
|
27
|
-
var ApiKeyController = class extends
|
|
27
|
+
var ApiKeyController = class extends SecurityApiController {
|
|
28
28
|
constructor(context) {
|
|
29
29
|
super("ApiKeyController", context);
|
|
30
30
|
}
|
|
@@ -32,15 +32,15 @@ var ApiKeyController = class extends Controller {
|
|
|
32
32
|
async create() {
|
|
33
33
|
const svc = this.#manager();
|
|
34
34
|
if (!svc || !svc.isEnabled()) return this.renderJson({ error: "API keys unavailable" }, 503);
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
const body = this.
|
|
35
|
+
const holder = await this.#currentHolder();
|
|
36
|
+
if (holder === null) return this.renderJson({ error: "Unauthorized" }, 401);
|
|
37
|
+
const body = this.body ?? {};
|
|
38
38
|
try {
|
|
39
|
-
const created = await svc.createForSubject(
|
|
39
|
+
const created = await svc.createForSubject(holder.identifier, "user", {
|
|
40
40
|
name: body.name,
|
|
41
41
|
scopes: body.scopes,
|
|
42
42
|
expiresInDays: body.expiresInDays
|
|
43
|
-
});
|
|
43
|
+
}, holder.roles);
|
|
44
44
|
return await this.renderJson(created, 201);
|
|
45
45
|
} catch (e) {
|
|
46
46
|
return this.#renderApiKeyError(e);
|
|
@@ -86,10 +86,18 @@ var ApiKeyController = class extends Controller {
|
|
|
86
86
|
}
|
|
87
87
|
/** Identifiant du porteur courant (session BFF revalidée), ou `null` si non authentifié. */
|
|
88
88
|
async #currentSubject() {
|
|
89
|
+
return (await this.#currentHolder())?.identifier ?? null;
|
|
90
|
+
}
|
|
91
|
+
async #currentHolder() {
|
|
89
92
|
const flow = this.#flow();
|
|
90
93
|
if (!flow) return null;
|
|
91
94
|
const me = await flow.me(this.context);
|
|
92
|
-
|
|
95
|
+
if (!me || typeof me.identifier !== "string") return null;
|
|
96
|
+
const roles = Array.isArray(me.roles) ? me.roles.filter((r) => typeof r === "string") : [];
|
|
97
|
+
return {
|
|
98
|
+
identifier: me.identifier,
|
|
99
|
+
roles
|
|
100
|
+
};
|
|
93
101
|
}
|
|
94
102
|
#renderApiKeyError(e) {
|
|
95
103
|
const code = e.code;
|
|
@@ -97,6 +105,7 @@ var ApiKeyController = class extends Controller {
|
|
|
97
105
|
const message = e.message;
|
|
98
106
|
return this.renderJson({ error: typeof message === "string" ? message : "Bad request" }, 400);
|
|
99
107
|
}
|
|
108
|
+
if (code === 403) return this.renderJson({ error: e instanceof Error ? e.message : "Forbidden" }, 403);
|
|
100
109
|
if (code === 409) return this.renderJson({ error: "API key limit reached" }, 409);
|
|
101
110
|
if (code === 503) return this.renderJson({ error: "API keys unavailable" }, 503);
|
|
102
111
|
throw e;
|
|
@@ -149,5 +158,4 @@ function mountApiKeyRoutes(frameworkModule) {
|
|
|
149
158
|
if (!Object.prototype.hasOwnProperty.call(ApiKeyController.prototype, "module")) router_default.setController(ApiKeyController, frameworkModule);
|
|
150
159
|
mounted = true;
|
|
151
160
|
}
|
|
152
|
-
//#endregion
|
|
153
161
|
export { ApiKeyController as default, mountApiKeyRoutes };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Controller from "../src/Controller.js";
|
|
2
2
|
import router_default from "../service/router.js";
|
|
3
|
-
|
|
3
|
+
// nodefony/controller/BenchController.ts
|
|
4
4
|
/**
|
|
5
5
|
* Corps servi par la cible de banc — **gelé et partagé** : ce qu'on chronomètre
|
|
6
6
|
* est le pipeline, pas la construction d'un objet. Le figer évite qu'une
|
|
@@ -128,5 +128,4 @@ function mountBenchRoutes(frameworkModule) {
|
|
|
128
128
|
if (!Object.prototype.hasOwnProperty.call(BenchController.prototype, "module")) router_default.setController(BenchController, frameworkModule);
|
|
129
129
|
mounted = true;
|
|
130
130
|
}
|
|
131
|
-
//#endregion
|
|
132
131
|
export { BenchController as default, mountBenchRoutes };
|
|
@@ -2,7 +2,7 @@ import Controller from "../src/Controller.js";
|
|
|
2
2
|
import router_default from "../service/router.js";
|
|
3
3
|
import { askedAuthority, onDeclaredAuthority } from "./oauthAuthority.js";
|
|
4
4
|
import { JWKS_PATH, authorizationServerMetadataPath, buildAuthorizationServerMetadata } from "nodefony";
|
|
5
|
-
|
|
5
|
+
// nodefony/controller/IssuerMetadataController.ts
|
|
6
6
|
let mounted = false;
|
|
7
7
|
/**
|
|
8
8
|
* Les deux documents qui rendent une application Nodefony **découvrable** comme
|
|
@@ -144,5 +144,4 @@ function mountIssuerMetadataRoutes(frameworkModule, issuer) {
|
|
|
144
144
|
if (!Object.prototype.hasOwnProperty.call(IssuerMetadataController.prototype, "module")) router_default.setController(IssuerMetadataController, frameworkModule);
|
|
145
145
|
mounted = true;
|
|
146
146
|
}
|
|
147
|
-
//#endregion
|
|
148
147
|
export { IssuerMetadataController as default, mountIssuerMetadataRoutes };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { isDevelopment } from "../src/singletonGuard.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
|
+
import { Csp } from "../decorators/routerDecorators.js";
|
|
4
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
5
|
+
import router_default from "../service/router.js";
|
|
6
|
+
import { LOGIN_PAGE_ASSET_FILES, LOGIN_PAGE_TEMPLATE, buildLoginPageView, keepSuccess } from "../src/loginPage.js";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_SKINS, loginPageSkinFile } from "nodefony";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { Eta } from "eta";
|
|
13
|
+
// nodefony/controller/LoginPageController.ts
|
|
14
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
15
|
+
let mounted = false;
|
|
16
|
+
let engine = null;
|
|
17
|
+
let defaultTemplate = null;
|
|
18
|
+
let appTemplate = null;
|
|
19
|
+
/** Dossier des fichiers de la page (`dist/login/` du paquet `nodefony`). */
|
|
20
|
+
function assetsDir() {
|
|
21
|
+
return path.dirname(fileURLToPath(import.meta.resolve("nodefony/login.js")));
|
|
22
|
+
}
|
|
23
|
+
function eta() {
|
|
24
|
+
return engine ??= new Eta({
|
|
25
|
+
autoEscape: true,
|
|
26
|
+
useWith: false
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function templateFor(page) {
|
|
30
|
+
if (page.template === null) return Promise.resolve(defaultTemplate ??= eta().compile(LOGIN_PAGE_TEMPLATE));
|
|
31
|
+
if (appTemplate?.file !== page.template) {
|
|
32
|
+
const file = page.template;
|
|
33
|
+
appTemplate = {
|
|
34
|
+
file,
|
|
35
|
+
template: keepSuccess(() => readFile(file, "utf8").then((source) => eta().compile(source)))
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return appTemplate.template();
|
|
39
|
+
}
|
|
40
|
+
const version = keepSuccess(async () => {
|
|
41
|
+
const dir = assetsDir();
|
|
42
|
+
const hash = createHash("sha256");
|
|
43
|
+
const skins = LOGIN_PAGE_SKINS.map(loginPageSkinFile).filter((file) => file !== null);
|
|
44
|
+
for (const file of [
|
|
45
|
+
LOGIN_PAGE_ASSET_FILES.script,
|
|
46
|
+
LOGIN_PAGE_ASSET_FILES.style,
|
|
47
|
+
...skins
|
|
48
|
+
]) hash.update(await readFile(path.join(dir, file)));
|
|
49
|
+
return hash.digest("hex").slice(0, 12);
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Page de connexion par défaut (ADR-0015) : gabarit F03 rendu côté serveur,
|
|
53
|
+
* feuille `nodefony/login.css`, script `nodefony/login.js`. Aucun front, aucun
|
|
54
|
+
* build côté application.
|
|
55
|
+
*
|
|
56
|
+
* Joignable sans session ; jamais mise en cache (`no-store`) ni affichable
|
|
57
|
+
* dans un cadre (`frame-ancestors 'none'`).
|
|
58
|
+
*/
|
|
59
|
+
var LoginPageController = class extends Controller {
|
|
60
|
+
constructor(context) {
|
|
61
|
+
super("LoginPageController", context);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Rend la page de connexion.
|
|
65
|
+
*
|
|
66
|
+
* @returns la page HTML, ou 404 quand security ne la décrit plus
|
|
67
|
+
*/
|
|
68
|
+
async page() {
|
|
69
|
+
const page = this.get("authFlow")?.describeLoginPage?.() ?? null;
|
|
70
|
+
if (page === null) return this.renderJson({ error: "Not Found" }, 404, NO_STORE);
|
|
71
|
+
const query = this.query;
|
|
72
|
+
const view = buildLoginPageView(page, {
|
|
73
|
+
from: query?.from,
|
|
74
|
+
reason: query?.reason,
|
|
75
|
+
theme: query?.theme,
|
|
76
|
+
skin: query?.skin,
|
|
77
|
+
layout: query?.layout,
|
|
78
|
+
preview: isDevelopment(this.kernel?.environment),
|
|
79
|
+
nonce: this.context?.cspNonce ?? "",
|
|
80
|
+
projectName: this.kernel?.projectName ?? "Nodefony",
|
|
81
|
+
assetsVersion: await version()
|
|
82
|
+
});
|
|
83
|
+
const template = await templateFor(page);
|
|
84
|
+
return this.render(template.call(eta(), view), "utf8", 200, {
|
|
85
|
+
...NO_STORE,
|
|
86
|
+
"content-type": "text/html; charset=utf-8"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
__decorate([Csp({ "frame-ancestors": ["'none'"] })], LoginPageController.prototype, "page", null);
|
|
91
|
+
/**
|
|
92
|
+
* Monte la page de connexion par défaut quand security la décrit : la route
|
|
93
|
+
* sur `loginPage.path`, et ses fichiers sous `LOGIN_PAGE_ASSETS_BASE`.
|
|
94
|
+
*
|
|
95
|
+
* La page est le chemin vers la session : elle échappe au pare-feu, comme les
|
|
96
|
+
* routes de connexion — sinon une application qui protège `^/` exigerait
|
|
97
|
+
* d'être connecté pour se connecter.
|
|
98
|
+
*
|
|
99
|
+
* @param frameworkModule - module framework, qui porte les contrôleurs
|
|
100
|
+
*/
|
|
101
|
+
function mountLoginPage(frameworkModule) {
|
|
102
|
+
if (mounted) return;
|
|
103
|
+
const page = frameworkModule.get("authFlow")?.describeLoginPage?.() ?? null;
|
|
104
|
+
if (page === null) return;
|
|
105
|
+
if (!page.password && !page.passkey && page.providers.length === 0) frameworkModule.log(`${page.path} : aucun moyen de connexion — loginPage.password à false, passkeys coupées, aucun fournisseur opérationnel`, "WARNING");
|
|
106
|
+
router_default.createRoute("security.login.page", {
|
|
107
|
+
path: page.path,
|
|
108
|
+
constructor: LoginPageController,
|
|
109
|
+
classMethod: "page",
|
|
110
|
+
requirements: { methods: ["GET", "HEAD"] },
|
|
111
|
+
bypassFirewall: true
|
|
112
|
+
});
|
|
113
|
+
frameworkModule.get("server-static")?.addMount(LOGIN_PAGE_ASSETS_BASE, assetsDir());
|
|
114
|
+
if (!Object.prototype.hasOwnProperty.call(LoginPageController.prototype, "module")) router_default.setController(LoginPageController, frameworkModule);
|
|
115
|
+
mounted = true;
|
|
116
|
+
}
|
|
117
|
+
export { LoginPageController as default, mountLoginPage };
|