Files
OmniRoute/scripts/build/postinstall.mjs
Diego Rodrigues de Sa e Souza 7d57d9f4a1 fix(providers): retire common ChatGPT Web provider (#11754)
Rebased onto the current release/v3.8.51 tip as part of a combined provider-retirement/provenance merge batch (Designer Web, Felo Web, Runtime, GPL-derived removal, Qwen Web already landed). Large conflict set (this is the biggest PR in the batch — the common ChatGPT Web provider touches chat, images, count-tokens, session leases, and combos). Conflicts resolved:

- `open-sse/config/providers/registry/chatgpt-web/*`, `open-sse/executors/chatgpt-web*`, `open-sse/handlers/imageGeneration/providers/chatgptWeb.ts`, and their tests: kept deleted, matching the PR's stated scope.
- `open-sse/config/providers/registry/minimax/web/index.ts`, `open-sse/handlers/imageGeneration/providers/geminiWeb.ts`, `open-sse/executors/gemini-web.ts`'s stale image-mode branch: base-drift collisions against already-merged sibling retirements (#11691, #11708) — kept deleted / dropped the dead code, since this PR's own branch forked before those merged.
- `src/shared/constants/reservedProviderPrefixes.ts`, `open-sse/executors/index.ts`, `executorProxy.ts`, `virtualFactory.ts`, `autoStrategy.ts`, `src/lib/db/providers.ts`, `src/sse/handlers/chat.ts`: combined the Designer + Runtime (Felo/Qwen) + common-ChatGPT-Web retirement guard calls at each shared chokepoint — compute-once-then-OR pattern, consistent with prior combinations in this batch.
- `src/sse/services/model.ts` / `src/sse/handlers/chatHelpers.ts`: adopted this PR's new `getModelInfoOrRetirementResponse()` central wrapper (a real improvement over ad-hoc try/catch), and extended it to also catch the Designer + Runtime retirement errors it didn't originally cover, so the consolidation doesn't regress the other two mechanisms.
- `src/app/api/v1/images/edits/route.ts`: this PR moved the retirement check earlier (before `enforceApiKeyPolicy`) but left the old later call+catch block in place from base drift — removed the now-redundant duplicate `resolveImageRouteModel()` call and merged the Designer catch into the earlier one.
- `open-sse/config/imageRegistry.ts`, `tests/snapshots/executors/executor-map.json` (`keyCount` recomputed to 133), `tests/snapshots/provider/translate-path.json`: same "both sides inserted a different retired provider at the same slot" pattern — resolved by dropping both.
- `tests/unit/chatcore-executor-proxy.test.ts`, `provider-node-reserved-prefix.test.ts`, `combo-auto-candidate-expansion.test.ts`, `messages-count-tokens-route.test.ts`, `virtual-auto-combo.test.ts`: split into independent per-mechanism test blocks (established pattern); `virtual-auto-combo.test.ts`'s old "includes cookie web-session providers" positive-inclusion test (which used chatgpt-web as its example) was retired along with the provider and replaced by this PR's negative-exclusion test for the same slot.
- `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` (+ 4 i18n mirrors), `README.md`, `FREE-TIERS-GUIDE.md`, `docs/diagrams/free-tier-budget.svg`, `docs/screenshots/free-tier-budget-card.svg`, `docs/reference/PROVIDER_REFERENCE.md`: recomputed every stale count from the real merged state — 104 executors (`countFiles` gate logic), 351 providers (regenerated via `gen:provider-reference`), 152/351 `hasFree` entries, 445/438/7 free-tier catalog rows, 13 ToS-avoid providers, budget-card regenerated via its real generator script. One doc conflict (`oauth/` module list) needed picking HEAD's side specifically — theirs still listed the already-removed `raycast` module instead of the real `openference`.
- `config/quality/test-masking-allowlist.json`: additive merge of the PR's 17 `_deletedWithReplacement` entries alongside the batch's existing ones (one real duplicate-key mistake in my first pass, caught and fixed via a `object_pairs_hook` duplicate-key check before finalizing).

Also fixed two real, unrelated-to-my-merge issues surfaced by the focused suite:
- `tests/unit/resolve-web-provider-host.test.ts`: the PR's own test had a typo — it asserted `perplexity-web`'s resolved host as `"perplexity.ai"`, but the provider's registered `website` is `"https://www.perplexity.ai"` and the resolver returns the URL's `host` verbatim (no www-stripping), so the correct value is `"www.perplexity.ai"` (consistent with the same test's own `url` assertion).
- `tests/unit/hard-session-lease-bypass-inventory.test.ts`: this golden call-site inventory was already stale on the pristine post-#11713 tip (confirmed via a throwaway probe worktree) — `src/lib/db/providers.ts`'s 3 connection-fallback sites and a third `src/app/api/providers/route.ts` site were never added to the golden list by the earlier-merged #11698/#11720 PRs. Updated it to the real current inventory (dated inline comments explain each delta and which PR introduced it), plus this PR's own legitimate deltas (image-edits duplicate-call removal, `ChatGptWebExecutor.execute()` site removed).

Focused suite green (433/433 across executor-proxy, reserved-prefix, hard-session-lease-bypass-inventory, resolve-web-provider-host, retirement/runtime-block/source-retirement/management-retirement/image-handler-retirement, migration-168, combo-auto-candidate-expansion, virtual-auto-combo, executor-map-golden and siblings), plus `typecheck:core`, `check-file-size`, and `check-changelog-integrity` clean. Thanks for the thorough provenance-hold retirement work — appreciated.
2026-08-28 06:52:46 -03:00

481 lines
17 KiB
JavaScript

#!/usr/bin/env node
/**
* OmniRoute — Postinstall Native Module Fix
*
* The npm package ships with a Next.js standalone build that includes
* native modules compiled for the build platform (Linux x64) inside
* dist/node_modules/. However, npm also installs these as top-level
* dependencies (in the root node_modules/), correctly compiled for
* the user's platform.
*
* This script copies the correctly-built native binaries from the root
* into the standalone dist directory — no rebuild or build tools needed.
*
* Modules repaired:
* - better-sqlite3 (SQLite bindings)
* - wreq-js (TLS client for OAuth providers)
* - tls-client-node (TLS client for claude-web/grok-web/lmarena/perplexity-web)
* - sql.js (WASM SQLite fallback runtime)
* - node-machine-id (local CLI machine-token server runtime)
*
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/129
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/321
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/426
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/1634
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/7802
*/
import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { PUBLISHED_BUILD_ARCH, PUBLISHED_BUILD_PLATFORM } from "./native-binary-compat.mjs";
import { hasStandaloneAppBundle, isTermux } from "./postinstallSupport.mjs";
import { colocateLlmlinguaOptionals } from "./colocateOptionals.mjs";
import { fixTlsClientNodeBinary } from "./fixTlsClientNodeBinary.mjs";
import { fixPlaywrightAndroid } from "./fixPlaywrightAndroid.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..", "..");
const requireFromPackage = createRequire(join(ROOT, "package.json"));
/**
* Patch node-gyp's common.gypi to include the android_ndk_path variable.
*
* On Termux/Android, node-gyp's bundled common.gypi (in ~/.cache/node-gyp/<version>/)
* does not define the `android_ndk_path` variable that the build system expects.
* Setting GYP_DEFINES="android_ndk_path=''" is not enough because common.gypi
* is parsed separately and the variable must be declared in the 'variables' section.
*
* This function finds and patches the common.gypi for the current Node.js version,
* adding `'android_ndk_path%': ''` to the variables block. The patch is idempotent.
*/
function patchNodeGypCommonGypi() {
try {
const nodeVersion = process.version; // e.g. "v26.4.0"
const gypDir = join(
process.env.HOME || process.env.USERPROFILE || "/root",
".cache",
"node-gyp",
nodeVersion.replace(/^v/, "")
);
const commonGypi = join(gypDir, "include", "node", "common.gypi");
if (!existsSync(commonGypi)) {
console.warn(` ⚠️ common.gypi not found at ${commonGypi}, skipping patch`);
return;
}
let content = readFileSync(commonGypi, "utf8");
// Check if already patched
if (content.includes("android_ndk_path")) {
return;
}
// Find the variables section and add android_ndk_path
// The pattern is: 'variables': { 'node_use_openssl%': ... }
// We insert our variable right after the opening of the variables block
const variablesMatch = content.match(/('variables'\s*:\s*\{)/);
if (variablesMatch) {
const insertPos = content.indexOf(variablesMatch[0]) + variablesMatch[0].length;
content =
content.slice(0, insertPos) + "\n 'android_ndk_path%': ''," + content.slice(insertPos);
writeFileSync(commonGypi, content, "utf8");
console.log(` ✅ Patched common.gypi for Android at ${commonGypi}`);
}
} catch (err) {
console.warn(` ⚠️ Could not patch common.gypi: ${err.message}`);
}
}
const appBinary = join(
ROOT,
"dist",
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
const rootBinary = join(
ROOT,
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
async function fixBetterSqliteBinary() {
if (!existsSync(join(ROOT, "dist", "node_modules", "better-sqlite3"))) {
return;
}
const platformMatch =
process.platform === PUBLISHED_BUILD_PLATFORM && process.arch === PUBLISHED_BUILD_ARCH;
if (platformMatch) {
try {
process.dlopen({ exports: {} }, appBinary);
return;
} catch (err) {
console.warn(` ⚠️ Bundled binary incompatible despite platform match: ${err.message}`);
}
}
console.log(`\n 🔧 Fixing better-sqlite3 binary for ${process.platform}-${process.arch}...`);
if (existsSync(rootBinary)) {
try {
mkdirSync(dirname(appBinary), { recursive: true });
copyFileSync(rootBinary, appBinary);
} catch (err) {
console.warn(` ⚠️ Failed to copy binary: ${err.message}`);
}
try {
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module fixed successfully!\n");
return;
} catch (err) {
console.warn(` ⚠️ Copied binary failed to load: ${err.message}`);
}
}
console.log(" 📥 Attempting to download prebuilt binary via node-pre-gyp...");
try {
const { execSync } = await import("node:child_process");
const preGypBin = join(
ROOT,
"dist",
"node_modules",
".bin",
process.platform === "win32" ? "node-pre-gyp.cmd" : "node-pre-gyp"
);
const preGypFallback = join(
ROOT,
"dist",
"node_modules",
"@mapbox",
"node-pre-gyp",
"bin",
"node-pre-gyp"
);
const preGypCmd = existsSync(preGypBin) ? preGypBin : preGypFallback;
if (existsSync(preGypCmd)) {
execSync(`"${process.execPath}" "${preGypCmd}" install --fallback-to-build=false`, {
cwd: join(ROOT, "dist", "node_modules", "better-sqlite3"),
stdio: "inherit",
timeout: 60_000,
});
mkdirSync(dirname(appBinary), { recursive: true });
try {
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Prebuilt binary downloaded and loaded successfully!\n");
return;
} catch (loadErr) {
console.warn(` ⚠️ Downloaded binary failed to load: ${loadErr.message}`);
}
} else {
console.warn(" ⚠️ node-pre-gyp not found, skipping prebuilt download.");
}
} catch (err) {
console.warn(` ⚠️ node-pre-gyp download failed: ${err.message.split("\n")[0]}`);
}
console.log(" ⚠️ Attempting npm rebuild (requires build tools)...");
try {
const { execSync } = await import("node:child_process");
// On Android/Termux, rebuild from source with --build-from-source flag
const isAndroid = process.platform === "android" || isTermux();
const rebuildCmd = isAndroid
? "npm install better-sqlite3 --build-from-source --force"
: "npm rebuild better-sqlite3";
const env = { ...process.env };
if (isAndroid) {
env.GYP_DEFINES = "android_ndk_path=''";
// Patch node-gyp's common.gypi to include android_ndk_path variable
// so the gyp build system doesn't fail with "Unknown variable"
patchNodeGypCommonGypi();
}
execSync(rebuildCmd, {
cwd: join(ROOT, "dist"),
stdio: "inherit",
timeout: isAndroid ? 600_000 : 300_000, // ARM compilation is slower
env,
});
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module rebuilt successfully!\n");
return;
} catch (err) {
const isTimeout = err.killed || err.signal === "SIGTERM";
if (isTimeout) {
const secs = isAndroid ? 600 : 300;
console.warn(` ⚠️ npm rebuild timed out after ${secs}s.`);
} else {
console.warn(` ⚠️ npm rebuild failed: ${err.message}`);
}
}
console.warn("\n ⚠️ Could not fix better-sqlite3 native module automatically.");
console.warn(" The server may not start correctly.");
console.warn(" Manual fix options:");
if (process.platform === "win32") {
console.warn(" Option A (easiest — no build tools needed):");
console.warn(` cd "${join(ROOT, "dist", "node_modules", "better-sqlite3")}"`);
console.warn(" npx @mapbox/node-pre-gyp install --fallback-to-build=false");
console.warn(" Option B (requires Build Tools for Visual Studio):");
console.warn(` cd "${join(ROOT, "dist")}" && npm rebuild better-sqlite3`);
console.warn(" Install from: https://visualstudio.microsoft.com/visual-cpp-build-tools/");
console.warn(" Also ensure Python is installed: https://python.org");
} else if (process.platform === "darwin") {
console.warn(` cd ${join(ROOT, "dist")} && npm rebuild better-sqlite3`);
console.warn(" If build tools are missing: xcode-select --install");
} else {
console.warn(` cd ${join(ROOT, "dist")} && npm rebuild better-sqlite3`);
}
console.warn("");
}
/**
* Fix wreq-js native binary for the standalone dist directory.
*
* wreq-js ships platform-specific .node binaries under rust/.
* The standalone build may only contain Linux binaries from the CI.
* This copies the correct platform binary from the root install.
*
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/1634
*/
async function fixWreqJsBinary() {
// wreq-js native module is not loadable in Termux (libgcc path mismatch).
// The runtime already falls back gracefully when wreq-js is unavailable.
if (process.platform === "android" || isTermux()) {
console.log(
" [postinstall] wreq-js: skipped on Termux/Android " +
"(libgcc not available — OAuth TLS fingerprinting will use the fallback path)"
);
return;
}
const appWreqDir = join(ROOT, "dist", "node_modules", "wreq-js", "rust");
const rootWreqDir = join(ROOT, "node_modules", "wreq-js", "rust");
if (!existsSync(join(ROOT, "dist", "node_modules", "wreq-js"))) {
return;
}
const binaryName = `wreq-js.${process.platform}-${process.arch}.node`;
const appBinaryPath = join(appWreqDir, binaryName);
const rootBinaryPath = join(rootWreqDir, binaryName);
// Check if the platform binary already exists and loads
if (existsSync(appBinaryPath)) {
try {
process.dlopen({ exports: {} }, appBinaryPath);
return; // Already working
} catch (err) {
console.warn(` ⚠️ wreq-js binary exists but failed to load: ${err.message}`);
}
}
console.log(`\n 🔧 Fixing wreq-js binary for ${process.platform}-${process.arch}...`);
// Strategy 1: Copy from root node_modules
if (existsSync(rootBinaryPath)) {
try {
mkdirSync(appWreqDir, { recursive: true });
copyFileSync(rootBinaryPath, appBinaryPath);
process.dlopen({ exports: {} }, appBinaryPath);
console.log(" ✅ wreq-js native module fixed successfully!\n");
return;
} catch (err) {
console.warn(` ⚠️ Copied wreq-js binary failed to load: ${err.message}`);
}
}
// Strategy 2: Copy entire rust/ directory from root (gets all platform binaries)
if (existsSync(rootWreqDir)) {
try {
mkdirSync(appWreqDir, { recursive: true });
const files = readdirSync(rootWreqDir);
for (const file of files) {
if (file.endsWith(".node")) {
copyFileSync(join(rootWreqDir, file), join(appWreqDir, file));
}
}
if (existsSync(appBinaryPath)) {
process.dlopen({ exports: {} }, appBinaryPath);
console.log(" ✅ wreq-js native module fixed (full copy) successfully!\n");
return;
}
} catch (err) {
console.warn(` ⚠️ wreq-js full copy failed: ${err.message}`);
}
}
// Strategy 3: Rebuild wreq-js inside dist/
console.log(" 📥 Attempting npm rebuild wreq-js...");
try {
const { execSync } = await import("node:child_process");
execSync("npm rebuild wreq-js", {
cwd: join(ROOT, "dist"),
stdio: "inherit",
timeout: 120_000,
});
if (existsSync(appBinaryPath)) {
process.dlopen({ exports: {} }, appBinaryPath);
console.log(" ✅ wreq-js native module rebuilt successfully!\n");
return;
}
} catch (err) {
console.warn(` ⚠️ wreq-js rebuild failed: ${err.message}`);
}
console.warn(
`\n ⚠️ Could not fix wreq-js native module for ${process.platform}-${process.arch}.`
);
console.warn(" OAuth-based providers (Codex, Cursor, etc.) may not work.");
console.warn(` Manual fix: cd ${join(ROOT, "dist")} && npm install wreq-js --no-save\n`);
}
async function ensureSwcHelpers() {
if (!hasStandaloneAppBundle(ROOT)) {
return;
}
const swcHelpersApp = join(ROOT, "dist", "node_modules", "@swc", "helpers");
const swcHelpersRoot = join(ROOT, "node_modules", "@swc", "helpers");
if (existsSync(swcHelpersApp)) {
return;
}
if (existsSync(swcHelpersRoot)) {
try {
const { cpSync } = await import("node:fs");
mkdirSync(join(ROOT, "dist", "node_modules", "@swc"), { recursive: true });
cpSync(swcHelpersRoot, swcHelpersApp, { recursive: true });
console.log(" ✅ @swc/helpers copied to standalone dist/node_modules.\n");
} catch (err) {
console.warn(` ⚠️ Could not copy @swc/helpers: ${err.message}`);
console.warn(
" Try manually: cp -r node_modules/@swc/helpers dist/node_modules/@swc/helpers\n"
);
}
return;
}
console.warn(" ⚠️ @swc/helpers not found in root node_modules either.");
console.warn(" Try: npm install --save-exact @swc/helpers@0.5.19\n");
}
async function syncProjectEnv() {
try {
const { syncEnv } = await import("./sync-env.mjs");
syncEnv({ rootDir: ROOT });
} catch (err) {
console.warn(` ⚠️ .env sync skipped: ${err.message}`);
}
}
/**
* Co-locate the LLMLingua-2 SLM optional dependency closure into dist/node_modules so the
* compression "ultra" SLM tier (PR #4257) resolves a single @huggingface/transformers instance at
* runtime. No-op unless the optionals were installed (`--include=optional`). See colocateOptionals.mjs.
*/
async function ensureLlmlinguaOptionals() {
try {
colocateLlmlinguaOptionals({ rootDir: ROOT, log: (m) => console.log(m) });
} catch (err) {
// Best-effort: the SLM tier is itself fail-open, so a co-location hiccup never fails the install.
console.warn(` ⚠️ LLMLingua optional co-location skipped: ${err.message}`);
}
}
/**
* Preflight check for development installs (when standalone dist/ bundle is not present).
* Warns or errors if critical native dependencies like better-sqlite3 were skipped by npm >= 11
* allowScripts restrictions.
*/
async function verifyDevNativeModules() {
if (hasStandaloneAppBundle(ROOT)) {
return;
}
const criticalModules = [
{ name: "better-sqlite3", fatal: true },
{ name: "esbuild", fatal: true },
];
for (const { name, fatal } of criticalModules) {
if (!existsSync(join(ROOT, "node_modules", name))) {
const level = fatal ? "🔴 CRITICAL" : "⚠️ WARNING";
console.error(`\n ${level}: '${name}' is missing from node_modules/`);
console.error(` This usually happens with npm ≥ 11, which blocks install`);
console.error(` scripts for optional dependencies by default.`);
console.error(`\n Fix options:`);
console.error(` 1. npm approve-scripts ${name} && npm install`);
console.error(` 2. npm pack ${name} && tar -xzf ${name}-*.tgz -C node_modules`);
console.error(` && mv node_modules/package node_modules/${name}`);
console.error(` 3. Downgrade to npm 10: npm install -g npm@10\n`);
}
}
}
async function ensureStandaloneRuntimePackages() {
for (const packageName of ["sql.js", "node-machine-id"]) {
let source;
try {
source = dirname(dirname(requireFromPackage.resolve(packageName)));
} catch {
console.warn(` ⚠️ ${packageName} could not be resolved from the npm install.`);
continue;
}
const destination = join(ROOT, "dist", "node_modules", packageName);
try {
mkdirSync(dirname(destination), { recursive: true });
cpSync(source, destination, { recursive: true, force: true });
console.log(`${packageName} copied to standalone dist/node_modules.`);
} catch (err) {
console.warn(` ⚠️ Could not copy ${packageName}: ${err.message}`);
}
}
}
await verifyDevNativeModules();
await fixBetterSqliteBinary();
await fixWreqJsBinary();
await fixTlsClientNodeBinary({ rootDir: ROOT });
await fixPlaywrightAndroid({ rootDir: ROOT });
await ensureSwcHelpers();
await ensureStandaloneRuntimePackages();
await ensureLlmlinguaOptionals();
await syncProjectEnv();
// Warm up native runtimes (better-sqlite3 in ~/.omniroute/runtime/).
// Non-fatal: errors are caught inside postinstall.mjs.
try {
await import("../postinstall.mjs");
} catch {
// Silently skip — runtime warm-up is best-effort.
}