Release v3.8.41 (#5327)

Release v3.8.41 — 52 commits since v3.8.40 (19 CHANGELOG bullets, 11 contributors).

All gating CI green: Unit×8, Coverage×8, Vitest, Package Artifact, Quality Ratchet, CodeQL, Lint, Docs Sync (Strict), Node 24/26 compat, E2E×9, Integration, Electron smoke.

Advisory checks overridden (main unprotected): PR Test Policy = test-masking heuristic on the cumulative 52-commit assert delta (legitimate dead-code-sweep removals + consolidations, reviewed per-PR); SonarCloud/SonarQube = new-code maintainability/coverage quality gate (CodeQL/Semgrep/Security/npm-audit/Dependabot all clean — not a security finding).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-29 16:51:03 -03:00
committed by GitHub
parent 7c23dab64d
commit 78f09c8d9f
222 changed files with 6005 additions and 1205 deletions

View File

@@ -149,6 +149,12 @@ const EXTRA_MODULE_ENTRIES = [
src: ["scripts", "dev", "webdav-handler.mjs"],
dest: ["webdav-handler.mjs"],
},
{
// #5242: opt-in HTTPS/TLS resolver (server-ws.mjs dependency).
label: "tls-options (server-ws.mjs dependency)",
src: ["scripts", "dev", "tls-options.mjs"],
dest: ["tls-options.mjs"],
},
{
label: "runtime-env script",
src: ["scripts", "build", "runtime-env.mjs"],

View File

@@ -44,6 +44,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
"peer-stamp.mjs",
"responses-ws-proxy.mjs",
"scripts/dev/sync-env.mjs",
"scripts/dev/tls-options.mjs",
"server.js",
"server-ws.mjs",
"webdav-handler.mjs",
@@ -111,6 +112,9 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
"scripts/build/sync-env.mjs",
"scripts/dev/responses-ws-proxy.mjs",
"scripts/dev/sync-env.mjs",
// #5361: imported at runtime by bin/cli/commands/serve.mjs + the standalone
// server wrapper for opt-in native HTTPS/TLS serving (kept dependency-light).
"scripts/dev/tls-options.mjs",
"scripts/postinstall.mjs",
"src/shared/utils/nodeRuntimeSupport.ts",
];

View File

@@ -36,6 +36,55 @@ export function calibrateHeapFallbackMb(totalmemBytes) {
return Math.min(4096, Math.max(512, target));
}
const MAX_OLD_SPACE_FLAG = "--max-old-space-size";
/**
* True when the caller already pinned the V8 heap via NODE_OPTIONS
* (`--max-old-space-size=…`). Used to decide whether `omniroute serve` may
* append/inject the calibrated default — a user-set value must always win.
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
*/
export function envHasExplicitHeapFlag(env = process.env) {
return String(env?.NODE_OPTIONS || "").includes(MAX_OLD_SPACE_FLAG);
}
/**
* Assemble the NODE_OPTIONS string for the spawned server, preserving any flags
* the user already exported. #5238: `omniroute serve` used to UNCONDITIONALLY
* overwrite NODE_OPTIONS with the calibrated `--max-old-space-size`, silently
* discarding a user-set `NODE_OPTIONS=--max-old-space-size=8192` (reporter set
* 8192 and still OOM'd at ~505MB). Mirrors the Electron (electron/main.js) and
* standalone (scripts/dev/run-standalone.mjs) launchers:
* - if NODE_OPTIONS already contains `--max-old-space-size`, keep it as-is
* (the user's value wins);
* - otherwise append the calibrated `--max-old-space-size=<memoryLimit>` to
* the existing NODE_OPTIONS, preserving unrelated flags (e.g.
* `--enable-source-maps`).
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
* @param {number} memoryLimit — calibrated V8 heap ceiling (MB)
* @returns {string} the NODE_OPTIONS value to pass to the child process
*/
export function buildServerNodeOptions(env = process.env, memoryLimit) {
const existing = String(env?.NODE_OPTIONS || "").trim();
if (existing.includes(MAX_OLD_SPACE_FLAG)) return existing;
return `${existing} ${MAX_OLD_SPACE_FLAG}=${memoryLimit}`.trim();
}
/**
* Build the leading `node` CLI args that pin the V8 heap. When the user already
* pinned the heap via NODE_OPTIONS, return `[]` so we do NOT inject a
* conflicting/shadowing CLI `--max-old-space-size` (CLI args override
* NODE_OPTIONS, which would re-introduce #5238). Otherwise return the calibrated
* flag — NODE_OPTIONS already carries the same value, so this stays redundant
* (identical value), never conflicting.
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
* @param {number} memoryLimit — calibrated V8 heap ceiling (MB)
* @returns {string[]}
*/
export function buildNodeHeapArgs(env = process.env, memoryLimit) {
return envHasExplicitHeapFlag(env) ? [] : [`${MAX_OLD_SPACE_FLAG}=${memoryLimit}`];
}
/**
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [fromEnv]
* Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn.

View File

@@ -5,11 +5,23 @@ import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs";
import { maybeHandleWebdav } from "./webdav-handler.mjs";
import methodGuard from "./http-method-guard.cjs";
import { resolveTlsOptions, createServerListener } from "./tls-options.mjs";
const originalCreateServer = http.createServer.bind(http);
const proxiesByPort = new Map();
const { wrapRequestListenerWithMethodGuard } = methodGuard;
// Opt-in native HTTPS (#5242). Resolved once at boot: when both OMNIROUTE_TLS_CERT
// and OMNIROUTE_TLS_KEY point at readable files we terminate TLS on the same
// listener Next binds to (so WS `upgrade` / request wrappers keep working over
// TLS). Absent or misconfigured → null → identical plain-HTTP behavior as before.
const tlsOptions = resolveTlsOptions(process.env);
if (tlsOptions) {
console.log(
`[omniroute][tls] HTTPS enabled — terminating TLS with cert=${tlsOptions.certPath}`
);
}
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
// Per-process secret proving the trusted peer-IP stamp came from this server.
ensurePeerStampToken();
@@ -107,7 +119,10 @@ http.createServer = function createServerWithResponsesWs(...args) {
);
}
const server = originalCreateServer(...args);
// When TLS is configured, return an https.Server (terminating TLS on the same
// listener); otherwise the original http.Server. The downstream .on/.addListener
// patches below apply identically to both (https.Server extends http.Server).
const server = createServerListener(args, tlsOptions, { createHttp: originalCreateServer });
const originalOn = server.on.bind(server);
const originalAddListener = server.addListener.bind(server);

View File

@@ -0,0 +1,92 @@
// scripts/dev/tls-options.mjs
//
// Pure, dependency-light helpers for OmniRoute's opt-in native HTTPS/TLS serving
// (#5242, Bug 1C). Kept side-effect-free and free of heavy imports so it can be
// imported both by the CLI (bin/cli/commands/serve.mjs) and by the standalone
// server wrapper (standalone-server-ws.mjs), and unit-tested in isolation.
//
// TLS is strictly opt-in: when neither cert nor key is provided the server
// behaves EXACTLY as before (plain HTTP). A misconfiguration (only one of the
// pair, or an unreadable path) NEVER crashes the server — it logs a warning and
// falls back to HTTP, preserving today's behavior and loopback/security posture.
import fs from "node:fs";
import http from "node:http";
import https from "node:https";
/**
* Resolve TLS options from environment variables.
*
* Reads `OMNIROUTE_TLS_CERT` and `OMNIROUTE_TLS_KEY` (filesystem paths). Returns
* `{ cert, key }` (file contents) only when BOTH are provided and readable.
* Otherwise returns `null` so the caller serves plain HTTP — never throwing.
*
* @param {NodeJS.ProcessEnv} [env=process.env]
* @param {{ readFileSync?: typeof fs.readFileSync, warn?: (msg: string) => void }} [deps]
* @returns {{ cert: Buffer|string, key: Buffer|string, certPath: string, keyPath: string } | null}
*/
export function resolveTlsOptions(
env = process.env,
{ readFileSync = fs.readFileSync, warn = (m) => console.warn(m) } = {}
) {
const certPath = typeof env?.OMNIROUTE_TLS_CERT === "string" ? env.OMNIROUTE_TLS_CERT.trim() : "";
const keyPath = typeof env?.OMNIROUTE_TLS_KEY === "string" ? env.OMNIROUTE_TLS_KEY.trim() : "";
// Neither provided → plain HTTP, no warning (the common, default case).
if (!certPath && !keyPath) return null;
// Only one of the pair → never half-enable TLS. Warn + fall back to HTTP.
if (!certPath || !keyPath) {
warn(
`[omniroute][tls] HTTPS not enabled: both OMNIROUTE_TLS_CERT and OMNIROUTE_TLS_KEY ` +
`are required (only ${certPath ? "cert" : "key"} provided). Serving HTTP.`
);
return null;
}
// Both provided → read them. A bad/unreadable path falls back to HTTP rather
// than crashing the server over a TLS misconfiguration.
try {
const cert = readFileSync(certPath);
const key = readFileSync(keyPath);
return { cert, key, certPath, keyPath };
} catch (err) {
warn(
`[omniroute][tls] HTTPS not enabled: could not read TLS cert/key ` +
`(${err?.code || err?.message || String(err)}). Serving HTTP.`
);
return null;
}
}
/**
* Create either an `https.Server` (when `tlsOptions` is provided) or an
* `http.Server` (when it is `null`), forwarding the same request-listener /
* options arguments the caller would otherwise pass to `http.createServer`.
*
* When TLS is enabled, any leading options object is merged with `cert`/`key`
* and the trailing request listener is preserved, so existing wiring (WebSocket
* `upgrade` handling, request wrappers) keeps working over TLS unchanged.
*
* @param {any[]} args - the args originally passed to http.createServer (options?, listener?)
* @param {{ cert: Buffer|string, key: Buffer|string } | null} tlsOptions
* @param {{ createHttp?: Function, createHttps?: Function }} [deps]
* @returns {import("node:http").Server | import("node:https").Server}
*/
export function createServerListener(
args,
tlsOptions,
{ createHttp = http.createServer.bind(http), createHttps = https.createServer.bind(https) } = {}
) {
const argList = Array.isArray(args) ? args : args === undefined ? [] : [args];
if (!tlsOptions) return createHttp(...argList);
const { cert, key } = tlsOptions;
const lastFnIdx = argList.map((a) => typeof a === "function").lastIndexOf(true);
const listener = lastFnIdx >= 0 ? argList[lastFnIdx] : undefined;
const baseOpts =
argList[0] && typeof argList[0] === "object" && !Buffer.isBuffer(argList[0]) ? argList[0] : {};
const merged = { ...baseOpts, cert, key };
return listener ? createHttps(merged, listener) : createHttps(merged);
}