chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const SALT = "omniroute-cli-auth-v1";
|
||||
export const CLI_TOKEN_HEADER = "x-omniroute-cli-token";
|
||||
|
||||
let _cached = null;
|
||||
|
||||
export async function getCliToken() {
|
||||
if (_cached !== null) return _cached;
|
||||
try {
|
||||
const { machineIdSync } = await import("node-machine-id");
|
||||
const mid = machineIdSync();
|
||||
_cached = crypto
|
||||
.createHash("sha256")
|
||||
.update(mid + SALT)
|
||||
.digest("hex")
|
||||
.substring(0, 32);
|
||||
} catch {
|
||||
_cached = "";
|
||||
}
|
||||
return _cached;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export function copyToClipboard(text) {
|
||||
try {
|
||||
const execOpts = { input: text, stdio: ["pipe", "ignore", "ignore"], timeout: 2000 };
|
||||
if (process.platform === "darwin") {
|
||||
execSync("pbcopy", execOpts);
|
||||
} else if (process.platform === "win32") {
|
||||
execSync("clip", execOpts);
|
||||
} else {
|
||||
try {
|
||||
execSync("xclip -selection clipboard", execOpts);
|
||||
} catch {
|
||||
try {
|
||||
execSync("xsel --clipboard --input", execOpts);
|
||||
} catch {
|
||||
execSync("wl-copy", execOpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isClipboardSupported() {
|
||||
if (process.platform === "darwin" || process.platform === "win32") return true;
|
||||
try {
|
||||
execSync("which xclip || which xsel || which wl-copy", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
|
||||
export function detectRestrictedEnvironment() {
|
||||
if (process.env.CODESPACES === "true" || process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN) {
|
||||
return { type: "github-codespaces", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
|
||||
return {
|
||||
type: "wsl",
|
||||
canOpenBrowser: true,
|
||||
canUseTray: false,
|
||||
hint: "Browser opens in Windows host.",
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.GITPOD_WORKSPACE_ID) {
|
||||
return { type: "gitpod", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (process.env.REPL_ID || process.env.REPL_SLUG) {
|
||||
return { type: "replit", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (process.env.CI) {
|
||||
return { type: "ci", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (existsSync("/.dockerenv")) {
|
||||
return { type: "docker", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
try {
|
||||
if (existsSync("/proc/1/cgroup") && readFileSync("/proc/1/cgroup", "utf8").includes("docker")) {
|
||||
return { type: "docker", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
return { type: "non-interactive", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
return { type: "desktop", canOpenBrowser: true, canUseTray: true };
|
||||
}
|
||||
|
||||
export function getEnvBanner() {
|
||||
const env = detectRestrictedEnvironment();
|
||||
if (env.type === "desktop") return null;
|
||||
return `[${env.type}] ${env.hint || "limited environment detected"}`;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
|
||||
const SERVICES = ["server", "mitm", "tunnel/cloudflared", "tunnel/tailscale"];
|
||||
|
||||
function getServicePidPath(service) {
|
||||
return join(resolveDataDir(), service, ".pid");
|
||||
}
|
||||
|
||||
export function writePidFile(service, pid) {
|
||||
try {
|
||||
const dir = join(resolveDataDir(), service);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(getServicePidPath(service), String(pid), "utf8");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function readPidFile(service) {
|
||||
try {
|
||||
const file = getServicePidPath(service);
|
||||
if (!existsSync(file)) return null;
|
||||
const pid = parseInt(readFileSync(file, "utf8").trim(), 10);
|
||||
return Number.isFinite(pid) ? pid : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupPidFile(service) {
|
||||
try {
|
||||
unlinkSync(getServicePidPath(service));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function killAllSubprocesses() {
|
||||
for (const service of SERVICES) {
|
||||
const pid = readPidFile(service);
|
||||
if (!pid) continue;
|
||||
try {
|
||||
process.kill(pid, "SIGTERM");
|
||||
} catch {}
|
||||
cleanupPidFile(service);
|
||||
}
|
||||
}
|
||||
|
||||
export function isPidRunning(pid) {
|
||||
if (!pid) return false;
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// #2460: Default raised from 15s to 60s so Windows users (slower Next.js
|
||||
// cold start due to filesystem watchers, antivirus, etc.) get a working
|
||||
// "server ready" signal instead of a phantom timeout while the server is
|
||||
// still booting. TCP fallback marks the server as ready when the port
|
||||
// has been listening for >= 3s consecutively but /api/monitoring/health
|
||||
// has not yet been mounted — common during dev cold start.
|
||||
export async function waitForServer(port, timeout = 60000) {
|
||||
const start = Date.now();
|
||||
let tcpListeningSince = null;
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (res.ok) return true;
|
||||
// Server responded but health endpoint is not ready yet — keep
|
||||
// polling, but the fact that we got a response means TCP is open.
|
||||
if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
||||
} catch {
|
||||
const listening = await isPortListening(port).catch(() => false);
|
||||
if (listening) {
|
||||
if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
||||
if (Date.now() - tcpListeningSince >= 3000) return true;
|
||||
} else {
|
||||
tcpListeningSince = null;
|
||||
}
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function isPortListening(port) {
|
||||
const net = await import("node:net");
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect({ host: "127.0.0.1", port, timeout: 1000 });
|
||||
const finish = (ok) => {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
resolve(ok);
|
||||
};
|
||||
socket.once("connect", () => finish(true));
|
||||
socket.once("error", () => finish(false));
|
||||
socket.once("timeout", () => finish(false));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Decide whether a CLI invocation should provision (generate + persist) the
|
||||
* STORAGE_ENCRYPTION_KEY into DATA_DIR/.env.
|
||||
*
|
||||
* Purely informational invocations must NOT create `~/.omniroute/.env` or write
|
||||
* a key — they never touch encrypted storage. Generating a 32-byte key and a
|
||||
* `.env` file just to print `omniroute --version` (or `--help`) is a surprising
|
||||
* side effect: a read-only command should not mutate the data dir.
|
||||
*
|
||||
* Returns FALSE only for: `--version`/`-V` or `--help`/`-h` anywhere in the args,
|
||||
* and the `help`/`completion` subcommands.
|
||||
*
|
||||
* Returns TRUE for everything else, INCLUDING a bare `omniroute` (no args) — the
|
||||
* `serve` command is `isDefault: true`, so a bare invocation starts the server,
|
||||
* which needs the encryption key. This preserves the #1622 persistence fix
|
||||
* (key generated on first real run and reused across restarts).
|
||||
*
|
||||
* @param {string[]} argv - process.argv (node + script + args).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const INFO_FLAGS = new Set(["-h", "--help", "-V", "--version"]);
|
||||
const INFO_COMMANDS = new Set(["help", "completion"]);
|
||||
|
||||
export function shouldProvisionStorageKey(argv) {
|
||||
const args = Array.isArray(argv) ? argv.slice(2) : [];
|
||||
// Bare `omniroute` runs the default `serve` command → must provision.
|
||||
if (args.length === 0) return true;
|
||||
if (args.some((a) => INFO_FLAGS.has(a))) return false;
|
||||
if (INFO_COMMANDS.has(args[0])) return false;
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user