@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.
Files changed (76) hide show
  1. package/README.md +4 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +15 -20
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  6. package/dist/nodefony/controller/AdminApiController.js +45 -11
  7. package/dist/nodefony/controller/ApiKeyController.js +18 -10
  8. package/dist/nodefony/controller/BenchController.js +1 -2
  9. package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
  10. package/dist/nodefony/controller/LoginPageController.js +117 -0
  11. package/dist/nodefony/controller/OAuth2Controller.js +133 -17
  12. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
  13. package/dist/nodefony/controller/SessionAuthController.js +50 -13
  14. package/dist/nodefony/controller/TokenAuthController.js +14 -8
  15. package/dist/nodefony/controller/TotpController.js +7 -8
  16. package/dist/nodefony/controller/WebAuthnController.js +28 -12
  17. package/dist/nodefony/controller/oauthAuthority.js +1 -2
  18. package/dist/nodefony/decorators/routerDecorators.js +268 -65
  19. package/dist/nodefony/service/AdminBroker.js +4 -6
  20. package/dist/nodefony/service/Eta.js +1 -2
  21. package/dist/nodefony/service/IdempotencyStore.js +4 -6
  22. package/dist/nodefony/service/router.js +97 -34
  23. package/dist/nodefony/src/Controller.js +71 -43
  24. package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
  25. package/dist/nodefony/src/KernelAdminApi.js +82 -18
  26. package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
  27. package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
  28. package/dist/nodefony/src/Resolver.js +105 -36
  29. package/dist/nodefony/src/ResourceController.js +2 -3
  30. package/dist/nodefony/src/Route.js +54 -8
  31. package/dist/nodefony/src/SecurityApiController.js +23 -0
  32. package/dist/nodefony/src/SyslogAdminApi.js +1 -2
  33. package/dist/nodefony/src/Template.js +1 -2
  34. package/dist/nodefony/src/configMutation.js +1 -2
  35. package/dist/nodefony/src/docsReader.js +39 -23
  36. package/dist/nodefony/src/idempotency.js +14 -22
  37. package/dist/nodefony/src/idempotencyGc.js +1 -2
  38. package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
  39. package/dist/nodefony/src/loginPage.js +430 -0
  40. package/dist/nodefony/src/requireAuthentication.js +35 -0
  41. package/dist/nodefony/src/routeSchema.js +81 -0
  42. package/dist/nodefony/src/scopeCatalog.js +1 -2
  43. package/dist/nodefony/src/singletonGuard.js +1 -2
  44. package/dist/nodefony/src/syslogFilters.js +1 -2
  45. package/dist/types/index.d.ts +8 -12
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
  47. package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
  48. package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
  49. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
  50. package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
  51. package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
  52. package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
  53. package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
  54. package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
  55. package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
  56. package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
  57. package/dist/types/nodefony/service/router.d.ts +40 -1
  58. package/dist/types/nodefony/src/Controller.d.ts +54 -17
  59. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  60. package/dist/types/nodefony/src/Resolver.d.ts +44 -1
  61. package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
  62. package/dist/types/nodefony/src/Route.d.ts +21 -1
  63. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
  64. package/dist/types/nodefony/src/docsReader.d.ts +10 -6
  65. package/dist/types/nodefony/src/idempotency.d.ts +12 -10
  66. package/dist/types/nodefony/src/loginPage.d.ts +144 -0
  67. package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
  68. package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
  69. package/docs/admin.md +83 -78
  70. package/docs/controller.md +112 -111
  71. package/docs/decorateurs.md +212 -163
  72. package/docs/idempotence.md +137 -135
  73. package/docs/routing.md +96 -95
  74. package/docs/templates.md +55 -55
  75. package/package.json +10 -13
  76. 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
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
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 __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
- 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, Kernel, Module, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices, services } from "nodefony";
33
- import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
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", runNeedsExternalServices(this.kernel));
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(runNeedsExternalServices(this.kernel)), "WARNING");
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")) mountSessionAuthRoutes(this);
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
- ]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Framework);
204
- const graphql = {
205
- mergeSchemas,
206
- makeExecutableSchema,
207
- mergeResolvers,
208
- mergeTypeDefs
209
- };
205
+ ])], Framework);
210
206
  var framework_default = Framework;
211
- //#endregion
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
- //#region nodefony/config/config.ts
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
- //#region nodefony/config/defineModuleConfig.ts
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
- //#region nodefony/controller/AdminApiController.ts
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.startsWith("websocket")) {
78
+ if (this.context?.type === "websocket") {
47
79
  if (status >= 400) {
48
80
  const message = body?.error ?? "admin error";
49
- throw new RpcError(message, -32e3, {
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(request.user),
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: Boolean((this.context?.type)?.startsWith("websocket")),
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.queryPost;
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
- //#endregion
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
- //#region nodefony/controller/ApiKeyController.ts
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 Controller {
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 subject = await this.#currentSubject();
36
- if (subject === null) return this.renderJson({ error: "Unauthorized" }, 401);
37
- const body = this.queryPost ?? {};
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(subject, "user", {
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
- return me && typeof me.username === "string" ? me.username : null;
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
- //#region nodefony/controller/BenchController.ts
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
- //#region nodefony/controller/IssuerMetadataController.ts
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 };