diff --git a/.env.example b/.env.example index 2b454d2fb1..5660c922f6 100644 --- a/.env.example +++ b/.env.example @@ -1531,6 +1531,14 @@ APP_LOG_TO_FILE=true # Default: 86400000 (24 hours) # OPENROUTER_CATALOG_TTL_MS=86400000 +# Enrich the dashboard providers list with OpenRouter weekly ranking stats. +# ON by default; set false to skip the background fetch entirely (#9324). +# Used by: src/lib/catalog/openrouterProviderStats.ts +# OPENROUTER_PROVIDER_STATS_ENABLED=true +# Cache TTL for the OpenRouter provider stats snapshot, in ms. +# Default: 86400000 (24 hours) +# OPENROUTER_PROVIDER_STATS_TTL_MS=86400000 + # ── Model catalog response shape ── # Include display-friendly name fields in /v1/models responses. # Disable for clients that expect model IDs only. @@ -2162,6 +2170,15 @@ INSPECTOR_MAX_BODY_KB=1024 INSPECTOR_MASK_SECRETS=true INSPECTOR_LLM_HOSTS_EXTRA= INSPECTOR_INTERNAL_INGEST_TOKEN= +# Shared secret for identity-preserving internal REST hops (#9260): when an +# OmniRoute component calls another local OmniRoute route, this token (sent as +# x-omniroute-internal-service-token) marks the request as internal so the +# original caller identity is preserved. OPT-IN: unset disables the mechanism. +# Used by: src/lib/api/internalServiceAuth.ts +# OMNIROUTE_INTERNAL_SERVICE_TOKEN= +# File-based variant (secret-file pattern; wins only when the inline var is +# unset): path to a file whose trimmed content is the token. +# OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE= # Quota Sharing (Group B — planos 16+22) QUOTA_STORE_DRIVER=sqlite # sqlite | redis # QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis) diff --git a/changelog.d/fixes/9553-prepublish-npm-entry-posix.md b/changelog.d/fixes/9553-prepublish-npm-entry-posix.md new file mode 100644 index 0000000000..af1b199ee3 --- /dev/null +++ b/changelog.d/fixes/9553-prepublish-npm-entry-posix.md @@ -0,0 +1 @@ +- **fix(build):** `npm run build:cli` (prepublish) no longer fails on POSIX with "npm-cli.js not found next to the running Node binary". The #8858 shim-free npm resolver only knew the Windows layout (`\node_modules\npm`); on GitHub hosted runners, nvm and system installs npm lives at `/lib/node_modules/npm` while node is `/bin/node`, so every fresh CI checkout died installing `@omniroute/opencode-plugin` deps (Fast Production Build + dast-smoke red on all PRs). The resolver, extracted to `scripts/build/resolveNpmEntry.ts`, now tries `npm_execpath` (exported by `npm run` itself) first, then the Windows layout, then the POSIX layout — covered by `tests/unit/build/resolve-npm-entry.test.ts` including a live POSIX regression guard. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index d29d17a67c..3e19495178 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1150,6 +1150,13 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), | `OMNIROUTE_LOCAL_ENDPOINTS_TOKEN` | _(unset)_ | `src/lib/security/localEndpoints.ts` | Bearer token for `/api/local/*` callers that aren't on loopback (e.g. the desktop app). When set, requests from non-loopback IPs must carry `Authorization: Bearer `. Required when `OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1` in non-loopback deployments. | | `OMNIROUTE_REDIS_CONTAINER_NAME` | `omniroute-redis` | `bin/cli/commands/redis.mjs` | Container name for the 1-click Redis launcher (`omniroute redis up`). Used by both the CLI and the `RedisLauncherPanel` GUI. | | `OMNIROUTE_REDIS_HOST_PORT` | `6379` | `bin/cli/commands/redis.mjs` | Host port for the 1-click Redis launcher. Bump if the host already binds 6379. The container's internal port stays 6379. | +| `OMNIROUTE_REDIS_BIND_HOST` | `127.0.0.1` | `bin/cli/commands/redis.mjs` | Host interface the 1-click Redis launcher publishes on. The launcher starts Redis WITHOUT a password, so binding `0.0.0.0` hands every host on your LAN an unauthenticated Redis — only widen this if you also set a password on the instance yourself. | +| `REDIS_BIND_HOST` | `127.0.0.1` | `docker-compose.yml` | Host interface docker-compose publishes the Redis sidecar on (#9286). The compose Redis runs without `requirepass`; app containers reach it over the compose network (`redis:6379`) — the published port exists only for host-side tooling. `0.0.0.0` exposes an unauthenticated Redis to the whole LAN. | +| `REDIS_PORT` | `6379` | `docker-compose.yml` | Host port for the compose Redis sidecar. | +| `OMNIROUTE_INTERNAL_SERVICE_TOKEN` | _(unset — mechanism disabled)_ | `src/lib/api/internalServiceAuth.ts` | Shared secret for identity-preserving internal REST hops (#9260): OmniRoute components calling other local OmniRoute routes send it as `x-omniroute-internal-service-token` so the original caller identity is preserved. Compared with `timingSafeEqual`. | +| `OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE` | _(unset)_ | `src/lib/api/internalServiceAuth.ts` | Secret-file variant of the internal service token: path to a file whose trimmed content is the token. Only consulted when the inline var is unset. | +| `OPENROUTER_PROVIDER_STATS_ENABLED` | `true` | `src/lib/catalog/openrouterProviderStats.ts` | Enrich the dashboard providers list with OpenRouter weekly ranking stats (#9324). On by default; set `false` to skip the background fetch entirely (non-blocking, never fatal). | +| `OPENROUTER_PROVIDER_STATS_TTL_MS` | `86400000` (24h) | `src/lib/catalog/openrouterProviderStats.ts` | Cache TTL for the OpenRouter provider-stats snapshot, in milliseconds. | | `OMNIROUTE_REDIS_IMAGE` | `redis:7-alpine` | `bin/cli/commands/redis.mjs` | Redis image used by the 1-click Redis launcher. Override to `redis:8-alpine` or a private registry mirror as needed. | | `QDRANT_HOST` | `qdrant` | _(opt-in cluster profile)_ | Hostname of the Qdrant sidecar when `--profile memory` is active. Default points to the in-network qdrant service name; override for an external deployment. Only consumed when `qdrantEnabled` is `true` in code (`src/lib/memory/vectorStore.ts:108`). | | `QDRANT_PORT` | `6333` | _(opt-in cluster profile)_ | REST port of the Qdrant sidecar. | diff --git a/scripts/build/prepublish.ts b/scripts/build/prepublish.ts index c899544d83..309180a2c7 100644 --- a/scripts/build/prepublish.ts +++ b/scripts/build/prepublish.ts @@ -27,6 +27,7 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { assembleStandalone } from "./assembleStandalone.mjs"; +import { resolveBundledNpmEntry } from "./resolveNpmEntry.ts"; import { APP_STAGING_ALLOWED_EXACT_PATHS, APP_STAGING_ALLOWED_PATH_PREFIXES, @@ -64,11 +65,6 @@ function resolveLocalBinEntry(packageName: string, binName: string): string | nu } } -function resolveBundledNpmEntry(name: "npm-cli.js" | "npx-cli.js"): string | null { - const candidate = join(dirname(process.execPath), "node_modules", "npm", "bin", name); - return existsSync(candidate) ? candidate : null; -} - /** * Runs a build tool without ever touching a `.cmd` shim. `packageName` is where the * tool lives in the local dependency tree; when it is not installed there the call diff --git a/scripts/build/resolveNpmEntry.ts b/scripts/build/resolveNpmEntry.ts new file mode 100644 index 0000000000..b19bcb1fb2 --- /dev/null +++ b/scripts/build/resolveNpmEntry.ts @@ -0,0 +1,40 @@ +import { existsSync } from "fs"; +import { dirname, join } from "path"; + +/** Injectable seams for {@link resolveBundledNpmEntry} (all default to the real ones). */ +export interface ResolveNpmEntryDeps { + execPath?: string; + /** `process.env.npm_execpath` — set by npm itself when running under `npm run`. */ + npmExecPath?: string; + exists?: (p: string) => boolean; +} + +/** + * Locate `npm-cli.js` / `npx-cli.js` so build steps can run npm/npx through + * `process.execPath` directly and never touch a `.cmd` shim (#8858), covering + * BOTH install layouts: + * - Windows: `\node_modules\npm\bin\` (npm beside the binary) + * - POSIX: `/../lib/node_modules/npm/bin/` (node under `/bin`, + * the shape of GitHub hosted runners, nvm and system installs) + * When the script itself runs under `npm run`, npm exports `npm_execpath` pointing at + * its own npm-cli.js — the most reliable source, tried first (npx-cli.js is its sibling). + */ +export function resolveBundledNpmEntry( + name: "npm-cli.js" | "npx-cli.js", + deps: ResolveNpmEntryDeps = {} +): string | null { + const execPath = deps.execPath ?? process.execPath; + const exists = deps.exists ?? existsSync; + const npmExecPath = deps.npmExecPath ?? process.env.npm_execpath; + + const binDir = dirname(execPath); + const candidates: string[] = []; + if (npmExecPath) candidates.push(join(dirname(npmExecPath), name)); + candidates.push(join(binDir, "node_modules", "npm", "bin", name)); + candidates.push(join(binDir, "..", "lib", "node_modules", "npm", "bin", name)); + + for (const candidate of candidates) { + if (exists(candidate)) return candidate; + } + return null; +} diff --git a/tests/unit/build/resolve-npm-entry.test.ts b/tests/unit/build/resolve-npm-entry.test.ts new file mode 100644 index 0000000000..23795c0b05 --- /dev/null +++ b/tests/unit/build/resolve-npm-entry.test.ts @@ -0,0 +1,71 @@ +/** + * #8858 follow-up — `resolveBundledNpmEntry` only knew the WINDOWS npm layout + * (`\node_modules\npm\bin\...`). On POSIX installs (GitHub + * hosted runners, nvm, system node) npm lives at + * `/lib/node_modules/npm/bin/...` while node is `/bin/node`, + * so the resolver returned null and `npm run build:cli` (Fast Production + * Build, dast-smoke) failed on every fresh CI checkout with + * "npm-cli.js not found next to the running Node binary". + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { resolveBundledNpmEntry } from "../../../scripts/build/resolveNpmEntry.ts"; + +const POSIX_PREFIX = "/opt/hostedtoolcache/node/24.18.0/x64"; +const POSIX_NODE = `${POSIX_PREFIX}/bin/node`; +const POSIX_NPM_CLI = `${POSIX_PREFIX}/lib/node_modules/npm/bin/npm-cli.js`; + +const WIN_STYLE_DIR = "/fake/nodejs"; // Windows layout shape (npm beside the binary) +const WIN_STYLE_NODE = `${WIN_STYLE_DIR}/node.exe`; +const WIN_STYLE_NPM_CLI = `${WIN_STYLE_DIR}/node_modules/npm/bin/npm-cli.js`; + +test("POSIX layout: finds npm-cli.js under /lib/node_modules (hosted runner shape)", () => { + const resolved = resolveBundledNpmEntry("npm-cli.js", { + execPath: POSIX_NODE, + npmExecPath: undefined, + exists: (p) => p === POSIX_NPM_CLI, + }); + assert.equal(resolved, POSIX_NPM_CLI); +}); + +test("Windows layout: still finds npm-cli.js beside the node binary", () => { + const resolved = resolveBundledNpmEntry("npm-cli.js", { + execPath: WIN_STYLE_NODE, + npmExecPath: undefined, + exists: (p) => p === WIN_STYLE_NPM_CLI, + }); + assert.equal(resolved, WIN_STYLE_NPM_CLI); +}); + +test("npm_execpath (set by `npm run`) wins and resolves npx-cli.js as its sibling", () => { + const npmExecPath = `${POSIX_PREFIX}/lib/node_modules/npm/bin/npm-cli.js`; + const npxSibling = `${POSIX_PREFIX}/lib/node_modules/npm/bin/npx-cli.js`; + const resolved = resolveBundledNpmEntry("npx-cli.js", { + execPath: POSIX_NODE, + npmExecPath, + exists: (p) => p === npxSibling, + }); + assert.equal(resolved, npxSibling); +}); + +test("returns null when no layout matches", () => { + const resolved = resolveBundledNpmEntry("npm-cli.js", { + execPath: POSIX_NODE, + npmExecPath: undefined, + exists: () => false, + }); + assert.equal(resolved, null); +}); + +test("live environment: the real node install can resolve npm-cli.js (POSIX regression guard)", (t) => { + if (process.platform === "win32") { + t.skip("POSIX-only live check"); + return; + } + const resolved = resolveBundledNpmEntry("npm-cli.js"); + assert.ok( + resolved, + `real install must resolve npm-cli.js (execPath=${process.execPath}) — the #8858 resolver returned null on POSIX` + ); +});