Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9d0640f6e | |||
| e19046116a | |||
| 82a621ec08 | |||
| ce560ebe9d | |||
| f900a81ec9 | |||
| 2a620b178d | |||
| 5aaaad529b |
@@ -6,61 +6,79 @@ description: Create a new release, bump version up to 1.x.10 threshold, update c
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (2.x.y → 2.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `2.(x+1).0` — e.g. `2.1.10` → `2.2.0`.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Determine new version
|
||||
|
||||
Check current version in `package.json` and increment the patch number:
|
||||
Check current version in `package.json` and increment the **patch** number only:
|
||||
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
Version format: `1.x.y` — increment `y` for patch, `x` for minor (threshold: y=10 triggers x+1).
|
||||
Version format: `2.x.y` — examples:
|
||||
|
||||
### 2. Finalize CHANGELOG.md
|
||||
|
||||
Replace `[Unreleased]` header with the new version and date:
|
||||
|
||||
```markdown
|
||||
## [1.x.y] — YYYY-MM-DD
|
||||
```
|
||||
|
||||
### 3. Bump version in package.json
|
||||
- `2.1.2` → `2.1.3` (patch)
|
||||
- `2.1.9` → `2.1.10` (patch)
|
||||
- `2.1.10` → `2.2.0` (minor threshold — do manually with `sed`)
|
||||
|
||||
```bash
|
||||
sed -i 's/"version": "OLD"/"version": "NEW"/' package.json
|
||||
# ALWAYS use patch:
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
### 4. Stage, commit, and tag
|
||||
### 2. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Finalize CHANGELOG.md
|
||||
|
||||
Replace `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it.
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [2.x.y] — YYYY-MM-DD
|
||||
```
|
||||
|
||||
### 4. Update openapi.yaml version
|
||||
|
||||
```bash
|
||||
sed -i 's/version: OLD/version: NEW/' docs/openapi.yaml
|
||||
```
|
||||
|
||||
### 5. Stage, commit, and tag
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat(release): vX.Y.Z — summary of changes"
|
||||
git tag -a vX.Y.Z -m "Release vX.Y.Z — summary"
|
||||
git add package.json package-lock.json CHANGELOG.md docs/openapi.yaml
|
||||
git commit -m "chore(release): v2.x.y — summary of changes"
|
||||
git tag -a v2.x.y -m "Release v2.x.y"
|
||||
```
|
||||
|
||||
### 5. Push to GitHub
|
||||
### 6. Push to GitHub
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
git push origin vX.Y.Z
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### 6. Publish to npm
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
Wait for completion (prepublishOnly runs `npm run build:cli` automatically).
|
||||
|
||||
### 7. Create GitHub release
|
||||
|
||||
```bash
|
||||
gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_notes.md
|
||||
gh release create v2.x.y --title "v2.x.y — summary" --notes "..."
|
||||
```
|
||||
|
||||
### 8. Deploy to VPS (if requested)
|
||||
@@ -68,7 +86,7 @@ gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_note
|
||||
See `/deploy-vps` workflow for Akamai VPS or use npm for local VPS:
|
||||
|
||||
```bash
|
||||
ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
|
||||
ssh root@<VPS_IP> "npm install -g omniroute@2.x.y && pm2 restart omniroute"
|
||||
```
|
||||
|
||||
## Notes
|
||||
@@ -76,3 +94,4 @@ ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
|
||||
- Always run `/update-docs` BEFORE this workflow (ensures CHANGELOG and README are current)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
|
||||
@@ -10,6 +10,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies (skip scripts to avoid heavy build)
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm install --ignore-scripts --no-audit --no-fund
|
||||
|
||||
- name: Sync version from release tag
|
||||
run: |
|
||||
@@ -39,6 +39,13 @@ jobs:
|
||||
run: node scripts/prepublish.mjs
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
# Check if this version is already published — skip instead of failing with E403
|
||||
if npm view "omniroute@${VERSION}" version --silent 2>/dev/null | grep -q "^${VERSION}$"; then
|
||||
echo "️⚠️ Version ${VERSION} is already published on npm — skipping."
|
||||
exit 0
|
||||
fi
|
||||
npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -102,7 +102,6 @@ cloud/
|
||||
security-analysis/
|
||||
|
||||
# Deploy workflow (contains sensitive VPS credentials)
|
||||
.agent/workflows/deploy.md
|
||||
clipr/
|
||||
app.log
|
||||
*.tgz
|
||||
|
||||
+41
-1
@@ -7,7 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
## [2.2.2] — 2026-03-10
|
||||
|
||||
> ### ✨ New Features · 🔀 Model Aliases
|
||||
|
||||
### New Features
|
||||
|
||||
- **system-info.mjs (#280)** — New `npm run system-info` command that collects Node.js version, OmniRoute version, OS info, CLI tool versions (iflow, gemini, claude, codex, antigravity, droid, openclaw, kilo, cursor, aider), Docker/PM2 status, and system packages. Outputs `system-info.txt` for easy attachment to bug reports.
|
||||
|
||||
### Model Aliases
|
||||
|
||||
- **Kimi K2/K2.5 Fireworks aliases (#265)** — Built-in aliases added: `fireworks/accounts/fireworks/models/kimi-k2p5` and `kimi-k2p5` → `moonshotai/Kimi-K2.5`; same for `kimi-k2` → `moonshotai/Kimi-K2`. Fireworks long path model names now auto-resolve.
|
||||
- **Mistral short aliases (#278)** — `mistral-large` → `mistral-large-latest`, `mistral-small` → `mistral-small-latest`, `codestral` → `codestral-latest`.
|
||||
- **Llama short aliases** — `llama-3.3` → `llama-3.3-70b-versatile`, `llama-3-70b` → `llama-3.3-70b-versatile`, `llama-3-8b` → `llama3-8b-8192`.
|
||||
- **Custom aliases** — Users can define their own aliases in **Settings → Model Aliases** tab. Example: `gpt-5.4` → `cx/gpt-5.4`.
|
||||
|
||||
---
|
||||
|
||||
## [2.2.1] — 2026-03-10
|
||||
|
||||
> ### 🐛 Bug Fixes · 🔐 Security · 🔧 CI
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Gemini image routing (#273)** — `gemini-3.1-flash-image-preview` was missing from the `antigravity` image provider registry in `imageRegistry.ts`, causing image generation to fall through to the chat handler. Added alongside `gemini-2.5-flash-preview-image-generation`.
|
||||
- **Ollama Cloud model listing (#276)** — `ollama-cloud` was absent from `PROVIDER_MODELS_CONFIG` in the models route, causing 400 errors when listing models from `api.ollama.com`. Entry added.
|
||||
- **Missing apiKey error clarity (#277)** — When login is disabled and a provider has no API key configured, the model import route now returns `400` with a clear message instead of a generic `401 Unauthorized`.
|
||||
|
||||
### Security
|
||||
|
||||
- **TLS validation re-enabled (GHSA-50)** — `mitm/server.ts`: `rejectUnauthorized` now defaults to `true`. Opt-out only via `MITM_DISABLE_TLS_VERIFY=1`.
|
||||
- **Path traversal hardening (GHSA-41–49)** — Added `safePath()`, `safeProfilePath()`, `safeLogPath()` helpers across `backupService.ts`, `db/backup.ts`, `codex-profiles/route.ts`, and `mitm/server.ts`. All user-supplied IDs/filenames are now anchored within their allowed directories using `path.resolve()` + bounds check.
|
||||
- **Prototype pollution fix (GHSA-18–20)** — `usageHistory.ts`: `pendingRequests` maps now use `Object.create(null)` + `hasOwnProperty` guards, preventing `__proto__` / `constructor` injection via crafted provider IDs.
|
||||
- **Dependency: dompurify updated to ^3.3.2** — Resolves CVE-2026-0540 (XSS in rendered HTML).
|
||||
- **GitHub Actions: added `permissions: contents: read`** — Prevents token over-permission in CI jobs.
|
||||
|
||||
### CI
|
||||
|
||||
- **Lock file sync** — Added `@swc/helpers: "^0.5.19"` override in `package.json`; regenerated `package-lock.json`. Fixes `npm ci` failures across `ci.yml` and `docker-publish.yml`.
|
||||
- **npm-publish: skip if version exists** — Workflow now checks registry before publishing; exits cleanly with a warning instead of failing with `E403` if the version is already on npm.
|
||||
- **npm-publish: use `npm install` instead of `npm ci`** — Prevents publish failures when a tag commit's lock file is slightly out of sync.
|
||||
- **Lint: `cursor.ts` any-budget** — Replaced `any` with `unknown` + type narrowing in `isToolBoundaryAbort()`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,10 @@ export const IMAGE_PROVIDERS = {
|
||||
authType: "oauth",
|
||||
authHeader: "bearer",
|
||||
format: "gemini-image", // Special format: uses Gemini generateContent API
|
||||
models: [{ id: "gemini-2.5-flash-preview-image-generation", name: "Nano Banana" }],
|
||||
models: [
|
||||
{ id: "gemini-2.5-flash-preview-image-generation", name: "Gemini 2.5 Flash Image" },
|
||||
{ id: "gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image Preview" },
|
||||
],
|
||||
supportedSizes: ["1024x1024"],
|
||||
},
|
||||
|
||||
|
||||
@@ -148,12 +148,17 @@ function parseCursorJsonErrorFrame(text: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function isToolBoundaryAbort(jsonError: any, toolCallCount: number) {
|
||||
function isToolBoundaryAbort(jsonError: unknown, toolCallCount: number) {
|
||||
if (!jsonError || toolCallCount <= 0) return false;
|
||||
const code = jsonError?.error?.code || "";
|
||||
const debugError = jsonError?.error?.details?.[0]?.debug?.error || "";
|
||||
const title = jsonError?.error?.details?.[0]?.debug?.details?.title || "";
|
||||
const detail = jsonError?.error?.details?.[0]?.debug?.details?.detail || "";
|
||||
const e = jsonError as Record<string, unknown>;
|
||||
const err = e?.error as Record<string, unknown> | undefined;
|
||||
const details = (err?.details as Record<string, unknown>[] | undefined)?.[0];
|
||||
const debug = details?.debug as Record<string, unknown> | undefined;
|
||||
const debugDetails = debug?.details as Record<string, unknown> | undefined;
|
||||
const code = (err?.code as string) || "";
|
||||
const debugError = (debug?.error as string) || "";
|
||||
const title = (debugDetails?.title as string) || "";
|
||||
const detail = (debugDetails?.detail as string) || "";
|
||||
const message = `${title} ${detail}`.toLowerCase();
|
||||
const isAbortedCode = code === "aborted" || debugError === "ERROR_USER_ABORTED_REQUEST";
|
||||
return isAbortedCode && message.includes("tool call ended before result was received");
|
||||
|
||||
@@ -31,6 +31,24 @@ const BUILT_IN_ALIASES: Record<string, string> = {
|
||||
"gpt-4-0125-preview": "gpt-4-turbo",
|
||||
"gpt-4-1106-preview": "gpt-4-turbo",
|
||||
"gpt-3.5-turbo-0125": "gpt-3.5-turbo",
|
||||
|
||||
// Kimi/Moonshot — Fireworks long-path aliases (#265)
|
||||
"accounts/fireworks/models/kimi-k2p5": "moonshotai/Kimi-K2.5",
|
||||
"fireworks/accounts/fireworks/models/kimi-k2p5": "moonshotai/Kimi-K2.5",
|
||||
"kimi-k2p5": "moonshotai/Kimi-K2.5",
|
||||
"accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
|
||||
"fireworks/accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
|
||||
"kimi-k2": "moonshotai/Kimi-K2",
|
||||
|
||||
// Mistral short aliases
|
||||
"mistral-large": "mistral-large-latest",
|
||||
"mistral-small": "mistral-small-latest",
|
||||
codestral: "codestral-latest",
|
||||
|
||||
// Llama short aliases
|
||||
"llama-3.3": "llama-3.3-70b-versatile",
|
||||
"llama-3-70b": "llama-3.3-70b-versatile",
|
||||
"llama-3-8b": "llama3-8b-8192",
|
||||
};
|
||||
|
||||
// ── Custom Aliases (persisted via Settings API) ─────────────────────────────
|
||||
|
||||
Generated
+33
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -18,6 +18,7 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"bottleneck": "^2.19.5",
|
||||
"dompurify": "^3.3.2",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
@@ -2985,9 +2986,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
@@ -5674,10 +5675,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
@@ -6864,6 +6868,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -8773,6 +8778,15 @@
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor/node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -8964,6 +8978,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl/node_modules/@swc/helpers": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
|
||||
+7
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -76,7 +76,8 @@
|
||||
"check": "npm run lint && npm run test",
|
||||
"prepublishOnly": "npm run build:cli",
|
||||
"postinstall": "node scripts/postinstall.mjs",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"system-info": "node scripts/system-info.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
@@ -84,6 +85,7 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"bottleneck": "^2.19.5",
|
||||
"dompurify": "^3.3.2",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
@@ -138,5 +140,8 @@
|
||||
"*.{json,md,yml,yaml,css}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@swc/helpers": "^0.5.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* system-info.mjs — OmniRoute System Information Reporter (#280)
|
||||
*
|
||||
* Collects system/environment info for bug reports.
|
||||
* Usage: node scripts/system-info.mjs [--output system-info.txt]
|
||||
*
|
||||
* Output includes:
|
||||
* - Node.js version
|
||||
* - OmniRoute version
|
||||
* - OS info
|
||||
* - Relevant system packages (if apt available)
|
||||
* - Agent CLI tools (iflow, gemini, claude, codex, antigravity, droid, etc.)
|
||||
* - Docker / PM2 status
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import os from "os";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function run(cmd, fallback = "N/A") {
|
||||
try {
|
||||
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function toolVersion(cmd, args = "--version") {
|
||||
const version = run(`${cmd} ${args}`, null);
|
||||
if (version === null) return "not installed";
|
||||
// Trim to first line, remove prefixes like "v", "Version: "
|
||||
return version
|
||||
.split("\n")[0]
|
||||
.replace(/^(version\s*:?\s*|v)/i, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
const line = "─".repeat(60);
|
||||
return `\n${line}\n ${title}\n${line}\n`;
|
||||
}
|
||||
|
||||
// ── Collect Info ──────────────────────────────────────────────────────────
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push("OmniRoute System Information Report");
|
||||
lines.push(`Generated: ${new Date().toISOString()}`);
|
||||
|
||||
// ── Node.js & Runtime ────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("Node.js & Runtime"));
|
||||
lines.push(`Node.js: ${process.version}`);
|
||||
lines.push(`npm: v${run("npm --version")}`);
|
||||
lines.push(`Platform: ${process.platform} (${process.arch})`);
|
||||
lines.push(`OS: ${os.type()} ${os.release()} (${os.arch()})`);
|
||||
lines.push(`Hostname: ${os.hostname()}`);
|
||||
lines.push(`CPUs: ${os.cpus().length}x ${os.cpus()[0]?.model || "unknown"}`);
|
||||
lines.push(`Total RAM: ${Math.round(os.totalmem() / 1024 / 1024)} MB`);
|
||||
lines.push(`Free RAM: ${Math.round(os.freemem() / 1024 / 1024)} MB`);
|
||||
|
||||
// ── OmniRoute Version ────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("OmniRoute"));
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
||||
lines.push(`Version: ${pkg.version}`);
|
||||
lines.push(`Name: ${pkg.name}`);
|
||||
} catch {
|
||||
lines.push("Version: unable to read package.json");
|
||||
}
|
||||
|
||||
const installedGlobal = run("npm list -g omniroute --depth=0 2>/dev/null | grep omniroute");
|
||||
lines.push(`Global npm: ${installedGlobal || "not installed globally"}`);
|
||||
|
||||
const pm2Status = run("pm2 list 2>/dev/null | grep omniroute | awk '{print $4, $10, $12}'");
|
||||
lines.push(`PM2 status: ${pm2Status || "not running via PM2"}`);
|
||||
|
||||
// ── Agent CLI Tools ──────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("Agent CLI Tools"));
|
||||
|
||||
const cliTools = [
|
||||
{ name: "iflow-cli", cmd: "iflow", args: "--version" },
|
||||
{ name: "gemini-cli", cmd: "gemini", args: "--version" },
|
||||
{ name: "claude-code", cmd: "claude", args: "--version" },
|
||||
{ name: "openai-codex", cmd: "codex", args: "--version" },
|
||||
{ name: "antigravity", cmd: "antigravity", args: "--version" },
|
||||
{ name: "droid", cmd: "droid", args: "--version" },
|
||||
{ name: "openclaw", cmd: "openclaw", args: "--version" },
|
||||
{ name: "kilo", cmd: "kilo", args: "--version" },
|
||||
{ name: "cursor", cmd: "cursor", args: "--version" },
|
||||
{ name: "aider", cmd: "aider", args: "--version" },
|
||||
];
|
||||
|
||||
for (const { name, cmd, args } of cliTools) {
|
||||
const v = toolVersion(cmd, args);
|
||||
lines.push(`${name.padEnd(20)} ${v}`);
|
||||
}
|
||||
|
||||
// ── Docker ───────────────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("Docker"));
|
||||
lines.push(`Docker: ${run("docker --version", "not installed")}`);
|
||||
|
||||
const dockerContainers = run(
|
||||
"docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null",
|
||||
"N/A"
|
||||
);
|
||||
lines.push(`Containers:\n${dockerContainers}`);
|
||||
|
||||
// ── System Packages ──────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("System Packages (relevant)"));
|
||||
|
||||
const relevantPkgs = ["build-essential", "libssl-dev", "openssl", "libsqlite3-dev", "python3"];
|
||||
for (const pkg of relevantPkgs) {
|
||||
const ver = run(`dpkg -l ${pkg} 2>/dev/null | grep '^ii' | awk '{print $3}'`, "not found");
|
||||
lines.push(`${pkg.padEnd(24)} ${ver}`);
|
||||
}
|
||||
|
||||
// ── Environment Variables (safe subset) ─────────────────────────────────
|
||||
|
||||
lines.push(section("Environment Variables (non-sensitive)"));
|
||||
|
||||
const safeEnvKeys = [
|
||||
"NODE_ENV",
|
||||
"PORT",
|
||||
"DATA_DIR",
|
||||
"DB_BACKUPS_DIR",
|
||||
"LOG_LEVEL",
|
||||
"NEXT_PUBLIC_APP_URL",
|
||||
"ROUTER_API_KEY_HINT",
|
||||
];
|
||||
|
||||
for (const key of safeEnvKeys) {
|
||||
const val = process.env[key];
|
||||
if (val !== undefined) {
|
||||
// Mask if looks like a secret
|
||||
const masked = val.length > 8 ? val.substring(0, 4) + "****" : "****";
|
||||
lines.push(`${key.padEnd(28)} ${masked}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Output ───────────────────────────────────────────────────────────────
|
||||
|
||||
const report = lines.join("\n") + "\n";
|
||||
|
||||
// Write to file
|
||||
const outArg = process.argv.find((a) => a.startsWith("--output="));
|
||||
const outFile = outArg
|
||||
? outArg.replace("--output=", "")
|
||||
: process.argv[process.argv.indexOf("--output") + 1] || "system-info.txt";
|
||||
|
||||
const outPath = join(ROOT, outFile);
|
||||
|
||||
writeFileSync(outPath, report);
|
||||
console.log(report);
|
||||
console.log(`\n✅ Report saved to: ${outPath}`);
|
||||
console.log(
|
||||
`📎 Attach this file when reporting issues at: https://github.com/diegosouzapw/OmniRoute/issues`
|
||||
);
|
||||
@@ -10,6 +10,19 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
const PROFILES_DIR = path.join(resolveDataDir(), "codex-profiles");
|
||||
|
||||
/**
|
||||
* Resolve a path inside PROFILES_DIR and verify it stays within bounds.
|
||||
* Throws on path traversal attempts.
|
||||
*/
|
||||
function safeProfilePath(...segments: string[]): string {
|
||||
const resolved = path.resolve(PROFILES_DIR, ...segments);
|
||||
const base = path.resolve(PROFILES_DIR);
|
||||
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
||||
throw new Error("Invalid path: directory traversal detected");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure profiles directory exists
|
||||
*/
|
||||
|
||||
@@ -247,6 +247,14 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
},
|
||||
"ollama-cloud": {
|
||||
url: "https://api.ollama.com/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.models || data.data || [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -389,7 +397,13 @@ export async function GET(request, { params }) {
|
||||
// Get auth token
|
||||
const token = accessToken || apiKey;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "No valid token found" }, { status: 401 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"No API key configured for this provider. Please add an API key in the provider settings.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build request URL
|
||||
|
||||
+17
-2
@@ -159,11 +159,26 @@ export async function listDbBackups() {
|
||||
|
||||
export async function restoreDbBackup(backupId: string) {
|
||||
const backupDir = DB_BACKUPS_DIR || path.join(DATA_DIR, "db_backups");
|
||||
const backupPath = path.join(backupDir, backupId);
|
||||
|
||||
if (!backupId.startsWith("db_") || !backupId.endsWith(".sqlite")) {
|
||||
// Validate format: must be db_<timestamp>_<reason>.sqlite, no path separators
|
||||
if (
|
||||
!backupId.startsWith("db_") ||
|
||||
!backupId.endsWith(".sqlite") ||
|
||||
backupId.includes(path.sep) ||
|
||||
backupId.includes("/")
|
||||
) {
|
||||
throw new Error("Invalid backup ID");
|
||||
}
|
||||
|
||||
const backupPath = path.resolve(backupDir, backupId);
|
||||
// Prevent path traversal: resolved path must stay within backupDir
|
||||
if (
|
||||
!backupPath.startsWith(path.resolve(backupDir) + path.sep) &&
|
||||
backupPath !== path.resolve(backupDir)
|
||||
) {
|
||||
throw new Error("Invalid backup ID: path traversal detected");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
throw new Error(`Backup not found: ${backupId}`);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ const pendingRequests: {
|
||||
byModel: Record<string, number>;
|
||||
byAccount: Record<string, Record<string, number>>;
|
||||
} = {
|
||||
byModel: {},
|
||||
byAccount: {},
|
||||
byModel: Object.create(null) as Record<string, number>,
|
||||
byAccount: Object.create(null) as Record<string, Record<string, number>>,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,16 +50,22 @@ export function trackPendingRequest(
|
||||
) {
|
||||
const modelKey = provider ? `${model} (${provider})` : model;
|
||||
|
||||
if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0;
|
||||
// Use hasOwnProperty guard to prevent prototype pollution via crafted keys
|
||||
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byModel, modelKey)) {
|
||||
pendingRequests.byModel[modelKey] = 0;
|
||||
}
|
||||
pendingRequests.byModel[modelKey] = Math.max(
|
||||
0,
|
||||
pendingRequests.byModel[modelKey] + (started ? 1 : -1)
|
||||
);
|
||||
|
||||
if (connectionId) {
|
||||
if (!pendingRequests.byAccount[connectionId]) pendingRequests.byAccount[connectionId] = {};
|
||||
if (!pendingRequests.byAccount[connectionId][modelKey])
|
||||
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byAccount, connectionId)) {
|
||||
pendingRequests.byAccount[connectionId] = Object.create(null) as Record<string, number>;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byAccount[connectionId], modelKey)) {
|
||||
pendingRequests.byAccount[connectionId][modelKey] = 0;
|
||||
}
|
||||
pendingRequests.byAccount[connectionId][modelKey] = Math.max(
|
||||
0,
|
||||
pendingRequests.byAccount[connectionId][modelKey] + (started ? 1 : -1)
|
||||
|
||||
+17
-3
@@ -45,12 +45,22 @@ const CHAT_URL_PATTERNS = [":generateContent", ":streamGenerateContent"];
|
||||
const LOG_DIR = path.join(__dirname, "../../logs/mitm");
|
||||
if (ENABLE_FILE_LOG && !fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
|
||||
// Safe log filename: only alphanumeric + hyphens, anchored inside LOG_DIR
|
||||
function safeLogPath(name) {
|
||||
const safe = name.replace(/[^a-zA-Z0-9_\-]/g, "_").substring(0, 80);
|
||||
const resolved = path.resolve(LOG_DIR, safe);
|
||||
if (!resolved.startsWith(path.resolve(LOG_DIR) + path.sep)) {
|
||||
throw new Error("Path traversal attempt detected in log filename");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function saveRequestLog(url, bodyBuffer) {
|
||||
if (!ENABLE_FILE_LOG) return;
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}.json`);
|
||||
const filePath = safeLogPath(`${ts}_${urlSlug}.json`);
|
||||
const body = JSON.parse(bodyBuffer.toString());
|
||||
fs.writeFileSync(filePath, JSON.stringify(body, null, 2));
|
||||
console.log(`💾 Saved request: ${filePath}`);
|
||||
@@ -64,7 +74,7 @@ function saveResponseLog(url, data) {
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}_response.txt`);
|
||||
const filePath = safeLogPath(`${ts}_${urlSlug}_response.txt`);
|
||||
fs.writeFileSync(filePath, data);
|
||||
console.log(`💾 Saved response: ${filePath}`);
|
||||
} catch {
|
||||
@@ -156,6 +166,10 @@ function getMappedModel(model) {
|
||||
async function passthrough(req, res, bodyBuffer) {
|
||||
const targetIP = await resolveTargetIP();
|
||||
|
||||
// TLS validation is enabled by default. Set MITM_DISABLE_TLS_VERIFY=1 only
|
||||
// in controlled local environments where the target uses a self-signed cert.
|
||||
const rejectUnauthorized = process.env.MITM_DISABLE_TLS_VERIFY !== "1";
|
||||
|
||||
const forwardReq = https.request(
|
||||
{
|
||||
hostname: targetIP,
|
||||
@@ -164,7 +178,7 @@ async function passthrough(req, res, bodyBuffer) {
|
||||
method: req.method,
|
||||
headers: { ...req.headers, host: TARGET_HOST },
|
||||
servername: TARGET_HOST,
|
||||
rejectUnauthorized: false,
|
||||
rejectUnauthorized,
|
||||
},
|
||||
(forwardRes) => {
|
||||
res.writeHead(forwardRes.statusCode, forwardRes.headers);
|
||||
|
||||
@@ -5,11 +5,24 @@ import { resolveDataDir } from "@/lib/dataPaths";
|
||||
const BACKUP_DIR = path.join(resolveDataDir(), "backups");
|
||||
const MAX_BACKUPS_PER_TOOL = 5;
|
||||
|
||||
/**
|
||||
* Resolve a path within BACKUP_DIR and verify it stays within bounds.
|
||||
* Throws if the resolved path escapes BACKUP_DIR (path traversal guard).
|
||||
*/
|
||||
function safePath(...segments: string[]): string {
|
||||
const resolved = path.resolve(BACKUP_DIR, ...segments);
|
||||
const base = path.resolve(BACKUP_DIR);
|
||||
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
||||
throw new Error("Invalid path: directory traversal detected");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup directory for a specific tool
|
||||
*/
|
||||
function getToolBackupDir(toolId: string) {
|
||||
return path.join(BACKUP_DIR, toolId);
|
||||
return safePath(toolId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +149,8 @@ export async function listBackups(toolId: string) {
|
||||
*/
|
||||
export async function restoreBackup(toolId: string, backupId: string) {
|
||||
const dir = getToolBackupDir(toolId);
|
||||
const backupPath = path.join(dir, backupId);
|
||||
// Anchor backupId within the tool dir — prevent path traversal via backupId
|
||||
const backupPath = safePath(toolId, backupId);
|
||||
const metaPath = backupPath + ".meta.json";
|
||||
|
||||
// Read metadata to find original path
|
||||
@@ -174,8 +188,8 @@ export async function restoreBackup(toolId: string, backupId: string) {
|
||||
* Delete a specific backup by its id.
|
||||
*/
|
||||
export async function deleteBackup(toolId: string, backupId: string) {
|
||||
const dir = getToolBackupDir(toolId);
|
||||
const backupPath = path.join(dir, backupId);
|
||||
// Anchor backupId within the tool dir — prevent path traversal via backupId
|
||||
const backupPath = safePath(toolId, backupId);
|
||||
const metaPath = backupPath + ".meta.json";
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user