@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.3
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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/config/config.js +44 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +140 -15
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +1 -9
- package/dist/nodefony/src/context/http/Response.js +16 -1
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +96 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +5 -1
- package/docs/observabilite.md +15 -15
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +94 -60
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +23 -23
- package/package.json +8 -8
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem } from "../src/proxy/rules.js";
|
|
2
|
+
import { forwardRequest, forwardUpgrade, refuseUpgrade } from "../src/proxy/forward.js";
|
|
3
|
+
import { Service } from "nodefony";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import https from "node:https";
|
|
7
|
+
import { TLSSocket } from "node:tls";
|
|
8
|
+
//#region nodefony/service/reverse-proxy.ts
|
|
9
|
+
/** Replis si la section `proxy` manque (module construit hors configuration). */
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
11
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
|
|
12
|
+
const DEFAULT_MAX_SOCKETS = 256;
|
|
13
|
+
/**
|
|
14
|
+
* Proxy inverse de Nodefony : relaie un préfixe d'URL vers un autre serveur
|
|
15
|
+
* HTTP — requêtes ET, sur demande, upgrades WebSocket — sur la MÊME origine
|
|
16
|
+
* que la page. Le client ne voit qu'un serveur, un certificat ; l'amont peut
|
|
17
|
+
* rester sur la boucle locale.
|
|
18
|
+
*
|
|
19
|
+
* Deux sources de montages, mêmes règles (`src/proxy/rules.ts`) : la section
|
|
20
|
+
* `proxy.mounts` de la configuration, lue à la construction, et
|
|
21
|
+
* {@link ReverseProxy.mount}, appelé par un module (`@nodefony/frontend` y
|
|
22
|
+
* relaie `/_vite/<famille>/` vers ses serveurs Vite de développement). Un
|
|
23
|
+
* module se résout ce service par NOM (`container.get("reverse-proxy")`) :
|
|
24
|
+
* il n'a pas à importer `@nodefony/http`.
|
|
25
|
+
*
|
|
26
|
+
* Consulté AVANT le routage : une route attrape-tout de l'application ne peut
|
|
27
|
+
* pas avaler un préfixe relayé. Sans montage, le pipeline ne paie qu'une
|
|
28
|
+
* lecture de champ (`mounts === null`) — c'est le cas de toute application
|
|
29
|
+
* qui ne s'en sert pas.
|
|
30
|
+
*
|
|
31
|
+
* Règles communes à tout montage : en-têtes de connexion jamais transmis
|
|
32
|
+
* (RFC 9110 §7.6.1), `X-Forwarded-For/-Proto/-Host` et `Via` posés par le
|
|
33
|
+
* proxy, Host hors `trustedHosts` jamais relayé (même barrière, même 421 que
|
|
34
|
+
* toute requête), amont injoignable → 502, amont muet → 504, boucle → 508.
|
|
35
|
+
*/
|
|
36
|
+
var ReverseProxy = class extends Service {
|
|
37
|
+
module;
|
|
38
|
+
/**
|
|
39
|
+
* Montages actifs, du préfixe le PLUS LONG au plus court : le premier qui
|
|
40
|
+
* couvre une URL est le plus spécifique. `null` tant qu'il n'y en a aucun.
|
|
41
|
+
*/
|
|
42
|
+
mounts = null;
|
|
43
|
+
/** Délai d'inactivité appliqué aux montages qui n'en fixent pas (`proxy.timeoutMs`). */
|
|
44
|
+
defaultTimeoutMs;
|
|
45
|
+
/** Délai d'établissement de la connexion amont (`proxy.connectTimeoutMs`). */
|
|
46
|
+
connectTimeoutMs;
|
|
47
|
+
/** Connexions simultanées par amont (`proxy.maxSockets`) : pool ET tunnels WebSocket. */
|
|
48
|
+
maxSockets;
|
|
49
|
+
/**
|
|
50
|
+
* Arrêt engagé ({@link closeAll}) : un relais encore en vol ne recrée plus
|
|
51
|
+
* de pool — il passerait après la fermeture et garderait ses sockets.
|
|
52
|
+
*/
|
|
53
|
+
#closing = false;
|
|
54
|
+
/**
|
|
55
|
+
* Nom de CE processus dans `Via` (RFC 9110 §7.6.3) : tiré au hasard au
|
|
56
|
+
* démarrage, il ne dit rien de la machine et distingue deux instances —
|
|
57
|
+
* un message qui le porte déjà est une boucle (508).
|
|
58
|
+
*/
|
|
59
|
+
pseudonym = `nodefony-${randomBytes(4).toString("hex")}`;
|
|
60
|
+
constructor(module) {
|
|
61
|
+
const container = module.container ?? void 0;
|
|
62
|
+
const event = container?.get("notificationsCenter");
|
|
63
|
+
super("reverse-proxy", container, event, {});
|
|
64
|
+
this.module = module;
|
|
65
|
+
const section = module.options.proxy;
|
|
66
|
+
this.defaultTimeoutMs = section?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
67
|
+
this.connectTimeoutMs = section?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
68
|
+
this.maxSockets = section?.maxSockets ?? DEFAULT_MAX_SOCKETS;
|
|
69
|
+
this.kernel?.prependOnceListener("onTerminate", () => this.closeAll());
|
|
70
|
+
for (const [prefix, mount] of Object.entries(section?.mounts ?? {})) this.#add(prefix, mount, "config");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Relaie un préfixe d'URL vers un amont. Idempotent pour un même module :
|
|
74
|
+
* un même préfixe est remplacé. Un module qui recouvre un préfixe de la
|
|
75
|
+
* CONFIGURATION le remplace aussi, mais le dit — l'auteur de l'application
|
|
76
|
+
* doit pouvoir lire pourquoi son montage ne sert plus.
|
|
77
|
+
*
|
|
78
|
+
* @param prefix - préfixe d'URL (normalisé : `/` en tête et en fin)
|
|
79
|
+
* @param options - cible et réglages du montage
|
|
80
|
+
* @throws Error qui nomme chaque refus (préfixe `/` ou réservé, cible qui
|
|
81
|
+
* n'est pas une origine http(s) nue, `secure: false` hors boucle locale…)
|
|
82
|
+
*/
|
|
83
|
+
mount(prefix, options) {
|
|
84
|
+
this.#add(prefix, options, "module");
|
|
85
|
+
}
|
|
86
|
+
#add(prefix, options, source) {
|
|
87
|
+
const problems = proxyMountProblems(prefix, options, source);
|
|
88
|
+
if (problems.length > 0) throw new Error(`reverse-proxy : montage « ${prefix} » refusé — ` + problems.map((p) => `${p.field} : ${p.message}`).join(" ; "));
|
|
89
|
+
const p = normalizePrefix(prefix);
|
|
90
|
+
const record = {
|
|
91
|
+
...options,
|
|
92
|
+
prefix: p,
|
|
93
|
+
source,
|
|
94
|
+
lastOrigin: void 0,
|
|
95
|
+
lastUrl: void 0,
|
|
96
|
+
agent: null,
|
|
97
|
+
tunnels: 0
|
|
98
|
+
};
|
|
99
|
+
if (options.methods) record.methods = options.methods.map((m) => m.toUpperCase());
|
|
100
|
+
if (options.stripHeaders) record.stripHeaders = options.stripHeaders.map((h) => h.toLowerCase());
|
|
101
|
+
record.timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
102
|
+
const mounts = this.mounts ??= [];
|
|
103
|
+
const i = mounts.findIndex((m) => m.prefix === p);
|
|
104
|
+
if (i >= 0) {
|
|
105
|
+
const previous = mounts[i];
|
|
106
|
+
if (previous?.source === "config" && source === "module") this.log(`le montage « ${p} » de la configuration est remplacé par un module`, "WARNING");
|
|
107
|
+
previous?.agent?.destroy();
|
|
108
|
+
mounts[i] = record;
|
|
109
|
+
} else {
|
|
110
|
+
mounts.push(record);
|
|
111
|
+
mounts.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
112
|
+
}
|
|
113
|
+
this.log(`mount ${p} (${source})`, "DEBUG");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Retire un montage. Le dernier retiré rend `mounts` à `null` : le pipeline
|
|
117
|
+
* cesse alors de le consulter.
|
|
118
|
+
*
|
|
119
|
+
* @param prefix - préfixe tel que monté (normalisé de la même façon)
|
|
120
|
+
*/
|
|
121
|
+
unmount(prefix) {
|
|
122
|
+
if (this.mounts === null) return;
|
|
123
|
+
const p = normalizePrefix(prefix);
|
|
124
|
+
const kept = [];
|
|
125
|
+
for (const m of this.mounts) if (m.prefix === p) m.agent?.destroy();
|
|
126
|
+
else kept.push(m);
|
|
127
|
+
this.mounts = kept.length > 0 ? kept : null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Ferme tous les pools (arrêt du noyau). Les montages restent déclarés ; un
|
|
131
|
+
* relais encore en vol part ensuite sur une connexion à usage unique.
|
|
132
|
+
*/
|
|
133
|
+
closeAll() {
|
|
134
|
+
this.#closing = true;
|
|
135
|
+
for (const m of this.mounts ?? []) {
|
|
136
|
+
m.agent?.destroy();
|
|
137
|
+
m.agent = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Montage le plus spécifique qui couvre une URL, s'il y en a un.
|
|
142
|
+
*
|
|
143
|
+
* @param url - cible brute de la requête (chemin + requête)
|
|
144
|
+
* @returns le montage au préfixe le plus long qui couvre l'URL
|
|
145
|
+
*/
|
|
146
|
+
match(url) {
|
|
147
|
+
const mounts = this.mounts;
|
|
148
|
+
if (mounts === null || url === void 0) return void 0;
|
|
149
|
+
for (const m of mounts) if (url.startsWith(m.prefix)) return m;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Origine courante d'un montage, analysée — mise en cache tant que la cible
|
|
153
|
+
* rend la même chaîne.
|
|
154
|
+
*
|
|
155
|
+
* @returns l'URL de l'amont, ou `undefined` si rien n'est relayable
|
|
156
|
+
*/
|
|
157
|
+
targetOf(mount) {
|
|
158
|
+
const origin = typeof mount.target === "string" ? mount.target : mount.target();
|
|
159
|
+
if (origin === void 0) return void 0;
|
|
160
|
+
if (origin === mount.lastOrigin) return mount.lastUrl;
|
|
161
|
+
const url = parseProxyOrigin(origin) ?? void 0;
|
|
162
|
+
if (url === void 0) this.log(`cible « ${origin} » invalide pour ${mount.prefix} — non relayé`, "WARNING");
|
|
163
|
+
mount.lastOrigin = origin;
|
|
164
|
+
mount.lastUrl = url;
|
|
165
|
+
return url;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Pool de connexions d'un montage, créé au premier relais et recréé si la
|
|
169
|
+
* cible change de scheme.
|
|
170
|
+
*
|
|
171
|
+
* Un `Agent` DÉDIÉ, jamais l'agent global : sous `NODE_USE_ENV_PROXY`, cas
|
|
172
|
+
* courant d'un poste d'entreprise, l'agent global envoie tout vers
|
|
173
|
+
* `HTTP_PROXY` — un amont sur la boucle locale serait demandé au proxy de
|
|
174
|
+
* l'entreprise. Un agent construit ici n'a pas de `proxyEnv` : il va droit
|
|
175
|
+
* à la cible. Keep-alive borné (`maxSockets`), `lifo` : le socket le plus
|
|
176
|
+
* récent, le moins susceptible d'avoir été fermé par l'amont.
|
|
177
|
+
*/
|
|
178
|
+
agentFor(mount, target) {
|
|
179
|
+
if (this.#closing) return false;
|
|
180
|
+
const wantsTls = target.protocol === "https:";
|
|
181
|
+
const agent = mount.agent;
|
|
182
|
+
if (agent !== null && agent instanceof https.Agent === wantsTls) return agent;
|
|
183
|
+
agent?.destroy();
|
|
184
|
+
const options = {
|
|
185
|
+
keepAlive: true,
|
|
186
|
+
maxSockets: this.maxSockets,
|
|
187
|
+
maxFreeSockets: Math.min(32, this.maxSockets),
|
|
188
|
+
scheduling: "lifo"
|
|
189
|
+
};
|
|
190
|
+
mount.agent = wantsTls ? new https.Agent(options) : new http.Agent(options);
|
|
191
|
+
return mount.agent;
|
|
192
|
+
}
|
|
193
|
+
/** Réglages de relais d'un montage. */
|
|
194
|
+
relayFor(mount, target) {
|
|
195
|
+
return {
|
|
196
|
+
pseudonym: this.pseudonym,
|
|
197
|
+
connectTimeoutMs: this.connectTimeoutMs,
|
|
198
|
+
agent: this.agentFor(mount, target)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Règles du noyau HTTP (résolues par nom), `undefined` hors serveur. */
|
|
202
|
+
rules() {
|
|
203
|
+
return this.container?.get("HttpKernel") ?? void 0;
|
|
204
|
+
}
|
|
205
|
+
/** Ce que le proxy annonce à l'amont sur le client. */
|
|
206
|
+
fromOf(req, scheme, host) {
|
|
207
|
+
const address = req.socket.remoteAddress;
|
|
208
|
+
return {
|
|
209
|
+
remoteAddress: address,
|
|
210
|
+
scheme,
|
|
211
|
+
host,
|
|
212
|
+
trustedPeer: this.rules()?.getTrustProxyChecker().isTrusted(address) === true,
|
|
213
|
+
httpVersion: req.httpVersion
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Relaie une requête HTTP si un montage la couvre — méthode comprise. Cible
|
|
218
|
+
* pas encore connue → `503` + `Retry-After`, sans aller jusqu'au routage.
|
|
219
|
+
*
|
|
220
|
+
* L'appelant a déjà tranché la barrière d'hôte : c'est le pipeline HTTP, qui
|
|
221
|
+
* la calcule pour toute requête (`Context.validDomain`). Un chemin ambigu
|
|
222
|
+
* sous un préfixe monté rend 400, sans contacter l'amont.
|
|
223
|
+
*
|
|
224
|
+
* ⚠️ Un préfixe monté est servi AVANT le pare-feu, la CSRF et les en-têtes
|
|
225
|
+
* de sécurité applicatifs : c'est une surface publique pour qui atteint
|
|
226
|
+
* Nodefony. L'amont porte sa propre politique d'accès.
|
|
227
|
+
*
|
|
228
|
+
* @param req - requête du client
|
|
229
|
+
* @param res - réponse au client
|
|
230
|
+
* @param scheme - scheme par lequel le client est arrivé
|
|
231
|
+
* @returns la promesse de fin d'échange, ou `undefined` si la requête n'est
|
|
232
|
+
* pas relayée (hors préfixe ou méthode non relayée : elle suit alors son
|
|
233
|
+
* chemin normal)
|
|
234
|
+
*/
|
|
235
|
+
forward(req, res, scheme) {
|
|
236
|
+
const mount = this.match(req.url);
|
|
237
|
+
if (mount === void 0) return void 0;
|
|
238
|
+
const method = req.method ?? "GET";
|
|
239
|
+
if (mount.methods && !mount.methods.includes(method)) return void 0;
|
|
240
|
+
const target = this.targetOf(mount);
|
|
241
|
+
if (target === void 0) return answerPlain(res, 503, "Service Unavailable", "1");
|
|
242
|
+
if (isAmbiguousPath(req.url ?? "/")) return answerPlain(res, 400, "Bad Request");
|
|
243
|
+
const host = req.headers.host ?? req.headers[":authority"];
|
|
244
|
+
return forwardRequest(req, res, mount, target, this.fromOf(req, scheme, host), this.relayFor(mount, target));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Relaie un upgrade WebSocket si un montage `websocket` couvre son URL.
|
|
248
|
+
* Appelé par le répartiteur d'`upgrade` des serveurs WebSocket, AVANT le
|
|
249
|
+
* serveur WebSocket de Nodefony.
|
|
250
|
+
*
|
|
251
|
+
* Contrôles, dans l'ordre, chacun avec la réponse que rendrait le serveur
|
|
252
|
+
* WebSocket de Nodefony : cible pas encore connue → 503 ; `Host` hors `trustedHosts` (si `domainCheck`) →
|
|
253
|
+
* 421 ; chemin ambigu ou handshake non conforme (RFC 6455 §4.2.1 — seul
|
|
254
|
+
* `Upgrade: websocket` est relayé, jamais un tunnel `h2c`) → 400 ;
|
|
255
|
+
* `Origin` refusée par la MÊME règle que le serveur WebSocket (anti-CSWSH,
|
|
256
|
+
* RFC 6455 §10.2) → 403 ; `maxSockets` tunnels déjà ouverts vers l'amont →
|
|
257
|
+
* 503 ; quotas par IP du noyau (débit de handshakes, connexions
|
|
258
|
+
* simultanées) → 429. Le socket est alors consommé.
|
|
259
|
+
*
|
|
260
|
+
* @returns `true` si le proxy a pris le socket en charge
|
|
261
|
+
*/
|
|
262
|
+
handleUpgrade(req, socket, head) {
|
|
263
|
+
const mount = this.match(req.url);
|
|
264
|
+
if (mount?.websocket !== true) return false;
|
|
265
|
+
const target = this.targetOf(mount);
|
|
266
|
+
if (target === void 0) {
|
|
267
|
+
refuseUpgrade(socket, 503, "Service Unavailable");
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
const host = req.headers.host;
|
|
271
|
+
const hostname = hostnameOf(host);
|
|
272
|
+
const rules = this.rules();
|
|
273
|
+
if (this.kernel?.options.domainCheck && (hostname === null || rules?.isTrustedHostname(hostname) !== true)) {
|
|
274
|
+
refuseUpgrade(socket, 421, "Misdirected Request");
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (hostname === null || isAmbiguousPath(req.url ?? "/") || websocketHandshakeProblem(req.method, req.headers) !== null) {
|
|
278
|
+
refuseUpgrade(socket, 400, "Bad Request");
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
const secure = req.socket instanceof TLSSocket;
|
|
282
|
+
if (rules !== void 0 && !rules.isWebsocketOriginAllowed(req.headers.origin, hostname, secure)) {
|
|
283
|
+
refuseUpgrade(socket, 403, "Forbidden");
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (mount.tunnels >= this.maxSockets) {
|
|
287
|
+
refuseUpgrade(socket, 503, "Service Unavailable");
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (rules?.websocketQuotaRefusal(req, socket) != null) {
|
|
291
|
+
refuseUpgrade(socket, 429, "Too Many Requests");
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
mount.tunnels++;
|
|
295
|
+
socket.once("close", () => {
|
|
296
|
+
mount.tunnels--;
|
|
297
|
+
});
|
|
298
|
+
forwardUpgrade(req, socket, head, mount, target, this.fromOf(req, secure ? "https" : "http", host), this.relayFor(mount, target));
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Nom d'hôte d'une autorité `Host`, sans port (`[::1]` garde ses crochets,
|
|
304
|
+
* comme `URL.hostname`).
|
|
305
|
+
*
|
|
306
|
+
* @returns le nom, ou `null` si l'autorité est absente ou invalide
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* Répond au client sans contacter l'amont : texte brut, jamais mis en cache.
|
|
310
|
+
*
|
|
311
|
+
* @param res - réponse au client (HTTP/1.1 ou HTTP/2, API de compatibilité)
|
|
312
|
+
* @param status - statut rendu
|
|
313
|
+
* @param body - corps texte
|
|
314
|
+
* @param retryAfter - valeur de `Retry-After` (secondes), si le refus est passager
|
|
315
|
+
* @returns une promesse déjà résolue — l'échange est terminé
|
|
316
|
+
*/
|
|
317
|
+
function answerPlain(res, status, body, retryAfter) {
|
|
318
|
+
const out = res;
|
|
319
|
+
out.writeHead(status, {
|
|
320
|
+
"content-type": "text/plain; charset=utf-8",
|
|
321
|
+
"cache-control": "no-store",
|
|
322
|
+
...retryAfter === void 0 ? {} : { "retry-after": retryAfter }
|
|
323
|
+
});
|
|
324
|
+
out.end(body);
|
|
325
|
+
return Promise.resolve();
|
|
326
|
+
}
|
|
327
|
+
function hostnameOf(host) {
|
|
328
|
+
if (!host) return null;
|
|
329
|
+
try {
|
|
330
|
+
return new URL(`http://${host}`).hostname;
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { ReverseProxy as default };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
|
|
2
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
4
4
|
import http_kernel_default from "../http-kernel.js";
|
|
5
5
|
import { handleClientError } from "./clientError.js";
|
|
6
6
|
import { createDrainTerminator } from "./serverShutdown.js";
|
|
7
7
|
import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
|
|
8
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
8
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
|
|
9
9
|
import { Module, Service, extend, inject, nodefonyError } from "nodefony";
|
|
10
10
|
import http from "node:http";
|
|
11
11
|
//#region nodefony/service/servers/server-http.ts
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
|
|
2
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
4
4
|
import http_kernel_default from "../http-kernel.js";
|
|
5
5
|
import { handleClientError } from "./clientError.js";
|
|
6
6
|
import { createDrainTerminator } from "./serverShutdown.js";
|
|
7
7
|
import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
|
|
8
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
8
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
|
|
9
9
|
import { Module, Service, extend, inject, nodefonyError } from "nodefony";
|
|
10
10
|
import http2 from "node:http2";
|
|
11
11
|
import https from "node:https";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { normalizePrefix } from "../../src/proxy/rules.js";
|
|
1
2
|
import { Service, extend } from "nodefony";
|
|
2
3
|
import http from "node:http";
|
|
4
|
+
import http2 from "node:http2";
|
|
3
5
|
import { URL } from "node:url";
|
|
4
6
|
import { existsSync } from "node:fs";
|
|
5
7
|
import { join } from "node:path";
|
|
6
8
|
import mime from "mime-types";
|
|
7
|
-
import http2 from "node:http2";
|
|
8
9
|
import tls from "node:tls";
|
|
9
10
|
import serveStatic from "serve-static";
|
|
10
11
|
//#region nodefony/service/servers/server-static.ts
|
|
@@ -109,14 +110,10 @@ var Statics = class extends Service {
|
|
|
109
110
|
* @param dir dossier absolu à servir
|
|
110
111
|
*/
|
|
111
112
|
addMount(prefix, dir) {
|
|
112
|
-
|
|
113
|
-
if (!p.startsWith("/")) p = `/${p}`;
|
|
114
|
-
if (!p.endsWith("/")) p = `${p}/`;
|
|
115
|
-
p = p.replace(/\/{2,}/g, "/");
|
|
116
|
-
const server = serveStatic(dir, extend({}, this.defaultOptions));
|
|
113
|
+
const p = normalizePrefix(prefix);
|
|
117
114
|
const entry = {
|
|
118
115
|
prefix: p,
|
|
119
|
-
server,
|
|
116
|
+
server: serveStatic(dir, extend({}, this.defaultOptions)),
|
|
120
117
|
dir
|
|
121
118
|
};
|
|
122
119
|
const i = this.mounts.findIndex((m) => m.prefix === p);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
|
|
2
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
4
4
|
import http_kernel_default from "../http-kernel.js";
|
|
5
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
|
|
6
6
|
import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
|
|
7
|
+
import { attachUpgradeDispatch } from "../../src/servers/upgradeDispatch.js";
|
|
7
8
|
import { Module, Service, inject } from "nodefony";
|
|
8
9
|
import Ws, { WebSocketServer } from "ws";
|
|
9
10
|
//#region nodefony/service/servers/server-websocket-secure.ts
|
|
@@ -12,6 +13,8 @@ let WebsocketSecure = class WebsocketSecure extends Service {
|
|
|
12
13
|
module;
|
|
13
14
|
ready = false;
|
|
14
15
|
server = null;
|
|
16
|
+
/** Retire l'écouteur `upgrade` posé par {@link attachUpgradeDispatch}. */
|
|
17
|
+
#detachUpgrade = null;
|
|
15
18
|
port;
|
|
16
19
|
domain;
|
|
17
20
|
protocol = "1.1";
|
|
@@ -46,9 +49,10 @@ let WebsocketSecure = class WebsocketSecure extends Service {
|
|
|
46
49
|
}
|
|
47
50
|
this.server = new WebSocketServer({
|
|
48
51
|
...this.options,
|
|
49
|
-
|
|
52
|
+
noServer: true,
|
|
50
53
|
clientTracking: true
|
|
51
54
|
});
|
|
55
|
+
this.#detachUpgrade = attachUpgradeDispatch(serverHttps.server, this.server, () => this.httpKernel.reverseProxy, (req) => this.httpKernel.websocketUpgradeRefusal(req));
|
|
52
56
|
this.server.on("connection", this.onConnection.bind(this));
|
|
53
57
|
this.heartbeatTimer = startHeartbeat(this.server, this.options);
|
|
54
58
|
this.kernel?.prependOnceListener("onTerminate", this.terminate.bind(this));
|
|
@@ -83,6 +87,8 @@ let WebsocketSecure = class WebsocketSecure extends Service {
|
|
|
83
87
|
});
|
|
84
88
|
setTimeout(() => {
|
|
85
89
|
try {
|
|
90
|
+
this.#detachUpgrade?.();
|
|
91
|
+
this.#detachUpgrade = null;
|
|
86
92
|
this.server?.close();
|
|
87
93
|
this.log(` SHUTDOWN WEBSOCKET Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
|
|
88
94
|
return resolve(true);
|
|
@@ -92,6 +98,8 @@ let WebsocketSecure = class WebsocketSecure extends Service {
|
|
|
92
98
|
}, 300);
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
101
|
+
this.#detachUpgrade?.();
|
|
102
|
+
this.#detachUpgrade = null;
|
|
95
103
|
return resolve(true);
|
|
96
104
|
});
|
|
97
105
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
|
|
2
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
4
4
|
import http_kernel_default from "../http-kernel.js";
|
|
5
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
|
|
6
6
|
import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
|
|
7
|
+
import { attachUpgradeDispatch } from "../../src/servers/upgradeDispatch.js";
|
|
7
8
|
import { Module, Service, inject } from "nodefony";
|
|
8
9
|
import Ws, { WebSocketServer } from "ws";
|
|
9
10
|
//#region nodefony/service/servers/server-websocket.ts
|
|
@@ -12,6 +13,8 @@ let Websocket = class Websocket extends Service {
|
|
|
12
13
|
module;
|
|
13
14
|
ready = false;
|
|
14
15
|
server = null;
|
|
16
|
+
/** Retire l'écouteur `upgrade` posé par {@link attachUpgradeDispatch}. */
|
|
17
|
+
#detachUpgrade = null;
|
|
15
18
|
port;
|
|
16
19
|
domain;
|
|
17
20
|
protocol = "1.1";
|
|
@@ -46,9 +49,10 @@ let Websocket = class Websocket extends Service {
|
|
|
46
49
|
}
|
|
47
50
|
this.server = new WebSocketServer({
|
|
48
51
|
...this.options,
|
|
49
|
-
|
|
52
|
+
noServer: true,
|
|
50
53
|
clientTracking: true
|
|
51
54
|
});
|
|
55
|
+
this.#detachUpgrade = attachUpgradeDispatch(serverHttp.server, this.server, () => this.httpKernel.reverseProxy, (req) => this.httpKernel.websocketUpgradeRefusal(req));
|
|
52
56
|
this.server.on("connection", this.onConnection.bind(this));
|
|
53
57
|
this.heartbeatTimer = startHeartbeat(this.server, this.options);
|
|
54
58
|
this.kernel?.prependOnceListener("onTerminate", this.terminate.bind(this));
|
|
@@ -83,6 +87,8 @@ let Websocket = class Websocket extends Service {
|
|
|
83
87
|
});
|
|
84
88
|
setTimeout(() => {
|
|
85
89
|
try {
|
|
90
|
+
this.#detachUpgrade?.();
|
|
91
|
+
this.#detachUpgrade = null;
|
|
86
92
|
this.server?.close();
|
|
87
93
|
this.log(` SHUTDOWN WEBSOCKET Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
|
|
88
94
|
return resolve(true);
|
|
@@ -92,6 +98,8 @@ let Websocket = class Websocket extends Service {
|
|
|
92
98
|
}, 300);
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
101
|
+
this.#detachUpgrade?.();
|
|
102
|
+
this.#detachUpgrade = null;
|
|
95
103
|
return resolve(true);
|
|
96
104
|
});
|
|
97
105
|
}
|
|
@@ -19,6 +19,35 @@ const trackPong = (ws) => {
|
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
+
* Un tick du keep-alive : pingue les connexions muettes depuis `interval`, et
|
|
23
|
+
* coupe celles dont le ping attend sa réponse depuis plus de `grace`.
|
|
24
|
+
*
|
|
25
|
+
* Fonction PURE du temps : l'horloge est passée en argument, si bien que la
|
|
26
|
+
* règle se prouve pas à pas, sans minuteur réel — un banc qui la prouverait à
|
|
27
|
+
* l'horloge murale confondrait une boucle d'événements chargée avec un zombie.
|
|
28
|
+
*
|
|
29
|
+
* @param clients - les connexions du serveur
|
|
30
|
+
* @param now - l'instant du tick (`performance.now()` en production)
|
|
31
|
+
* @param interval - délai sans signe de vie avant un ping, ms
|
|
32
|
+
* @param grace - délai accordé au pong avant de couper, ms
|
|
33
|
+
*/
|
|
34
|
+
const heartbeatTick = (clients, now, interval, grace) => {
|
|
35
|
+
for (const client of clients) {
|
|
36
|
+
if (client.readyState !== Ws.OPEN) continue;
|
|
37
|
+
const sock = client;
|
|
38
|
+
sock._nfLastPong ??= now;
|
|
39
|
+
const pingedAt = sock._nfPingedAt ?? 0;
|
|
40
|
+
if (pingedAt > sock._nfLastPong) {
|
|
41
|
+
if (now - pingedAt > grace) client.terminate();
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (now - sock._nfLastPong >= interval) {
|
|
45
|
+
sock._nfPingedAt = now;
|
|
46
|
+
client.ping();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
22
51
|
* Démarre le heartbeat keep-alive du serveur : détecte les connexions zombies
|
|
23
52
|
* (half-open — TCP encore ouvert mais le pair a disparu sans frame Close) et les
|
|
24
53
|
* `terminate()` pour libérer slot mémoire + descripteur de fichier.
|
|
@@ -42,23 +71,10 @@ const startHeartbeat = (server, options) => {
|
|
|
42
71
|
if (interval <= 0) return null;
|
|
43
72
|
const grace = options.keepaliveGracePeriod ?? 0;
|
|
44
73
|
const timer = setInterval(() => {
|
|
45
|
-
|
|
46
|
-
for (const client of server.clients) {
|
|
47
|
-
if (client.readyState !== Ws.OPEN) continue;
|
|
48
|
-
const sock = client;
|
|
49
|
-
sock._nfLastPong ??= now;
|
|
50
|
-
if ((sock._nfPingedAt ?? 0) > sock._nfLastPong) {
|
|
51
|
-
if (now - sock._nfPingedAt > grace) client.terminate();
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (now - sock._nfLastPong >= interval) {
|
|
55
|
-
sock._nfPingedAt = now;
|
|
56
|
-
client.ping();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
74
|
+
heartbeatTick(server.clients, performance.now(), interval, grace);
|
|
59
75
|
}, Math.max(250, grace > 0 ? Math.min(interval, grace) : interval));
|
|
60
76
|
timer.unref();
|
|
61
77
|
return timer;
|
|
62
78
|
};
|
|
63
79
|
//#endregion
|
|
64
|
-
export { startHeartbeat, trackPong };
|
|
80
|
+
export { heartbeatTick, startHeartbeat, trackPong };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
|
|
2
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
4
4
|
import http_kernel_default from "../http-kernel.js";
|
|
5
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
|
|
6
6
|
import Session from "../../src/session/session.js";
|
|
7
7
|
import MemorySessionStorage from "../../src/session/storage/MemorySessionStorage.js";
|
|
8
8
|
import RevocationGuardStorage from "../../src/session/storage/RevocationGuardStorage.js";
|
|
@@ -481,23 +481,44 @@ let SessionsService = class SessionsService extends Service {
|
|
|
481
481
|
return destroyed;
|
|
482
482
|
}
|
|
483
483
|
/**
|
|
484
|
-
*
|
|
485
|
-
*
|
|
484
|
+
* Détruit TOUTES les sessions dont le contenu satisfait `match` — la primitive
|
|
485
|
+
* des révocations groupées (« déconnecter partout », déconnexion demandée par
|
|
486
|
+
* un fournisseur d'identité). Ni journal ni audit : l'appelant, qui connaît le
|
|
487
|
+
* motif, les émet.
|
|
488
|
+
*
|
|
489
|
+
* @remarks Quand la méthode rend la main, il ne reste AUCUNE session qui
|
|
490
|
+
* satisfait `match` (au plus `MAX_LOGOUT_PASSES` passages). Une révocation qui
|
|
491
|
+
* en laisse une seule n'est pas une imprécision, c'est une faille.
|
|
492
|
+
*
|
|
493
|
+
* @param filter - restriction poussée au store (`user` : WHERE indexable) ;
|
|
494
|
+
* `undefined` parcourt tout le parc.
|
|
495
|
+
* @param match - prédicat sur la session sérialisée — il doit RE-VÉRIFIER ce
|
|
496
|
+
* que `filter` exprime, un store pouvant l'ignorer.
|
|
497
|
+
* @returns le nombre de sessions détruites.
|
|
498
|
+
* @throws Error si le store actif ne sait pas énumérer ses sessions.
|
|
486
499
|
*/
|
|
487
|
-
async
|
|
500
|
+
async destroyWhere(filter, match) {
|
|
488
501
|
const storage = this.enumerable();
|
|
489
502
|
let destroyed = 0;
|
|
490
503
|
for (let pass = 0; pass < MAX_LOGOUT_PASSES; pass += 1) {
|
|
491
504
|
let destroyedThisPass = 0;
|
|
492
|
-
await this.eachSessionRecord(
|
|
493
|
-
if (rec.data
|
|
505
|
+
await this.eachSessionRecord(filter, async (rec) => {
|
|
506
|
+
if (!match(rec.data)) return false;
|
|
494
507
|
if (await storage.destroy(rec.id)) destroyedThisPass += 1;
|
|
495
508
|
return false;
|
|
496
509
|
});
|
|
497
510
|
destroyed += destroyedThisPass;
|
|
498
511
|
if (destroyedThisPass === 0) break;
|
|
499
|
-
if (pass === 9) this.log(`sessions:
|
|
512
|
+
if (pass === 9) this.log(`sessions: révocation groupée interrompue après ${MAX_LOGOUT_PASSES} passages — filtre=${JSON.stringify(filter ?? {})} (révocation potentiellement incomplète)`, "WARNING");
|
|
500
513
|
}
|
|
514
|
+
return destroyed;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* « Déconnexion partout » : détruit TOUTES les sessions d'un utilisateur (scan
|
|
518
|
+
* O(N) — pas d'index inverse, acceptable en admin). Renvoie le nombre détruit.
|
|
519
|
+
*/
|
|
520
|
+
async destroyByUser(identifier, actor) {
|
|
521
|
+
const destroyed = await this.destroyWhere({ user: identifier }, (data) => data.user === identifier);
|
|
501
522
|
if (destroyed > 0) {
|
|
502
523
|
this.log(`sessions revoked by admin (logout-all) — user=${identifier} count=${destroyed} actor=${actor ?? "admin"}`, "INFO");
|
|
503
524
|
this.emitAudit({
|
|
@@ -1,7 +1,7 @@
|
|
|
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.153.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
|
|
5
5
|
import { Cli, FileClass, Module, Service, inject } from "nodefony";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import fsp from "node:fs/promises";
|