mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
fix(cli): restore packaged machine-token authentication (#10468)
Obrigado por restaurar e endurecer a autenticação por machine-token no CLI empacotado. Validação (worktree combinado a partir de origin/release/v3.8.50, merge limpo, 0 conflitos — 34 arquivos, +1078/-247): - `npm run typecheck:core` — limpo - `node scripts/check/check-complexity.mjs` — OK (2558 violações vs baseline 2774) - `node scripts/check/check-cognitive-complexity.mjs` — OK (1152 violações vs baseline 1223) - `node scripts/check/check-file-size.mjs` — OK - `node scripts/check/check-changelog-integrity.mjs` — OK - Testes focados (8 arquivos: cli-doctor-command, cli-machine-token, lib/machineToken, lib/managementCliToken, agentSkills-generator, api/settings-audit, check-pack-boot, next-config) — 95/95 passando Os dois achados de segurança do maintainer-feedback original (checagem de loopback tipo SSRF, escopo de cookie/CSRF) já estavam corrigidos e cobertos por teste no commit `2b785f0068a862fbd867221294325ad921787782` desta branch.
This commit is contained in:
@@ -52,6 +52,19 @@ function resolveUrl(path, opts) {
|
||||
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
/** The machine-derived token is valid only for the local loopback server. */
|
||||
export function isLoopbackUrl(value) {
|
||||
try {
|
||||
const hostname = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
||||
if (hostname === "localhost" || hostname === "::1") return true;
|
||||
if (/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)) return true;
|
||||
if (/^::ffff:(?:127\.|7f[0-9a-f]{2}:)/i.test(hostname)) return true;
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildHeaders(opts) {
|
||||
const headers = new Headers(opts.headers || {});
|
||||
if (!headers.has("accept")) headers.set("accept", "application/json");
|
||||
@@ -87,10 +100,17 @@ export async function buildHeaders(opts) {
|
||||
if (auth && !headers.has("authorization")) {
|
||||
headers.set("authorization", `Bearer ${auth}`);
|
||||
}
|
||||
// Inject machine-id derived CLI token; env var override for testing.
|
||||
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
|
||||
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
|
||||
headers.set(CLI_TOKEN_HEADER, cliToken);
|
||||
// Inject the machine-derived credential only for an explicit local loopback
|
||||
// destination. Remote contexts and absolute remote URLs use scoped access
|
||||
// tokens and must never receive this machine-bound local credential.
|
||||
const destinationUrl = opts.destinationUrl ?? getBaseUrl(opts);
|
||||
if (!isLoopbackUrl(destinationUrl)) {
|
||||
headers.delete(CLI_TOKEN_HEADER);
|
||||
} else {
|
||||
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
|
||||
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
|
||||
headers.set(CLI_TOKEN_HEADER, cliToken);
|
||||
}
|
||||
}
|
||||
if (opts.idempotencyKey && !headers.has("idempotency-key")) {
|
||||
headers.set("idempotency-key", opts.idempotencyKey);
|
||||
@@ -195,8 +215,12 @@ function fetchOnce(url, init, timeoutMs) {
|
||||
export async function apiFetch(path, opts = {}) {
|
||||
const method = String(opts.method || "GET").toUpperCase();
|
||||
const url = resolveUrl(path, opts);
|
||||
const headers = await buildHeaders(opts);
|
||||
const headers = await buildHeaders({ ...opts, destinationUrl: url });
|
||||
const body = serializeBody(opts.body, headers);
|
||||
// Undici preserves custom headers across cross-origin redirects. A local server
|
||||
// redirect must never turn the loopback machine credential into an outbound
|
||||
// secret, so fail redirects whenever this header is present.
|
||||
const redirect = headers.has(CLI_TOKEN_HEADER) ? "error" : opts.redirect;
|
||||
const timeout =
|
||||
opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000);
|
||||
const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts);
|
||||
@@ -205,7 +229,7 @@ export async function apiFetch(path, opts = {}) {
|
||||
let lastErr;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const res = await fetchOnce(url, { method, headers, body }, timeout);
|
||||
const res = await fetchOnce(url, { method, headers, body, redirect }, timeout);
|
||||
if (res.ok) return enrichResponse(res, opts);
|
||||
if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) {
|
||||
const delay = computeBackoff(attempt, res.headers.get("retry-after"));
|
||||
|
||||
@@ -4,7 +4,9 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createDecipheriv, scryptSync } from "node:crypto";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { isLoopbackUrl } from "../api.mjs";
|
||||
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
|
||||
import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
|
||||
import { printHeading } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
|
||||
@@ -378,11 +380,11 @@ function checkMemory() {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url) {
|
||||
async function fetchWithTimeout(url, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(url, { signal: controller.signal });
|
||||
return await fetch(url, { ...options, signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
@@ -471,6 +473,98 @@ async function checkServerLiveness(options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkMachineTokenAuth(options = {}) {
|
||||
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") {
|
||||
return warn("CLI machine token", "CLI machine-token authentication is disabled", {
|
||||
derived: false,
|
||||
accepted: false,
|
||||
disabled: true,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
const parsed = new URL(resolveLivenessUrl(options));
|
||||
if (
|
||||
!["http:", "https:"].includes(parsed.protocol) ||
|
||||
parsed.username ||
|
||||
parsed.password ||
|
||||
!isLoopbackUrl(parsed.toString())
|
||||
) {
|
||||
return warn(
|
||||
"CLI machine token",
|
||||
"Machine-token probes are limited to HTTP(S) loopback endpoints",
|
||||
{ derived: false, accepted: false, tokenExposed: false }
|
||||
);
|
||||
}
|
||||
parsed.pathname = "/api/cli/whoami";
|
||||
parsed.search = "";
|
||||
parsed.hash = "";
|
||||
url = parsed.toString();
|
||||
} catch {
|
||||
return warn("CLI machine token", "Could not resolve the management endpoint", {
|
||||
derived: false,
|
||||
accepted: false,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
|
||||
const token = await getCliToken();
|
||||
if (!token) {
|
||||
return fail(
|
||||
"CLI machine token",
|
||||
"Could not derive a machine token; verify the node-machine-id runtime is installed",
|
||||
{ derived: false, accepted: false, tokenExposed: false }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
headers: { [CLI_TOKEN_HEADER]: token },
|
||||
redirect: "error",
|
||||
});
|
||||
if (response.ok) {
|
||||
return ok("CLI machine token", "Server accepted the local machine token", {
|
||||
url,
|
||||
status: response.status,
|
||||
derived: true,
|
||||
accepted: true,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return warn(
|
||||
"CLI machine token",
|
||||
"Server rejected the local machine token; if the CLI and server are on different hosts or container boundaries, run `omniroute connect <host> --key <oma_live_...>`",
|
||||
{
|
||||
url,
|
||||
status: response.status,
|
||||
derived: true,
|
||||
accepted: false,
|
||||
containerBoundaryLikely: true,
|
||||
tokenExposed: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
return warn("CLI machine token", `Machine-token probe returned HTTP ${response.status}`, {
|
||||
url,
|
||||
status: response.status,
|
||||
derived: true,
|
||||
accepted: false,
|
||||
tokenExposed: false,
|
||||
});
|
||||
} catch {
|
||||
return warn("CLI machine token", "Machine-token endpoint could not be reached", {
|
||||
url,
|
||||
status: 0,
|
||||
derived: true,
|
||||
accepted: false,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
const rootDir =
|
||||
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
@@ -488,6 +582,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
|
||||
if (!options.skipLiveness) {
|
||||
checks.push(await checkServerLiveness(options));
|
||||
checks.push(await checkMachineTokenAuth(options));
|
||||
}
|
||||
|
||||
// CLI tool health checks
|
||||
|
||||
@@ -12,25 +12,39 @@ function getActiveSalt() {
|
||||
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
|
||||
}
|
||||
|
||||
export async function getCliToken() {
|
||||
const salt = getActiveSalt();
|
||||
if (_cached !== null && _cachedSalt === salt) return _cached;
|
||||
export function deriveCliToken(machineIdModule, salt) {
|
||||
try {
|
||||
// node-machine-id is CommonJS: under `await import()` its exports land on
|
||||
// `.default`, so destructuring `machineIdSync` off the namespace yields
|
||||
// undefined and calling it throws — which the catch below turned into an
|
||||
// empty token, silently disabling CLI auth for every management request.
|
||||
// Same resolution order as src/lib/machineToken.ts.
|
||||
const mod = await import("node-machine-id");
|
||||
const machineIdSync = mod.machineIdSync ?? mod.default?.machineIdSync;
|
||||
if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable");
|
||||
const machineIdSync =
|
||||
machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
|
||||
if (typeof machineIdSync !== "function") return "";
|
||||
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
|
||||
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
|
||||
const mid = machineIdSync(true);
|
||||
_cached = crypto.createHmac("sha256", mid).update(salt).digest("hex");
|
||||
const rawId = machineIdSync(true);
|
||||
if (!rawId) return "";
|
||||
return crypto.createHmac("sha256", rawId).update(salt).digest("hex");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCliToken() {
|
||||
const salt = getActiveSalt();
|
||||
if (_cached !== null && _cachedSalt === salt) return _cached;
|
||||
try {
|
||||
const imported = await import("node-machine-id");
|
||||
const token = deriveCliToken(imported, salt);
|
||||
if (!token) {
|
||||
// Swallowing here changes control flow (every management call goes out
|
||||
// unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
|
||||
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled");
|
||||
}
|
||||
_cached = token;
|
||||
} catch (e) {
|
||||
// Swallowing here changes control flow (every management call goes out
|
||||
// unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
|
||||
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled:", e);
|
||||
_cached = "";
|
||||
}
|
||||
|
||||
@@ -20,21 +20,26 @@ password on every invocation.
|
||||
(falls back to an empty string on failure, disabling CLI auth).
|
||||
2. It computes `HMAC-SHA256(machine_id, salt)` and returns the full 64-char
|
||||
hex digest — a deterministic, non-reversible token tied to this machine.
|
||||
3. The CLI sends the token as `x-omniroute-cli-token` on every request to
|
||||
`http://localhost:<port>/api/...`.
|
||||
3. The CLI sends the token as `x-omniroute-cli-token` only when the resolved
|
||||
destination is an explicit loopback URL (`localhost`, `127.0.0.0/8`, or
|
||||
loopback IPv6). Requests carrying the token use `redirect: error`, so a local
|
||||
redirect cannot forward it to another origin. Remote contexts use scoped
|
||||
access tokens instead. If derivation is unavailable, the CLI omits the header
|
||||
and `omniroute doctor` reports the failure instead of treating an empty token
|
||||
as valid.
|
||||
4. The server (`src/server/authz/policies/management.ts`) recomputes the
|
||||
expected token with the same salt and compares via `timingSafeEqual` to
|
||||
prevent timing-based extraction.
|
||||
|
||||
## Security properties
|
||||
|
||||
| Property | Detail |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Loopback-only** | Accepted only when `Host` is `localhost`, `127.0.0.1`, or `::1`. |
|
||||
| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. |
|
||||
| **Non-reversible** | HMAC output cannot recover the machine-id. |
|
||||
| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. |
|
||||
| **Non-exportable** | Token is never written to disk or logged. |
|
||||
| Property | Detail |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Loopback-only** | Accepted only when the server's trusted peer-locality stamp (derived from the real TCP peer address) says loopback. The client-controlled `Host` header is never trusted for locality. |
|
||||
| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. |
|
||||
| **Non-reversible** | HMAC output cannot recover the machine-id. |
|
||||
| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. |
|
||||
| **Non-exportable** | Token is never written to disk or logged. |
|
||||
|
||||
## Salt rotation
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* - better-sqlite3 (SQLite bindings)
|
||||
* - wreq-js (TLS client for OAuth providers)
|
||||
* - tls-client-node (TLS client for chatgpt-web/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
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
readdirSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -45,6 +48,7 @@ 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.
|
||||
@@ -437,12 +441,33 @@ async function verifyDevNativeModules() {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
* 0 = boots and reports the right version · 1 = boot failed · 2 = missing build.
|
||||
*/
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { createHmac } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const POLL_INTERVAL_MS = 2_000;
|
||||
const BOOT_DEADLINE_MS = 240_000;
|
||||
const MAX_SERVER_OUTPUT_CHARS = 1_000_000;
|
||||
const SQLJS_STARTUP_MARKER = "Pre-initializing sql.js WASM";
|
||||
const DEFAULT_CLI_SALT = "omniroute-cli-auth-v1";
|
||||
|
||||
export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([
|
||||
"dist/node_modules/sql.js/package.json",
|
||||
@@ -28,6 +32,11 @@ export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([
|
||||
"dist/node_modules/sql.js/dist/sql-wasm.wasm",
|
||||
]);
|
||||
|
||||
export const REQUIRED_MACHINE_TOKEN_RUNTIME_FILES = Object.freeze([
|
||||
"node_modules/node-machine-id/package.json",
|
||||
"node_modules/node-machine-id/index.js",
|
||||
]);
|
||||
|
||||
/** Parse `npm pack --json` output into the generated tarball filename. */
|
||||
export function pickTarball(packJsonOutput) {
|
||||
const parsed = JSON.parse(packJsonOutput);
|
||||
@@ -62,6 +71,39 @@ export function findMissingSqlJsRuntimeFiles(packageRoot, exists = fs.existsSync
|
||||
);
|
||||
}
|
||||
|
||||
export function findMissingMachineTokenRuntimeFiles(packageRoot, exists = fs.existsSync) {
|
||||
return REQUIRED_MACHINE_TOKEN_RUNTIME_FILES.filter(
|
||||
(relativePath) => !exists(path.join(packageRoot, relativePath))
|
||||
);
|
||||
}
|
||||
|
||||
export function evaluateMachineTokenAuth({
|
||||
cliToken,
|
||||
unauthenticatedStatus,
|
||||
invalidStatus,
|
||||
authenticatedStatus,
|
||||
salt = process.env.OMNIROUTE_CLI_SALT || DEFAULT_CLI_SALT,
|
||||
}) {
|
||||
const failures = [];
|
||||
if (!/^[0-9a-f]{64}$/.test(cliToken || "")) {
|
||||
failures.push("packaged CLI derived an empty or malformed machine token");
|
||||
}
|
||||
const emptyMachineIdToken = createHmac("sha256", "").update(salt).digest("hex");
|
||||
if (cliToken === emptyMachineIdToken) {
|
||||
failures.push("packaged CLI derived the public empty-machine-id token");
|
||||
}
|
||||
if (unauthenticatedStatus !== 401) {
|
||||
failures.push(`no-credential request returned ${unauthenticatedStatus} (expected 401)`);
|
||||
}
|
||||
if (invalidStatus !== 401) {
|
||||
failures.push(`invalid-token request returned ${invalidStatus} (expected 401)`);
|
||||
}
|
||||
if (authenticatedStatus !== 200) {
|
||||
failures.push(`packaged CLI token request returned ${authenticatedStatus} (expected 200)`);
|
||||
}
|
||||
return { ok: failures.length === 0, failures };
|
||||
}
|
||||
|
||||
export function evaluateSqlJsRoundTrip({
|
||||
startupOutput,
|
||||
beforeValue,
|
||||
@@ -106,8 +148,9 @@ async function readJsonResponse(url, options) {
|
||||
return { response, body };
|
||||
}
|
||||
|
||||
async function verifySettingsRoundTrip(baseUrl, startupOutput) {
|
||||
const initial = await readJsonResponse(`${baseUrl}/api/settings`);
|
||||
async function verifySettingsRoundTrip(baseUrl, startupOutput, cliToken) {
|
||||
const authHeaders = { "x-omniroute-cli-token": cliToken };
|
||||
const initial = await readJsonResponse(`${baseUrl}/api/settings`, { headers: authHeaders });
|
||||
if (initial.response.status !== 200 || !initial.body || typeof initial.body !== "object") {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -119,7 +162,7 @@ async function verifySettingsRoundTrip(baseUrl, startupOutput) {
|
||||
const expectedValue = !beforeValue;
|
||||
const patched = await readJsonResponse(`${baseUrl}/api/settings`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { ...authHeaders, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ debugMode: expectedValue }),
|
||||
});
|
||||
if (patched.response.status !== 200 || !patched.body || typeof patched.body !== "object") {
|
||||
@@ -129,7 +172,7 @@ async function verifySettingsRoundTrip(baseUrl, startupOutput) {
|
||||
};
|
||||
}
|
||||
|
||||
const readBack = await readJsonResponse(`${baseUrl}/api/settings`);
|
||||
const readBack = await readJsonResponse(`${baseUrl}/api/settings`, { headers: authHeaders });
|
||||
if (readBack.response.status !== 200 || !readBack.body || typeof readBack.body !== "object") {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -242,22 +285,61 @@ function spawnServer(binPath, port, dataDir) {
|
||||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1",
|
||||
OMNIROUTE_PACK_BOOT_SMOKE: "1",
|
||||
OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1",
|
||||
INITIAL_PASSWORD: "pack-boot-machine-token-auth-required",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
});
|
||||
const tail = [];
|
||||
let retainedChars = 0;
|
||||
const keepTail = (chunk) => {
|
||||
tail.push(String(chunk));
|
||||
while (tail.length > 80) tail.shift();
|
||||
const text = String(chunk);
|
||||
tail.push(text);
|
||||
retainedChars += text.length;
|
||||
while (retainedChars > MAX_SERVER_OUTPUT_CHARS && tail.length > 1) {
|
||||
retainedChars -= tail.shift().length;
|
||||
}
|
||||
};
|
||||
child.stdout.on("data", keepTail);
|
||||
child.stderr.on("data", keepTail);
|
||||
return { child, tail };
|
||||
}
|
||||
|
||||
function derivePackagedCliToken(packageRoot) {
|
||||
const cliModuleUrl = pathToFileURL(
|
||||
path.join(packageRoot, "bin", "cli", "utils", "cliToken.mjs")
|
||||
).href;
|
||||
return execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
"import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))",
|
||||
cliModuleUrl,
|
||||
],
|
||||
{ encoding: "utf8", env: { ...process.env } }
|
||||
).trim();
|
||||
}
|
||||
|
||||
async function verifyMachineTokenAuth(baseUrl, cliToken) {
|
||||
const endpoint = `${baseUrl}/api/cli/whoami`;
|
||||
const unauthenticatedStatus = (await fetch(endpoint)).status;
|
||||
const invalidStatus = (
|
||||
await fetch(endpoint, { headers: { "x-omniroute-cli-token": "0".repeat(64) } })
|
||||
).status;
|
||||
const authenticatedStatus = (
|
||||
await fetch(endpoint, { headers: { "x-omniroute-cli-token": cliToken } })
|
||||
).status;
|
||||
return evaluateMachineTokenAuth({
|
||||
cliToken,
|
||||
unauthenticatedStatus,
|
||||
invalidStatus,
|
||||
authenticatedStatus,
|
||||
});
|
||||
}
|
||||
|
||||
/** Poll /api/monitoring/health until the packed version answers or the boot deadline passes. */
|
||||
async function waitForHealthy(port, child, expectedVersion) {
|
||||
async function waitForHealthy(port, child, expectedVersion, cliToken) {
|
||||
// Seed from authoritative state (Node sets these synchronously at death), then attach a
|
||||
// named once-listener, then re-check: a child that died before this call, or in the gap
|
||||
// before the listener attached, would otherwise never fire "exit" and waste the deadline.
|
||||
@@ -279,7 +361,9 @@ async function waitForHealthy(port, child, expectedVersion) {
|
||||
return { ok: false, failures: [`process exited (${childExit}) before serving`] };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`);
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, {
|
||||
headers: { "x-omniroute-cli-token": cliToken },
|
||||
});
|
||||
const body = await res.json().catch(() => null);
|
||||
verdict = evaluateBoot(res.status, body, expectedVersion);
|
||||
if (verdict.ok) return verdict;
|
||||
@@ -299,8 +383,10 @@ async function waitForHealthy(port, child, expectedVersion) {
|
||||
* field throws: coercing with `=== true` would read `false` for a malformed response and
|
||||
* could falsely "pass" persistence whenever the expected value happens to be false.
|
||||
*/
|
||||
async function readSettingsDebugMode(baseUrl) {
|
||||
const { response, body } = await readJsonResponse(`${baseUrl}/api/settings`);
|
||||
async function readSettingsDebugMode(baseUrl, cliToken) {
|
||||
const { response, body } = await readJsonResponse(`${baseUrl}/api/settings`, {
|
||||
headers: { "x-omniroute-cli-token": cliToken },
|
||||
});
|
||||
if (response.status !== 200 || !body || typeof body !== "object") {
|
||||
throw new Error(`settings GET HTTP ${response.status} or non-JSON body`);
|
||||
}
|
||||
@@ -350,21 +436,38 @@ async function main() {
|
||||
);
|
||||
}
|
||||
log("installed package contains the complete sql.js WASM runtime");
|
||||
const missingMachineTokenFiles = findMissingMachineTokenRuntimeFiles(packageRoot);
|
||||
if (missingMachineTokenFiles.length > 0) {
|
||||
throw new Error(
|
||||
`installed package is missing the node-machine-id runtime contract: ${missingMachineTokenFiles.join(", ")}`
|
||||
);
|
||||
}
|
||||
log("installed package contains the node-machine-id runtime");
|
||||
|
||||
const port = pickPort();
|
||||
const dataDir = path.join(tmp, "data");
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
const binPath = path.join(prefix, "bin", "omniroute");
|
||||
const packagedCliToken = derivePackagedCliToken(packageRoot);
|
||||
|
||||
// BOOT #1 — boot, prove the forced sql.js tier, PATCH a setting, then shut down cleanly
|
||||
// so the sql.js adapter's graceful persist actually lands on disk. The in-flow stopChild
|
||||
// THROWS on failure; that lands in catch as primaryError and boot #2 never starts.
|
||||
log(`boot #1: installed CLI on :${port} (DATA_DIR isolated)…`);
|
||||
({ child, tail } = spawnServer(binPath, port, dataDir));
|
||||
let verdict = await waitForHealthy(port, child, expectedVersion);
|
||||
let verdict = await waitForHealthy(port, child, expectedVersion, packagedCliToken);
|
||||
if (verdict.ok) {
|
||||
log(`healthy: HTTP 200, version ${expectedVersion}`);
|
||||
const roundTrip = await verifySettingsRoundTrip(`http://127.0.0.1:${port}`, tail.join(""));
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const machineAuth = await verifyMachineTokenAuth(baseUrl, packagedCliToken);
|
||||
if (!machineAuth.ok) {
|
||||
verdict = machineAuth;
|
||||
} else {
|
||||
log("machine-token auth passed with no/invalid/valid contrast controls");
|
||||
}
|
||||
const roundTrip = verdict.ok
|
||||
? await verifySettingsRoundTrip(baseUrl, tail.join(""), packagedCliToken)
|
||||
: { ok: false, failures: verdict.failures };
|
||||
if (roundTrip.ok) {
|
||||
log("settings write/read succeeded through the forced sql.js driver");
|
||||
await stopChild(child); // throws here → primaryError; boot #2 is skipped
|
||||
@@ -373,10 +476,13 @@ async function main() {
|
||||
// BOOT #2 — same DATA_DIR, fresh process: the value must be read back FROM DISK.
|
||||
log("boot #2: rebooting on the same DATA_DIR to prove disk persistence…");
|
||||
({ child, tail } = spawnServer(binPath, port, dataDir));
|
||||
verdict = await waitForHealthy(port, child, expectedVersion);
|
||||
verdict = await waitForHealthy(port, child, expectedVersion, packagedCliToken);
|
||||
if (verdict.ok) {
|
||||
log(`healthy: HTTP 200, version ${expectedVersion}`);
|
||||
const restartValue = await readSettingsDebugMode(`http://127.0.0.1:${port}`);
|
||||
const restartValue = await readSettingsDebugMode(
|
||||
`http://127.0.0.1:${port}`,
|
||||
packagedCliToken
|
||||
);
|
||||
const persistence = evaluateRestartPersistence({
|
||||
expectedValue: roundTrip.expectedValue,
|
||||
restartValue,
|
||||
|
||||
@@ -29,7 +29,7 @@ Create API key
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/keys \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -49,7 +49,7 @@ Update API key
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/keys/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ Manage API key authentication and session tokens. Start here to authenticate req
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require a valid Bearer token or session cookie. Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development.
|
||||
Remote API requests use a Bearer credential. Dashboard login is different: `POST /api/auth/login` accepts a management password and returns an `auth_token` session cookie.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -20,9 +20,9 @@ Authenticate user
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/auth/login \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
-c cookie.jar \
|
||||
-d '{"password":"<management-password>"}'
|
||||
```
|
||||
|
||||
### POST /api/auth/logout
|
||||
@@ -30,8 +30,10 @@ curl -X POST https://localhost:20128/api/auth/login \
|
||||
Log out
|
||||
|
||||
```bash
|
||||
CSRF_TOKEN=$(curl -s https://localhost:20128/api/auth/csrf -b cookie.jar | jq -r .token)
|
||||
curl -X POST https://localhost:20128/api/auth/logout \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-b cookie.jar \
|
||||
-H "x-omniroute-csrf: $CSRF_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -48,7 +50,7 @@ remains available as a fallback while OIDC is enabled.
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/auth/oidc/login \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-b cookie.jar
|
||||
```
|
||||
|
||||
### GET /api/auth/oidc/callback
|
||||
@@ -64,7 +66,7 @@ JWT used by password login and redirects to `/dashboard`.
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/auth/oidc/callback \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-b cookie.jar
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
@@ -29,7 +29,7 @@ Update rate limit configuration
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/rate-limit \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ Create CLI tool backup
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/backups \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -67,7 +67,7 @@ Update Antigravity MITM proxy settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/antigravity-mitm \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -96,7 +96,7 @@ Update Antigravity MITM alias configuration
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/cli-tools/antigravity-mitm/alias \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -116,7 +116,7 @@ Apply Claude CLI settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/claude-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -145,7 +145,7 @@ Apply Cline CLI settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/cline-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -174,7 +174,7 @@ Create Codex profile
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/codex-profiles \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -185,7 +185,7 @@ Update Codex profile
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/cli-tools/codex-profiles \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -214,7 +214,7 @@ Apply Codex CLI settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/codex-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -243,7 +243,7 @@ Apply Droid CLI settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/droid-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -272,7 +272,7 @@ Apply Kilo CLI settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/kilo-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -301,7 +301,7 @@ Apply OpenClaw CLI settings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/openclaw-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -334,7 +334,7 @@ Local-only. Registers OmniRoute as an `openai-compat` provider in Crush's config
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/crush-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -369,7 +369,7 @@ Local-only. Writes the OmniRoute config block in CodeWhale TOML format.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/codewhale-settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ Create routing combo
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/combos \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -40,7 +40,7 @@ Update combo
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/combos/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -69,7 +69,7 @@ Test a combo configuration
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/combos/test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -93,7 +93,7 @@ Registers a fallback routing chain for a model.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/fallback/chains \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ Preview compression for a message payload
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/compression/preview \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ Update RTK compression settings
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/context/rtk/config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -49,7 +49,7 @@ Validate or install an RTK TOML schema v1 filter file
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/context/rtk/import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -60,7 +60,7 @@ Run RTK compression preview for text
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/context/rtk/test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -40,7 +40,7 @@ OpenAI-compatible chat completions endpoint. Routes to configured providers.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -64,7 +64,7 @@ Routes to a specific provider by name.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/providers/{provider}/chat/completions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -77,7 +77,7 @@ Provides compatibility with Ollama's /api/chat format.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/api/chat \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -90,7 +90,7 @@ Anthropic Messages API endpoint. Routes to Claude providers.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/messages \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -101,7 +101,7 @@ Count tokens for a message
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/messages/count_tokens \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -114,7 +114,7 @@ OpenAI Responses API endpoint.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/responses \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -125,7 +125,7 @@ Create embeddings
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/embeddings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -158,7 +158,7 @@ Create embeddings (provider-specific)
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/providers/{provider}/embeddings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -169,7 +169,7 @@ Generate images
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/images/generations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -180,7 +180,7 @@ Generate images (provider-specific)
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/providers/{provider}/images/generations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -193,7 +193,7 @@ Text-to-speech endpoint. Routes to configured TTS providers.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/audio/speech \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -206,7 +206,7 @@ Audio-to-text transcription endpoint.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/audio/transcriptions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -219,7 +219,7 @@ Content moderation endpoint. Routes to configured moderation providers.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/moderations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -232,7 +232,7 @@ Document reranking endpoint.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/rerank \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -278,7 +278,7 @@ Creates a subscription record. If `mode` is `rule`, at least one entry in `ruleP
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/management/proxy-subscriptions \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -300,7 +300,7 @@ Partial update — only fields present in the body are changed (name/url/mode/ru
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/v1/management/proxy-subscriptions/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -335,7 +335,7 @@ Re-fetches and re-parses the subscription URL, syncs its nodes into `proxy_regis
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/management/proxy-subscriptions/{id}/refresh \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -348,7 +348,7 @@ Multi-provider document OCR endpoint (Mistral OCR–compatible request and respo
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/ocr \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -361,7 +361,7 @@ OpenAI Whisper–compatible audio translation (multipart/form-data). Unlike `/ap
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/audio/translations \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -40,7 +40,7 @@ Create or update a model alias
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/models/alias \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ Create provider connection
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -49,7 +49,7 @@ Update provider connection
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/providers/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -69,7 +69,7 @@ Test provider connection
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/{id}/test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -100,7 +100,7 @@ Test multiple providers at once
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/test-batch \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -111,7 +111,7 @@ Validate provider credentials
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/validate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -131,7 +131,7 @@ Import an Antigravity CLI (agy) token file as an `agy` connection
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/agy-auth/import \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -142,7 +142,7 @@ Bulk-import multiple Antigravity CLI (agy) token files (up to 50)
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/agy-auth/import-bulk \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -153,7 +153,7 @@ Extract `.json` token files from an uploaded ZIP for agy bulk import
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/agy-auth/zip-extract \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -164,7 +164,7 @@ Auto-detect and import the local Antigravity CLI (agy) login from disk
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/providers/agy-auth/apply-local \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -184,7 +184,7 @@ Create provider node
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/provider-nodes \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -195,7 +195,7 @@ Update provider node
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/provider-nodes/{id} \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -215,7 +215,7 @@ Validate a provider node
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/provider-nodes/validate \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ Update any subset of the extended memory settings. All fields are optional; only
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/memory \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -57,7 +57,7 @@ Update Qdrant configuration. Pass `apiKey: ""` to remove the stored key. Schema:
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/qdrant \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -81,7 +81,7 @@ Performs a test semantic search against the Qdrant collection. Useful for valida
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/qdrant/search \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -94,7 +94,7 @@ Removes Qdrant points for memories that have expired or exceeded the configured
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/qdrant/cleanup \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -125,7 +125,7 @@ Update settings
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/settings \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -138,7 +138,7 @@ Deletes `call_logs`, legacy `request_detail_logs`, and local request artifact fi
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/purge-request-history \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -158,7 +158,7 @@ Update global compression settings
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/compression \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -180,7 +180,7 @@ Partial-merge update. Numeric floors (e.g. a maxTextChars below the truncation-t
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/compression/mcp-accessibility \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -212,7 +212,7 @@ Requires a dashboard management session cookie when management auth is enabled.
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/payload-rules \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -241,7 +241,7 @@ Update proxy settings
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://localhost:20128/api/settings/proxy \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -252,7 +252,7 @@ Test proxy connection
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/proxy/test \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -263,7 +263,7 @@ Toggle login requirement
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/require-login \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -288,7 +288,7 @@ Configure IP filtering with blacklist/whitelist modes, add/remove individual IPs
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/ip-filter \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -310,7 +310,7 @@ Update system prompt configuration
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/system-prompt \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -332,7 +332,7 @@ Update thinking budget configuration
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/thinking-budget \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -365,7 +365,7 @@ Update quota store driver settings
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/settings/quota-store \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -378,7 +378,7 @@ Dashboard-only. Purges stored usage-history records.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/settings/purge-usage-history \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ Authenticates with the OmniRoute cloud worker for remote access.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cloud/auth \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -33,7 +33,7 @@ Update cloud worker credentials
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/cloud/credentials/update \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -46,7 +46,7 @@ Resolves a model request through the cloud worker.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cloud/model/resolve \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -66,7 +66,7 @@ Update cloud model alias
|
||||
|
||||
```bash
|
||||
curl -X PUT https://localhost:20128/api/cloud/models/alias \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -77,7 +77,7 @@ Sync with cloud
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/sync/cloud \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -88,7 +88,7 @@ Initialize cloud sync
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/sync/initialize \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ Set or update budget limits for usage tracking.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/usage/budget \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ Installs the `9router` npm package under DATA_DIR/services/9router/. Uses execFi
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/install \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -35,7 +35,7 @@ Spawns the 9Router process. Idempotent if already running. **LOCAL_ONLY** — lo
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -48,7 +48,7 @@ Gracefully stops 9Router (SIGTERM → 15 s → SIGKILL). Idempotent. **LOCAL_ONL
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/stop \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -61,7 +61,7 @@ Equivalent to stop() then start() under the operation lock. **LOCAL_ONLY** — l
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/restart \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -74,7 +74,7 @@ Stops the service (if running), installs the newer npm version, then restarts. *
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/update \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -87,7 +87,7 @@ Generates a new API key, encrypts it at-rest, and restarts the service to apply
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/rotate-key \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -111,7 +111,7 @@ When enabled, 9Router starts automatically on the next OmniRoute boot. **LOCAL_O
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/auto-start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -124,7 +124,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) 9Router process is r
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/9router/auto-restart-adopted \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -137,7 +137,7 @@ Installs the CLIProxyAPI package under DATA_DIR/services/cliproxy/. **LOCAL_ONLY
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/install \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -150,7 +150,7 @@ Spawns the CLIProxyAPI process. Idempotent if already running. **LOCAL_ONLY**
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -163,7 +163,7 @@ Gracefully stops CLIProxyAPI. Idempotent. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/stop \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -176,7 +176,7 @@ stop() then start() under the operation lock. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/restart \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -189,7 +189,7 @@ Stops, installs newer version, restarts. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/update \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -213,7 +213,7 @@ When enabled, CLIProxyAPI starts automatically on the next OmniRoute boot. **LOC
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/auto-start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -226,7 +226,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) CLIProxyAPI process
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/cliproxy/auto-restart-adopted \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -239,7 +239,7 @@ Installs the `mux` npm package (coder/mux — local agent-orchestration daemon)
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/install \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -252,7 +252,7 @@ Spawns `mux server --host 127.0.0.1 --port <port>`. Idempotent if already runnin
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -265,7 +265,7 @@ Gracefully stops Mux. Idempotent. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/stop \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -278,7 +278,7 @@ stop() then start() under the operation lock. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/restart \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -291,7 +291,7 @@ Stops, installs newer version, restarts. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/update \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -315,7 +315,7 @@ When enabled, Mux starts automatically on the next OmniRoute boot. **LOCAL_ONLY*
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/auto-start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -328,7 +328,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) Mux process is resta
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/mux/auto-restart-adopted \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -341,7 +341,7 @@ Installs the `@maximhq/bifrost` npm package under DATA_DIR/services/bifrost/. Th
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/install \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -354,7 +354,7 @@ Starts the supervised Bifrost process. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -367,7 +367,7 @@ Stops the supervised Bifrost process. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/stop \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -380,7 +380,7 @@ Restarts the supervised Bifrost process. **LOCAL_ONLY** — loopback only.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/restart \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -393,7 +393,7 @@ Updates Bifrost to the latest npm version. Stops the running process, installs t
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/update \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -417,7 +417,7 @@ When enabled, Bifrost starts automatically on the next OmniRoute boot. **LOCAL_O
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/auto-start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -430,7 +430,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) Bifrost process is r
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/bifrost/auto-restart-adopted \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -443,7 +443,7 @@ Installs the `@askalf/dario` npm package (Claude-account-pool proxy) under DATA_
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/install \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -456,7 +456,7 @@ Spawns the Dario process. Idempotent if already running. **LOCAL_ONLY** — loop
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -469,7 +469,7 @@ Gracefully stops Dario. Idempotent — returns a stopped status even if no super
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/stop \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -482,7 +482,7 @@ Equivalent to stop() then start() under the operation lock. **LOCAL_ONLY** — l
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/restart \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -495,7 +495,7 @@ Stops the service (if running), installs the newer npm version, then restarts it
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/update \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -519,7 +519,7 @@ When enabled, Dario starts automatically on the next OmniRoute boot. **LOCAL_ONL
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/auto-start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -532,7 +532,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) Dario process is res
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/auto-restart-adopted \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -545,7 +545,7 @@ Forwards to the running Dario instance's `POST /admin/login/start` using the sto
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/admin/login-start \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -558,7 +558,7 @@ Forwards to the running Dario instance's `POST /admin/login/complete`. On succes
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/admin/login-complete \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -604,7 +604,7 @@ Writes the source connection's access/refresh token pair directly into Dario's o
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/services/dario/admin/import-from-omniroute \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
AUTHZ_HEADER_AUTH_KIND,
|
||||
AUTHZ_HEADER_PEER_LOCALITY,
|
||||
} from "@/server/authz/headers";
|
||||
import { readSubjectFromHeaders } from "@/server/authz/assertAuth";
|
||||
|
||||
/**
|
||||
* Force this route to run dynamically per-request and never be cached/prerendered.
|
||||
@@ -133,6 +134,8 @@ async function deriveAuditActor(request: Request): Promise<string> {
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
const subject = readSubjectFromHeaders(request.headers);
|
||||
if (subject.kind === "management_key" && subject.label === "local-cli-token") return "cli";
|
||||
try {
|
||||
if (await isCliTokenAuthValid(request)) return "cli";
|
||||
} catch {
|
||||
|
||||
@@ -76,6 +76,7 @@ function extractCustomBlock(content: string): string | null {
|
||||
function buildApiBody(skill: AgentSkill, sources: BuildSources): string {
|
||||
const areaMap = sources.openapi.areas;
|
||||
const ops = areaMap.get(skill.area as Parameters<typeof areaMap.get>[0]) ?? [];
|
||||
const usesDashboardSession = skill.id === "omni-auth";
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -84,10 +85,17 @@ function buildApiBody(skill: AgentSkill, sources: BuildSources): string {
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Authentication\n");
|
||||
lines.push(
|
||||
"All requests require a valid Bearer token or session cookie. " +
|
||||
"Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development."
|
||||
);
|
||||
if (usesDashboardSession) {
|
||||
lines.push(
|
||||
"Remote API requests use a Bearer credential. Dashboard login is different: " +
|
||||
"`POST /api/auth/login` accepts a management password and returns an `auth_token` session cookie."
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"All requests require a valid Bearer token or session cookie. " +
|
||||
"Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development."
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Endpoints\n");
|
||||
@@ -105,14 +113,41 @@ function buildApiBody(skill: AgentSkill, sources: BuildSources): string {
|
||||
lines.push(op.description);
|
||||
lines.push("");
|
||||
}
|
||||
// Minimal curl example
|
||||
const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `;
|
||||
// Minimal curl example. Only omni-auth establishes and consumes a dashboard
|
||||
// session; generic API skills use independently usable Bearer examples.
|
||||
lines.push("```bash");
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(' -H "Authorization: Bearer $OMNIROUTE_TOKEN"');
|
||||
if (["POST", "PUT", "PATCH"].includes(op.method)) {
|
||||
if (usesDashboardSession && op.path === "/api/auth/login" && op.method === "POST") {
|
||||
lines.push(`curl -X POST https://localhost:20128${op.path} \\`);
|
||||
lines.push(' -H "Content-Type: application/json" \\');
|
||||
lines.push(" -d '{}'");
|
||||
lines.push(" -c cookie.jar \\");
|
||||
lines.push(' -d \'{"password":"<management-password>"}\'');
|
||||
} else if (usesDashboardSession) {
|
||||
const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `;
|
||||
if (op.method === "GET") {
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(" -b cookie.jar");
|
||||
} else {
|
||||
lines.push(
|
||||
"CSRF_TOKEN=$(curl -s https://localhost:20128/api/auth/csrf -b cookie.jar | jq -r .token)"
|
||||
);
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(" -b cookie.jar \\");
|
||||
const hasJsonBody = ["POST", "PUT", "PATCH"].includes(op.method);
|
||||
lines.push(` -H "x-omniroute-csrf: $CSRF_TOKEN"${hasJsonBody ? " \\" : ""}`);
|
||||
if (hasJsonBody) {
|
||||
lines.push(' -H "Content-Type: application/json" \\');
|
||||
lines.push(" -d '{}'");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `;
|
||||
const hasJsonBody = ["POST", "PUT", "PATCH"].includes(op.method);
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(` -H "Authorization: Bearer $OMNIROUTE_TOKEN"${hasJsonBody ? " \\" : ""}`);
|
||||
if (hasJsonBody) {
|
||||
lines.push(' -H "Content-Type: application/json" \\');
|
||||
lines.push(" -d '{}'");
|
||||
}
|
||||
}
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getApiKeyMetadata } from "@/lib/db/apiKeys";
|
||||
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
|
||||
import { evaluateAccessTokenAuth } from "@/server/authz/accessTokenAuth";
|
||||
import { isTrustedLoopbackInternalServiceRequest } from "@/lib/api/internalServiceAuth";
|
||||
import { AUTHZ_HEADER_AUTH_KIND, AUTHZ_HEADER_AUTH_LABEL } from "@/server/authz/headers";
|
||||
import {
|
||||
MANAGE_SCOPE,
|
||||
hasManageScope as hasManageScopeShared,
|
||||
@@ -52,7 +53,17 @@ export async function requireManagementAuth(
|
||||
return null;
|
||||
}
|
||||
|
||||
// CLI machine-id token allows localhost CLI access without an explicit API key.
|
||||
// The authz pipeline strips the raw machine-token header after it validates it
|
||||
// and forwards this trusted subject stamp to route handlers.
|
||||
if (
|
||||
request.headers.get(AUTHZ_HEADER_AUTH_KIND) === "management_key" &&
|
||||
request.headers.get(AUTHZ_HEADER_AUTH_LABEL) === "local-cli-token"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Direct/raw-Node callers without the central pipeline can still validate the
|
||||
// CLI token here, including the trusted peer-locality stamp path.
|
||||
if (await isCliTokenAuthValid(request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createHash, createHmac } from "node:crypto";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
let machineIdSync: (original?: boolean) => string;
|
||||
try {
|
||||
// Use require() to bypass webpack static analysis that breaks the default export
|
||||
const mod = require("node-machine-id");
|
||||
// Anchor runtime resolution to the process entrypoint. Turbopack rewrites
|
||||
// createRequire(import.meta.url) into an in-bundle resolver, which cannot load
|
||||
// external CommonJS packages from the installed standalone node_modules tree.
|
||||
const runtimeRequire = createRequire(process.argv[1] || process.cwd());
|
||||
const mod = runtimeRequire("node-machine-id");
|
||||
machineIdSync = mod.machineIdSync || mod.default?.machineIdSync;
|
||||
} catch {
|
||||
machineIdSync = () => "";
|
||||
@@ -15,10 +19,19 @@ function getActiveSalt(): string {
|
||||
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
|
||||
}
|
||||
|
||||
function deriveToken(rawId: string, salt: string): string {
|
||||
export function deriveMachineToken(rawId: string, salt: string): string {
|
||||
if (!rawId) return "";
|
||||
return createHmac("sha256", rawId).update(salt).digest("hex");
|
||||
}
|
||||
|
||||
export function deriveLegacyCliToken(machineId: string, salt: string): string {
|
||||
if (!machineId) return "";
|
||||
return createHash("sha256")
|
||||
.update(machineId + salt)
|
||||
.digest("hex")
|
||||
.substring(0, 32);
|
||||
}
|
||||
|
||||
let cached: string | null = null;
|
||||
let cachedSalt: string | null = null;
|
||||
|
||||
@@ -27,8 +40,9 @@ export function getMachineTokenSync(salt?: string): string {
|
||||
try {
|
||||
// machineIdSync(true) returns the original unhashed hardware ID.
|
||||
const rawId = machineIdSync(true);
|
||||
if (!rawId) return "";
|
||||
if (activeSalt === cachedSalt && cached !== null) return cached;
|
||||
const token = deriveToken(rawId, activeSalt);
|
||||
const token = deriveMachineToken(rawId, activeSalt);
|
||||
if (!salt) {
|
||||
cached = token;
|
||||
cachedSalt = activeSalt;
|
||||
@@ -43,10 +57,7 @@ export function getLegacyCliTokenSync(salt?: string): string {
|
||||
const activeSalt = salt ?? getActiveSalt();
|
||||
try {
|
||||
const machineId = machineIdSync();
|
||||
return createHash("sha256")
|
||||
.update(machineId + activeSalt)
|
||||
.digest("hex")
|
||||
.substring(0, 32);
|
||||
return deriveLegacyCliToken(machineId, activeSalt);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
AUTHZ_HEADER_REQUEST_ID,
|
||||
AUTHZ_HEADER_ROUTE_CLASS,
|
||||
AUTHZ_TRUSTED_HEADERS,
|
||||
CLI_TOKEN_HEADER,
|
||||
PEER_IP_HEADER,
|
||||
VIA_PROXY_HEADER,
|
||||
} from "./headers";
|
||||
@@ -330,6 +331,11 @@ export async function runAuthzPipeline(
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN
|
||||
);
|
||||
requestHeaders.set(AUTHZ_HEADER_PEER_LOCALITY, peerLocality);
|
||||
// Local CLI-token auth is decided centrally above. Preserve that trusted
|
||||
// decision for route-level requireManagementAuth without forwarding the
|
||||
// machine token itself: custom client auth headers are stripped before the
|
||||
// route runs, so the route consumes only the stamped auth subject.
|
||||
requestHeaders.delete(CLI_TOKEN_HEADER);
|
||||
|
||||
if (method === "OPTIONS") {
|
||||
const preflight = new NextResponse(null, { status: 204 });
|
||||
|
||||
@@ -77,6 +77,7 @@ function isPrivateLanRequest(ctx: PolicyContext): boolean {
|
||||
}
|
||||
|
||||
function hasValidCliToken(ctx: PolicyContext): boolean {
|
||||
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") return false;
|
||||
if (!isLoopbackRequest(ctx)) return false;
|
||||
const headers = ctx.request.headers;
|
||||
const provided = headers.get(CLI_TOKEN_HEADER);
|
||||
|
||||
@@ -15,13 +15,10 @@ import os from "node:os";
|
||||
|
||||
// ── Dynamic imports (tsx/esm resolves TS imports) ────────────────────────────
|
||||
|
||||
const { generateAgentSkills, buildSkillMarkdown, __testing } = await import(
|
||||
"../../src/lib/agentSkills/generator.ts"
|
||||
);
|
||||
const { generateAgentSkills, buildSkillMarkdown, __testing } =
|
||||
await import("../../src/lib/agentSkills/generator.ts");
|
||||
|
||||
const { getCatalog, refreshCatalog } = await import(
|
||||
"../../src/lib/agentSkills/catalog.ts"
|
||||
);
|
||||
const { getCatalog, refreshCatalog } = await import("../../src/lib/agentSkills/catalog.ts");
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -66,7 +63,7 @@ test("dry-run (default) returns report without writing any files", async () => {
|
||||
assert.equal(
|
||||
report.generated.length + report.unchanged.length,
|
||||
46,
|
||||
`Expected 46 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`,
|
||||
`Expected 46 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`
|
||||
);
|
||||
assert.equal(report.errors.length, 0, `Unexpected errors: ${JSON.stringify(report.errors)}`);
|
||||
|
||||
@@ -75,7 +72,7 @@ test("dry-run (default) returns report without writing any files", async () => {
|
||||
assert.equal(
|
||||
entries.length,
|
||||
0,
|
||||
`Dry-run wrote ${entries.length} entries to ${tmpDir}: ${entries.join(", ")}`,
|
||||
`Dry-run wrote ${entries.length} entries to ${tmpDir}: ${entries.join(", ")}`
|
||||
);
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
@@ -128,7 +125,7 @@ test("apply mode writes SKILL.md with valid frontmatter for omni-providers", asy
|
||||
// Generated comment present
|
||||
assert.ok(
|
||||
content.includes("<!-- generated by src/lib/agentSkills/generator.ts"),
|
||||
"Missing generated comment",
|
||||
"Missing generated comment"
|
||||
);
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
@@ -200,6 +197,66 @@ test("apply mode writes SKILL.md for an API skill with correct sections", async
|
||||
assert.ok(content.includes("## Authentication"), "Missing Authentication section");
|
||||
assert.ok(content.includes("## Endpoints"), "Missing endpoints section");
|
||||
assert.ok(content.includes("## Payloads"), "Missing payloads section");
|
||||
assert.ok(content.includes('-d \'{"password":"<management-password>"}\''));
|
||||
assert.ok(content.includes("-c cookie.jar"), "login must save the dashboard session cookie");
|
||||
assert.ok(content.includes("-b cookie.jar"), "auth examples must send the session cookie");
|
||||
assert.ok(content.includes("x-omniroute-csrf"), "mutations must include a CSRF token");
|
||||
const loginExample = content.slice(
|
||||
content.indexOf("### POST /api/auth/login"),
|
||||
content.indexOf("### POST /api/auth/logout")
|
||||
);
|
||||
assert.ok(!loginExample.includes("Authorization: Bearer"));
|
||||
const logoutExample = content.slice(
|
||||
content.indexOf("### POST /api/auth/logout"),
|
||||
content.indexOf("### GET /api/auth/oidc/login")
|
||||
);
|
||||
assert.ok(logoutExample.includes("-b cookie.jar"));
|
||||
assert.ok(logoutExample.includes("x-omniroute-csrf"));
|
||||
assert.ok(!logoutExample.includes("Authorization: Bearer"));
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test("generic API skill GET and mutation examples use standalone Bearer auth", async () => {
|
||||
const tmpDir = mkTmpDir();
|
||||
try {
|
||||
refreshCatalog();
|
||||
const report = await generateAgentSkills({
|
||||
dryRun: false,
|
||||
prune: false,
|
||||
outputDir: tmpDir,
|
||||
onlyIds: ["omni-providers", "omni-settings"],
|
||||
});
|
||||
|
||||
assert.equal(report.errors.length, 0, `Errors: ${JSON.stringify(report.errors)}`);
|
||||
for (const id of ["omni-providers", "omni-settings"]) {
|
||||
const content = fs.readFileSync(path.join(tmpDir, id, "SKILL.md"), "utf-8");
|
||||
assert.ok(content.includes(' -H "Authorization: Bearer $OMNIROUTE_TOKEN"'));
|
||||
assert.ok(!content.includes("cookie.jar"), `${id} must not assume a session cookie`);
|
||||
assert.ok(!content.includes("CSRF_TOKEN"), `${id} must not assume a CSRF token`);
|
||||
}
|
||||
|
||||
const providers = fs.readFileSync(path.join(tmpDir, "omni-providers", "SKILL.md"), "utf-8");
|
||||
const providersGet = providers.slice(
|
||||
providers.indexOf("### GET /api/providers"),
|
||||
providers.indexOf("### POST /api/providers")
|
||||
);
|
||||
const providersPost = providers.slice(
|
||||
providers.indexOf("### POST /api/providers"),
|
||||
providers.indexOf("### GET /api/providers/{id}")
|
||||
);
|
||||
assert.ok(providersGet.includes(' -H "Authorization: Bearer $OMNIROUTE_TOKEN"'));
|
||||
assert.ok(providersPost.includes(' -H "Authorization: Bearer $OMNIROUTE_TOKEN" \\\n'));
|
||||
assert.ok(providersPost.includes(' -H "Content-Type: application/json" \\\n'));
|
||||
|
||||
const settings = fs.readFileSync(path.join(tmpDir, "omni-settings", "SKILL.md"), "utf-8");
|
||||
const settingsPatch = settings.slice(
|
||||
settings.indexOf("### PATCH /api/settings"),
|
||||
settings.indexOf("### POST /api/settings/purge-request-history")
|
||||
);
|
||||
assert.ok(settingsPatch.includes(' -H "Authorization: Bearer $OMNIROUTE_TOKEN" \\\n'));
|
||||
assert.ok(settingsPatch.includes(' -H "Content-Type: application/json" \\\n'));
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
}
|
||||
@@ -283,10 +340,7 @@ test("prune apply mode deletes orphan dirs", async () => {
|
||||
assert.ok(report.pruned.includes("old-orphan-to-delete"), "Orphan not pruned");
|
||||
|
||||
// Orphan dir should be gone
|
||||
assert.ok(
|
||||
!fs.existsSync(orphanDir),
|
||||
"Orphan dir was NOT deleted in apply+prune mode",
|
||||
);
|
||||
assert.ok(!fs.existsSync(orphanDir), "Orphan dir was NOT deleted in apply+prune mode");
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
}
|
||||
@@ -316,7 +370,7 @@ test("prune does not delete catalog skill dirs", async () => {
|
||||
// omni-providers should not be in orphansDetected
|
||||
assert.ok(
|
||||
!report.orphansDetected.includes("omni-providers"),
|
||||
"Valid skill mistakenly flagged as orphan",
|
||||
"Valid skill mistakenly flagged as orphan"
|
||||
);
|
||||
assert.ok(report.orphansDetected.includes("orphan-xyz"), "Orphan not detected");
|
||||
} finally {
|
||||
@@ -343,7 +397,8 @@ test("marker preservation: custom block survives regeneration", async () => {
|
||||
const originalContent = fs.readFileSync(skillFile, "utf-8");
|
||||
|
||||
// Inject a custom block
|
||||
const customBlock = "<!-- skill:custom-start -->\nMy custom content here.\n<!-- skill:custom-end -->";
|
||||
const customBlock =
|
||||
"<!-- skill:custom-start -->\nMy custom content here.\n<!-- skill:custom-end -->";
|
||||
const contentWithCustom = originalContent + "\n" + customBlock + "\n";
|
||||
fs.writeFileSync(skillFile, contentWithCustom, "utf-8");
|
||||
|
||||
@@ -355,27 +410,17 @@ test("marker preservation: custom block survives regeneration", async () => {
|
||||
onlyIds: ["omni-providers"],
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
report2.errors.length,
|
||||
0,
|
||||
`Errors: ${JSON.stringify(report2.errors)}`,
|
||||
);
|
||||
assert.equal(report2.errors.length, 0, `Errors: ${JSON.stringify(report2.errors)}`);
|
||||
|
||||
const newContent = fs.readFileSync(skillFile, "utf-8");
|
||||
|
||||
// Custom block should still be present
|
||||
assert.ok(
|
||||
newContent.includes("My custom content here."),
|
||||
"Custom content was lost during regeneration",
|
||||
);
|
||||
assert.ok(
|
||||
newContent.includes("<!-- skill:custom-start -->"),
|
||||
"Custom start marker missing",
|
||||
);
|
||||
assert.ok(
|
||||
newContent.includes("<!-- skill:custom-end -->"),
|
||||
"Custom end marker missing",
|
||||
"Custom content was lost during regeneration"
|
||||
);
|
||||
assert.ok(newContent.includes("<!-- skill:custom-start -->"), "Custom start marker missing");
|
||||
assert.ok(newContent.includes("<!-- skill:custom-end -->"), "Custom end marker missing");
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
}
|
||||
@@ -391,7 +436,10 @@ test("buildSkillMarkdown returns valid frontmatter + body for omni-providers", (
|
||||
assert.ok(typeof result.frontmatter === "object", "frontmatter must be an object");
|
||||
assert.equal(result.frontmatter.name, "omni-providers");
|
||||
assert.ok(result.frontmatter.description.length > 0, "description must be non-empty");
|
||||
assert.ok(typeof result.body === "string" && result.body.length > 0, "body must be a non-empty string");
|
||||
assert.ok(
|
||||
typeof result.body === "string" && result.body.length > 0,
|
||||
"body must be a non-empty string"
|
||||
);
|
||||
});
|
||||
|
||||
test("buildSkillMarkdown body has no erroneously escaped characters", () => {
|
||||
@@ -406,20 +454,11 @@ test("buildSkillMarkdown body has no erroneously escaped characters", () => {
|
||||
// Check for common escape errors: \\n in rendered text, &, <, >
|
||||
assert.ok(
|
||||
!result.body.includes("\\\\n"),
|
||||
`Skill ${id}: body contains \\\\n (double-escaped newline)`,
|
||||
);
|
||||
assert.ok(
|
||||
!result.body.includes("&"),
|
||||
`Skill ${id}: body contains HTML entity &`,
|
||||
);
|
||||
assert.ok(
|
||||
!result.body.includes("<"),
|
||||
`Skill ${id}: body contains HTML entity <`,
|
||||
);
|
||||
assert.ok(
|
||||
!result.body.includes(">"),
|
||||
`Skill ${id}: body contains HTML entity >`,
|
||||
`Skill ${id}: body contains \\\\n (double-escaped newline)`
|
||||
);
|
||||
assert.ok(!result.body.includes("&"), `Skill ${id}: body contains HTML entity &`);
|
||||
assert.ok(!result.body.includes("<"), `Skill ${id}: body contains HTML entity <`);
|
||||
assert.ok(!result.body.includes(">"), `Skill ${id}: body contains HTML entity >`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -430,7 +469,7 @@ test("buildSkillMarkdown throws for unknown skillId", () => {
|
||||
assert.throws(
|
||||
() => buildSkillMarkdown("non-existent-skill", sources),
|
||||
/non-existent-skill/,
|
||||
"Should throw with skill ID in message",
|
||||
"Should throw with skill ID in message"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -464,7 +503,7 @@ test("buildSkillMarkdown description is at most 2000 chars", () => {
|
||||
const result = buildSkillMarkdown(skill.id, sources);
|
||||
assert.ok(
|
||||
result.frontmatter.description.length <= 2000,
|
||||
`Skill ${skill.id}: description too long (${result.frontmatter.description.length} > 2000)`,
|
||||
`Skill ${skill.id}: description too long (${result.frontmatter.description.length} > 2000)`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -509,8 +548,10 @@ test("generated SKILL.md contains the mandatory generated comment", async () =>
|
||||
|
||||
const content = fs.readFileSync(path.join(tmpDir, "omni-providers", "SKILL.md"), "utf-8");
|
||||
assert.ok(
|
||||
content.includes("<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->"),
|
||||
"Missing mandatory generated comment",
|
||||
content.includes(
|
||||
"<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->"
|
||||
),
|
||||
"Missing mandatory generated comment"
|
||||
);
|
||||
} finally {
|
||||
rmTmpDir(tmpDir);
|
||||
|
||||
@@ -108,6 +108,28 @@ test("AC-9: successful PATCH writes settings.update with diff of changed keys",
|
||||
});
|
||||
});
|
||||
|
||||
test("CLI subject stamp preserves actor attribution after the raw token is stripped", async () => {
|
||||
await bootstrapWithPassword("initial-pass-cli-actor");
|
||||
await settingsDb.updateSettings({ theme: "light" });
|
||||
|
||||
const response = await settingsRoute.PATCH(
|
||||
new Request("http://localhost/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-omniroute-auth-kind": "management_key",
|
||||
"x-omniroute-auth-label": "local-cli-token",
|
||||
},
|
||||
body: JSON.stringify({ theme: "dark" }),
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const rows = settingsRows().filter((r) => r.action === "settings.update");
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].actor, "cli");
|
||||
});
|
||||
|
||||
// ─── AC-10 — failure rows for each rejection path ────────────────────────
|
||||
|
||||
test("AC-10a: PASSWORD_REQUIRED failure writes settings.update_failed", async () => {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHmac } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
REQUIRED_SQLJS_RUNTIME_FILES,
|
||||
REQUIRED_MACHINE_TOKEN_RUNTIME_FILES,
|
||||
pickTarball,
|
||||
evaluateBoot,
|
||||
pickPort,
|
||||
findMissingSqlJsRuntimeFiles,
|
||||
findMissingMachineTokenRuntimeFiles,
|
||||
evaluateMachineTokenAuth,
|
||||
evaluateSqlJsRoundTrip,
|
||||
evaluateRestartPersistence,
|
||||
} from "../../scripts/check/check-pack-boot.mjs";
|
||||
@@ -74,6 +78,64 @@ test("installed package contract requires sql.js metadata, entrypoint, and WASM"
|
||||
);
|
||||
});
|
||||
|
||||
test("installed package contract requires a resolvable node-machine-id CommonJS runtime", () => {
|
||||
const present = new Set(
|
||||
REQUIRED_MACHINE_TOKEN_RUNTIME_FILES.map((file) => path.join("/pkg", file))
|
||||
);
|
||||
assert.deepEqual(
|
||||
findMissingMachineTokenRuntimeFiles("/pkg", (file) => present.has(file)),
|
||||
[]
|
||||
);
|
||||
|
||||
present.delete(path.join("/pkg", "node_modules/node-machine-id/index.js"));
|
||||
assert.deepEqual(
|
||||
findMissingMachineTokenRuntimeFiles("/pkg", (file) => present.has(file)),
|
||||
["node_modules/node-machine-id/index.js"]
|
||||
);
|
||||
});
|
||||
|
||||
test("machine-token smoke requires no/invalid credentials to fail and the packaged CLI token to pass", () => {
|
||||
assert.deepEqual(
|
||||
evaluateMachineTokenAuth({
|
||||
cliToken: "a".repeat(64),
|
||||
unauthenticatedStatus: 401,
|
||||
invalidStatus: 401,
|
||||
authenticatedStatus: 200,
|
||||
}),
|
||||
{ ok: true, failures: [] }
|
||||
);
|
||||
|
||||
for (const candidate of [
|
||||
{ cliToken: "", unauthenticatedStatus: 401, invalidStatus: 401, authenticatedStatus: 200 },
|
||||
{
|
||||
cliToken: "a".repeat(64),
|
||||
unauthenticatedStatus: 200,
|
||||
invalidStatus: 401,
|
||||
authenticatedStatus: 200,
|
||||
},
|
||||
{
|
||||
cliToken: "a".repeat(64),
|
||||
unauthenticatedStatus: 401,
|
||||
invalidStatus: 200,
|
||||
authenticatedStatus: 200,
|
||||
},
|
||||
{
|
||||
cliToken: "a".repeat(64),
|
||||
unauthenticatedStatus: 401,
|
||||
invalidStatus: 401,
|
||||
authenticatedStatus: 401,
|
||||
},
|
||||
{
|
||||
cliToken: createHmac("sha256", "").update("omniroute-cli-auth-v1").digest("hex"),
|
||||
unauthenticatedStatus: 401,
|
||||
invalidStatus: 401,
|
||||
authenticatedStatus: 200,
|
||||
},
|
||||
]) {
|
||||
assert.equal(evaluateMachineTokenAuth(candidate).ok, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("sql.js round trip requires the forced-driver marker plus PATCH and GET persistence", () => {
|
||||
const passing = evaluateSqlJsRoundTrip({
|
||||
startupOutput: "[DB] Pre-initializing sql.js WASM (synchronous drivers unavailable)...",
|
||||
@@ -103,10 +165,20 @@ test("source guard: the gate polls the real health endpoint of the INSTALLED bin
|
||||
);
|
||||
assert.ok(src.includes("/api/monitoring/health"), "must poll the health endpoint");
|
||||
assert.ok(src.includes("/api/settings"), "must verify a real application write and read");
|
||||
assert.ok(src.includes("/api/cli/whoami"), "must exercise the machine-token auth endpoint");
|
||||
assert.ok(src.includes("x-omniroute-cli-token"), "must send the official machine-token header");
|
||||
const postinstall = readFileSync(
|
||||
fileURLToPath(new URL("../../scripts/build/postinstall.mjs", import.meta.url)),
|
||||
"utf8"
|
||||
);
|
||||
assert.ok(postinstall.includes('["sql.js", "node-machine-id"]'));
|
||||
assert.ok(postinstall.includes('join(ROOT, "dist", "node_modules", packageName)'));
|
||||
assert.ok(
|
||||
src.includes('OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1"'),
|
||||
"must force the packaged sql.js tier during this smoke"
|
||||
);
|
||||
assert.ok(src.includes("MAX_SERVER_OUTPUT_CHARS"));
|
||||
assert.ok(!src.includes("while (tail.length > 80)"), "must not discard early startup proof");
|
||||
assert.ok(src.indexOf("npm") < src.indexOf("spawn"), "pack+install must precede the boot spawn");
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ const ORIGINAL_STORAGE_ENCRYPTION_KEY = process.env.STORAGE_ENCRYPTION_KEY;
|
||||
interface DoctorCheck {
|
||||
name: string;
|
||||
status: string;
|
||||
message?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface DoctorResult {
|
||||
@@ -109,3 +111,162 @@ test("doctor fails when encrypted credentials exist without storage key", async
|
||||
assert.equal(getCheck(result, "Storage/encryption")?.status, "fail");
|
||||
});
|
||||
});
|
||||
|
||||
test("doctor probes the real machine-token endpoint without exposing the token", async () => {
|
||||
await withDoctorEnv(async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let observedToken = "";
|
||||
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
assert.match(url, /\/api\/cli\/whoami$/);
|
||||
assert.equal(init?.redirect, "error");
|
||||
observedToken = new Headers(init?.headers).get("x-omniroute-cli-token") || "";
|
||||
return new Response(JSON.stringify({ authenticated: true }), {
|
||||
status: observedToken ? 200 : 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs");
|
||||
const check = await checkMachineTokenAuth({
|
||||
livenessUrl: "http://127.0.0.1:21999/api/health/degradation",
|
||||
});
|
||||
|
||||
assert.equal(check.status, "ok");
|
||||
assert.match(observedToken, /^[0-9a-f]{64}$/);
|
||||
assert.ok(
|
||||
!JSON.stringify(check).includes(observedToken),
|
||||
"doctor output must never expose token"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("doctor only sends the machine token to supported loopback URL shapes", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const observedUrls: string[] = [];
|
||||
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||
observedUrls.push(String(input));
|
||||
assert.equal(init?.redirect, "error");
|
||||
assert.match(new Headers(init?.headers).get("x-omniroute-cli-token") || "", /^[0-9a-f]{64}$/);
|
||||
return new Response(null, { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs");
|
||||
const loopbackUrls = [
|
||||
"http://localhost:21999/health",
|
||||
"http://127.0.0.42:21999/health",
|
||||
"http://[::1]:21999/health",
|
||||
"http://[::ffff:127.0.0.1]:21999/health",
|
||||
];
|
||||
|
||||
for (const livenessUrl of loopbackUrls) {
|
||||
const check = await checkMachineTokenAuth({ livenessUrl });
|
||||
assert.equal(check.status, "ok", livenessUrl);
|
||||
}
|
||||
assert.equal(observedUrls.length, loopbackUrls.length);
|
||||
assert.ok(observedUrls.every((url) => url.endsWith("/api/cli/whoami")));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("doctor refuses remote, deceptive, credential-bearing, and unsupported probe URLs", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response(null, { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs");
|
||||
const rejectedUrls = [
|
||||
"https://remote.example.test/health",
|
||||
"http://localhost.example.test/health",
|
||||
"http://127.0.0.1.example.test/health",
|
||||
"http://localhost@remote.example.test/health",
|
||||
"http://token-user:credential-sentinel@127.0.0.1:21999/health",
|
||||
"ftp://localhost:21999/health",
|
||||
"http://0.0.0.0:21999/health",
|
||||
"http://[::2]:21999/health",
|
||||
];
|
||||
|
||||
for (const livenessUrl of rejectedUrls) {
|
||||
const check = await checkMachineTokenAuth({ livenessUrl });
|
||||
assert.equal(check.status, "warn", livenessUrl);
|
||||
assert.equal(check.details?.accepted, false);
|
||||
assert.equal(check.details?.tokenExposed, false);
|
||||
assert.ok(!JSON.stringify(check).includes("credential-sentinel"));
|
||||
}
|
||||
assert.equal(fetchCalls, 0, "rejected targets must never receive a fetch call");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("doctor never follows a machine-token redirect to another origin", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let crossOriginRequests = 0;
|
||||
let crossOriginTokenObserved = false;
|
||||
globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
|
||||
if (init?.redirect !== "error") {
|
||||
crossOriginRequests += 1;
|
||||
crossOriginTokenObserved = new Headers(init?.headers).has("x-omniroute-cli-token");
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
throw new TypeError("redirect blocked");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs");
|
||||
const check = await checkMachineTokenAuth({
|
||||
livenessUrl: "http://127.0.0.1:21999/redirect-to-other-origin",
|
||||
});
|
||||
|
||||
assert.equal(check.status, "warn");
|
||||
assert.equal(crossOriginRequests, 0);
|
||||
assert.equal(crossOriginTokenObserved, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("doctor gives connect guidance when the server rejects a machine token", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => new Response(null, { status: 401 })) as typeof fetch;
|
||||
try {
|
||||
const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs");
|
||||
const check = await checkMachineTokenAuth({
|
||||
livenessUrl: "http://127.0.0.1:21999/api/health/degradation",
|
||||
});
|
||||
assert.equal(check.status, "warn");
|
||||
assert.match(check.message || "", /omniroute connect/i);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("doctor reports explicitly disabled machine-token auth without probing", async () => {
|
||||
const previous = process.env.OMNIROUTE_DISABLE_CLI_TOKEN;
|
||||
const originalFetch = globalThis.fetch;
|
||||
process.env.OMNIROUTE_DISABLE_CLI_TOKEN = "true";
|
||||
globalThis.fetch = (async () => {
|
||||
throw new Error("fetch should not run");
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs");
|
||||
const check = await checkMachineTokenAuth();
|
||||
assert.equal(check.status, "warn");
|
||||
assert.equal(check.details?.disabled, true);
|
||||
assert.match(check.message || "", /disabled/i);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (previous === undefined) delete process.env.OMNIROUTE_DISABLE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_DISABLE_CLI_TOKEN = previous;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import http from "node:http";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
@@ -12,6 +13,37 @@ test("cliToken.mjs pode ser importado sem erro", async () => {
|
||||
assert.equal(mod.CLI_TOKEN_HEADER, "x-omniroute-cli-token");
|
||||
});
|
||||
|
||||
test("packaged CLI derives the same current machine token as the server", async () => {
|
||||
const salt = `cli-machine-token-${process.pid}`;
|
||||
const previousSalt = process.env.OMNIROUTE_CLI_SALT;
|
||||
process.env.OMNIROUTE_CLI_SALT = salt;
|
||||
try {
|
||||
const { getCliToken } = await import(`../../bin/cli/utils/cliToken.mjs?current=${Date.now()}`);
|
||||
const { getMachineTokenSync } = await import("../../src/lib/machineToken.ts");
|
||||
const token = await getCliToken();
|
||||
|
||||
assert.match(token, /^[0-9a-f]{64}$/, "CLI token must be a non-empty HMAC-SHA256 digest");
|
||||
assert.equal(token, getMachineTokenSync(salt));
|
||||
} finally {
|
||||
if (previousSalt === undefined) delete process.env.OMNIROUTE_CLI_SALT;
|
||||
else process.env.OMNIROUTE_CLI_SALT = previousSalt;
|
||||
}
|
||||
});
|
||||
|
||||
test("getCliToken returns an empty string when machine-id derivation is unavailable", async () => {
|
||||
const { deriveCliToken } = await import("../../bin/cli/utils/cliToken.mjs");
|
||||
assert.equal(deriveCliToken({}, "test-salt"), "");
|
||||
assert.equal(deriveCliToken({ default: { machineIdSync: () => "" } }, "test-salt"), "");
|
||||
const throwingModule = {
|
||||
default: {
|
||||
machineIdSync: () => {
|
||||
throw new Error("unavailable");
|
||||
},
|
||||
},
|
||||
};
|
||||
assert.equal(deriveCliToken(throwingModule, "test-salt"), "");
|
||||
});
|
||||
|
||||
test("getCliToken retorna string de 64 chars ou string vazia", async () => {
|
||||
const { getCliToken } = await import("../../bin/cli/utils/cliToken.mjs");
|
||||
const token = await getCliToken();
|
||||
@@ -97,6 +129,124 @@ test("OMNIROUTE_CLI_TOKEN env sobrescreve token gerado em apiFetch", async () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("apiFetch never sends an implicit machine token to remote contexts", async () => {
|
||||
const originalBaseUrl = process.env.OMNIROUTE_BASE_URL;
|
||||
const originalOverride = process.env.OMNIROUTE_CLI_TOKEN;
|
||||
process.env.OMNIROUTE_BASE_URL = "https://remote.example.test";
|
||||
delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
try {
|
||||
const { buildHeaders } = await import(`../../bin/cli/api.mjs?remote=${Date.now()}`);
|
||||
const headers = await buildHeaders({});
|
||||
assert.equal(headers.has("x-omniroute-cli-token"), false);
|
||||
} finally {
|
||||
if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL;
|
||||
else process.env.OMNIROUTE_BASE_URL = originalBaseUrl;
|
||||
if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_CLI_TOKEN = originalOverride;
|
||||
}
|
||||
});
|
||||
|
||||
test("apiFetch sends the implicit machine token only to loopback destinations", async () => {
|
||||
const originalBaseUrl = process.env.OMNIROUTE_BASE_URL;
|
||||
const originalOverride = process.env.OMNIROUTE_CLI_TOKEN;
|
||||
process.env.OMNIROUTE_BASE_URL = "http://127.0.0.1:20128";
|
||||
delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
try {
|
||||
const [{ buildHeaders, isLoopbackUrl }, { getCliToken }] = await Promise.all([
|
||||
import(`../../bin/cli/api.mjs?loopback=${Date.now()}`),
|
||||
import("../../bin/cli/utils/cliToken.mjs"),
|
||||
]);
|
||||
assert.equal(isLoopbackUrl("http://localhost:20128"), true);
|
||||
assert.equal(isLoopbackUrl("http://127.0.0.42:20128"), true);
|
||||
assert.equal(isLoopbackUrl("http://[::1]:20128"), true);
|
||||
assert.equal(isLoopbackUrl("https://remote.example.test"), false);
|
||||
const headers = await buildHeaders({});
|
||||
assert.equal(headers.get("x-omniroute-cli-token"), await getCliToken());
|
||||
} finally {
|
||||
if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL;
|
||||
else process.env.OMNIROUTE_BASE_URL = originalBaseUrl;
|
||||
if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_CLI_TOKEN = originalOverride;
|
||||
}
|
||||
});
|
||||
|
||||
test("CLI-token overrides are also suppressed for remote contexts", async () => {
|
||||
const originalBaseUrl = process.env.OMNIROUTE_BASE_URL;
|
||||
const originalOverride = process.env.OMNIROUTE_CLI_TOKEN;
|
||||
process.env.OMNIROUTE_BASE_URL = "https://remote.example.test";
|
||||
process.env.OMNIROUTE_CLI_TOKEN = "must-not-leave-loopback";
|
||||
try {
|
||||
const { buildHeaders } = await import(`../../bin/cli/api.mjs?override=${Date.now()}`);
|
||||
const headers = await buildHeaders({ cliToken: "also-local-only" });
|
||||
assert.equal(headers.has("x-omniroute-cli-token"), false);
|
||||
} finally {
|
||||
if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL;
|
||||
else process.env.OMNIROUTE_BASE_URL = originalBaseUrl;
|
||||
if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_CLI_TOKEN = originalOverride;
|
||||
}
|
||||
});
|
||||
|
||||
test("absolute remote URLs cannot inherit a local context machine token", async () => {
|
||||
const originalBaseUrl = process.env.OMNIROUTE_BASE_URL;
|
||||
const originalOverride = process.env.OMNIROUTE_CLI_TOKEN;
|
||||
const originalFetch = globalThis.fetch;
|
||||
process.env.OMNIROUTE_BASE_URL = "http://127.0.0.1:20128";
|
||||
process.env.OMNIROUTE_CLI_TOKEN = "must-stay-local";
|
||||
let receivedHeaders: Headers | null = null;
|
||||
globalThis.fetch = (async (_url, init) => {
|
||||
receivedHeaders = new Headers(init?.headers);
|
||||
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const { apiFetch } = await import(`../../bin/cli/api.mjs?absolute=${Date.now()}`);
|
||||
await apiFetch("https://remote.example.test/probe", { retry: false });
|
||||
assert.equal(receivedHeaders?.has("x-omniroute-cli-token"), false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL;
|
||||
else process.env.OMNIROUTE_BASE_URL = originalBaseUrl;
|
||||
if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_CLI_TOKEN = originalOverride;
|
||||
}
|
||||
});
|
||||
|
||||
test("apiFetch refuses redirects while carrying a local machine token", async () => {
|
||||
const originalBaseUrl = process.env.OMNIROUTE_BASE_URL;
|
||||
const originalOverride = process.env.OMNIROUTE_CLI_TOKEN;
|
||||
let redirectedRequests = 0;
|
||||
const destination = http.createServer((_request, response) => {
|
||||
redirectedRequests += 1;
|
||||
response.end("unexpected");
|
||||
});
|
||||
const redirector = http.createServer((_request, response) => {
|
||||
const destinationAddress = destination.address();
|
||||
assert.ok(destinationAddress && typeof destinationAddress === "object");
|
||||
response.writeHead(302, { location: `http://127.0.0.1:${destinationAddress.port}/target` });
|
||||
response.end();
|
||||
});
|
||||
await new Promise<void>((resolve) => destination.listen(0, "127.0.0.1", resolve));
|
||||
await new Promise<void>((resolve) => redirector.listen(0, "127.0.0.1", resolve));
|
||||
const redirectorAddress = redirector.address();
|
||||
assert.ok(redirectorAddress && typeof redirectorAddress === "object");
|
||||
process.env.OMNIROUTE_BASE_URL = `http://127.0.0.1:${redirectorAddress.port}`;
|
||||
process.env.OMNIROUTE_CLI_TOKEN = "redirect-secret";
|
||||
try {
|
||||
const { apiFetch } = await import(`../../bin/cli/api.mjs?redirect=${Date.now()}`);
|
||||
await assert.rejects(() => apiFetch("/redirect", { retry: false }), /fetch failed/i);
|
||||
assert.equal(redirectedRequests, 0);
|
||||
} finally {
|
||||
await Promise.all([
|
||||
new Promise<void>((resolve) => redirector.close(() => resolve())),
|
||||
new Promise<void>((resolve) => destination.close(() => resolve())),
|
||||
]);
|
||||
if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL;
|
||||
else process.env.OMNIROUTE_BASE_URL = originalBaseUrl;
|
||||
if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_CLI_TOKEN = originalOverride;
|
||||
}
|
||||
});
|
||||
|
||||
// --- testes server-side: isLoopback ---
|
||||
|
||||
test("isLoopback aceita 127.0.0.1", async () => {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getMachineTokenSync } from "../../../src/lib/machineToken.ts";
|
||||
import {
|
||||
deriveLegacyCliToken,
|
||||
deriveMachineToken,
|
||||
getMachineTokenSync,
|
||||
} from "../../../src/lib/machineToken.ts";
|
||||
|
||||
test("machine-token derivation fails closed for a missing machine ID", () => {
|
||||
assert.equal(deriveMachineToken("", "omniroute-cli-auth-v1"), "");
|
||||
assert.equal(deriveLegacyCliToken("", "omniroute-cli-auth-v1"), "");
|
||||
});
|
||||
|
||||
test("getMachineTokenSync returns a 64-character hex string (full SHA-256)", () => {
|
||||
const token = getMachineTokenSync();
|
||||
@@ -22,10 +31,15 @@ test("getMachineTokenSync with empty string salt does not throw", () => {
|
||||
});
|
||||
|
||||
test("getMachineTokenSync respects OMNIROUTE_CLI_SALT env var", () => {
|
||||
const before = getMachineTokenSync();
|
||||
process.env.OMNIROUTE_CLI_SALT = "__test_salt__";
|
||||
const withEnv = getMachineTokenSync();
|
||||
delete process.env.OMNIROUTE_CLI_SALT;
|
||||
assert.notEqual(before, withEnv, "env salt must produce a different token");
|
||||
assert.match(withEnv, /^[0-9a-f]{64}$/, "env-derived token must still be 64-char hex");
|
||||
const previous = process.env.OMNIROUTE_CLI_SALT;
|
||||
try {
|
||||
const before = getMachineTokenSync();
|
||||
process.env.OMNIROUTE_CLI_SALT = "__test_salt__";
|
||||
const withEnv = getMachineTokenSync();
|
||||
assert.notEqual(before, withEnv, "env salt must produce a different token");
|
||||
assert.match(withEnv, /^[0-9a-f]{64}$/, "env-derived token must still be 64-char hex");
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OMNIROUTE_CLI_SALT;
|
||||
else process.env.OMNIROUTE_CLI_SALT = previous;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,9 +20,9 @@ await settingsDb.updateSettings({
|
||||
password: "test-password-hash",
|
||||
});
|
||||
|
||||
const { getLegacyCliTokenSync, getMachineTokenSync } = await import(
|
||||
"../../../src/lib/machineToken.ts"
|
||||
);
|
||||
const { getLegacyCliTokenSync, getMachineTokenSync } =
|
||||
await import("../../../src/lib/machineToken.ts");
|
||||
const { requireManagementAuth } = await import("../../../src/lib/api/requireManagementAuth.ts");
|
||||
const { managementPolicy } = await import("../../../src/server/authz/policies/management.ts");
|
||||
const { CLI_TOKEN_HEADER } = await import("../../../src/server/authz/headers.ts");
|
||||
|
||||
@@ -103,3 +103,34 @@ test("management policy rejects wrong CLI token from localhost", async () => {
|
||||
const outcome = await managementPolicy.evaluate(ctx);
|
||||
assert.equal(outcome.allow, false);
|
||||
});
|
||||
|
||||
test("route-level auth trusts only the central local-CLI subject stamp", async () => {
|
||||
const request = new Request("http://localhost/api/cli/whoami", {
|
||||
headers: {
|
||||
"x-omniroute-auth-kind": "management_key",
|
||||
"x-omniroute-auth-label": "local-cli-token",
|
||||
},
|
||||
});
|
||||
assert.equal(await requireManagementAuth(request, { alwaysRequireAuth: true }), null);
|
||||
|
||||
const spoofedLabelOnly = new Request("http://localhost/api/cli/whoami", {
|
||||
headers: { "x-omniroute-auth-label": "local-cli-token" },
|
||||
});
|
||||
assert.notEqual(await requireManagementAuth(spoofedLabelOnly, { alwaysRequireAuth: true }), null);
|
||||
});
|
||||
|
||||
test("management policy rejects machine tokens when CLI-token auth is disabled", async () => {
|
||||
const previous = process.env.OMNIROUTE_DISABLE_CLI_TOKEN;
|
||||
process.env.OMNIROUTE_DISABLE_CLI_TOKEN = "true";
|
||||
try {
|
||||
const ctx = makeCtx(
|
||||
{ host: "localhost", [CLI_TOKEN_HEADER]: getMachineTokenSync() },
|
||||
{ socket: { remoteAddress: "127.0.0.1" } }
|
||||
);
|
||||
const outcome = await managementPolicy.evaluate(ctx);
|
||||
assert.equal(outcome.allow, false);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OMNIROUTE_DISABLE_CLI_TOKEN;
|
||||
else process.env.OMNIROUTE_DISABLE_CLI_TOKEN = previous;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,6 +105,7 @@ test("next config declares Turbopack aliases, runtime assets and server external
|
||||
// sqlite-vec ships a native vec0.so loaded at runtime; without externalizing it
|
||||
// the Turbopack build fails with "Unknown module type" on the .so (issue #3066).
|
||||
"sqlite-vec",
|
||||
"node-machine-id",
|
||||
"wreq-js",
|
||||
"fs",
|
||||
"path",
|
||||
@@ -126,10 +127,7 @@ test("Turbopack aliases @/mitm/manager to the stub ONLY when OMNIROUTE_MITM_STUB
|
||||
|
||||
process.env.OMNIROUTE_MITM_STUB = "1";
|
||||
const { default: docker } = await loadNextConfig("mitm-docker");
|
||||
assert.equal(
|
||||
docker.turbopack.resolveAlias["@/mitm/manager"],
|
||||
"./src/mitm/manager.stub.ts"
|
||||
);
|
||||
assert.equal(docker.turbopack.resolveAlias["@/mitm/manager"], "./src/mitm/manager.stub.ts");
|
||||
} finally {
|
||||
if (original === undefined) delete process.env.OMNIROUTE_MITM_STUB;
|
||||
else process.env.OMNIROUTE_MITM_STUB = original;
|
||||
@@ -198,7 +196,11 @@ test("manager.stub.ts exports every name statically imported from @/mitm/manager
|
||||
}
|
||||
for (const m of stubSrc.matchAll(/export\s*\{([^}]*)\}/g)) {
|
||||
for (const part of m[1].split(",")) {
|
||||
const exported = part.trim().split(/\s+as\s+/).pop()?.trim(); // `x as y` exports y
|
||||
const exported = part
|
||||
.trim()
|
||||
.split(/\s+as\s+/)
|
||||
.pop()
|
||||
?.trim(); // `x as y` exports y
|
||||
if (exported) stubExports.add(exported);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user