@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.6

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 (81) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +29 -26
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +4 -2
  7. package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
  8. package/dist/nodefony/service/error-renderer.js +6 -4
  9. package/dist/nodefony/service/http-kernel.js +112 -34
  10. package/dist/nodefony/service/pretty-request-logger.js +1 -1
  11. package/dist/nodefony/service/request-logger.js +1 -1
  12. package/dist/nodefony/service/reverse-proxy.js +7 -2
  13. package/dist/nodefony/service/servers/clientError.js +28 -4
  14. package/dist/nodefony/service/servers/server-h2c.js +5 -5
  15. package/dist/nodefony/service/servers/server-http.js +5 -3
  16. package/dist/nodefony/service/servers/server-https.js +12 -5
  17. package/dist/nodefony/service/servers/server-static.js +1 -6
  18. package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
  19. package/dist/nodefony/service/servers/server-websocket.js +6 -10
  20. package/dist/nodefony/service/servers/serverShutdown.js +63 -4
  21. package/dist/nodefony/service/sessions/sessions-service.js +3 -3
  22. package/dist/nodefony/service/upload/upload-service.js +2 -2
  23. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  24. package/dist/nodefony/src/context/http/ByteStream.js +359 -0
  25. package/dist/nodefony/src/context/http/HttpContext.js +55 -26
  26. package/dist/nodefony/src/context/http/Request.js +15 -6
  27. package/dist/nodefony/src/context/http/Response.js +354 -35
  28. package/dist/nodefony/src/context/http/SseStream.js +81 -146
  29. package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
  30. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  31. package/dist/nodefony/src/context/http2/Request.js +1 -1
  32. package/dist/nodefony/src/context/http2/Response.js +8 -4
  33. package/dist/nodefony/src/context/transport.js +20 -1
  34. package/dist/nodefony/src/context/websocket/Response.js +4 -2
  35. package/dist/nodefony/src/context/websocket/WebsocketContext.js +71 -14
  36. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  37. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  38. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  39. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  40. package/dist/nodefony/src/cookies/cookie.js +54 -13
  41. package/dist/nodefony/src/profiler/Profiler.js +14 -1
  42. package/dist/nodefony/src/proxy/forward.js +109 -9
  43. package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
  44. package/dist/nodefony/src/proxy/rules.js +29 -14
  45. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  46. package/dist/types/index.d.ts +6 -0
  47. package/dist/types/nodefony/config/config.d.ts +4 -0
  48. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  49. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
  50. package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
  51. package/dist/types/nodefony/service/http-kernel.d.ts +15 -2
  52. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  53. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
  54. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  55. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  56. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +45 -3
  57. package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
  58. package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
  59. package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
  60. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  61. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  62. package/dist/types/nodefony/src/context/http2/Response.d.ts +5 -3
  63. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  64. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +40 -0
  65. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  66. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  67. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  68. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  69. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  70. package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
  71. package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
  72. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  73. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  74. package/docs/byte-stream.md +371 -0
  75. package/docs/cookies.md +13 -3
  76. package/docs/index.md +8 -0
  77. package/docs/reverse-proxy.md +15 -3
  78. package/docs/servers.md +26 -15
  79. package/docs/session.md +3 -3
  80. package/docs/sse.md +27 -20
  81. package/package.json +16 -16
@@ -1,9 +1,10 @@
1
- import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
3
1
  import { handleClientError } from "./clientError.js";
2
+ import { routeSocketTimeout } from "../../src/context/http/HttpContext.js";
4
3
  import { createDrainTerminator } from "./serverShutdown.js";
5
4
  import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
6
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
5
+ import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
6
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
7
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
7
8
  import { Inject, Service, extend, nodefonyError } from "nodefony";
8
9
  import http from "node:http";
9
10
  // nodefony/service/servers/server-http.ts
@@ -41,6 +42,7 @@ let ServerHttp = class ServerHttp extends Service {
41
42
  this.server = http.createServer(opt);
42
43
  this.httpTerminator = createDrainTerminator(this.server, this.options.shutdownTimeout);
43
44
  if (this.options.maxHeadersCount) this.server.maxHeadersCount = this.options.maxHeadersCount;
45
+ this.server.on("timeout", routeSocketTimeout);
44
46
  if (this.options.timeout) {
45
47
  this.server.setTimeout(this.options.timeout, () => {
46
48
  this.fire("onTimeout", this);
@@ -1,10 +1,11 @@
1
- import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
3
- import { handleClientError, handleSessionError } from "./clientError.js";
4
1
  import { createDrainTerminator } from "./serverShutdown.js";
5
- import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
6
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
7
2
  import { http2ServerLimits, setHttp2SessionTimeout } from "../../src/servers/http2Limits.js";
3
+ import { handleClientError, handleSessionError } from "./clientError.js";
4
+ import { routeSocketTimeout } from "../../src/context/http/HttpContext.js";
5
+ import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
6
+ import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
7
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
8
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
8
9
  import { Inject, Service, extend, nodefonyError } from "nodefony";
9
10
  import http2 from "node:http2";
10
11
  import https from "node:https";
@@ -57,6 +58,7 @@ let ServerHttps = class ServerHttps extends Service {
57
58
  const server = https.createServer(opt);
58
59
  this.server = server;
59
60
  this.httpTerminator = this.terminator();
61
+ server.on("timeout", routeSocketTimeout);
60
62
  if (this.options.timeout) {
61
63
  server.setTimeout(this.options.timeout, () => {
62
64
  this.fire("onTimeout", this);
@@ -106,6 +108,7 @@ let ServerHttps = class ServerHttps extends Service {
106
108
  Object.assign(opt, http2ServerLimits(this.module.options.http2));
107
109
  this.server = http2.createSecureServer(opt);
108
110
  this.httpTerminator = this.terminator();
111
+ this.server.on("timeout", routeSocketTimeout);
109
112
  setHttp2SessionTimeout(this.server, this.options.timeout, () => {
110
113
  this.fire("onTimeout", this);
111
114
  });
@@ -129,6 +132,10 @@ let ServerHttps = class ServerHttps extends Service {
129
132
  this.server.on("sessionError", (error) => {
130
133
  handleSessionError(error, (e) => this.log(e, "ERROR", "HTTP2 Server sessionError"));
131
134
  });
135
+ this.server.on("clientError", (e, socket) => {
136
+ this.fire("onClientError", e, socket);
137
+ handleClientError(e, socket);
138
+ });
132
139
  this.server.on("streamError", (error) => {
133
140
  this.log(error, "ERROR", "HTTP2 Server streamError");
134
141
  });
@@ -6,7 +6,6 @@ import http2 from "node:http2";
6
6
  import { URL } from "node:url";
7
7
  import { existsSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import mime from "mime-types";
10
9
  import tls from "node:tls";
11
10
  import serveStatic from "serve-static";
12
11
  // nodefony/service/servers/server-static.ts
@@ -174,11 +173,7 @@ var Statics = class extends Service {
174
173
  break;
175
174
  }
176
175
  }
177
- for (const ele of Object.values(this.servers)) {
178
- await this.getStatic(ele, request, response);
179
- const type = mime.lookup(pathname);
180
- response.setHeader("Content-Type", type);
181
- }
176
+ for (const ele of Object.values(this.servers)) await this.getStatic(ele, request, response);
182
177
  return Promise.resolve(response);
183
178
  }
184
179
  };
@@ -1,10 +1,10 @@
1
- import { startHeartbeat } from "./wsHeartbeat.js";
2
1
  import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
3
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
5
- import { attachUpgradeDispatch } from "../../src/servers/upgradeDispatch.js";
2
+ import { startHeartbeat } from "./wsHeartbeat.js";
3
+ import { attachUpgradeDispatch, createNodefonyWebSocketServer } from "../../src/servers/upgradeDispatch.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
5
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
6
6
  import { Inject, Service } from "nodefony";
7
- import Ws, { WebSocketServer } from "ws";
7
+ import Ws from "ws";
8
8
  // nodefony/service/servers/server-websocket-secure.ts
9
9
  let WebsocketSecure = class WebsocketSecure extends Service {
10
10
  httpKernel;
@@ -45,11 +45,7 @@ let WebsocketSecure = class WebsocketSecure extends Service {
45
45
  this.family = this.infos.family;
46
46
  this.protocol = serverHttps.protocol;
47
47
  }
48
- this.server = new WebSocketServer({
49
- ...this.options,
50
- noServer: true,
51
- clientTracking: true
52
- });
48
+ this.server = createNodefonyWebSocketServer(this.options);
53
49
  this.#detachUpgrade = attachUpgradeDispatch(serverHttps.server, this.server, () => this.httpKernel.reverseProxy, (req) => this.httpKernel.websocketUpgradeRefusal(req), (req, admission) => {
54
50
  this.httpKernel.onWebsocketUpgrade(req, admission, this.type).catch(() => {});
55
51
  });
@@ -1,10 +1,10 @@
1
- import { startHeartbeat } from "./wsHeartbeat.js";
2
1
  import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
3
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
5
- import { attachUpgradeDispatch } from "../../src/servers/upgradeDispatch.js";
2
+ import { startHeartbeat } from "./wsHeartbeat.js";
3
+ import { attachUpgradeDispatch, createNodefonyWebSocketServer } from "../../src/servers/upgradeDispatch.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
5
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
6
6
  import { Inject, Service } from "nodefony";
7
- import Ws, { WebSocketServer } from "ws";
7
+ import Ws from "ws";
8
8
  // nodefony/service/servers/server-websocket.ts
9
9
  let Websocket = class Websocket extends Service {
10
10
  httpKernel;
@@ -45,11 +45,7 @@ let Websocket = class Websocket extends Service {
45
45
  this.family = this.infos.family;
46
46
  this.protocol = serverHttp.protocol;
47
47
  }
48
- this.server = new WebSocketServer({
49
- ...this.options,
50
- noServer: true,
51
- clientTracking: true
52
- });
48
+ this.server = createNodefonyWebSocketServer(this.options);
53
49
  this.#detachUpgrade = attachUpgradeDispatch(serverHttp.server, this.server, () => this.httpKernel.reverseProxy, (req) => this.httpKernel.websocketUpgradeRefusal(req), (req, admission) => {
54
50
  this.httpKernel.onWebsocketUpgrade(req, admission, this.type).catch(() => {});
55
51
  });
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { constants } from "node:http2";
2
3
  import https from "node:https";
3
4
  import tls from "node:tls";
4
5
  // nodefony/service/servers/serverShutdown.ts
@@ -6,9 +7,11 @@ import tls from "node:tls";
6
7
  * Fabrique l'arrêt gracieux d'un serveur HTTP/1.1, HTTPS, HTTP/2 sur TLS ou h2c.
7
8
  *
8
9
  * Au `terminate()` (SIGTERM, `docker stop`), le serveur cesse d'accepter, puis :
9
- * - **HTTP/2** : chaque session reçoit un GOAWAY (`session.close()`) — les flux
10
- * en vol se terminent, les nouveaux sont refusés, et le client sait qu'il doit
11
- * rouvrir ailleurs au lieu de constater une coupure ;
10
+ * - **HTTP/2** : chaque session reçoit un GOAWAY en deux temps — d'abord à
11
+ * `2^31-1` ({@link announceShutdown}), puis, un aller-retour plus tard, le
12
+ * définitif (`session.close()`). Les flux en vol se terminent, les nouveaux
13
+ * sont refusés, et le client sait qu'il doit rouvrir ailleurs au lieu de
14
+ * constater une coupure ;
12
15
  * - **HTTP/1.1** : la réponse en vol part avec `Connection: close`, la socket
13
16
  * inactive est fermée tout de suite, une requête qui arrive encore sur une
14
17
  * socket gardée ouverte reçoit aussi `Connection: close`.
@@ -59,6 +62,9 @@ function createDrainTerminator(server, shutdownTimeout) {
59
62
  if (sessions !== null) for (const s of sessions) s.destroy();
60
63
  for (const socket of sockets) socket.destroy();
61
64
  }, timeout);
65
+ announceShutdown(sessions, Math.min(timeout, ANNOUNCE_CAP_MS), () => closeAll(resolve, deadline));
66
+ });
67
+ const closeAll = (resolve, deadline) => {
62
68
  server.close(() => {
63
69
  clearTimeout(deadline);
64
70
  resolve();
@@ -72,12 +78,65 @@ function createDrainTerminator(server, shutdownTimeout) {
72
78
  else if (!response.headersSent) response.setHeader("connection", "close");
73
79
  }
74
80
  }
75
- });
81
+ };
76
82
  return { terminate() {
77
83
  terminating ??= drain();
78
84
  return terminating;
79
85
  } };
80
86
  }
87
+ /** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas l'arrêt. */
88
+ const ANNOUNCE_CAP_MS = 1e3;
89
+ /** Plus grand identifiant de flux (RFC 9113 §6.8) : « aucun flux n'est encore écarté ». */
90
+ const LAST_POSSIBLE_STREAM = 2 ** 31 - 1;
91
+ /**
92
+ * Annonce l'arrêt à chaque session HTTP/2 SANS écarter un flux déjà en route :
93
+ * un PING mesure l'aller-retour, puis GOAWAY à `2^31-1`, puis l'attente de cet
94
+ * aller-retour — après elle, tout flux ouvert par le client avant l'annonce est
95
+ * arrivé. Le GOAWAY définitif (`session.close()`, dernier flux traité) suit
96
+ * dans `done`. Un pair qui ne répond pas au PING est réglé au délai `cap`.
97
+ * RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last stream
98
+ * identifier set to 2^31-1 […] After allowing time for any in-flight stream
99
+ * creation (at least one round-trip time) ». Sans annonce, un flux croisé avec
100
+ * le GOAWAY est refusé alors qu'il aurait pu être servi.
101
+ *
102
+ * @param sessions - sessions HTTP/2 ouvertes, `null` pour un serveur HTTP/1.1
103
+ * @param cap - délai maximal d'attente des échos (ms)
104
+ * @param done - appelé UNE fois, quand tous les échos sont revenus ou au délai
105
+ */
106
+ function announceShutdown(sessions, cap, done) {
107
+ let pending = 0;
108
+ let finished = false;
109
+ const finish = () => {
110
+ if (finished) return;
111
+ finished = true;
112
+ clearTimeout(timer);
113
+ done();
114
+ };
115
+ const timer = setTimeout(finish, cap);
116
+ if (sessions !== null) for (const session of sessions) {
117
+ if (session.closed || session.destroyed) continue;
118
+ let settled = false;
119
+ let wait = null;
120
+ const settle = () => {
121
+ if (settled) return;
122
+ settled = true;
123
+ if (wait !== null) clearTimeout(wait);
124
+ session.removeListener("close", settle);
125
+ if (--pending === 0) finish();
126
+ };
127
+ pending++;
128
+ session.once("close", settle);
129
+ if (!session.ping((error, rttMs) => {
130
+ if (error !== null || settled || session.closed) return settle();
131
+ session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
132
+ wait = setTimeout(settle, Math.max(1, Math.ceil(rttMs)));
133
+ })) {
134
+ session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
135
+ settle();
136
+ }
137
+ }
138
+ if (pending === 0) finish();
139
+ }
81
140
  function track(set, socket) {
82
141
  set.add(socket);
83
142
  socket.once("close", () => set.delete(socket));
@@ -1,9 +1,9 @@
1
- import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
3
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
4
1
  import Session from "../../src/session/session.js";
5
2
  import MemorySessionStorage from "../../src/session/storage/MemorySessionStorage.js";
6
3
  import RevocationGuardStorage from "../../src/session/storage/RevocationGuardStorage.js";
4
+ import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
5
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
6
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
7
7
  import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Inject, Nodefony, RequestContext, Service, countFacets, extend, injectable, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
8
8
  import { createHash, createHmac } from "node:crypto";
9
9
  // nodefony/service/sessions/sessions-service.ts
@@ -1,5 +1,5 @@
1
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
2
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
1
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
3
3
  import { Cli, FileClass, Inject, Service } from "nodefony";
4
4
  import fs from "node:fs";
5
5
  import fsp from "node:fs/promises";
@@ -1,3 +1,4 @@
1
+ import { cspNonceMetaTag } from "nodefony";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  // nodefony/src/assets/prebuiltUi.ts
@@ -67,8 +68,8 @@ function resolveUiDelivery(opts) {
67
68
  * requête. Le SPA fallback reste au controller du module (routes littérales,
68
69
  * cf. StudioController).
69
70
  *
70
- * Perf : l'index est lu UNE fois (lazy, caché) ; seule l'injection du nonce
71
- * coûte un `replaceAll` par rendu — route d'entrée UI, jamais un hot path.
71
+ * Perf : l'index est lu UNE fois (lazy, caché) et découpé UNE fois autour des
72
+ * emplacements du nonce ; un rendu ne coûte qu'un `join`.
72
73
  */
73
74
  var PrebuiltUi = class {
74
75
  publicPath;
@@ -76,6 +77,8 @@ var PrebuiltUi = class {
76
77
  indexPath;
77
78
  /** Template HTML caché — `null` tant que non lu (lazy). */
78
79
  template = null;
80
+ /** Index découpé autour des emplacements du nonce — `null` tant que non calculé. */
81
+ nonceParts = null;
79
82
  constructor(opts) {
80
83
  let p = opts.publicPath.trim();
81
84
  if (!p.startsWith("/")) p = `/${p}`;
@@ -107,8 +110,11 @@ var PrebuiltUi = class {
107
110
  * Document HTML d'entrée de l'UI (index pré-buildé par Vite).
108
111
  *
109
112
  * @param nonce nonce CSP de la requête (`Context.cspNonce`) — injecté sur
110
- * chaque `<script>` ; les assets `src` same-origin passent déjà
111
- * par `'self'`, le nonce couvre un éventuel inline Vite.
113
+ * chaque `<script>`, `src` compris : sous `'strict-dynamic'`
114
+ * (ADR-0024 §C1), `'self'` est ignoré et seul le nonce autorise
115
+ * une balise ; ce qu'elle importe ensuite hérite de la confiance.
116
+ * Annoncé aussi par `<meta property="csp-nonce">` en tête de
117
+ * `<head>` (ADR-0024 §C2) : les `<style>` créés à l'exécution.
112
118
  * @returns le HTML, ou un commentaire HTML fail-loud si l'index est absent
113
119
  */
114
120
  renderIndex(nonce) {
@@ -118,7 +124,19 @@ var PrebuiltUi = class {
118
124
  return `<!DOCTYPE html><!-- prebuilt UI index missing: ${this.indexPath} -->`;
119
125
  }
120
126
  if (!nonce) return this.template;
121
- return this.template.replaceAll("<script", `<script nonce="${nonce}"`);
127
+ this.nonceParts ??= splitAroundNonce(this.template);
128
+ return this.nonceParts.join(nonce);
122
129
  }
123
130
  };
131
+ /** Repère des emplacements du nonce : un octet nul ne figure dans aucun HTML. */
132
+ const NONCE_SLOT = "\0";
133
+ /**
134
+ * Découpe un index HTML autour des emplacements du nonce : l'attribut de chaque
135
+ * `<script>` et la balise meta posée juste après l'ouverture de `<head>` (rien
136
+ * si le document n'en a pas — les `<script>` restent signés).
137
+ */
138
+ function splitAroundNonce(template) {
139
+ const marked = template.replaceAll("<script", `<script nonce="${NONCE_SLOT}"`).replace(/<head\b[^>]*>/i, (open) => open + cspNonceMetaTag(NONCE_SLOT));
140
+ return Object.freeze(marked.split(NONCE_SLOT));
141
+ }
124
142
  export { PrebuiltUi, resolveUiDelivery };