@blamejs/core 0.4.1

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 (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
package/lib/router.js ADDED
@@ -0,0 +1,437 @@
1
+ "use strict";
2
+ /**
3
+ * Custom HTTP router — zero-dependency replacement for express/koa/fastify.
4
+ *
5
+ * Why rolled-our-own: blamejs principle #1 forbids npm runtime dependencies.
6
+ * This router covers what a route concretely requires (path params,
7
+ * middleware chain, static file serving, MIME sniffing) and leaves no
8
+ * attack surface we haven't read.
9
+ *
10
+ * Middleware / handler dispatch (see roadmap "Naming conventions" — verb
11
+ * conventions section, "on/off/emit" vs explicit chain control):
12
+ * - handler.length >= 3 → treated as middleware. Chain stops unless the
13
+ * handler calls next(). Using 2-arg handlers as middleware is
14
+ * structurally fragile and will silently fall through.
15
+ * - handler.length <= 2 → terminal handler. Always falls through to the
16
+ * next entry in the chain if it doesn't end the response.
17
+ *
18
+ * Patterns are compiled ONCE at registration time (compilePattern) — no
19
+ * regex construction on the hot path. Route table is scanned linearly;
20
+ * ordering matters (first match wins).
21
+ */
22
+ var http = require("http");
23
+ var http2 = require("http2");
24
+ var fs = require("fs");
25
+ var path = require("path");
26
+ var { URL } = require("url");
27
+ var C = require("./constants");
28
+ var websocket = require("./websocket");
29
+ var { boot } = require("./log");
30
+
31
+ var log = boot("router");
32
+
33
+ function compilePattern(pattern) {
34
+ var keys = [];
35
+ var regexStr = pattern
36
+ .replace(/:([^/]+)/g, function (_, key) { keys.push(key); return "([^/]+)"; })
37
+ .replace(/\//g, "\\/");
38
+ return { pattern: pattern, regex: new RegExp("^" + regexStr + "$"), keys: keys };
39
+ }
40
+
41
+ var MIME_TYPES = {
42
+ ".html": "text/html",
43
+ ".css": "text/css",
44
+ ".js": "application/javascript",
45
+ ".json": "application/json",
46
+ ".png": "image/png",
47
+ ".jpg": "image/jpeg",
48
+ ".jpeg": "image/jpeg",
49
+ ".gif": "image/gif",
50
+ ".svg": "image/svg+xml",
51
+ ".ico": "image/x-icon",
52
+ ".woff2": "font/woff2",
53
+ ".woff": "font/woff",
54
+ };
55
+
56
+ class Router {
57
+ constructor() {
58
+ this.routes = [];
59
+ this.middleware = [];
60
+ // WebSocket routes are kept separate from HTTP routes — they're
61
+ // matched on the upgrade / Extended CONNECT path, not on a method
62
+ // verb. Map<path, { handler, opts }>.
63
+ this._wsRoutes = new Map();
64
+ // Active WebSocket connections opened through router.ws(). Tracked
65
+ // so router.closeWebSockets() can do a clean rolling-shutdown.
66
+ // h1-upgrade detaches the socket from http.Server's connection
67
+ // tracking — without our own registry there's no other way to
68
+ // enumerate active WS connections for graceful close.
69
+ this._activeWsConns = new Set();
70
+ }
71
+
72
+ // Active WebSocket connections opened via router.ws(). Useful for
73
+ // ops dashboards / health endpoints.
74
+ activeWebSockets() {
75
+ return this._activeWsConns.size;
76
+ }
77
+
78
+ // Graceful shutdown of all WebSocket connections opened via
79
+ // router.ws(). Sends close frame (code 1001 'going away') to each;
80
+ // awaits each connection's 'close' event up to opts.timeoutMs
81
+ // (default 5s). Returns the count of connections closed.
82
+ //
83
+ // Operators call this during rolling deploy:
84
+ // await router.closeWebSockets({ timeoutMs: 10_000 });
85
+ // await new Promise(r => server.close(r));
86
+ // process.exit(0);
87
+ //
88
+ // Tests use the same primitive in teardown — no parallel cleanup
89
+ // path, no h1-upgrade detached-socket workaround.
90
+ async closeWebSockets(opts) {
91
+ opts = opts || {};
92
+ var timeoutMs = typeof opts.timeoutMs === "number" ? opts.timeoutMs : C.TIME.seconds(5);
93
+ var code = opts.code || 1001; // 1001 = going away
94
+ var reason = opts.reason || "server shutting down";
95
+
96
+ var conns = Array.from(this._activeWsConns);
97
+ if (conns.length === 0) return 0;
98
+
99
+ var closes = conns.map(function (conn) {
100
+ return new Promise(function (resolve) {
101
+ if (conn.readyState === "closed") return resolve();
102
+ conn.once("close", resolve);
103
+ try { conn.close(code, reason); }
104
+ catch (_e) {
105
+ // close() may throw if already closed; resolve immediately
106
+ // since the 'close' event won't fire again.
107
+ resolve();
108
+ }
109
+ });
110
+ });
111
+
112
+ var allClosed = Promise.all(closes);
113
+ var timeout = new Promise(function (resolve) {
114
+ var t = setTimeout(resolve, timeoutMs);
115
+ t.unref();
116
+ });
117
+ await Promise.race([allClosed, timeout]);
118
+ // Force-destroy any laggards — at this point we've waited the full
119
+ // timeout and they didn't ack. The operator chose timeoutMs; honor it.
120
+ this._activeWsConns.forEach(function (conn) {
121
+ try { if (conn.socket && conn.socket.destroy) conn.socket.destroy(); }
122
+ catch (_e) {}
123
+ });
124
+ return conns.length;
125
+ }
126
+
127
+ use(fn) {
128
+ this.middleware.push(fn);
129
+ }
130
+
131
+ get(pattern, ...handlers) {
132
+ this.routes.push({ method: "GET", ...compilePattern(pattern), handlers });
133
+ }
134
+
135
+ post(pattern, ...handlers) {
136
+ this.routes.push({ method: "POST", ...compilePattern(pattern), handlers });
137
+ }
138
+
139
+ put(pattern, ...handlers) {
140
+ this.routes.push({ method: "PUT", ...compilePattern(pattern), handlers });
141
+ }
142
+
143
+ patch(pattern, ...handlers) {
144
+ this.routes.push({ method: "PATCH", ...compilePattern(pattern), handlers });
145
+ }
146
+
147
+ delete(pattern, ...handlers) {
148
+ this.routes.push({ method: "DELETE", ...compilePattern(pattern), handlers });
149
+ }
150
+
151
+ // ---- WebSocket route registration ----
152
+ //
153
+ // ws(path, handler, opts?)
154
+ // path — exact match. Path-param patterns aren't supported on
155
+ // upgrade requests; operators that need dynamic paths
156
+ // register one ws route per stable shape.
157
+ // handler — function(conn, req) — called with the WebSocketConnection
158
+ // and the original HTTP request (req for h1, request
159
+ // headers object for h2 Extended CONNECT). Operator owns
160
+ // the conn lifecycle from there.
161
+ // opts:
162
+ // transport: "auto" (default) | "h1-only" | "h2-only"
163
+ // auto — accept both transports per ALPN negotiation
164
+ // h1-only — refuse h2 Extended CONNECT with :status 405
165
+ // h2-only — refuse h1 upgrade with 426 Upgrade Required +
166
+ // `Upgrade: h2c` advisory header
167
+ // origins: string[] | "*" | undefined — operator allowlist;
168
+ // omitted = accept all (a startup warning fires when
169
+ // the path is registered, since omitting origin
170
+ // policy on a public-facing path is rarely intended)
171
+ // subprotocols: string[] — first match wins
172
+ // maxMessageBytes / pingIntervalMs / pongTimeoutMs — passed
173
+ // through to WebSocketConnection
174
+ ws(pathStr, handler, opts) {
175
+ if (typeof pathStr !== "string" || pathStr.length === 0) {
176
+ throw new Error("router.ws: path must be a non-empty string");
177
+ }
178
+ if (typeof handler !== "function") {
179
+ throw new Error("router.ws: handler must be a function");
180
+ }
181
+ opts = opts || {};
182
+ var transport = opts.transport || "auto";
183
+ if (transport !== "auto" && transport !== "h1-only" && transport !== "h2-only") {
184
+ throw new Error("router.ws: transport must be 'auto' | 'h1-only' | 'h2-only'");
185
+ }
186
+ if (!opts.origins) {
187
+ log.warn("WebSocket route '" + pathStr + "' registered without origins allowlist — accepting all origins. Pass { origins: [...] } or { origins: '*' } to silence.");
188
+ }
189
+ this._wsRoutes.set(pathStr, { handler: handler, opts: opts, transport: transport });
190
+ }
191
+
192
+ _match(route, pathname) {
193
+ var match = pathname.match(route.regex);
194
+ if (!match) return null;
195
+ var params = {};
196
+ route.keys.forEach((key, i) => (params[key] = match[i + 1]));
197
+ return params;
198
+ }
199
+
200
+ async handle(req, res) {
201
+ var parsed = new URL(req.url, "http://" + req.headers.host);
202
+ req.pathname = parsed.pathname;
203
+ req.query = Object.fromEntries(parsed.searchParams);
204
+
205
+ // Run middleware
206
+ for (var mw of this.middleware) {
207
+ var next = false;
208
+ try {
209
+ await mw(req, res, () => (next = true));
210
+ } catch (mwErr) {
211
+ console.error("[middleware error]", mw.name || "anonymous", req.method, req.url, mwErr.message,
212
+ mwErr.stack ? mwErr.stack.split("\n").slice(0, 3).join(" | ") : "");
213
+ throw mwErr;
214
+ }
215
+ if (!next || res.writableEnded) return;
216
+ }
217
+
218
+ // Match route
219
+ for (var route of this.routes) {
220
+ if (route.method !== req.method) continue;
221
+ var params = this._match(route, req.pathname);
222
+ if (!params) continue;
223
+ req.params = params;
224
+ // Expose the route TEMPLATE so framework middleware (metrics,
225
+ // tracing) can label by template instead of the actual URL —
226
+ // otherwise every distinct path-param value becomes its own
227
+ // cardinality bucket.
228
+ req.routePattern = route.pattern;
229
+
230
+ for (var handler of route.handlers) {
231
+ if (res.writableEnded) return;
232
+ if (handler.length >= 3) {
233
+ var proceeded = false;
234
+ await handler(req, res, () => (proceeded = true));
235
+ if (!proceeded) return;
236
+ } else {
237
+ await handler(req, res);
238
+ }
239
+ }
240
+ return;
241
+ }
242
+
243
+ // Not found
244
+ if (this.notFoundHandler) {
245
+ this.notFoundHandler(req, res);
246
+ } else {
247
+ res.writeHead(404, { "Content-Type": "text/html" });
248
+ res.end("<h1>404 Not Found</h1>");
249
+ }
250
+ }
251
+
252
+ getReservedSlugs() {
253
+ var slugs = new Set();
254
+ for (var i = 0; i < this.routes.length; i++) {
255
+ var parts = this.routes[i].pattern.split("/").filter(Boolean);
256
+ if (parts.length > 0 && !parts[0].startsWith(":")) {
257
+ slugs.add(parts[0].toLowerCase());
258
+ }
259
+ }
260
+ return slugs;
261
+ }
262
+
263
+ onNotFound(handler) {
264
+ this.notFoundHandler = handler;
265
+ }
266
+
267
+ onError(handler) {
268
+ this.errorHandler = handler;
269
+ }
270
+
271
+ listen(port, cb, tlsOptions, host) {
272
+ var self = this;
273
+ var requestHandler = (req, res) => {
274
+ // Response helpers
275
+ res.json = (data) => {
276
+ res.writeHead(res.statusCode || 200, { "Content-Type": "application/json" });
277
+ res.end(JSON.stringify(data));
278
+ };
279
+ res.redirect = (url) => {
280
+ // Same-origin redirects only by default. Apps that need cross-origin
281
+ // redirects (OAuth, SSO) wrap res.redirect with their own allowlist.
282
+ var safe = "/";
283
+ if (typeof url === "string" && url.startsWith("/") && !url.startsWith("//")) {
284
+ safe = url;
285
+ }
286
+ res.writeHead(302, { Location: safe });
287
+ res.end();
288
+ };
289
+ res.status = (code) => {
290
+ res.statusCode = code;
291
+ return res;
292
+ };
293
+
294
+ self.handle(req, res).catch((err) => {
295
+ console.error("[route error]", req.method, req.url, err.message,
296
+ err.stack ? err.stack.split("\n").slice(0, 5).join(" | ") : "");
297
+ if (self.errorHandler) {
298
+ try { self.errorHandler(err, req, res); } catch (_) {
299
+ if (!res.writableEnded) {
300
+ res.writeHead(500, { "Content-Type": "text/plain" });
301
+ res.end("Internal Server Error");
302
+ }
303
+ }
304
+ } else if (!res.writableEnded) {
305
+ res.writeHead(500, { "Content-Type": "text/plain" });
306
+ res.end("Internal Server Error");
307
+ }
308
+ });
309
+ };
310
+ var server;
311
+ if (tlsOptions) {
312
+ // h2-capable server with h1 fallback via ALPN. ["h2", "http/1.1"]
313
+ // means modern clients negotiate h2 (preferred); legacy clients
314
+ // fall back to h1. allowHTTP1: true is what makes the same server
315
+ // accept both. enableConnectProtocol: true is what enables h2
316
+ // WebSocket (RFC 8441) — clients refuse to issue Extended CONNECT
317
+ // until they see this in the server's SETTINGS frame.
318
+ server = http2.createSecureServer(Object.assign({
319
+ allowHTTP1: true,
320
+ ALPNProtocols: ["h2", "http/1.1"],
321
+ settings: { enableConnectProtocol: true },
322
+ }, tlsOptions), requestHandler);
323
+ } else {
324
+ // Cleartext path is h1-only. Operators wanting h2c on cleartext
325
+ // are typically running behind a TLS-terminating LB that does
326
+ // h1↔h2 translation; the framework's TLS path covers that.
327
+ server = http.createServer(requestHandler);
328
+ }
329
+
330
+ // ---- WebSocket wiring ----
331
+ // Only registers handlers when there are ws routes — keeps the
332
+ // server's emitter list clean for HTTP-only deployments.
333
+ if (self._wsRoutes.size > 0) {
334
+ // h1 upgrade event — fires for "Upgrade: websocket" from h1
335
+ // clients. Routes by path; refuses with 426 in h2-only mode.
336
+ server.on("upgrade", function (req, socket, head) {
337
+ var pathname = String(req.url || "/").split("?")[0];
338
+ var route = self._wsRoutes.get(pathname);
339
+ if (!route) {
340
+ socket.destroy();
341
+ return;
342
+ }
343
+ if (route.transport === "h2-only") {
344
+ // RFC-correct way to say "use h2": 426 Upgrade Required plus
345
+ // an Upgrade advisory pointing to h2c.
346
+ var body = "WebSocket on this path requires HTTP/2";
347
+ var resp =
348
+ "HTTP/1.1 426 Upgrade Required\r\n" +
349
+ "Upgrade: h2c\r\n" +
350
+ "Connection: close\r\n" +
351
+ "Content-Type: text/plain; charset=utf-8\r\n" +
352
+ "Content-Length: " + Buffer.byteLength(body, "utf8") + "\r\n" +
353
+ "\r\n" +
354
+ body;
355
+ try { socket.write(resp); } catch (_e) {}
356
+ try { socket.destroy(); } catch (_e) {}
357
+ return;
358
+ }
359
+ var conn = websocket.handleUpgrade(req, socket, head, route.opts);
360
+ if (conn) {
361
+ self._activeWsConns.add(conn);
362
+ conn.once("close", function () { self._activeWsConns.delete(conn); });
363
+ try { route.handler(conn, req); }
364
+ catch (err) { log.error("ws handler threw: " + err.message); conn._abort(websocket.CLOSE_INTERNAL_ERROR, "handler error"); }
365
+ }
366
+ });
367
+
368
+ // h2 Extended CONNECT — only fires on h2-capable (TLS) server.
369
+ // The 'stream' event filter checks for :method=CONNECT,
370
+ // :protocol=websocket. Other CONNECT methods (e.g. tunnel) and
371
+ // ordinary requests pass through.
372
+ if (tlsOptions) {
373
+ server.on("stream", function (stream, headers) {
374
+ if (headers[":method"] !== "CONNECT") return;
375
+ if (headers[":protocol"] !== "websocket") return;
376
+ var pathname = String(headers[":path"] || "/").split("?")[0];
377
+ var route = self._wsRoutes.get(pathname);
378
+ if (!route) {
379
+ try { stream.respond({ ":status": 404 }); stream.end(); } catch (_e) {}
380
+ return;
381
+ }
382
+ if (route.transport === "h1-only") {
383
+ try {
384
+ stream.respond({ ":status": 405, "content-type": "text/plain; charset=utf-8" });
385
+ stream.end("WebSocket on this path requires HTTP/1.1 Upgrade");
386
+ } catch (_e) {}
387
+ return;
388
+ }
389
+ var conn = websocket.handleExtendedConnect(stream, headers, route.opts);
390
+ if (conn) {
391
+ self._activeWsConns.add(conn);
392
+ conn.once("close", function () { self._activeWsConns.delete(conn); });
393
+ try { route.handler(conn, headers); }
394
+ catch (err) { log.error("ws handler threw: " + err.message); conn._abort(websocket.CLOSE_INTERNAL_ERROR, "handler error"); }
395
+ }
396
+ });
397
+ }
398
+ }
399
+
400
+ if (host) server.listen(port, host, cb);
401
+ else server.listen(port, cb);
402
+ server.timeout = C.TIME.minutes(5);
403
+ return server;
404
+ }
405
+ }
406
+
407
+ // Static file serving middleware
408
+ function serveStatic(dir) {
409
+ var root = path.resolve(dir);
410
+ return (req, res, next) => {
411
+ if (req.method !== "GET") return next();
412
+ var rel = req.pathname;
413
+ if (rel.includes("\0")) return next();
414
+ var filePath = path.resolve(path.join(root, rel));
415
+ if (!filePath.startsWith(root)) return next();
416
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) return next();
417
+
418
+ var ext = path.extname(filePath).toLowerCase();
419
+ var mime = MIME_TYPES[ext] || "application/octet-stream";
420
+ var stat = fs.statSync(filePath);
421
+ var hasVersion = req.url && req.url.includes("?v=");
422
+ var cacheControl = hasVersion
423
+ ? "public, max-age=31536000, immutable"
424
+ : "public, max-age=3600";
425
+ res.writeHead(200, {
426
+ "Content-Type": mime,
427
+ "Content-Length": stat.size,
428
+ "Cache-Control": cacheControl,
429
+ });
430
+ fs.createReadStream(filePath).pipe(res);
431
+ };
432
+ }
433
+
434
+ module.exports = {
435
+ Router: Router,
436
+ serveStatic: serveStatic,
437
+ };