chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "Branch Protection — main"
|
||||
---
|
||||
|
||||
# Branch protection — `main` (OpenSSF Scorecard: Branch-Protection)
|
||||
|
||||
Owner action. Apply via Settings → Branches → Add rule, or:
|
||||
|
||||
```bash
|
||||
gh api -X PUT repos/diegosouzapw/OmniRoute/branches/main/protection \
|
||||
--input - <<'JSON'
|
||||
{ "required_status_checks": { "strict": true, "contexts": ["Quality Ratchet", "Quality Gates (Extended)", "Fast Quality Gates"] },
|
||||
"enforce_admins": false,
|
||||
"required_pull_request_reviews": { "required_approving_review_count": 0, "dismiss_stale_reviews": true },
|
||||
"restrictions": null,
|
||||
"required_linear_history": false,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false }
|
||||
JSON
|
||||
```
|
||||
|
||||
Lifts Scorecard Branch-Protection from 0. `enforce_admins:false` keeps the existing
|
||||
forward-merge flow workable; tighten to `true` once stable.
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: "Test Coverage Plan"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Test Coverage Plan
|
||||
|
||||
Last updated: 2026-06-28
|
||||
|
||||
> Status measured on 2026-05-13: lines 82.58%, statements 82.58%, functions 84.23%, branches 75.22%. Phases 1-5 are complete. Current focus is Phase 6 (>=85%) and Phase 7 (>=90%).
|
||||
|
||||
## Baseline
|
||||
|
||||
There are multiple coverage numbers depending on how the report is computed. For planning, only one of them is useful.
|
||||
|
||||
| Metric | Scope | Statements / Lines | Branches | Functions | Notes |
|
||||
| -------------------- | ----------------------------------------------------- | -----------------: | -------: | --------: | --------------------------------------------------- |
|
||||
| Legacy | Old `npm run test:coverage` | 79.42% | 75.15% | 67.94% | Inflated: counts test files and excludes `open-sse` |
|
||||
| Diagnostic | Source-only, excluding tests and excluding `open-sse` | 68.16% | 63.55% | 64.06% | Useful only to isolate `src/**` |
|
||||
| Recommended baseline | Source-only, excluding tests and including `open-sse` | 82.58% | 75.22% | 84.23% | This is the project-wide baseline to improve |
|
||||
|
||||
The recommended baseline is the number to optimize against.
|
||||
|
||||
## Rules
|
||||
|
||||
- Coverage targets apply to source files, not to `tests/**`.
|
||||
- `open-sse/**` is part of the product and must remain in scope.
|
||||
- New code should not reduce coverage in touched areas.
|
||||
- Prefer testing behavior and branch outcomes over implementation details.
|
||||
- Prefer temp SQLite databases and small fixtures over broad mocks for `src/lib/db/**`.
|
||||
|
||||
## Current command set
|
||||
|
||||
- `npm run test:coverage`
|
||||
- Main source coverage gate for the unit test suite
|
||||
- Generates `text-summary`, `html`, `json-summary`, and `lcov`
|
||||
- `npm run coverage:report`
|
||||
- Detailed file-by-file report from the latest run
|
||||
- `npm run test:coverage:legacy`
|
||||
- Historical comparison only
|
||||
|
||||
## Milestones
|
||||
|
||||
| Phase | Target | Focus | Status |
|
||||
| ------- | ---------------------: | ------------------------------------------------- | ----------- |
|
||||
| Phase 1 | 60% statements / lines | Quick wins and low-risk utility coverage | ✅ Done |
|
||||
| Phase 2 | 65% statements / lines | DB and route foundations | ✅ Done |
|
||||
| Phase 3 | 70% statements / lines | Provider validation and usage analytics | ✅ Done |
|
||||
| Phase 4 | 75% statements / lines | `open-sse` translators and helpers | ✅ Done |
|
||||
| Phase 5 | 80% statements / lines | `open-sse` handlers and executor branches | ✅ Done |
|
||||
| Phase 6 | 85% statements / lines | Harder edge cases, branch debt, regression suites | In progress |
|
||||
| Phase 7 | 90% statements / lines | Final sweep, gap closure, strict ratchet | Pending |
|
||||
|
||||
Branches and functions should ratchet upward with each phase, but the primary hard target is statements / lines.
|
||||
|
||||
## Priority hotspots
|
||||
|
||||
These files have the lowest line coverage today (< 60%) and offer the best return for Phases 6-7. Generated from `coverage/coverage-summary.json` on 2026-05-13:
|
||||
|
||||
| # | File | Lines % |
|
||||
| --- | ------------------------------------------------------------ | ------: |
|
||||
| 1 | `open-sse/services/compression/validation.ts` | 7.87% |
|
||||
| 2 | `src/app/api/v1/batches/route.ts` | 9.67% |
|
||||
| 3 | `src/app/docs/components/FeedbackWidget.tsx` | 9.80% |
|
||||
| 4 | `open-sse/services/compression/toolResultCompressor.ts` | 10.00% |
|
||||
| 5 | `src/app/docs/components/DocCodeBlocks.tsx` | 10.63% |
|
||||
| 6 | `open-sse/services/compression/engines/rtk/lineFilter.ts` | 10.96% |
|
||||
| 7 | `open-sse/services/specificityRules.ts` | 11.28% |
|
||||
| 8 | `src/mitm/systemCommands.ts` | 12.19% |
|
||||
| 9 | `open-sse/services/compression/aggressive.ts` | 12.77% |
|
||||
| 10 | `src/app/api/v1/batches/[id]/cancel/route.ts` | 12.98% |
|
||||
| 11 | `open-sse/services/compression/progressiveAging.ts` | 13.26% |
|
||||
| 12 | `open-sse/services/compression/engines/rtk/smartTruncate.ts` | 13.43% |
|
||||
| 13 | `open-sse/services/compression/engines/rtk/deduplicator.ts` | 13.51% |
|
||||
| 14 | `src/lib/cloudAgent/agents/jules.ts` | 13.52% |
|
||||
| 15 | `open-sse/services/compression/lite.ts` | 14.46% |
|
||||
| 16 | `src/app/api/v1/rerank/route.ts` | 14.94% |
|
||||
| 17 | `open-sse/services/compression/preservation.ts` | 15.07% |
|
||||
| 18 | `src/lib/cloudAgent/agents/codex.ts` | 15.54% |
|
||||
| 19 | `open-sse/services/tierResolver.ts` | 16.66% |
|
||||
| 20 | `src/app/docs/components/DocsLazyWrapper.tsx` | 16.66% |
|
||||
|
||||
Themes for Phases 6-7:
|
||||
|
||||
- `open-sse/services/compression/**` is the densest cluster of low coverage and dominates the remaining gap.
|
||||
- Batch and rerank API routes (`src/app/api/v1/batches/**`, `src/app/api/v1/rerank/route.ts`) need handler-level tests.
|
||||
- Cloud agent adapters (`src/lib/cloudAgent/agents/jules.ts`, `codex.ts`) and `tierResolver.ts` need scenario tests.
|
||||
- Docs UI components and `src/mitm/systemCommands.ts` are lower priority but cheap branch wins.
|
||||
|
||||
## Execution checklist
|
||||
|
||||
### Phase 1: 56.95% -> 60%
|
||||
|
||||
- [x] Fix coverage metric so it reflects source code instead of test files
|
||||
- [x] Keep a legacy coverage script for comparison
|
||||
- [x] Record the baseline and hotspots in-repo
|
||||
- [ ] Add focused tests for low-risk utilities:
|
||||
- `src/shared/utils/upstreamError.ts`
|
||||
- `src/shared/utils/fetchTimeout.ts`
|
||||
- `src/lib/api/errorResponse.ts`
|
||||
- `src/shared/utils/apiAuth.ts`
|
||||
- `src/lib/display/names.ts`
|
||||
- [ ] Add route tests for:
|
||||
- `src/app/api/settings/require-login/route.ts`
|
||||
- `src/app/api/providers/[id]/models/route.ts`
|
||||
|
||||
### Phase 2: 60% -> 65%
|
||||
|
||||
- [ ] Add DB-backed tests for:
|
||||
- `src/lib/db/modelComboMappings.ts`
|
||||
- `src/lib/db/settings.ts`
|
||||
- `src/lib/db/registeredKeys.ts`
|
||||
- [ ] Cover branch behavior in:
|
||||
- `src/lib/providers/validation.ts`
|
||||
- `src/app/api/v1/embeddings/route.ts`
|
||||
- `src/app/api/v1/moderations/route.ts`
|
||||
|
||||
### Phase 3: 65% -> 70%
|
||||
|
||||
- [ ] Add usage analytics tests for:
|
||||
- `src/lib/usage/usageHistory.ts`
|
||||
- `src/lib/usage/usageStats.ts`
|
||||
- `src/lib/usage/costCalculator.ts`
|
||||
- [ ] Expand route coverage for proxy management and settings branches
|
||||
|
||||
### Phase 4: 70% -> 75%
|
||||
|
||||
- [ ] Cover translator helpers and central translation paths:
|
||||
- `open-sse/translator/index.ts`
|
||||
- `open-sse/translator/helpers/*`
|
||||
- `open-sse/translator/request/*`
|
||||
- `open-sse/translator/response/*`
|
||||
|
||||
### Phase 5: 75% -> 80%
|
||||
|
||||
- [ ] Add handler-level tests for:
|
||||
- `open-sse/handlers/chatCore.ts`
|
||||
- `open-sse/handlers/responsesHandler.js`
|
||||
- `open-sse/handlers/imageGeneration.js`
|
||||
- `open-sse/handlers/embeddings.js`
|
||||
- [ ] Add executor branch coverage for provider-specific auth, retries, and endpoint overrides
|
||||
|
||||
### Phase 6: 80% -> 85%
|
||||
|
||||
- [ ] Merge more edge-case suites into the main coverage path
|
||||
- [ ] Increase function coverage for DB modules with weak constructor/helper coverage
|
||||
- [ ] Close branch gaps in `settings.ts`, `registeredKeys.ts`, `validation.ts`, and translator helpers
|
||||
|
||||
### Phase 7: 85% -> 90%
|
||||
|
||||
- [ ] Treat the remaining low-coverage files as blockers
|
||||
- [ ] Add regression tests for every uncovered production bug fixed during the push to 90%
|
||||
- [ ] Raise the coverage gate in CI only after the local baseline is stable for at least two consecutive runs
|
||||
|
||||
## Ratchet policy
|
||||
|
||||
Update `npm run test:coverage` thresholds only after the project actually exceeds the next milestone with a comfortable buffer.
|
||||
|
||||
**Current gate:** `npm run test:coverage` enforces **60 statements / 60 lines / 60 functions / 60 branches** (the metric was rebased in Quality-Gates Fase 6A.1 — the earlier 82.58% baseline was inflated because it counted test files and excluded `open-sse`). The `test:coverage:legacy` command preserves the old 50/50/50 metric for historical comparison.
|
||||
|
||||
For ad-hoc threshold checks against the latest report use:
|
||||
|
||||
```bash
|
||||
node scripts/check/test-report-summary.mjs --threshold 75
|
||||
```
|
||||
|
||||
Recommended ratchet sequence (order is `statements-lines / branches / functions`):
|
||||
|
||||
1. 55/60/55
|
||||
2. 60/62/58
|
||||
3. 65/64/62
|
||||
4. 70/66/66
|
||||
5. 75/70/72 <-- current gate (75/70/75)
|
||||
6. 80/75/78
|
||||
7. 85/80/84
|
||||
8. 90/85/88
|
||||
|
||||
Next ratchet target is `80/75/78` once branch coverage holds above 78% for two consecutive runs.
|
||||
|
||||
## Known gap
|
||||
|
||||
The current coverage command measures the main Node unit suite and includes source reached from it, including `open-sse`. It does not yet merge Vitest coverage into a single unified report. That merge is worth doing later, but it is not a blocker for starting the 60% -> 80% climb.
|
||||
@@ -0,0 +1,626 @@
|
||||
---
|
||||
title: "Database Schema & Operations Guide"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Database Schema & Operations Guide
|
||||
|
||||
> **TL;DR**: OmniRoute uses **SQLite with WAL journaling** as its primary store, with **AES-256-GCM** encryption at rest for sensitive fields. This guide covers the schema, migrations, backup/recovery, and operational runbooks.
|
||||
|
||||
**Sources:**
|
||||
|
||||
- `src/lib/db/core.ts` — singleton + SCHEMA_SQL (17 base tables)
|
||||
- `src/lib/db/migrationRunner.ts` — versioned migrations
|
||||
- `src/lib/db/migrations/` — 106 versioned SQL files
|
||||
- `src/lib/db/encryption.ts` — encryption helpers
|
||||
- `src/lib/db/backup.ts` — backup export/import
|
||||
- `src/lib/db/healthCheck.ts` — health diagnostics
|
||||
|
||||
---
|
||||
|
||||
## Why SQLite?
|
||||
|
||||
OmniRoute chose SQLite over PostgreSQL/MySQL for several reasons:
|
||||
|
||||
| Factor | SQLite | PostgreSQL |
|
||||
| --------------- | --------------------------------- | --------------------------------- |
|
||||
| **Deployment** | Embedded — no separate server | Requires server setup |
|
||||
| **Encryption** | Application-layer (AES-256-GCM) | Built-in TDE |
|
||||
| **Performance** | Faster for small/medium workloads | Better for huge concurrent writes |
|
||||
| **Concurrency** | WAL mode allows concurrent reads | Full MVCC |
|
||||
| **Backup** | Single-file copy | `pg_dump` or filesystem snapshot |
|
||||
| **Use case** | Per-user install, embedded | Multi-tenant SaaS |
|
||||
|
||||
For **single-user, single-instance** deployments (the primary OmniRoute use case), SQLite is simpler and faster.
|
||||
|
||||
### WAL Journaling
|
||||
|
||||
`core.ts` opens the database with **WAL (Write-Ahead Logging) mode**:
|
||||
|
||||
```ts
|
||||
// src/lib/db/core.ts
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("busy_timeout = 2000");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
// Settings > System & Storage > Cache Size is applied as KiB.
|
||||
db.pragma("cache_size = -16384");
|
||||
```
|
||||
|
||||
WAL allows **concurrent reads** during writes — important for the dashboard, which queries while requests are being recorded.
|
||||
|
||||
---
|
||||
|
||||
## Database Location
|
||||
|
||||
The SQLite file is stored at:
|
||||
|
||||
| OS | Path |
|
||||
| ------- | -------------------------------------------------------- |
|
||||
| Linux | `~/.omniroute/storage.sqlite` |
|
||||
| macOS | `~/.omniroute/storage.sqlite` |
|
||||
| Windows | `%USERPROFILE%\.omniroute\storage.sqlite` |
|
||||
| Docker | `/app/data/storage.sqlite` (configurable via `DATA_DIR`) |
|
||||
|
||||
Companion files:
|
||||
|
||||
- `storage.sqlite-wal` — write-ahead log
|
||||
- `storage.sqlite-shm` — shared memory file
|
||||
- `call_logs/` — request payload artifacts (if enabled)
|
||||
|
||||
**Override the location:**
|
||||
|
||||
```bash
|
||||
DATA_DIR=/custom/path omniroute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Domain Module Architecture
|
||||
|
||||
OmniRoute's database has **94 domain modules** in `src/lib/db/`. Each module:
|
||||
|
||||
- Owns one or more specific tables
|
||||
- Exports typed CRUD functions
|
||||
- Never touches another module's tables
|
||||
- Uses `getDbInstance()` from `core.ts` to access the DB
|
||||
|
||||
### The 94 DB Modules
|
||||
|
||||
OmniRoute has **94 module files** in `src/lib/db/`. Below is a sampling of core modules; see the directory listing for the complete list:
|
||||
|
||||
| Module | Tables | Responsibility |
|
||||
| ----------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `providers.ts` | `provider_connections` | OAuth/API key provider registration and credentials |
|
||||
| `models.ts` | `key_value` (model data) | Model definitions, capabilities, pricing |
|
||||
| `combos.ts` | `combos` | Combo routing configs and ordering |
|
||||
| `apiKeys.ts` | `api_keys` | API key lifecycle, scopes, quota tracking |
|
||||
| `settings.ts` | `key_value`, `api_keys`, `combos` | System configuration and shared KV store |
|
||||
| `backup.ts` | — | Backup export/import operations |
|
||||
| `proxies.ts` | `proxy_registry`, `proxy_assignments`, `provider_connections` | Proxy configs and routing rules |
|
||||
| `prompts.ts` | `prompt_templates` | Reusable prompt templates, versioning |
|
||||
| `webhooks.ts` | `webhooks` | Event-driven webhook subscriptions and logs |
|
||||
| `detailedLogs.ts` | `request_detail_logs` | Per-request audit logging (optional, high volume) |
|
||||
| `domainState.ts` | `domain_*` (5 tables) | Domain budgets, circuit breakers, lockouts, fallback chains, cost history |
|
||||
| `registeredKeys.ts` | `registered_keys`, `account_key_limits`, `provider_key_limits` | Whitelisted API keys for MCP/A2A |
|
||||
| `quotaSnapshots.ts` | `quota_snapshots` | Historical quota usage |
|
||||
| `modelComboMappings.ts` | `model_combo_mappings` | Map models to combo defaults |
|
||||
| `cliToolState.ts` | `cli_tool_state` | CLI-specific persistent state |
|
||||
| `encryption.ts` | — | Helpers for encrypting/decrypting fields |
|
||||
| `readCache.ts` | — | In-memory cache for read-heavy ops |
|
||||
| `secrets.ts` | `key_value` (encrypted entries) | Encrypted secret storage |
|
||||
| `stateReset.ts` | — | Wipe/reset DB state for testing |
|
||||
| `contextHandoffs.ts` | `context_handoffs` | Session context for agent handoff |
|
||||
| `usage*.ts` | `usage_history`, `call_logs`, `proxy_logs` | Usage tracking |
|
||||
| `compression*.ts` | `compression_settings`, `compression_combos` | Compression config |
|
||||
|
||||
### Module Boundaries
|
||||
|
||||
A core architectural rule: **modules don't access each other's tables directly**. To work with another module's data, import the function from that module.
|
||||
|
||||
```ts
|
||||
// ❌ WRONG: direct SQL from another module
|
||||
db.prepare("SELECT * FROM provider_connections").all();
|
||||
|
||||
// ✅ RIGHT: use the providers module function
|
||||
import { listProviders } from "@/lib/db/providers";
|
||||
const providers = await listProviders();
|
||||
```
|
||||
|
||||
This rule is enforced by code review — there's no static check, but violations are flagged.
|
||||
|
||||
---
|
||||
|
||||
## Base Schema (17 tables)
|
||||
|
||||
`core.ts` defines the 17 base tables in `SCHEMA_SQL`. These are created by migration `001_initial_schema.sql` and form the core schema.
|
||||
|
||||
### Core Tables (created in initial migration)
|
||||
|
||||
| Table | Purpose | Key columns |
|
||||
| -------------------------- | -------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `provider_connections` | Provider credentials (encrypted) | `id`, `provider`, `auth_type`, `api_key`, `is_active` |
|
||||
| `provider_nodes` | Provider node routing info | `id`, `type`, `name`, `base_url`, `created_at` |
|
||||
| `key_value` | General KV store | `namespace`, `key`, `value` |
|
||||
| `combos` | Routing combo definitions | `id`, `name`, `data`, `sort_order` |
|
||||
| `api_keys` | API keys for the gateway | `id`, `name`, `key`, `machine_id`, `allowed_models` |
|
||||
| `db_meta` | Database metadata | `key`, `value` |
|
||||
| `usage_history` | Request usage records | `id`, `provider`, `model`, `tokens_input`, `tokens_output`, `timestamp` |
|
||||
| `call_logs` | Request payloads & responses | `id`, `timestamp`, `status`, `model`, `provider`, `latency_ms` |
|
||||
| `proxy_logs` | Proxy request logs | `id`, `timestamp`, `proxy_type`, `status`, `provider` |
|
||||
| `domain_fallback_chains` | Model-to-provider chains | `model`, `chain` |
|
||||
| `domain_budgets` | Per-domain spend budgets | `api_key_id`, `daily_limit_usd`, `warning_threshold`, `reset_interval` |
|
||||
| `domain_budget_reset_logs` | Budget reset history | `id`, `api_key_id`, `reset_interval`, `previous_spend`, `reset_at` |
|
||||
| `domain_cost_history` | Per-domain cost tracking | `id`, `api_key_id`, `cost`, `timestamp` |
|
||||
| `domain_lockout_state` | Domain rate-limit state | `identifier`, `attempts`, `locked_until` |
|
||||
| `domain_circuit_breakers` | Circuit breaker state per domain | `name`, `state`, `failure_count`, `last_failure_time` |
|
||||
| `semantic_cache` | LLM response cache | `id`, `signature`, `model`, `prompt_hash`, `response` |
|
||||
| `quota_snapshots` | Historical quota snapshots | `id`, `provider`, `connection_id`, `window_key`, `remaining_percentage` |
|
||||
|
||||
### Additional Tables (added by later migrations)
|
||||
|
||||
Subsequent migrations add tables such as:
|
||||
|
||||
- `cli_tool_state` (migration 011) — CLI tool state
|
||||
- `mcp_*` tables — MCP server audit
|
||||
- `a2a_*` tables — A2A task state
|
||||
- `usage_*` tables — usage tracking
|
||||
- `plugin_*` tables — plugin system
|
||||
- `skill_executions` — skill execution history
|
||||
- `memory_*` tables — memory system
|
||||
- `compression_*` tables — compression system
|
||||
- `webhook_*` tables — webhook delivery log
|
||||
- `acp_*` tables — Agent Client Protocol
|
||||
- `oneproxy_*` tables — 1proxy marketplace
|
||||
- `proxy_assignments` — proxy scope bindings
|
||||
- `detailed_call_artifacts` — call log artifacts metadata
|
||||
- `quota_alert_history` — quota alert audit
|
||||
- `command_code_auth_sessions` — Command Code OAuth sessions
|
||||
|
||||
The full list of ~30+ tables is in `src/lib/db/migrations/`.
|
||||
|
||||
---
|
||||
|
||||
## Migrations
|
||||
|
||||
OmniRoute uses **versioned, idempotent migrations** in `src/lib/db/migrations/`. Each migration is a single SQL file named `NNN_description.sql`.
|
||||
|
||||
### Migration Naming
|
||||
|
||||
```
|
||||
001_initial_schema.sql
|
||||
002_mcp_a2a_tables.sql
|
||||
003_provider_node_custom_paths.sql
|
||||
...
|
||||
021_combo_call_log_targets.sql
|
||||
```
|
||||
|
||||
### How Migrations Run
|
||||
|
||||
At startup, `migrationRunner.ts`:
|
||||
|
||||
1. Creates `_omniroute_migrations` table if not exists
|
||||
2. Queries for already-applied migrations
|
||||
3. Applies any new migrations in order, each in a transaction
|
||||
4. Records each applied migration with timestamp
|
||||
|
||||
```ts
|
||||
// src/lib/db/migrationRunner.ts (simplified)
|
||||
export async function runMigrations(db: SqliteDatabase, migrationsDir: string) {
|
||||
const applied = getAppliedMigrations(db);
|
||||
const available = readMigrationFiles(migrationsDir);
|
||||
|
||||
for (const migration of available) {
|
||||
if (applied.includes(migration.id)) continue;
|
||||
db.transaction(() => {
|
||||
db.exec(migration.sql);
|
||||
recordAppliedMigration(db, migration.id);
|
||||
})();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Idempotency
|
||||
|
||||
Migrations must be **idempotent** — running them twice should be a no-op:
|
||||
|
||||
```sql
|
||||
-- 004_proxy_registry.sql
|
||||
CREATE TABLE IF NOT EXISTS proxy_registry (
|
||||
id TEXT PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
Use `IF NOT EXISTS`, `IF EXISTS`, and `OR IGNORE` / `OR REPLACE` clauses liberally.
|
||||
|
||||
### Adding a New Migration
|
||||
|
||||
1. **Identify the next number**: `ls src/lib/db/migrations/ | tail -1`
|
||||
2. **Create the file**: `NNN_my_change.sql`
|
||||
3. **Use safe DDL**: `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN`
|
||||
4. **Backfill data carefully**: use `UPDATE ... WHERE ...` to handle existing rows
|
||||
5. **Test on a copy**: never run untested migrations on production
|
||||
|
||||
Example:
|
||||
|
||||
```sql
|
||||
-- 022_add_combo_priority.sql
|
||||
ALTER TABLE combos ADD COLUMN priority INTEGER DEFAULT 100;
|
||||
UPDATE combos SET priority = 100 WHERE priority IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_combos_priority ON combos(priority);
|
||||
```
|
||||
|
||||
> **Backwards-incompatible changes** (e.g., dropping columns) are tricky. OmniRoute does NOT support downgrade — once a migration is applied, the schema change is permanent. Plan accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Encryption at Rest
|
||||
|
||||
Sensitive fields (API keys, OAuth tokens, connection strings) are encrypted at rest using **AES-256-GCM**.
|
||||
|
||||
### How It Works
|
||||
|
||||
```ts
|
||||
// src/lib/db/encryption.ts (simplified)
|
||||
const key = deriveKeyFromPassphrase(passphrase, salt);
|
||||
const iv = randomBytes(12);
|
||||
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
||||
const authTag = cipher.getAuthTag();
|
||||
return { encrypted, iv, authTag };
|
||||
```
|
||||
|
||||
### Where It's Used
|
||||
|
||||
- `provider_connections.api_key` — encrypted at application level
|
||||
- `provider_connections.access_token`, `refresh_token`, `id_token` — encrypted at application level
|
||||
- `key_value` entries with `namespace = "secrets"` — encrypted at application level
|
||||
- `proxy_registry.auth` — encrypted at application level (if present)
|
||||
|
||||
### Encryption Key
|
||||
|
||||
The encryption key is derived from a **passphrase** (set via `STORAGE_ENCRYPTION_KEY` env var) and a **salt** (stored in the DB). Both are required to decrypt data.
|
||||
|
||||
```bash
|
||||
# Generate a secure passphrase
|
||||
openssl rand -hex 32
|
||||
|
||||
# Set in .env
|
||||
STORAGE_ENCRYPTION_KEY=<your-key>
|
||||
```
|
||||
|
||||
> **Critical**: Losing the encryption key means losing access to all encrypted data. **Back up the key separately from the database**.
|
||||
|
||||
### What's NOT Encrypted
|
||||
|
||||
For performance reasons, the following are stored in plaintext:
|
||||
|
||||
- Provider display names
|
||||
- Model definitions (already public)
|
||||
- Routing rules
|
||||
- Usage records (no PII)
|
||||
|
||||
---
|
||||
|
||||
## Encryption Caveats (v3.8.16+)
|
||||
|
||||
OmniRoute uses **`migrateLegacyEncryptedString()`** to handle two encryption schemes transparently:
|
||||
|
||||
- **Legacy** (pre-v3.5.0): XOR-based "encryption" (not real crypto)
|
||||
- **Current**: AES-256-GCM with proper IV and auth tag
|
||||
|
||||
The migration helper detects the legacy format and re-encrypts with the new scheme on first read. This means you can upgrade an old database without losing credentials.
|
||||
|
||||
---
|
||||
|
||||
## Read Cache
|
||||
|
||||
For frequently-read data (models, providers, settings), `readCache.ts` provides an **in-memory cache**:
|
||||
|
||||
```ts
|
||||
// Cached at startup, invalidated on write
|
||||
const providers = await getCachedProviders(); // Fast, in-memory
|
||||
const fresh = await listProviders(); // Slow, hits DB
|
||||
```
|
||||
|
||||
| Cached entity | Cache key | TTL |
|
||||
| ---------------------- | -------------- | ----------- |
|
||||
| `models` | `models:v1` | Until write |
|
||||
| `provider_connections` | `providers:v1` | Until write |
|
||||
| `settings` | `settings:v1` | Until write |
|
||||
| `combos` | `combos:v1` | Until write |
|
||||
|
||||
Cache is invalidated on every write to the corresponding table.
|
||||
|
||||
---
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Manual Backup
|
||||
|
||||
```bash
|
||||
# Use the CLI to create a local backup
|
||||
omniroute backup create --name pre-migration
|
||||
|
||||
# Or via the API
|
||||
curl -X PUT http://localhost:20128/api/db-backups \
|
||||
-H "Authorization: Bearer $MANAGEMENT_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "pre-migration"}'
|
||||
```
|
||||
|
||||
The backup file includes:
|
||||
|
||||
- All DB tables (serialized to JSON)
|
||||
- Call log artifacts (base64-encoded, optional)
|
||||
- Settings + secrets (encrypted)
|
||||
- Plugin configuration
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
# Via CLI
|
||||
omniroute restore pre-migration
|
||||
|
||||
# Via API
|
||||
curl -X POST http://localhost:20128/api/db-backups/restore \
|
||||
-H "Authorization: Bearer $MANAGEMENT_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "pre-migration"}'
|
||||
```
|
||||
|
||||
> **Warning**: Restore overwrites the entire DB. Stop all clients first.
|
||||
|
||||
### Automated Backups
|
||||
|
||||
```bash
|
||||
# Enable automated daily backups via CLI
|
||||
omniroute backup auto enable --cron "0 2 * * *" --retention 7
|
||||
```
|
||||
|
||||
### SQLite Hot Backup
|
||||
|
||||
For zero-downtime backup of a live DB:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite ".backup /backups/omniroute-hot.db"
|
||||
```
|
||||
|
||||
This uses SQLite's online backup API — safe to run while OmniRoute is running.
|
||||
|
||||
---
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### WAL Mode
|
||||
|
||||
WAL is enabled by default. For high-write workloads, consider:
|
||||
|
||||
```sql
|
||||
PRAGMA wal_autocheckpoint = 1000; -- Checkpoint every 1000 pages
|
||||
PRAGMA journal_size_limit = 67108864; -- 64MB WAL cap
|
||||
```
|
||||
|
||||
### Indexes
|
||||
|
||||
Key indexes for performance (auto-created by migrations):
|
||||
|
||||
- `idx_models_provider` — model lookups by provider
|
||||
- `idx_combo_targets_combo_id` — combo target expansion
|
||||
- `idx_usage_history_api_key_timestamp` — usage analytics
|
||||
- `idx_quota_snapshots_api_key_window` — quota tracking
|
||||
- `idx_call_logs_timestamp` — call log queries
|
||||
|
||||
To add a new index, create a migration:
|
||||
|
||||
```sql
|
||||
-- 023_add_my_index.sql
|
||||
CREATE INDEX IF NOT EXISTS idx_my_table_my_column ON my_table(my_column);
|
||||
```
|
||||
|
||||
### Memory-Mapped I/O
|
||||
|
||||
For very large databases (>10GB), memory mapping can be adjusted via SQLite pragma:
|
||||
|
||||
```sql
|
||||
-- Set via SQLite pragma (adjust in core.ts or runtime)
|
||||
PRAGMA mmap_size = 268435456; -- 256MB
|
||||
```
|
||||
|
||||
### Compaction
|
||||
|
||||
Long-running OmniRoute instances benefit from occasional `VACUUM`:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite "VACUUM;"
|
||||
```
|
||||
|
||||
Run monthly during low-traffic windows. (WAL mode reduces the need, but doesn't eliminate it.)
|
||||
|
||||
---
|
||||
|
||||
## Health Check
|
||||
|
||||
`src/lib/db/healthCheck.ts` provides **DB-level health diagnostics**:
|
||||
|
||||
````bash
|
||||
GET /api/db/health
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"checks": {
|
||||
"writable": { "status": "pass" },
|
||||
"integrity": { "status": "pass", "result": "ok" },
|
||||
"foreign_keys": { "status": "pass", "violations": 0 },
|
||||
"orphaned_artifacts": { "status": "warn", "count": 12 },
|
||||
"table_sizes": {
|
||||
"usage_history": { "rows": 12345, "size_mb": 12.3 },
|
||||
"call_logs": { "rows": 567, "size_mb": 2.1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
Run `PRAGMA integrity_check` to detect corruption:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite "PRAGMA integrity_check;"
|
||||
# Should print: ok
|
||||
```
|
||||
|
||||
If it returns anything other than `ok`, **stop using the database immediately** and restore from backup.
|
||||
|
||||
---
|
||||
|
||||
## Disaster Recovery
|
||||
|
||||
### Scenario 1: WAL File Lost
|
||||
|
||||
The `-wal` file is missing but `-shm` and main DB are intact:
|
||||
|
||||
```bash
|
||||
# Recovers automatically on next open
|
||||
omniroute
|
||||
```
|
||||
|
||||
If SQLite can't auto-recover:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite ".recover" > recovered.sql
|
||||
sqlite3 recovered.db < recovered.sql
|
||||
mv recovered.db ~/.omniroute/storage.sqlite
|
||||
```
|
||||
|
||||
### Scenario 2: Main DB File Corrupted
|
||||
|
||||
Restore from backup:
|
||||
|
||||
```bash
|
||||
omniroute sync pull --merge # or: omniroute backup restore <backup-id>
|
||||
```
|
||||
|
||||
### Scenario 3: Encryption Key Lost
|
||||
|
||||
**No recovery possible** without the key. The encrypted fields are unreadable. Re-add all providers manually with new credentials.
|
||||
|
||||
> **Mitigation**: Always back up the encryption key separately, ideally in a password manager or KMS.
|
||||
|
||||
### Scenario 4: Disk Full
|
||||
|
||||
SQLite will return `SQLITE_FULL` errors. Free disk space, then:
|
||||
|
||||
```bash
|
||||
# Checkpoint WAL to free up space
|
||||
sqlite3 ~/.omniroute/storage.sqlite "PRAGMA wal_checkpoint(TRUNCATE);"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Inspect a Table
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite "SELECT * FROM api_keys LIMIT 5;"
|
||||
```
|
||||
|
||||
### Count Rows in All Tables
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite <<EOF
|
||||
SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
||||
EOF
|
||||
```
|
||||
|
||||
### Reset (Wipe) All Data
|
||||
|
||||
```bash
|
||||
# Stop OmniRoute first
|
||||
omniroute stop
|
||||
|
||||
# Delete the DB file
|
||||
rm ~/.omniroute/storage.sqlite*
|
||||
|
||||
# Restart (will recreate empty DB)
|
||||
omniroute
|
||||
```
|
||||
|
||||
For a **selective** reset (keep providers, wipe usage):
|
||||
|
||||
```bash
|
||||
DELETE FROM usage_history WHERE timestamp < datetime('now', '-30 day');
|
||||
DELETE FROM call_logs WHERE timestamp < datetime('now', '-30 day');
|
||||
DELETE FROM proxy_logs WHERE timestamp < datetime('now', '-30 day');
|
||||
```
|
||||
|
||||
### Export Single Table
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.omniroute/storage.sqlite <<EOF
|
||||
.mode csv
|
||||
.output api_keys.csv
|
||||
SELECT * FROM api_keys;
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Database is locked"
|
||||
|
||||
Another process is holding a write lock. Either:
|
||||
|
||||
- Wait for the other process to finish (check `lsof | grep storage.sqlite`)
|
||||
- Kill the other process
|
||||
- If persistent, restart OmniRoute
|
||||
|
||||
### "Foreign key constraint failed"
|
||||
|
||||
A domain module is violating referential integrity. Check:
|
||||
|
||||
- Orphaned rows in dependent tables
|
||||
- Cascading deletes that didn't propagate
|
||||
- Recent migration that changed a foreign key
|
||||
|
||||
Run `PRAGMA foreign_key_check;` to find violations.
|
||||
|
||||
### "Out of memory"
|
||||
|
||||
SQLite's memory-mapped I/O is exceeding the OS limit. Reduce via SQLite pragma:
|
||||
|
||||
```sql
|
||||
PRAGMA mmap_size = 134217728; -- 128MB instead of 256MB
|
||||
```
|
||||
|
||||
Or disable:
|
||||
|
||||
```sql
|
||||
PRAGMA mmap_size = 0;
|
||||
```
|
||||
|
||||
### "Migration failed mid-way"
|
||||
|
||||
The migration ran in a transaction, so it should have rolled back. If not:
|
||||
|
||||
1. **Stop OmniRoute** (prevent further attempts)
|
||||
2. **Check the DB state** with `sqlite3`
|
||||
3. **Manually fix** the partial migration
|
||||
4. **Re-run** OmniRoute (the migration will be retried)
|
||||
|
||||
To prevent this, always test migrations on a copy first.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [USAGE_QUOTA_GUIDE.md](../guides/USAGE_QUOTA_GUIDE.md) — usage tables
|
||||
- [MONITORING_GUIDE.md](./MONITORING_GUIDE.md) — health monitoring
|
||||
- [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md) — release flow
|
||||
- Source: `src/lib/db/` (80+ files, ~25K LOC)
|
||||
@@ -0,0 +1,498 @@
|
||||
---
|
||||
title: "OmniRoute Fly.io Deployment Guide"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute Fly.io Deployment Guide
|
||||
|
||||
This document describes the actual deployment process for OmniRoute on Fly.io, covering two scenarios:
|
||||
|
||||
- Deploying the current project to Fly.io for the first time
|
||||
- Publishing subsequent code updates
|
||||
- New projects following the same deployment workflow
|
||||
|
||||
This guide is based on a verified working configuration for the current project. The application name is `omniroute`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Deployment Goals
|
||||
|
||||
- Platform: Fly.io
|
||||
- Deployment method: Local `flyctl` direct publish
|
||||
- Runtime: Using the existing `Dockerfile` and `fly.toml` in the repository
|
||||
- Data persistence: Fly Volume mounted to `/data`
|
||||
- Access URL: `https://omniroute.fly.dev/`
|
||||
|
||||
---
|
||||
|
||||
## 2. Current Project Key Configuration
|
||||
|
||||
The `fly.toml` in the current repository has been confirmed to contain the following key items:
|
||||
|
||||
```toml
|
||||
app = 'omniroute'
|
||||
primary_region = 'sin'
|
||||
|
||||
[[mounts]]
|
||||
source = 'data'
|
||||
destination = '/data'
|
||||
|
||||
[processes]
|
||||
app = 'node run-standalone.mjs'
|
||||
|
||||
[http_service]
|
||||
internal_port = 20128
|
||||
|
||||
[env]
|
||||
TZ = "Asia/Shanghai"
|
||||
HOST = "0.0.0.0"
|
||||
HOSTNAME = "0.0.0.0"
|
||||
BIND = "0.0.0.0"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `app = 'omniroute'` determines which Fly application the deployment targets
|
||||
- `destination = '/data'` determines the persistent volume mount directory
|
||||
- This project must set `DATA_DIR=/data`, otherwise the database and keys will be written to the container's temporary directory
|
||||
|
||||
---
|
||||
|
||||
## 3. Prerequisites
|
||||
|
||||
### 3.1 Installing the Fly CLI
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"
|
||||
```
|
||||
|
||||
If the install script fails in your environment, you can also manually download the `flyctl` binary and add it to your `PATH`.
|
||||
|
||||
### 3.2 Logging in to Your Fly Account
|
||||
|
||||
```powershell
|
||||
flyctl auth login
|
||||
```
|
||||
|
||||
### 3.3 Verifying Login Status
|
||||
|
||||
```powershell
|
||||
flyctl auth whoami
|
||||
flyctl version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. First-Time Deployment of the Current Project
|
||||
|
||||
### 4.1 Clone the Code and Enter the Directory
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/diegosouzapw/OmniRoute.git
|
||||
cd OmniRoute
|
||||
```
|
||||
|
||||
### 4.2 Confirm the Application Name
|
||||
|
||||
Open `fly.toml` and verify the following line:
|
||||
|
||||
```toml
|
||||
app = 'omniroute'
|
||||
```
|
||||
|
||||
If you are deploying to your own new application, you can change it to a globally unique name, for example:
|
||||
|
||||
```toml
|
||||
app = 'omniroute-yourname'
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
- Make sure the application you see in the console matches the `app` value in `fly.toml`
|
||||
- If you previously used a different name, such as `oroute`, do not confuse it with `omniroute`
|
||||
|
||||
### 4.3 Create the Application
|
||||
|
||||
If the application does not yet exist:
|
||||
|
||||
```powershell
|
||||
flyctl apps create omniroute
|
||||
```
|
||||
|
||||
If you changed the application name, replace `omniroute` with your chosen name.
|
||||
|
||||
### 4.4 First Deploy
|
||||
|
||||
```powershell
|
||||
flyctl deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Required Parameters
|
||||
|
||||
This project recommends configuring at least the following parameters on Fly.io.
|
||||
|
||||
### 5.1 Verified Parameters
|
||||
|
||||
These parameters have been used in actual deployments on the current `omniroute` application:
|
||||
|
||||
- `API_KEY_SECRET`
|
||||
- `DATA_DIR`
|
||||
- `JWT_SECRET`
|
||||
- `MACHINE_ID_SALT`
|
||||
- `NEXT_PUBLIC_BASE_URL`
|
||||
- `OMNIROUTE_WS_BRIDGE_SECRET` (required in production — used for WebSocket bridge authentication)
|
||||
- `STORAGE_ENCRYPTION_KEY`
|
||||
|
||||
### 5.2 About `INITIAL_PASSWORD`
|
||||
|
||||
The current project does not set `INITIAL_PASSWORD` because this deployment does not require it.
|
||||
|
||||
If it is not set:
|
||||
|
||||
- The startup log will indicate the default password is `CHANGEME`
|
||||
- You should change the login password in system settings as soon as possible after deployment
|
||||
|
||||
If you want to initialize the backend password unattended, you can add it later:
|
||||
|
||||
- `INITIAL_PASSWORD`
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommended Parameters
|
||||
|
||||
### 6.1 Secrets Configuration
|
||||
|
||||
The following variables are recommended for Fly Secrets:
|
||||
|
||||
| Variable | Recommendation | Description |
|
||||
| ----------------------------- | ---------------------- | ----------------------------------------------------- |
|
||||
| `API_KEY_SECRET` | Required | Used for API Key generation and validation |
|
||||
| `JWT_SECRET` | Required | Used for login sessions and JWT signing |
|
||||
| `OMNIROUTE_WS_BRIDGE_SECRET` | Required in production | WebSocket bridge authentication secret |
|
||||
| `STORAGE_ENCRYPTION_KEY` | Strongly recommended | Encrypts sensitive connection information at rest |
|
||||
| `MACHINE_ID_SALT` | Recommended | Generates a stable machine identifier |
|
||||
| `INITIAL_PASSWORD` | Optional | Sets the initial backend password at first deployment |
|
||||
| OAuth/API private credentials | As needed | External platform authentication configuration |
|
||||
|
||||
### 6.2 Recommended Values for the Current Project
|
||||
|
||||
| Variable | Recommended Value |
|
||||
| ---------------------- | --------------------------- |
|
||||
| `DATA_DIR` | `/data` |
|
||||
| `NEXT_PUBLIC_BASE_URL` | `https://omniroute.fly.dev` |
|
||||
|
||||
Notes:
|
||||
|
||||
- `DATA_DIR=/data` is critical and must match the Fly Volume mount point
|
||||
- `NEXT_PUBLIC_BASE_URL` is used by the scheduler, frontend callbacks, and similar scenarios
|
||||
|
||||
### 6.3 OAuth Callback URL Configuration
|
||||
|
||||
If you need to enable OAuth-based providers (e.g. Antigravity, Gemini, Cursor) on the Fly.io deployment, make sure of the following two points:
|
||||
|
||||
1. **Set `NEXT_PUBLIC_BASE_URL` to your public HTTPS domain**
|
||||
|
||||
```powershell
|
||||
flyctl secrets set NEXT_PUBLIC_BASE_URL=https://omniroute.fly.dev -a omniroute
|
||||
```
|
||||
|
||||
If you are using a custom domain, replace it with the corresponding domain (e.g. `https://omniroute.yourdomain.com`).
|
||||
|
||||
2. **Configure the callback URL on the provider console**
|
||||
|
||||
All OAuth providers share the single callback path `/callback` — there is NO per-provider callback route:
|
||||
|
||||
```text
|
||||
<NEXT_PUBLIC_BASE_URL>/callback
|
||||
```
|
||||
|
||||
For example, regardless of Gemini, Antigravity, Cursor, or GitLab Duo:
|
||||
- `https://omniroute.fly.dev/callback`
|
||||
|
||||
If `NEXT_PUBLIC_BASE_URL` does not match the callback URL registered with the provider, the OAuth flow will fail at the browser redirect step.
|
||||
|
||||
---
|
||||
|
||||
## 7. One-Command Secret Setup
|
||||
|
||||
The following commands generate secure random values and write all required parameters for the current project to Fly Secrets in one step.
|
||||
|
||||
Notes:
|
||||
|
||||
- Does not include `INITIAL_PASSWORD`
|
||||
- Intended for the current project `omniroute`
|
||||
|
||||
```powershell
|
||||
$apiKeySecret = [Convert]::ToHexString((1..32 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })).ToLower()
|
||||
$jwtSecret = [Convert]::ToHexString((1..64 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })).ToLower()
|
||||
$machineIdSalt = [Convert]::ToHexString((1..32 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })).ToLower()
|
||||
$storageKey = [Convert]::ToHexString((1..32 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })).ToLower()
|
||||
$wsBridgeSecret = [Convert]::ToHexString((1..32 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })).ToLower()
|
||||
|
||||
flyctl secrets set `
|
||||
API_KEY_SECRET=$apiKeySecret `
|
||||
JWT_SECRET=$jwtSecret `
|
||||
MACHINE_ID_SALT=$machineIdSalt `
|
||||
STORAGE_ENCRYPTION_KEY=$storageKey `
|
||||
OMNIROUTE_WS_BRIDGE_SECRET=$wsBridgeSecret `
|
||||
DATA_DIR=/data `
|
||||
NEXT_PUBLIC_BASE_URL=https://omniroute.fly.dev `
|
||||
-a omniroute
|
||||
```
|
||||
|
||||
On Linux / macOS, you can also use `openssl rand -hex 32`:
|
||||
|
||||
```bash
|
||||
flyctl secrets set OMNIROUTE_WS_BRIDGE_SECRET=$(openssl rand -hex 32) -a omniroute
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `OMNIROUTE_WS_BRIDGE_SECRET` is required in production; missing it will break the WebSocket bridge handshake
|
||||
|
||||
If you also want to set an initial password:
|
||||
|
||||
```powershell
|
||||
flyctl secrets set INITIAL_PASSWORD=your-strong-password -a omniroute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Viewing Current Parameters
|
||||
|
||||
```powershell
|
||||
flyctl secrets list -a omniroute
|
||||
```
|
||||
|
||||
If the `Secrets` page in the console does not show the expected variables, check:
|
||||
|
||||
- That you are viewing the `omniroute` application
|
||||
- That the `app` value in `fly.toml` matches the application in the console
|
||||
|
||||
---
|
||||
|
||||
## 9. Subsequent Updates and Releases
|
||||
|
||||
After code updates, the release process is straightforward:
|
||||
|
||||
```powershell
|
||||
git pull
|
||||
flyctl deploy
|
||||
```
|
||||
|
||||
If you only need to update parameters without changing code:
|
||||
|
||||
```powershell
|
||||
flyctl secrets set KEY=value -a omniroute
|
||||
```
|
||||
|
||||
Fly will automatically perform a rolling update of machines.
|
||||
|
||||
### 9.1 Tracking Upstream Repository Updates While Preserving Your Fork's `fly.toml`
|
||||
|
||||
If the current repository is a fork and you want to sync updates from the upstream `https://github.com/diegosouzapw/OmniRoute`, follow the workflow below.
|
||||
|
||||
First, verify your remotes:
|
||||
|
||||
```powershell
|
||||
git remote -v
|
||||
```
|
||||
|
||||
You should see at least:
|
||||
|
||||
- `origin` pointing to your own fork
|
||||
- `upstream` pointing to the original repository
|
||||
|
||||
If `upstream` is not configured, add it:
|
||||
|
||||
```powershell
|
||||
git remote add upstream https://github.com/diegosouzapw/OmniRoute.git
|
||||
```
|
||||
|
||||
Before syncing with upstream, fetch the latest commits and tags:
|
||||
|
||||
```powershell
|
||||
git fetch upstream --tags
|
||||
```
|
||||
|
||||
Check the current version and upstream tags:
|
||||
|
||||
```powershell
|
||||
git describe --tags --always
|
||||
git show --no-patch --oneline v3.4.7
|
||||
```
|
||||
|
||||
> Note: The current project version is `v3.8.0`. The `v3.4.7` references below are kept as historical examples only. For actual releases, use `:latest` or the current version tag (e.g. `:v3.8.0`).
|
||||
|
||||
If you want to merge the latest upstream `main` while forcefully keeping your fork's `fly.toml`, follow this workflow:
|
||||
|
||||
```powershell
|
||||
git merge upstream/main
|
||||
git checkout HEAD~1 -- fly.toml
|
||||
git add -- fly.toml
|
||||
git commit -m "chore(deploy): keep fork fly.toml"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `git merge upstream/main` syncs the latest code from the original repository
|
||||
- `git checkout HEAD~1 -- fly.toml` restores your fork's own `fly.toml` from before the merge
|
||||
- If upstream did not modify `fly.toml`, this step will not introduce any differences
|
||||
- If upstream did modify `fly.toml`, this step ensures your Fly application name, volume mount, region, and other fork-specific deployment configuration are not overwritten
|
||||
|
||||
If you want to align with a specific release tag (e.g. `v3.4.7`), first verify that the tag is already included in `upstream/main`:
|
||||
|
||||
```powershell
|
||||
git merge-base --is-ancestor v3.4.7 upstream/main
|
||||
```
|
||||
|
||||
A successful return means `upstream/main` already contains that version; you can simply merge `upstream/main`.
|
||||
|
||||
### 9.2 Standard Release Sequence After Syncing Upstream
|
||||
|
||||
After syncing with the original repository, follow this recommended release order:
|
||||
|
||||
1. `git fetch upstream --tags`
|
||||
2. `git merge upstream/main`
|
||||
3. Restore the fork's `fly.toml`
|
||||
4. `git push origin main`
|
||||
5. `flyctl deploy`
|
||||
6. `flyctl status -a omniroute`
|
||||
7. `flyctl logs --no-tail -a omniroute`
|
||||
|
||||
This is the actual workflow used when upgrading the current project to `v3.4.7` (the example refers to a historical version; the current actual version is `v3.8.0`).
|
||||
|
||||
---
|
||||
|
||||
## 10. Post-Deployment Checks
|
||||
|
||||
### 10.1 Check Application Status
|
||||
|
||||
```powershell
|
||||
flyctl status -a omniroute
|
||||
```
|
||||
|
||||
### 10.2 View Startup Logs
|
||||
|
||||
```powershell
|
||||
flyctl logs --no-tail -a omniroute
|
||||
```
|
||||
|
||||
### 10.3 Verify Site Accessibility
|
||||
|
||||
```powershell
|
||||
try {
|
||||
(Invoke-WebRequest -Uri "https://omniroute.fly.dev" -MaximumRedirection 5 -UseBasicParsing).StatusCode
|
||||
} catch {
|
||||
if ($_.Exception.Response) {
|
||||
$_.Exception.Response.StatusCode.value__
|
||||
} else {
|
||||
throw
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A return value of `200` indicates the site is responding normally.
|
||||
|
||||
---
|
||||
|
||||
## 11. Success Indicators
|
||||
|
||||
After a successful deployment, the logs should show content similar to:
|
||||
|
||||
```text
|
||||
[bootstrap] Secrets persisted to: /data/server.env
|
||||
[DB] SQLite database ready: /data/storage.sqlite
|
||||
```
|
||||
|
||||
These two points are critical:
|
||||
|
||||
- `/data/server.env` confirms the runtime secrets are written to the persistent volume
|
||||
- `/data/storage.sqlite` confirms the database is written to the persistent volume
|
||||
|
||||
If you see `/app/data/...` instead, `DATA_DIR` is misconfigured and must be corrected immediately.
|
||||
|
||||
---
|
||||
|
||||
## 12. Common Issues
|
||||
|
||||
### 12.1 `Secrets` Page Is Empty
|
||||
|
||||
There are usually two reasons:
|
||||
|
||||
- You have not yet run `flyctl secrets set`
|
||||
- You are viewing a different application (e.g. `oroute` instead of `omniroute`)
|
||||
|
||||
### 12.2 `flyctl deploy` Reports `app not found`
|
||||
|
||||
Create the application first:
|
||||
|
||||
```powershell
|
||||
flyctl apps create omniroute
|
||||
```
|
||||
|
||||
### 12.3 `fly.toml` Parsing Fails
|
||||
|
||||
Check the following:
|
||||
|
||||
- Whether there are garbled characters in comments
|
||||
- Whether TOML quoting and indentation are correct
|
||||
|
||||
### 12.4 Data Is Not Persisting
|
||||
|
||||
Verify both of the following:
|
||||
|
||||
- `fly.toml` contains `destination = '/data'`
|
||||
- `DATA_DIR` is set to `/data`
|
||||
|
||||
### 12.5 Can It Run Without `INITIAL_PASSWORD`?
|
||||
|
||||
Yes, it can run. It will fall back to the default `CHANGEME` password. It is recommended to change the backend password as soon as possible in production.
|
||||
|
||||
---
|
||||
|
||||
## 13. Reusing for New Projects
|
||||
|
||||
If you are deploying a new project following this document, you only need to change these items:
|
||||
|
||||
1. Change the `app` value in `fly.toml`
|
||||
2. Change `NEXT_PUBLIC_BASE_URL`
|
||||
3. Keep `DATA_DIR=/data`
|
||||
4. Regenerate `API_KEY_SECRET`, `JWT_SECRET`, `MACHINE_ID_SALT`, and `STORAGE_ENCRYPTION_KEY`
|
||||
5. After the first deployment, verify that logs are written to `/data`
|
||||
|
||||
Do not reuse keys from a previous project.
|
||||
|
||||
---
|
||||
|
||||
## 14. Minimal Release Checklist for the Current Project
|
||||
|
||||
The most commonly used commands for subsequent releases are:
|
||||
|
||||
```powershell
|
||||
flyctl auth whoami
|
||||
flyctl status -a omniroute
|
||||
flyctl secrets list -a omniroute
|
||||
flyctl deploy
|
||||
flyctl logs --no-tail -a omniroute
|
||||
```
|
||||
|
||||
For a normal release, the core command is simply:
|
||||
|
||||
```powershell
|
||||
flyctl deploy
|
||||
```
|
||||
|
||||
For a first-time deployment in a new environment, the core steps are:
|
||||
|
||||
1. `flyctl auth login`
|
||||
2. `flyctl apps create omniroute`
|
||||
3. `flyctl secrets set ... -a omniroute`
|
||||
4. `flyctl deploy`
|
||||
5. `flyctl logs --no-tail -a omniroute`
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "Quality-Gate Maturity Re-evaluation (Fase 9)"
|
||||
---
|
||||
|
||||
# Maturity Re-evaluation — post-Waves 0–3 (Quality-Gate v2)
|
||||
|
||||
> **What this document is.** A re-measurement of the quality-gates system maturity
|
||||
> **after** Waves 0–3 of the Quality-Gate v2 program, compared to the baseline recorded in
|
||||
> [`QUALITY_GATE_PLAYBOOK.md`](./QUALITY_GATE_PLAYBOOK.md) (2026-06-16). Measures what changed,
|
||||
> against DSOMM L5 / OpenSSF Scorecard 9 / SLSA L3, separating what is **CI-measurable**
|
||||
> (already delivered / deliverable by code) from what is **process/owner** (organization settings).
|
||||
>
|
||||
> **Date:** 2026-06-30. Generated from the actual state of the repository, not from memory.
|
||||
> **Benchmarks:** OWASP DSOMM · OpenSSF Scorecard · SLSA · SonarQube "Clean as You Code".
|
||||
|
||||
---
|
||||
|
||||
## 1. Updated verdict
|
||||
|
||||
**Overall grade: A− → A ("Advanced", top ~5%).** The **two biggest structural weaknesses**
|
||||
of the 06-16 baseline — the _fast-gates gap_ and the _mutation-score-not-a-ratchet_ — have been **closed**.
|
||||
The residual gaps for "absolute maximum" are almost all **owner/infra-gated** (branch-protection,
|
||||
SLSA L3, CodeQL advanced); the code side of the program is essentially complete.
|
||||
|
||||
| Reference framework | Baseline 06-16 | Now 06-30 | Movement | Evidence |
|
||||
| --------------------------------- | ------------------------------ | ----------------------------------------------------------------- | -------- | --------------------------------------------------------------------- |
|
||||
| **OWASP DSOMM** (5 levels) | L3→L4 | **L4** in _Test Intensity_ and _Static Depth_; solid L3 in others | ▲ | blocking mutation-ratchet + deterministic suite at merge gate |
|
||||
| **OpenSSF Scorecard** | ~7–8/10 | ~7–8/10 (unchanged — gate is the **owner**) | = | missing Branch-Protection on `main` (owner setting) + actions pinning |
|
||||
| **SLSA** | L2→L3 | **L2** (approaching L3) | = | missing hermetic/reproducible builder (infra/owner) |
|
||||
| **SonarQube "Clean as You Code"** | Aligned with caveat | Aligned with caveat | = | _sprawl_ caveat (~46+ gates) persists — ROI review pending |
|
||||
| **Quality-Ratchet pattern** | Exemplar | **Exemplar+** | ▲ | new `dedicatedGate` for `mutationScore` (direction up) |
|
||||
| **Mutation testing** | "Almost there" (not a ratchet) | **Active ratchet** | ▲▲ | `check-mutation-ratchet.mjs` + seeded baseline + blocking nightly job |
|
||||
|
||||
---
|
||||
|
||||
## 2. Deltas since 2026-06-16 (what Waves 0–3 delivered)
|
||||
|
||||
### 2.1 🔴→✅ Fast-gates gap CLOSED (was structural weakness #1)
|
||||
|
||||
The baseline warned: `quality.yml` (PR→`release/**`) ran **only filesystem gates** — no
|
||||
typecheck, tests, or build —, so deterministic regressions only exploded on PR→`main`.
|
||||
**Today** `.github/workflows/quality.yml` runs, in the _Fast Quality Gates_ job: `typecheck:core`,
|
||||
**blocking impacted unit tests (TIA) with fail-safe to the full suite**, the
|
||||
vitest fast-path, and unit shards. The gate now runs **where the merge happens** (shift-left),
|
||||
exactly the cross-cutting principle the playbook prescribes.
|
||||
|
||||
### 2.2 🟠→✅ Mutation score became a RATCHET (was weakness #3 / P0 #1)
|
||||
|
||||
The strongest antidote against coverage-gaming was **advisory**. **Today**:
|
||||
|
||||
- `scripts/check/check-mutation-ratchet.mjs` (advisory by default, `--ratchet` blocking, graceful skip);
|
||||
- `config/quality/quality-baseline.json` has seeded `mutationScore.<module>` entries (`direction: up`, `dedicatedGate`);
|
||||
- `.github/workflows/nightly-mutation.yml` has the **"Mutation score ratchet (blocking)"** job that unifies batch reports and ratchets merged per-module scores.
|
||||
|
||||
Result: the per-module mutation score **cannot regress** — coverage has ceased to be a vanity metric.
|
||||
|
||||
### 2.3 ✅ Quick-win gates (Phase 6A/7) delivered
|
||||
|
||||
- **a11y axe-core "fake-green" fixed:** `@axe-core/playwright` in devDeps; `a11y.spec.ts` with conditional `REQUIRE_AXE` skip; job in `nightly-resilience.yml`.
|
||||
- **complexity scans `bin/`+`electron`:** `check-complexity.mjs` includes those directories in `ESLINT_ARGS`.
|
||||
- **tracked-artifacts in pre-commit + pre-push:** `.husky/pre-commit` + `pre-push` block accidentally tracked artifacts.
|
||||
|
||||
---
|
||||
|
||||
## 3. The 12 categories — status (delta-focused)
|
||||
|
||||
| # | Category | Status 06-30 |
|
||||
| --- | -------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| 1 | Style & formatting | ✅ unchanged (Prettier+ESLint lint-staged) |
|
||||
| 2 | Types | ✅ **reinforced** — `typecheck:core` now also in the PR→release gate |
|
||||
| 3 | Tests (intensity) | ✅ **reinforced** — mutation testing became a ratchet; deterministic suite at merge gate |
|
||||
| 4 | Test policy (anti-gaming) | ✅ unchanged (pr-test-policy/test-masking/pr-evidence) |
|
||||
| 5 | Complexity & health | ✅ **reinforced** — complexity scans bin/electron |
|
||||
| 6 | Static security (SAST+secrets) | 🟡 CodeQL default-setup (advanced = owner); semgrep cloud not versioned |
|
||||
| 7 | Supply-chain (deps) | ✅ unchanged (osv/audit/Trivy/Dependabot + allowlist) |
|
||||
| 8 | Supply-chain (build/release) | 🟡 SLSA L2 (L3 = hermetic builder, owner/infra) |
|
||||
| 9 | Contracts & API | 🟡 oasdiff/osv advisory (candidates for blocking-with-scope, P1) |
|
||||
| 10 | Docs & i18n (anti-rot) | ✅ **reinforced** — `fabricated-docs --strict` blocking (exit 0 verified) |
|
||||
| 11 | Anti-hallucination / consistency | ✅ unchanged (known-symbols/fetch-targets/docs-symbols/db-rules) |
|
||||
| 12 | Resilience & domain | ✅ unchanged (chaos/heap/k6/promptfoo/garak nightly) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Residual gaps for "absolute maximum"
|
||||
|
||||
### 4.1 CI-measurable / deliverable by code (this program's backlog)
|
||||
|
||||
- **P1 — osv/oasdiff → blocking with the right scope:** osv only `CRITICAL`+fixable (two-step like Trivy); oasdiff blocks contract-breaking changes.
|
||||
- **P1 — `require-tighten` blocking (end of cycle):** locks metric gains (prevents loosening the baseline without recording).
|
||||
- **P1/P2 — ROI review / gate sprawl:** consolidate doc-sync micro-gates; measure per-gate timing in `ci-summary` (combats fatigue — SonarQube/DORA caveat). Deferred ROI merges (unified complexity; unified `/api` anti-hallucination) fall here.
|
||||
- **P2 — CodeQL config committed + semgrep versioned:** more control/reproducibility.
|
||||
|
||||
### 4.2 Process / owner (CI cannot move — organization settings)
|
||||
|
||||
- **Branch-protection on `main`** (raises Scorecard, closes the DSOMM gap). See [`BRANCH_PROTECTION_MAIN.md`](./BRANCH_PROTECTION_MAIN.md).
|
||||
- **CodeQL Default → Advanced setup.**
|
||||
- **SLSA L3** — hermetic/reproducible builder (GitHub SLSA generator). Stretch (diminishing returns).
|
||||
|
||||
### 4.3 Explicitly out of scope
|
||||
|
||||
- **DSOMM L5** is largely **org-level / process** (not CI-encodable).
|
||||
- **SLSA L4** (bit-for-bit reproducibility) is a declared stretch goal.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deferred / removed items (tail housekeeping)
|
||||
|
||||
- **`semcheck.yaml` (LLM layer for semantic drift docs↔code) — REMOVED.** It was **orphaned**
|
||||
(no workflow/script invoked it) and had stale counts in the rules. Deterministic coverage
|
||||
already exists (`check:fabricated-docs --strict` + `check:docs-counts-sync` + `check:docs-symbols`),
|
||||
and the _gate sprawl_ caveat discourages adding an LLM advisory gate with recurring cost.
|
||||
It may be re-introduced in the future as an opt-in nightly job if semantic drift becomes a real problem.
|
||||
- **`agent-lsp` scaffold — DEFERRED / opt-in not enabled.** Exists as a mention in docs
|
||||
(`docs/architecture/QUALITY_GATES.md`, CHANGELOG) but **without wiring** and without `.mcp.json.example`
|
||||
in the repo. Remains as a documented opt-in scaffold; it is not an active gate nor a maturity gap.
|
||||
@@ -0,0 +1,476 @@
|
||||
---
|
||||
title: "Monitoring & Observability Guide"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Monitoring & Observability Guide
|
||||
|
||||
> **TL;DR**: OmniRoute ships with built-in health monitoring, provider autopilot, quota tracking, and observability hooks. This guide covers the dashboard, alerts, and troubleshooting.
|
||||
|
||||
**Sources:**
|
||||
|
||||
- `src/lib/monitoring/observability.ts` — observability snapshot
|
||||
- `src/lib/monitoring/comboHealthAutopilot.ts` — combo health autopilot
|
||||
- `src/lib/monitoring/providerHealthAutopilot.ts` — provider autopilot
|
||||
- `src/lib/monitoring/providerHealthMatrix.ts` — provider health matrix
|
||||
- `src/lib/localHealthCheck.ts` — local health check
|
||||
- `src/lib/tokenHealthCheck.ts` — token refresh health
|
||||
- `src/lib/proxyHealth.ts` — proxy health cache (covered in PROXY_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
OmniRoute has **3 layers of monitoring**:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1: System Health (server-level) │
|
||||
│ ├─ localHealthCheck.ts — DB, ports, native deps │
|
||||
│ ├─ db/healthCheck.ts — integrity, FK, orphaned artifacts │
|
||||
│ └─ Dashboard: /dashboard/health │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: Provider Health (per-provider resilience) │
|
||||
│ ├─ providerHealthAutopilot.ts — circuit breaker, cooldowns │
|
||||
│ ├─ providerHealthMatrix.ts — health scores by provider/model │
|
||||
│ └─ Dashboard: /dashboard/providers │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Layer 3: Live Observability (runtime snapshots) │
|
||||
│ ├─ observability.ts — circuit breakers, sessions, quota │
|
||||
│ ├─ tokenHealthCheck.ts — OAuth token refresh health │
|
||||
│ └─ MCP tools: omniroute_get_health, omniroute_get_session_snapshot │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Pages
|
||||
|
||||
### `/dashboard/health` (System Health)
|
||||
|
||||
The top-level health dashboard shows:
|
||||
|
||||
| Section | What it shows |
|
||||
| -------------------- | -------------------------------------------------- |
|
||||
| **Server status** | Uptime, version, port, active connections |
|
||||
| **Database** | Connection, integrity, WAL size, recent migrations |
|
||||
| **Provider summary** | Active count, healthy count, breaker open count |
|
||||
| **Quota monitors** | Active sessions, alerting, exhausted |
|
||||
| **Recent errors** | Last 10 errors with stack traces |
|
||||
| **Resource usage** | Memory, CPU, heap pressure indicator |
|
||||
|
||||
### `/dashboard/providers` (Provider Health)
|
||||
|
||||
Per-provider dashboard:
|
||||
|
||||
| Column | Description |
|
||||
| ----------- | ------------------------------------- |
|
||||
| Provider | Provider ID + display name |
|
||||
| Health | Green/yellow/red status |
|
||||
| Circuit | Open/closed/half-open state |
|
||||
| Connections | Count of connections, last refresh |
|
||||
| Models | Available models, health per model |
|
||||
| Cost | Today's cost, 7-day trend |
|
||||
| Errors | Last 24h error count, top error class |
|
||||
|
||||
Click a provider to see:
|
||||
|
||||
- Recent requests with latency breakdown
|
||||
- Per-connection health scores
|
||||
- Per-model lockouts
|
||||
- Autopilot recommendations
|
||||
|
||||
### `/dashboard/quota` (Quota Tracking)
|
||||
|
||||
For each API key:
|
||||
|
||||
- Current usage vs limit (progress bar)
|
||||
- Quota trend (30-day chart)
|
||||
- Next reset time
|
||||
- Alert history
|
||||
|
||||
### `/dashboard/combos` (Combo Health)
|
||||
|
||||
Per-combo:
|
||||
|
||||
- Strategy + targets
|
||||
- Health per target
|
||||
- Recent fallback events
|
||||
- Success rate (24h, 7d, 30d)
|
||||
|
||||
---
|
||||
|
||||
## Health Check API
|
||||
|
||||
> **Note:** Only `GET /api/monitoring/health` is exposed as a REST endpoint. All other monitoring data (provider health, autopilot issues, quota monitors, token health, latency) is accessed via the **MCP tool** `observability_snapshot` or the **dashboard** pages — there are no dedicated REST routes for these.
|
||||
|
||||
### System Health
|
||||
|
||||
```bash
|
||||
GET /api/monitoring/health
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"version": "3.8.16",
|
||||
"uptime": 123456,
|
||||
"checks": {
|
||||
"database": { "status": "pass", "latency_ms": 2 },
|
||||
"writeable": { "status": "pass" },
|
||||
"integrity": { "status": "pass", "result": "ok" },
|
||||
"foreign_keys": { "status": "pass", "violations": 0 },
|
||||
"heap_pressure": { "status": "pass", "usage_mb": 142, "threshold_mb": 512 },
|
||||
"active_sessions": 12,
|
||||
"providers": {
|
||||
"total": 7,
|
||||
"healthy": 6,
|
||||
"degraded": 1,
|
||||
"down": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Provider Health
|
||||
|
||||
> **No REST endpoint.** Provider health data is available via the MCP tool `observability_snapshot` or the dashboard `/dashboard/providers` page.
|
||||
|
||||
### Provider Detail
|
||||
|
||||
> **No REST endpoint.** Per-provider detail is available via the dashboard `/dashboard/providers` page.
|
||||
|
||||
---
|
||||
|
||||
## Provider Health Autopilot
|
||||
|
||||
The `providerHealthAutopilot.ts` module is a **self-healing system** that:
|
||||
|
||||
1. Detects provider issues (circuit open, cooldowns, lockouts, quota warnings)
|
||||
2. Generates **recommended actions** to resolve them
|
||||
3. Optionally **auto-executes** low-risk actions
|
||||
|
||||
### Issue Types Detected
|
||||
|
||||
| Issue kind | Severity | Example condition |
|
||||
| ---------------------------- | -------- | ------------------------------------- |
|
||||
| `provider_circuit_open` | critical | Circuit breaker open after 5 failures |
|
||||
| `provider_circuit_half_open` | warning | Circuit testing recovery |
|
||||
| `connection_cooldown` | warning | Connection in cooldown after 429 |
|
||||
| `stale_connection_error` | warning | Last refresh failed 30+ minutes ago |
|
||||
| `terminal_connection_error` | critical | OAuth revoked, key invalid |
|
||||
| `inactive_connection` | info | Connection disabled in settings |
|
||||
| `model_lockout` | warning | Specific model in quarantine |
|
||||
| `quota_monitor_warning` | warning | Quota at 80%+ usage |
|
||||
|
||||
### Action Types Generated
|
||||
|
||||
| Action | Risk | Description |
|
||||
| ------------------------------ | ------ | ----------------------------------- |
|
||||
| `clear_provider_breaker` | medium | Reset the circuit breaker to closed |
|
||||
| `clear_connection_cooldown` | low | Remove cooldown from a connection |
|
||||
| `clear_stale_connection_error` | low | Clear stale error flag |
|
||||
| `clear_model_lockout` | low | Re-enable a quarantined model |
|
||||
| `reactivate_connection` | medium | Re-enable a deactivated connection |
|
||||
| `deactivate_connection` | high | Disable a problematic connection |
|
||||
|
||||
### API
|
||||
|
||||
> **No REST endpoint.** Autopilot issues are available via the MCP tool `observability_snapshot` or the dashboard. The autopilot runs internally; its behavior is configured via the settings DB (per-connection `autopilotMode` field), not environment variables — `grep -rn` for an autopilot-mode env var returns zero hits.
|
||||
|
||||
### Autopilot Mode
|
||||
|
||||
The autopilot operates in **manual mode** by default — it detects issues and generates recommended actions, but does not auto-apply them. Actions can be applied via the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Combo Health Autopilot
|
||||
|
||||
`comboHealthAutopilot.ts` is the **combo-specific** equivalent of the provider autopilot. It:
|
||||
|
||||
- Detects unhealthy combos
|
||||
- Recommends target reordering
|
||||
- Suggests disabling broken targets
|
||||
- Auto-removes dead targets after N failures
|
||||
|
||||
### Combo Issue Examples
|
||||
|
||||
```
|
||||
Combo "always-on" (priority strategy)
|
||||
├─ Target 1: openai/gpt-5 (healthy)
|
||||
├─ Target 2: anthropic/claude-opus-4-6 (⚠️ model lockout until 14:00)
|
||||
└─ Target 3: kiro/claude-sonnet-4-5 (healthy)
|
||||
|
||||
Recommended action: Reorder — move kiro above anthropic until lockout expires
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quota Monitors
|
||||
|
||||
`observability.ts` exposes **per-session quota monitors** for subscription providers (Claude Code, Codex, GitHub Copilot):
|
||||
|
||||
```ts
|
||||
interface QuotaMonitorSnapshot {
|
||||
sessionId: string;
|
||||
provider: string;
|
||||
accountId: string;
|
||||
status: "starting" | "idle" | "healthy" | "warning" | "exhausted" | "error";
|
||||
lastQuotaPercent: number | null; // 0-100
|
||||
lastQuotaUsed: number | null;
|
||||
lastQuotaTotal: number | null;
|
||||
lastResetAt: string | null;
|
||||
nextPollAt: string | null;
|
||||
totalPolls: number;
|
||||
totalAlerts: number;
|
||||
consecutiveFailures: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Status Meanings
|
||||
|
||||
| Status | When | UI action |
|
||||
| ----------- | ------------------------ | --------------------------------- |
|
||||
| `starting` | Initial poll in progress | Spinner |
|
||||
| `idle` | No recent activity | Hidden from dashboard |
|
||||
| `healthy` | Quota > 50% remaining | Green dot |
|
||||
| `warning` | Quota < 50% remaining | Yellow alert |
|
||||
| `exhausted` | Quota = 0% | Red block, route to next provider |
|
||||
| `error` | Polling failed | Red dot, retry soon |
|
||||
|
||||
### API
|
||||
|
||||
> **No REST endpoint.** Quota monitor data is available via the MCP tool `observability_snapshot` or the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Observability Snapshot
|
||||
|
||||
The MCP tool `observability_snapshot` returns a **complete system snapshot** for AI agents:
|
||||
|
||||
```json
|
||||
{
|
||||
"circuitBreakers": [
|
||||
{
|
||||
"name": "openai",
|
||||
"state": "closed",
|
||||
"failureCount": 0,
|
||||
"lastFailureTime": null,
|
||||
"retryAfterMs": null
|
||||
}
|
||||
],
|
||||
"sessions": [
|
||||
{
|
||||
"sessionId": "sess-123",
|
||||
"createdAt": 1234567890,
|
||||
"lastActive": 1234567999,
|
||||
"requestCount": 42,
|
||||
"connectionId": "conn-456",
|
||||
"ageMs": 109
|
||||
}
|
||||
],
|
||||
"quotaMonitors": {
|
||||
/* see above */
|
||||
},
|
||||
"uptime": 12345,
|
||||
"version": "3.8.16"
|
||||
}
|
||||
```
|
||||
|
||||
Agents use this to make **routing decisions** — for example, "if openai's circuit is open, route to anthropic first".
|
||||
|
||||
---
|
||||
|
||||
## Token Health Check
|
||||
|
||||
OAuth providers (Claude Code, GitHub Copilot, Cursor) need **periodic token refresh**. `src/lib/tokenHealthCheck.ts` runs a background scheduler:
|
||||
|
||||
- **Sweep tick**: every 60 seconds (sweep in `TICK_MS = 60 * 1000` at `src/lib/tokenHealthCheck.ts:30`)
|
||||
- **Per-connection health check interval**: default 60 minutes (`DEFAULT_HEALTH_CHECK_INTERVAL_MIN = 60`); configurable via the settings DB
|
||||
- **Pre-emptive refresh on 401**: handled by the per-connection interceptor
|
||||
|
||||
### Token Health Status
|
||||
|
||||
```ts
|
||||
interface TokenHealth {
|
||||
connectionId: string;
|
||||
provider: string;
|
||||
status: "valid" | "expiring_soon" | "expired" | "refresh_failed";
|
||||
expiresAt: string;
|
||||
lastRefresh: string;
|
||||
nextRefresh: string;
|
||||
consecutiveFailures: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Token health check configuration is handled internally by `tokenHealthCheck.ts`.
|
||||
|
||||
### Token Health
|
||||
|
||||
> **No REST endpoint.** Token health data is available via the dashboard or the MCP tool `observability_snapshot`.
|
||||
|
||||
---
|
||||
|
||||
## Alerting
|
||||
|
||||
### Built-in Channels
|
||||
|
||||
OmniRoute supports **3 alert channels**:
|
||||
|
||||
| Channel | Setup | Use case |
|
||||
| ---------------- | ------------- | ---------------------------- |
|
||||
| Dashboard banner | Always on | In-app notifications |
|
||||
| Webhook | Configure URL | Slack, Discord, PagerDuty |
|
||||
| Log | Default | For external log aggregation |
|
||||
|
||||
### Webhook Configuration
|
||||
|
||||
> **Note:** Webhook alerting configuration is handled via the dashboard Settings page. See the Settings UI for webhook URL, event filtering, and payload customization.
|
||||
|
||||
### Alert Types
|
||||
|
||||
| Alert | When | Default severity |
|
||||
| ---------------------------- | -------------------------------- | ---------------- |
|
||||
| `provider_circuit_open` | Circuit opens | critical |
|
||||
| `provider_circuit_half_open` | Circuit testing recovery | info |
|
||||
| `quota_warning` | Quota at 80%+ | warning |
|
||||
| `quota_exhausted` | Quota at 100% | critical |
|
||||
| `token_refresh_failed` | 3+ consecutive refresh failures | warning |
|
||||
| `token_expired` | Token past expiry | critical |
|
||||
| `combo_target_unhealthy` | Combo target in cooldown for 1h+ | warning |
|
||||
| `db_integrity_warning` | FK violations > 0 | warning |
|
||||
| `heap_pressure` | Heap usage > 80% of threshold | warning |
|
||||
|
||||
---
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
### Tracked Metrics
|
||||
|
||||
| Metric | Type | Source |
|
||||
| ----------------------- | --------- | ------------------------------- |
|
||||
| `request_count` | counter | `services/usage.ts` |
|
||||
| `request_latency_ms` | histogram | `services/usage.ts` |
|
||||
| `tokens_consumed` | counter | `services/usage.ts` |
|
||||
| `cost_usd` | counter | `services/usage.ts` |
|
||||
| `provider_errors` | counter | `services/errorClassifier.ts` |
|
||||
| `circuit_state_changes` | counter | `services/resilience.ts` |
|
||||
| `cache_hits` | counter | `services/signatureCache.ts` |
|
||||
| `compression_savings` | histogram | `services/compression/stats.ts` |
|
||||
| `quota_used` | gauge | `services/quotaMonitor.ts` |
|
||||
| `memory_used_mb` | gauge | `observability.ts` |
|
||||
|
||||
### Latency Percentiles (p50/p95/p99)
|
||||
|
||||
> **No REST endpoint.** Latency percentile data is available via the dashboard `/dashboard/health` page. Prometheus/OpenTelemetry export is planned for v3.9.
|
||||
|
||||
### Prometheus / OpenTelemetry Export (Phase 2)
|
||||
|
||||
Planned for v3.9: native export to Prometheus, OpenTelemetry, Datadog.
|
||||
|
||||
For now, scrape `/api/monitoring/health` with any HTTP-based monitoring system (Prometheus blackbox exporter, Datadog HTTP check, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Alerting Recipes
|
||||
|
||||
### Slack
|
||||
|
||||
> **Note:** Webhook alerting is configured through the dashboard Settings page — there are no dedicated webhook env vars (`grep -rn` returns zero hits). See the Settings UI for webhook URL, event filtering, and payload customization.
|
||||
|
||||
### Discord
|
||||
|
||||
> Webhook alerting uses the same Settings UI flow as Slack. Discord accepts the same JSON payload shape.
|
||||
|
||||
### PagerDuty
|
||||
|
||||
> Webhook alerting uses the same Settings UI flow. PagerDuty Events API v2 routing keys are configured in the Settings UI.
|
||||
|
||||
### Custom Webhook (JSON)
|
||||
|
||||
> Any HTTP endpoint that accepts POST with JSON body will work. Configure the URL in the Settings UI.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Configuration
|
||||
|
||||
### Customize the Health Dashboard
|
||||
|
||||
Create a `~/.omniroute/dashboard.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"health": {
|
||||
"sections": ["server_status", "database", "providers", "quota_monitors", "recent_errors"],
|
||||
"refresh_interval_ms": 5000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pin a Provider to the Top
|
||||
|
||||
```json
|
||||
{
|
||||
"health": {
|
||||
"pinned_providers": ["openai", "anthropic"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Provider says healthy but requests fail"
|
||||
|
||||
1. Check the **autopilot issues** — maybe a model is locked out
|
||||
2. Look at **recent errors** for the specific error class
|
||||
3. Try the **connection test** in the provider card
|
||||
4. Check if the provider is **rate-limited at upstream** (not visible locally)
|
||||
|
||||
### "Quota says healthy but I see 429s"
|
||||
|
||||
- 429 means the provider says you've used your quota
|
||||
- OmniRoute's quota tracking may be **stale** — the provider's truth is upstream
|
||||
- Quota data refreshes automatically via the internal quota monitor
|
||||
|
||||
### "Combo is failing but all targets look healthy"
|
||||
|
||||
- Check **combo health** dashboard for target ordering issues
|
||||
- Look at **fallback events** — maybe the combo is exhausting too quickly
|
||||
- Verify the **strategy** matches your use case (priority vs round-robin vs auto)
|
||||
|
||||
### "Database health check is failing"
|
||||
|
||||
- Run `sqlite3 ~/.omniroute/storage.sqlite "PRAGMA integrity_check;"`
|
||||
- If "ok" — false alarm, the health check is being too strict
|
||||
- If anything else — **stop OmniRoute** and follow the [disaster recovery guide](./DATABASE_GUIDE.md#disaster-recovery)
|
||||
|
||||
### "Memory heap pressure is critical"
|
||||
|
||||
```bash
|
||||
# Check current heap
|
||||
node -e "console.log(process.memoryUsage())"
|
||||
|
||||
# Trigger manual GC (if --expose-gc)
|
||||
node --expose-gc -e "global.gc(); console.log(process.memoryUsage())"
|
||||
|
||||
# Reduce concurrent requests (set via the dashboard Settings page, not an env var)
|
||||
# There is no `MAX_CONCURRENT_REQUESTS` env var — configure it in Settings → Concurrency.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [USAGE_QUOTA_GUIDE.md](../guides/USAGE_QUOTA_GUIDE.md) — usage & cost tracking
|
||||
- [DATABASE_GUIDE.md](./DATABASE_GUIDE.md) — DB schema + health
|
||||
- [PROXY_GUIDE.md](./PROXY_GUIDE.md) — proxy health (separate cache)
|
||||
- [ARCHITECTURE.md](../architecture/ARCHITECTURE.md) — system architecture
|
||||
- [RESILIENCE_GUIDE.md](../architecture/RESILIENCE_GUIDE.md) — circuit breaker details
|
||||
- Source: `src/lib/monitoring/` (4 files, 2121 LOC)
|
||||
@@ -0,0 +1,824 @@
|
||||
---
|
||||
title: "🌐 OmniRoute Proxy Guide"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# 🌐 OmniRoute Proxy Guide
|
||||
|
||||
> **Bypass geographic blocks, protect your identity, and route AI traffic through any proxy — with zero configuration complexity.**
|
||||
|
||||
OmniRoute includes a full-featured proxy management system that lets you route upstream AI provider traffic through HTTP, HTTPS, or SOCKS5 proxies. Whether you're in a blocked region, need IP rotation, or want stealth fingerprinting — this guide covers everything.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Why Use Proxies?](#why-use-proxies)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [4-Level Proxy System](#4-level-proxy-system)
|
||||
- [Proxy Registry (CRUD)](#proxy-registry-crud)
|
||||
- [1proxy Free Marketplace](#1proxy-free-proxy-marketplace)
|
||||
- [Proxy Rotation](#proxy-rotation)
|
||||
- [Anti-Detection & Stealth](#anti-detection--stealth)
|
||||
- [Upstream Proxy Modes](#upstream-proxy-modes)
|
||||
- [Dashboard UI](#dashboard-ui)
|
||||
- [API Reference](#api-reference)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Why Use Proxies?
|
||||
|
||||
Many AI providers restrict access by geographic region. Developers in **Russia, China, Iran, Cuba, Turkey**, and other countries encounter errors like:
|
||||
|
||||
```
|
||||
unsupported_country_region_territory
|
||||
```
|
||||
|
||||
Even outside blocked regions, proxies are useful for:
|
||||
|
||||
| Use Case | Description |
|
||||
| --------------------- | --------------------------------------------------------------- |
|
||||
| **Geographic bypass** | Access OpenAI, Anthropic, Codex, Copilot from blocked countries |
|
||||
| **IP rotation** | Distribute requests across multiple IPs to avoid rate limiting |
|
||||
| **Privacy** | Hide your real IP from upstream providers |
|
||||
| **Compliance** | Route traffic through specific jurisdictions |
|
||||
| **Testing** | Simulate requests from different regions |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ OmniRoute Server │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ Proxy │ │ Proxy │ │ Proxy │ │
|
||||
│ │ Registry │───▶│ Dispatcher │───▶│ Fetch (undici) │ │
|
||||
│ │ (SQLite) │ │ (cached) │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ └────────┬─────────┘ │
|
||||
│ ▲ │ │
|
||||
│ │ ▼ │
|
||||
│ ┌──────┴──────┐ ┌──────────────────┐ │
|
||||
│ │ 1proxy Sync │ │ Upstream │ │
|
||||
│ │ (free pool) │ │ Provider API │ │
|
||||
│ └─────────────┘ └──────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
| Component | File | Role |
|
||||
| -------------------- | -------------------------------------------- | ---------------------------------------------------------- |
|
||||
| **Proxy Registry** | `src/lib/db/proxies.ts` | CRUD for proxy entries + scope assignments |
|
||||
| **Proxy Dispatcher** | `open-sse/utils/proxyDispatcher.ts` | Creates `undici` ProxyAgent/SOCKS dispatchers with caching |
|
||||
| **Proxy Fetch** | `open-sse/utils/proxyFetch.ts` | Wraps `fetch()` with proxy dispatcher injection |
|
||||
| **Settings Route** | `src/app/api/settings/proxy/route.ts` | Legacy proxy config API (GET/PUT/DELETE) |
|
||||
| **Management Route** | `src/app/api/v1/management/proxies/route.ts` | Registry CRUD API (GET/POST/PATCH/DELETE) |
|
||||
| **1proxy DB** | `src/lib/db/oneproxy.ts` | Free proxy marketplace persistence |
|
||||
| **1proxy Sync** | `src/lib/oneproxySync.ts` | Fetches proxies from 1proxy API |
|
||||
| **1proxy Rotator** | `src/lib/oneproxyRotator.ts` | Rotation strategies (quality/random/sequential) |
|
||||
|
||||
---
|
||||
|
||||
## 4-Level Proxy System
|
||||
|
||||
OmniRoute supports proxy configuration at **four independent scopes**, resolved in priority order:
|
||||
|
||||
```
|
||||
Priority Resolution Order (highest → lowest):
|
||||
|
||||
1. 🔵 Account/Connection Proxy → per API key / OAuth connection
|
||||
2. 🟡 Provider Proxy → per provider (e.g., all OpenAI traffic)
|
||||
3. 🟠 Combo Proxy → per combo/routing configuration
|
||||
4. 🟢 Global Proxy → all traffic, all providers
|
||||
```
|
||||
|
||||
### How Resolution Works
|
||||
|
||||
When OmniRoute sends a request to an upstream provider, it calls `resolveProxyForConnectionFromRegistry()` which checks each level in order:
|
||||
|
||||
1. **Account-level** — Is there a proxy assigned to this specific connection ID?
|
||||
2. **Provider-level** — Is there a proxy assigned to this provider (e.g., `openai`)?
|
||||
3. **Global-level** — Is there a global proxy configured?
|
||||
4. **No proxy** — Direct connection to the provider.
|
||||
|
||||
The first match wins. This means you can set a global proxy as a fallback but override it for specific providers or connections.
|
||||
|
||||
### What Gets Proxied
|
||||
|
||||
| Traffic Type | Proxied? | Notes |
|
||||
| -------------------- | -------- | --------------------------------------------- |
|
||||
| Chat completions | ✅ | All `/v1/chat/completions` requests |
|
||||
| Embeddings | ✅ | `/v1/embeddings` |
|
||||
| Image generation | ✅ | `/v1/images/generations` |
|
||||
| Audio (TTS/STT) | ✅ | `/v1/audio/*` |
|
||||
| OAuth token exchange | ✅ | Solves `unsupported_country_region_territory` |
|
||||
| Connection tests | ✅ | "Test Connection" button uses proxy |
|
||||
| Token refresh | ✅ | Background OAuth renewal |
|
||||
| Model sync | ✅ | Model listing and discovery |
|
||||
|
||||
---
|
||||
|
||||
## Proxy Registry (CRUD)
|
||||
|
||||
The proxy registry is a SQLite table (`proxy_registry`) that stores all your proxies. Each proxy has:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------- | ------- | ----------------------------------- |
|
||||
| `id` | UUID | Unique identifier |
|
||||
| `name` | String | Human-readable label |
|
||||
| `type` | String | Protocol: `http`, `https`, `socks5` |
|
||||
| `host` | String | Proxy hostname or IP |
|
||||
| `port` | Integer | Port number |
|
||||
| `username` | String | Auth username (encrypted at rest) |
|
||||
| `password` | String | Auth password (encrypted at rest) |
|
||||
| `region` | String | Geographic region label |
|
||||
| `notes` | String | Free-text notes |
|
||||
| `status` | String | `active` or `inactive` |
|
||||
| `source` | String | `manual` or `oneproxy` |
|
||||
|
||||
### Creating a Proxy
|
||||
|
||||
**Via Dashboard:**
|
||||
|
||||
1. Go to **Settings → Proxy**
|
||||
2. Click **Add Proxy**
|
||||
3. Fill in the type, host, port, and optional auth credentials
|
||||
4. Save
|
||||
|
||||
**Via API:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:20128/api/v1/management/proxies \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "US Proxy",
|
||||
"type": "http",
|
||||
"host": "proxy.example.com",
|
||||
"port": 8080,
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"region": "US"
|
||||
}'
|
||||
```
|
||||
|
||||
### Updating a Proxy
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://localhost:20128/api/v1/management/proxies \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": "proxy-uuid-here",
|
||||
"host": "new-proxy.example.com",
|
||||
"port": 9090
|
||||
}'
|
||||
```
|
||||
|
||||
> **Note:** Credentials are preserved unless you explicitly send non-empty replacements. Sending empty strings for `username`/`password` will keep the stored values.
|
||||
|
||||
### Deleting a Proxy
|
||||
|
||||
```bash
|
||||
# Fails if proxy is assigned to any scope
|
||||
curl -X DELETE "http://localhost:20128/api/v1/management/proxies?id=proxy-uuid"
|
||||
|
||||
# Force delete (removes assignments too)
|
||||
curl -X DELETE "http://localhost:20128/api/v1/management/proxies?id=proxy-uuid&force=1"
|
||||
```
|
||||
|
||||
### Listing Proxies
|
||||
|
||||
```bash
|
||||
curl "http://localhost:20128/api/v1/management/proxies?limit=50&offset=0"
|
||||
```
|
||||
|
||||
### Assigning Proxies to Scopes
|
||||
|
||||
```bash
|
||||
# Assign to global scope
|
||||
curl -X PUT http://localhost:20128/api/settings/proxy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"level": "global", "proxy": {"type":"http","host":"proxy.example.com","port":8080}}'
|
||||
|
||||
# Assign to a specific provider
|
||||
curl -X PUT http://localhost:20128/api/settings/proxy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"level": "provider", "id": "openai", "proxy": {"type":"socks5","host":"socks.example.com","port":1080}}'
|
||||
|
||||
# Assign to a specific connection/key
|
||||
curl -X PUT http://localhost:20128/api/settings/proxy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"level": "key", "id": "connection-uuid", "proxy": {"type":"http","host":"key-proxy.com","port":3128}}'
|
||||
```
|
||||
|
||||
### Resolving Effective Proxy
|
||||
|
||||
Check which proxy would be used for a given connection:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:20128/api/settings/proxy?resolve=connection-uuid"
|
||||
```
|
||||
|
||||
Returns the resolved proxy with its level (`account`, `provider`, or `global`) and source.
|
||||
|
||||
### Bulk Assignment
|
||||
|
||||
Assign one proxy to multiple providers or connections at once:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:20128/api/v1/management/proxies/bulk-assign \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"scope": "provider",
|
||||
"scopeIds": ["openai", "anthropic", "codex"],
|
||||
"proxyId": "proxy-uuid"
|
||||
}'
|
||||
```
|
||||
|
||||
### Import/Export
|
||||
|
||||
Proxies are included in the **Backup/Restore** system. When you export your OmniRoute configuration:
|
||||
|
||||
1. Go to **Dashboard → Settings → Backup**
|
||||
2. Click **Export** — proxy registry and assignments are included
|
||||
3. To restore, click **Import** and upload the backup file
|
||||
|
||||
The proxy registry also supports **upsert by host+port** — if you import a proxy that already exists (same host and port), it updates instead of creating a duplicate.
|
||||
|
||||
### Legacy Migration
|
||||
|
||||
If you configured proxies in an older version (pre-registry), OmniRoute automatically migrates them:
|
||||
|
||||
```
|
||||
Legacy key_value store → proxy_registry + proxy_assignments
|
||||
```
|
||||
|
||||
This happens once on first startup after upgrade. Use `migrateLegacyProxyConfigToRegistry({ force: true })` to re-run.
|
||||
|
||||
---
|
||||
|
||||
## 1proxy Free Proxy Marketplace
|
||||
|
||||
> 🆕 **Contributed by [@oyi77](https://github.com/oyi77)** — PR [#1847](https://github.com/diegosouzapw/OmniRoute/pull/1847) (Issue [#1788](https://github.com/diegosouzapw/OmniRoute/issues/1788))
|
||||
|
||||
OmniRoute integrates with the **[1proxy](https://1proxy-api.aitradepulse.com)** community platform to provide access to **hundreds of free, validated proxies** from around the world. This is perfect for users who don't have their own proxy infrastructure.
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌─────────────┐ Sync ┌─────────────────┐ Rotate ┌──────────┐
|
||||
│ 1proxy API │ ────────────▶ │ proxy_registry │ ────────────▶ │ Provider │
|
||||
│ (external) │ up to 500 │ source=oneproxy │ by quality │ API │
|
||||
└─────────────┘ proxies └─────────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
1. **Sync** — OmniRoute fetches validated proxies from the 1proxy API
|
||||
2. **Store** — Proxies are saved in the same `proxy_registry` table with `source = 'oneproxy'`
|
||||
3. **Filter** — Filter by protocol, country, quality score
|
||||
4. **Rotate** — Pick the best proxy using quality, random, or sequential strategies
|
||||
5. **Auto-degrade** — Failed proxies get their quality score reduced; below threshold → marked inactive
|
||||
|
||||
### Syncing Proxies
|
||||
|
||||
**Via Dashboard:**
|
||||
|
||||
1. Go to **Settings → 1proxy** tab
|
||||
2. Click **"Sync Now"**
|
||||
3. View stats: total proxies, active count, average quality, by-country breakdown
|
||||
|
||||
**Via API:**
|
||||
|
||||
```bash
|
||||
# Trigger sync
|
||||
curl -X POST http://localhost:20128/api/settings/oneproxy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
|
||||
# Response:
|
||||
# { "success": true, "added": 127, "updated": 45, "failed": 2, "total": 172 }
|
||||
```
|
||||
|
||||
### Filtering Proxies
|
||||
|
||||
```bash
|
||||
# Filter by protocol
|
||||
curl "http://localhost:20128/api/settings/oneproxy?protocol=socks5"
|
||||
|
||||
# Filter by country
|
||||
curl "http://localhost:20128/api/settings/oneproxy?countryCode=US"
|
||||
|
||||
# Filter by minimum quality score
|
||||
curl "http://localhost:20128/api/settings/oneproxy?minQuality=80"
|
||||
|
||||
# Combine filters
|
||||
curl "http://localhost:20128/api/settings/oneproxy?protocol=http&countryCode=DE&minQuality=70"
|
||||
```
|
||||
|
||||
### Proxy Quality Scores
|
||||
|
||||
Each 1proxy proxy comes with metadata:
|
||||
|
||||
| Field | Description |
|
||||
| --------------- | -------------------------------------------- |
|
||||
| `qualityScore` | 0-100 rating from 1proxy validation |
|
||||
| `latencyMs` | Measured network latency |
|
||||
| `anonymity` | `transparent`, `anonymous`, or `elite` |
|
||||
| `googleAccess` | Whether the proxy can access Google services |
|
||||
| `countryCode` | Two-letter ISO country code |
|
||||
| `lastValidated` | Timestamp of last validation |
|
||||
|
||||
Quality scores are dynamically adjusted:
|
||||
|
||||
- **Failed requests** reduce the score by 10 points
|
||||
- **Score drops to ≤10** → proxy is marked `inactive`
|
||||
- Inactive proxies are excluded from rotation
|
||||
|
||||
### Rotation Strategies
|
||||
|
||||
```bash
|
||||
# Rotate by quality (best proxy first) — default
|
||||
curl -X POST http://localhost:20128/api/settings/oneproxy/rotate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"strategy": "quality"}'
|
||||
|
||||
# Random rotation
|
||||
curl -X POST http://localhost:20128/api/settings/oneproxy/rotate \
|
||||
-d '{"strategy": "random"}'
|
||||
|
||||
# Sequential (least recently validated first)
|
||||
curl -X POST http://localhost:20128/api/settings/oneproxy/rotate \
|
||||
-d '{"strategy": "sequential"}'
|
||||
```
|
||||
|
||||
### Circuit Breaker
|
||||
|
||||
The 1proxy sync has a built-in circuit breaker:
|
||||
|
||||
- After **5 consecutive sync failures**, further sync attempts are blocked
|
||||
- Reset with: `resetOneproxyCircuitBreaker()` or restart the server
|
||||
- Sync status is available at `GET /api/settings/oneproxy?action=status`
|
||||
|
||||
### Clearing 1proxy Proxies
|
||||
|
||||
```bash
|
||||
# Delete a single 1proxy proxy
|
||||
curl -X DELETE "http://localhost:20128/api/settings/oneproxy?id=proxy-uuid"
|
||||
|
||||
# Clear ALL 1proxy proxies (manual proxies are untouched)
|
||||
curl -X DELETE "http://localhost:20128/api/settings/oneproxy?clearAll=1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Detection & Stealth
|
||||
|
||||
OmniRoute doesn't just route traffic through a proxy — it makes the traffic look legitimate:
|
||||
|
||||
### TLS Fingerprint Spoofing
|
||||
|
||||
Uses `wreq-js` to generate browser-like TLS fingerprints, bypassing bot detection systems that flag non-browser TLS handshakes.
|
||||
|
||||
### CLI Fingerprint Matching
|
||||
|
||||
The **CLI Fingerprint Toggle** (`Settings → Security`) reorders HTTP headers and JSON body fields to match the exact signature of native CLI binaries (Claude Code, Codex, etc.). This works **on top of** the proxy:
|
||||
|
||||
```
|
||||
Your IP (blocked) → Proxy IP (US) → Provider API
|
||||
+ TLS spoof
|
||||
+ CLI fingerprint
|
||||
```
|
||||
|
||||
You get both **IP masking** and **request authenticity** simultaneously.
|
||||
|
||||
### Proxy IP Preservation
|
||||
|
||||
Color-coded badges in the dashboard show which proxy level is active:
|
||||
|
||||
| Badge | Level | Meaning |
|
||||
| ----- | ---------- | ----------------------------------------- |
|
||||
| 🟢 | Global | All traffic goes through this proxy |
|
||||
| 🟡 | Provider | Only this provider's traffic is proxied |
|
||||
| 🔵 | Connection | This specific key/account uses this proxy |
|
||||
|
||||
The badge also shows the resolved proxy IP for verification.
|
||||
|
||||
---
|
||||
|
||||
## Upstream Proxy Modes
|
||||
|
||||
For providers that use the CLIProxyAPI pattern, OmniRoute supports three upstream proxy modes:
|
||||
|
||||
| Mode | Description |
|
||||
| ------------- | -------------------------------------------------- |
|
||||
| `native` | OmniRoute handles proxy routing directly (default) |
|
||||
| `cliproxyapi` | Delegates to an external CLIProxyAPI instance |
|
||||
| `fallback` | Tries native first, falls back to CLIProxyAPI |
|
||||
|
||||
Configure per-provider:
|
||||
|
||||
```bash
|
||||
curl -X PUT "http://localhost:20128/api/upstream-proxy/openai" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"mode": "native", "enabled": true}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard UI
|
||||
|
||||
### Settings → Proxy Tab
|
||||
|
||||
- **Global proxy** configuration (set once for all traffic)
|
||||
- **Per-provider proxy** overrides
|
||||
- **Per-connection proxy** assignments
|
||||
- **Connection test** through configured proxy
|
||||
- **Color-coded badges** showing active proxy level
|
||||
|
||||
### Settings → 1proxy Tab
|
||||
|
||||
- **Sync Now** button to fetch free proxies
|
||||
- **Stats cards**: Total, Active, Avg Quality, Last Sync
|
||||
- **Filters**: Protocol, Country Code, Min Quality
|
||||
- **Proxy table** with host, protocol, country, quality score, latency, anonymity, Google access
|
||||
- **Sync status** panel with success/failure tracking and consecutive failure count
|
||||
- **Clear All** to remove all 1proxy entries
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Proxy Settings API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| -------- | ---------------------------------------------- | ----------------------- |
|
||||
| `GET` | `/api/settings/proxy` | Get full proxy config |
|
||||
| `GET` | `/api/settings/proxy?level=global` | Get global proxy |
|
||||
| `GET` | `/api/settings/proxy?level=provider&id=openai` | Get provider proxy |
|
||||
| `GET` | `/api/settings/proxy?resolve=connectionId` | Resolve effective proxy |
|
||||
| `PUT` | `/api/settings/proxy` | Update proxy config |
|
||||
| `DELETE` | `/api/settings/proxy?level=provider&id=openai` | Remove proxy at level |
|
||||
|
||||
### Proxy Registry API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| -------- | ------------------------------------------------- | --------------------- |
|
||||
| `GET` | `/api/v1/management/proxies` | List all proxies |
|
||||
| `GET` | `/api/v1/management/proxies?id=uuid` | Get proxy by ID |
|
||||
| `GET` | `/api/v1/management/proxies?id=uuid&where_used=1` | Get proxy assignments |
|
||||
| `POST` | `/api/v1/management/proxies` | Create proxy |
|
||||
| `PATCH` | `/api/v1/management/proxies` | Update proxy |
|
||||
| `DELETE` | `/api/v1/management/proxies?id=uuid` | Delete proxy |
|
||||
| `DELETE` | `/api/v1/management/proxies?id=uuid&force=1` | Force delete |
|
||||
| `POST` | `/api/v1/management/proxies/bulk-assign` | Bulk assign |
|
||||
| `GET` | `/api/v1/management/proxies/assignments` | List assignments |
|
||||
| `GET` | `/api/v1/management/proxies/health` | Proxy health stats |
|
||||
|
||||
### Tunnels API
|
||||
|
||||
For exposing your OmniRoute instance to the public internet (Cloudflare/ngrok/Tailscale) instead of routing outbound through a proxy, see [TUNNELS_GUIDE.md](./TUNNELS_GUIDE.md). The tunnel REST API lives under `/api/tunnels/{cloudflared,ngrok,tailscale}/*` and is orthogonal to the outbound proxy chain documented above.
|
||||
|
||||
### 1proxy API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| -------- | -------------------------------------- | ----------------------- |
|
||||
| `GET` | `/api/settings/oneproxy` | List 1proxy proxies |
|
||||
| `GET` | `/api/settings/oneproxy?action=stats` | Get stats + sync status |
|
||||
| `GET` | `/api/settings/oneproxy?action=status` | Get sync status only |
|
||||
| `POST` | `/api/settings/oneproxy` | Trigger sync |
|
||||
| `POST` | `/api/settings/oneproxy/rotate` | Rotate to next proxy |
|
||||
| `DELETE` | `/api/settings/oneproxy?id=uuid` | Delete one |
|
||||
| `DELETE` | `/api/settings/oneproxy?clearAll=1` | Clear all |
|
||||
|
||||
### Upstream Proxy API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| -------- | --------------------------------- | ---------------------------- |
|
||||
| `GET` | `/api/upstream-proxy/:providerId` | Get upstream proxy config |
|
||||
| `PUT` | `/api/upstream-proxy/:providerId` | Set upstream proxy mode |
|
||||
| `DELETE` | `/api/upstream-proxy/:providerId` | Remove upstream proxy config |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------------------------- | ------------------------------------- | -------------------------------------------------------------- |
|
||||
| `ENABLE_SOCKS5_PROXY` | `true` | Enable SOCKS5 proxy support (default `true` in `.env.example`) |
|
||||
| `ONEPROXY_ENABLED` | `true` | Enable 1proxy integration |
|
||||
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | 1proxy API endpoint |
|
||||
| `ONEPROXY_MAX_PROXIES` | `500` | Maximum proxies to sync |
|
||||
| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | Minimum quality score to import |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "SOCKS5 proxy is disabled"
|
||||
|
||||
Set `ENABLE_SOCKS5_PROXY=true` in your `.env` file and restart.
|
||||
|
||||
### "socket hang up" errors through proxy
|
||||
|
||||
This is normal with cheap proxies that drop idle connections. OmniRoute already handles this by:
|
||||
|
||||
- Disabling keep-alive on proxy connections (`keepAliveTimeout: 1`)
|
||||
- Disabling pipelining (`pipelining: 0`)
|
||||
- Caching dispatchers to avoid repeated handshakes
|
||||
|
||||
If it persists, try a different proxy or use the 1proxy rotation feature.
|
||||
|
||||
### "unsupported_country_region_territory" during OAuth
|
||||
|
||||
Make sure the proxy is configured **before** starting the OAuth flow. OmniRoute routes OAuth token exchange through the configured proxy. Set a global or provider-level proxy first, then connect.
|
||||
|
||||
### Proxy not being used
|
||||
|
||||
Check the resolution order:
|
||||
|
||||
1. Verify with `GET /api/settings/proxy?resolve=your-connection-id`
|
||||
2. Check if the proxy `status` is `active` (not `inactive`)
|
||||
3. Ensure the proxy assignment scope matches your connection
|
||||
|
||||
### 1proxy sync failing
|
||||
|
||||
Check the sync status:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:20128/api/settings/oneproxy?action=status"
|
||||
```
|
||||
|
||||
If `consecutiveFailures >= 5`, the circuit breaker has tripped. Restart the server to reset, or wait for manual reset.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### `proxy_registry` Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE proxy_registry (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'http',
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
username TEXT DEFAULT '',
|
||||
password TEXT DEFAULT '',
|
||||
region TEXT,
|
||||
notes TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
source TEXT NOT NULL DEFAULT 'manual', -- 'manual' or 'oneproxy'
|
||||
quality_score INTEGER, -- 0-100 (1proxy only)
|
||||
latency_ms INTEGER, -- milliseconds (1proxy only)
|
||||
anonymity TEXT, -- transparent/anonymous/elite
|
||||
google_access INTEGER DEFAULT 0, -- can access Google? (1proxy)
|
||||
last_validated TEXT, -- ISO timestamp (1proxy)
|
||||
country_code TEXT, -- ISO 2-letter code (1proxy)
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### `proxy_assignments` Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE proxy_assignments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
proxy_id TEXT NOT NULL REFERENCES proxy_registry(id),
|
||||
scope TEXT NOT NULL, -- 'global', 'provider', 'account', 'combo'
|
||||
scope_id TEXT, -- provider ID, connection ID, or combo ID
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(scope, scope_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Proxy Health Checking (v3.8.16+)
|
||||
|
||||
OmniRoute's **proxy fast-fail** mechanism (`src/lib/proxyHealth.ts`) detects dead proxies in <2s via a quick TCP connection check, then **caches the result** to avoid per-request overhead.
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
Request ──▶ ProxyHealthCache.get(url)
|
||||
│
|
||||
├─ Cache hit + fresh? ──▶ return cached status
|
||||
│
|
||||
└─ Cache miss / stale? ──▶ TCP connect to host:port
|
||||
(timeout: FAST_FAIL_TIMEOUT_MS)
|
||||
──▶ cache for HEALTH_CACHE_TTL_MS
|
||||
──▶ return result
|
||||
```
|
||||
|
||||
Without this, a dead proxy would block every request for the full `PROXY_TIMEOUT_MS` (default 30s) before failing.
|
||||
|
||||
### Tunable Environment Variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| ---------------------------- | ------- | --------------------------------------- |
|
||||
| `PROXY_FAST_FAIL_TIMEOUT_MS` | `2000` | TCP connection timeout per health check |
|
||||
| `PROXY_HEALTH_CACHE_TTL_MS` | `30000` | How long a health result is cached |
|
||||
|
||||
**Recommended values:**
|
||||
|
||||
| Scenario | Fast-fail timeout | Cache TTL | Reasoning |
|
||||
| --------------------------- | ----------------- | --------- | --------------------------------------------------------------- |
|
||||
| High-throughput API gateway | 1500ms | 60000ms | Aggressive fail-fast, longer cache to reduce checks |
|
||||
| Geo-distributed nodes | 3000ms | 15000ms | Slower networks need more time; shorter cache for fast failover |
|
||||
| Dev / testing | 1000ms | 10000ms | Quick iteration on local proxies |
|
||||
| Stealth / anti-detection | 2500ms | 45000ms | Avoid rapid probing that could trigger rate limits |
|
||||
|
||||
### Inspecting Proxy Health
|
||||
|
||||
```ts
|
||||
import { getAllProxyHealthStatuses, invalidateProxyHealth } from "omniroute/proxyHealth";
|
||||
|
||||
const statuses = getAllProxyHealthStatuses();
|
||||
for (const s of statuses) {
|
||||
console.log(`${s.proxyUrl} → healthy=${s.healthy}, stale=${s.stale}`);
|
||||
}
|
||||
|
||||
// Force re-check a specific proxy
|
||||
invalidateProxyHealth("http://user:pass@1.2.3.4:8080");
|
||||
```
|
||||
|
||||
The `stale` flag is `true` when the cache entry has exceeded `HEALTH_CACHE_TTL_MS` and the next request will trigger a fresh check.
|
||||
|
||||
### Per-Proxy Type Defaults
|
||||
|
||||
The health check uses sensible defaults based on the URL scheme:
|
||||
|
||||
| Scheme | Default port |
|
||||
| -------------------------- | ------------ |
|
||||
| `http://` | 8080 |
|
||||
| `https://` | 443 |
|
||||
| `socks5://` / `socks5h://` | 1080 |
|
||||
|
||||
Custom ports in the URL (`http://host:9999`) always take precedence over the scheme default.
|
||||
|
||||
---
|
||||
|
||||
## Proxy Analytics & Observability
|
||||
|
||||
OmniRoute tracks per-proxy usage to help operators diagnose routing patterns, latency spikes, and recurring failures.
|
||||
|
||||
### What's Tracked
|
||||
|
||||
For every request through a configured proxy, OmniRoute records:
|
||||
|
||||
| Metric | Description |
|
||||
| ------------ | ----------------------------------------------- |
|
||||
| `proxy_url` | Full proxy URL (with auth credentials masked) |
|
||||
| `provider` | Upstream provider ID (openai, anthropic, etc.) |
|
||||
| `latency_ms` | Total round-trip time including proxy handshake |
|
||||
| `connect_ms` | TCP connect time only |
|
||||
| `status` | HTTP status code from upstream |
|
||||
| `error` | Error class if request failed |
|
||||
| `timestamp` | ISO 8601 UTC |
|
||||
|
||||
### Accessing the Data
|
||||
|
||||
```bash
|
||||
# Recent proxy events
|
||||
curl -H "Authorization: Bearer $OMNIROUTE_KEY" \
|
||||
"http://localhost:20128/api/usage/proxy-logs?limit=100"
|
||||
```
|
||||
|
||||
The real endpoint is `/api/usage/proxy-logs` (see `src/app/api/usage/proxy-logs/route.ts`). This endpoint supports:
|
||||
|
||||
- `GET /api/usage/proxy-logs` — retrieve proxy logs
|
||||
- `DELETE /api/usage/proxy-logs` — clear all proxy logs
|
||||
|
||||
Aggregate stats can be queried directly from the `proxy_logs` table via SQL if needed. The dashboard UI may offer aggregate views.
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Detect a flapping proxy** (alternates between success/failure):
|
||||
|
||||
```sql
|
||||
SELECT proxy_url,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END) AS errors,
|
||||
ROUND(100.0 * SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END) / COUNT(*), 1) AS error_pct
|
||||
FROM proxy_logs
|
||||
WHERE timestamp > datetime('now', '-1 hour')
|
||||
GROUP BY proxy_url
|
||||
HAVING error_pct > 5
|
||||
ORDER BY error_pct DESC;
|
||||
```
|
||||
|
||||
**Find slow proxies** (p95 latency > 2s):
|
||||
|
||||
```sql
|
||||
WITH ranked AS (
|
||||
SELECT proxy_url, latency_ms,
|
||||
PERCENT_RANK() OVER (PARTITION BY proxy_url ORDER BY latency_ms) AS pct
|
||||
FROM proxy_logs
|
||||
WHERE timestamp > datetime('now', '-24 hour')
|
||||
)
|
||||
SELECT proxy_url, latency_ms
|
||||
FROM ranked
|
||||
WHERE pct >= 0.95
|
||||
ORDER BY latency_ms DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Strategy Decision Tree
|
||||
|
||||
When multiple proxies are assigned to a scope, OmniRoute uses a **rotation strategy** to pick which one to use for each request. The strategy is configured at the scope level (global, per-provider, per-account, per-combo).
|
||||
|
||||
### Available Strategies
|
||||
|
||||
| Strategy | When to use | Trade-off |
|
||||
| ------------------- | ------------------------------------- | ----------------------------------------------------- |
|
||||
| `quality` (default) | Production with mixed-quality proxies | Favors high-rated proxies; may starve low-rated ones |
|
||||
| `random` | Load distribution, privacy | Even distribution; ignores quality signals |
|
||||
| `sequential` | Debugging, deterministic testing | Cycles through proxies in order; easy to reason about |
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
Do you have quality scores for your proxies?
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ │
|
||||
YES NO
|
||||
│ │
|
||||
Are all proxies │
|
||||
roughly equal │
|
||||
in quality? │
|
||||
│ │
|
||||
┌────┴────┐ │
|
||||
│ │ │
|
||||
YES NO Use
|
||||
│ │ `random`
|
||||
│ │ (even spread
|
||||
│ │ builds quality
|
||||
│ │ data over time)
|
||||
│ │
|
||||
│ Use `quality`
|
||||
│ (best for
|
||||
│ mixed quality)
|
||||
│
|
||||
Use `random`
|
||||
(spread load
|
||||
evenly)
|
||||
```
|
||||
|
||||
### Configuring Rotation Strategy
|
||||
|
||||
```ts
|
||||
import { rotateOneproxyProxy } from "omniroute/oneproxyRotator";
|
||||
|
||||
// In a one-off script
|
||||
const proxy = await rotateOneproxyProxy({ strategy: "quality" });
|
||||
if (proxy) {
|
||||
console.log(`Selected: ${proxy.host}:${proxy.port}, quality=${proxy.qualityScore}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Resetting Sequential Index
|
||||
|
||||
When using `sequential` strategy, the internal index accumulates. To reset:
|
||||
|
||||
```ts
|
||||
import { resetSequentialIndex } from "omniroute/oneproxyRotator";
|
||||
|
||||
resetSequentialIndex();
|
||||
```
|
||||
|
||||
Useful when:
|
||||
|
||||
- Restarting a load test
|
||||
- Recovering from a proxy outage (so you don't cycle through dead ones first)
|
||||
- Manually rebalancing after adding new proxies
|
||||
|
||||
### Marking a Proxy as Failed
|
||||
|
||||
When a proxy consistently fails, mark it manually so the rotator will skip it:
|
||||
|
||||
```ts
|
||||
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
|
||||
|
||||
const removed = await failOneproxyProxy("1.2.3.4", 8080);
|
||||
if (removed) {
|
||||
console.log("Proxy marked as failed; rotator will skip it");
|
||||
}
|
||||
```
|
||||
|
||||
The proxy is **not deleted** — it's marked unhealthy and won't be selected until the next successful health check (via `proxyHealth.ts`) or manual reset.
|
||||
|
||||
---
|
||||
|
||||
> 📖 **Related documentation:**
|
||||
>
|
||||
> - [User Guide](../guides/USER_GUIDE.md) — General setup and configuration
|
||||
> - [API Reference](../reference/API_REFERENCE.md) — Full API documentation
|
||||
> - [Environment Config](../reference/ENVIRONMENT.md) — All environment variables
|
||||
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: "Quality Gate Playbook"
|
||||
---
|
||||
|
||||
# Quality-Gate System — Critical Assessment, Catalog and Replication Playbook
|
||||
|
||||
> **What this document is.** A critical assessment of OmniRoute's quality-gate system,
|
||||
> compared to industry best practices, **plus** a comprehensive catalog of all quality
|
||||
> checkpoints and a **tool-agnostic replication plan** to apply the same system to
|
||||
> any project. Generated on 2026-06-16 from the real repository state (not from memory).
|
||||
>
|
||||
> Benchmarks: OWASP DSOMM · OpenSSF Scorecard · SLSA · SonarQube "Clean as You Code" ·
|
||||
> Quality-Ratchet pattern · DORA 2024 · OWASP LLM Top 10 (2025) · mutation-testing best practices.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — Verdict and Maturity Classification
|
||||
|
||||
**Overall grade: A− / "Advanced". Top ~5–10% of projects.** The system independently
|
||||
implements several patterns that the industry explicitly names — which is the strongest
|
||||
alignment signal (we didn't copy a checklist; we converged on the right practices).
|
||||
|
||||
| Reference framework | Where we stand | Grade |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
||||
| **OWASP DSOMM** (5 levels, 5 dimensions) | Solid Level 3, reaching 4 in _Test Intensity_ and _Static Depth_. Most orgs sit at 1–2. | **L3→L4** |
|
||||
| **OpenSSF Scorecard** (18 checks) | We pass CI-Tests, Code-Review, Dependency-Update-Tool, Fuzzing, SAST, Signed-Releases (provenance), Token-Permissions, Vulnerabilities, Dangerous-Workflow. **Gaps:** Branch-Protection on `main` OFF; some actions not pinned. | **~7–8/10** |
|
||||
| **SLSA** (4 levels) | `npm publish --provenance` + `id-token: write` + GitHub-hosted build = **L2**, approaching L3. Missing hardened/hermetic builder for L3+. | **L2→L3** |
|
||||
| **SonarQube "Clean as You Code"** | Identical philosophy: the ratchet gates _non-regression_ (new code doesn't worsen the metric). **Divergence:** Sonar recommends **few** conditions; we have ~46 gates (fatigue risk). | **Aligned, with caveat** |
|
||||
| **Quality-Ratchet pattern** | Reference implementation: ratchet + `dedicatedGate` + `tightenSlack` + `--require-tighten` + graceful-skip. More sophisticated than most public examples. | **Exemplary** |
|
||||
| **DORA 2024** | Very strong on _stability_ axis. Risk: heavy gates can cost _lead time_ — mitigated by fast-gates split, but with coverage gap (see Part 2). | **Strong (stability)** |
|
||||
| **OWASP LLM Top 10 (2025)** | We cover risk #1 (prompt-injection) with runtime guard + promptfoo (eval) + garak (red-team). Standard industry tools. | **Covered** |
|
||||
| **Mutation testing** | Stryker nightly, thresholds 70/50, 8 critical modules. Industry consensus (60% existing / 80% new, nightly) — **we beat it**. **Gap:** score is not yet a ratchet. | **Almost there** |
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Critical Assessment (strengths + honest weaknesses)
|
||||
|
||||
### Strengths (what's above average)
|
||||
|
||||
1. **Multi-metric ratchet engine.** The heart of the system. 24 metrics in `quality-baseline.json`
|
||||
- 4 dedicated baselines, each with direction (`up`/`down`), tolerance (`eps`), slack
|
||||
(`tightenSlack`) and `dedicatedGate` flag. Things that get fixed **stay** fixed — it's the
|
||||
antidote to codebase entropy.
|
||||
2. **Defense-in-depth for supply-chain.** SAST (CodeQL/Sonar) + secrets (gitleaks with
|
||||
`useDefault`) + SCA (osv/npm-audit/Trivy/Dependabot) + licenses + lockfile + SBOM + SLSA
|
||||
provenance + Scorecard + workflow hardening (zizmor). Few codebases have this complete stack.
|
||||
3. **Antidotes against Goodhart's Law.** Coverage as a target is a classic anti-pattern
|
||||
("when the measure becomes the target, it ceases to be a good measure"). We have the
|
||||
counterweights: **mutation testing** (measures whether the test catches the bug, not just
|
||||
whether it executes the line), **`check-test-masking`** (blocks weakening asserts to pass),
|
||||
**per-module coverage floors** (forces testing HIGH-risk code, not just the easy parts) and
|
||||
**`check-pr-evidence`** (Hard Rule #18).
|
||||
4. **Anti-hallucination / consistency gates.** A rare and valuable category: `check-known-symbols`,
|
||||
`check-fetch-targets`, `check-openapi-routes`, `check-docs-symbols` ensure that docs, specs and
|
||||
string dispatches point to living symbols. Catches "rot" that lint/test don't.
|
||||
5. **Advisory→blocking lifecycle.** New gates enter as advisory (don't block merges while
|
||||
maturing), then become blocking at cycle end. Reduces friction without losing the ceiling.
|
||||
6. **Graceful skip when infra is missing.** Scanners (`--ratchet`) exit `exit 0` if the binary/network
|
||||
fails — missing infra never blocks a legitimate PR. Mature engineering.
|
||||
7. **Codified culture.** Hard Rules + `trust-but-verify` + stale-allowlist + evidence-gate
|
||||
turn discipline into automated verification.
|
||||
|
||||
### Honest weaknesses (real gaps)
|
||||
|
||||
1. **🔴 The fast-gates split is a structural hole.** `quality.yml` (PR→`release/**`) runs **only
|
||||
filesystem gates** — no typecheck, no tests, no build, no coverage. A typecheck/test regression
|
||||
passes in a release PR and only blows up on the forward-merge to `main`. The motivation
|
||||
(speed) is valid, but the gate should be where the merge happens (shift-left). **Largest
|
||||
pending structural fix.**
|
||||
2. **🟠 Gate sprawl/fatigue risk.** ~46 gates + 25 jobs is A LOT. Sonar itself warns:
|
||||
too many conditions cause "gate fatigue" and priority debates, with risk of a gate being
|
||||
ignored. DORA warns that heavy gates cost lead-time. We mitigate with advisory tiers and
|
||||
non-absolute ratchets, but a **periodic ROI review per gate** is missing (some micro-gates for
|
||||
doc-sync are consolidatable).
|
||||
3. **🟠 Mutation score is not yet a ratchet.** The strongest antidote against coverage-gaming is
|
||||
**advisory**. It's the highest-value pending item (and already 90% built).
|
||||
4. **🟡 Advisories that should block (with the right scope).** `osv` (vulnCount) and `oasdiff` are
|
||||
advisory despite frozen baselines. osv-advisory makes sense (a new CVE on an old dep would block
|
||||
an unrelated PR) — but there's a middle ground (block only CRITICAL+fixable, as we did with
|
||||
Trivy). oasdiff advisory means a contract-breaking change can pass.
|
||||
5. **🟡 Runtime security is nightly-only.** schemathesis/garak/promptfoo/chaos/k6 run at night.
|
||||
Correct decision (slow, need a live server), but a PR can introduce an injection-guard regression
|
||||
that only gets caught the following night.
|
||||
6. **🟡 Branch-protection on `main` is OFF.** `BRANCH_LOCK_TOKEN` locks _release_ branches, but
|
||||
`main` itself is unprotected. Scorecard/DSOMM ding. Owner action required.
|
||||
7. **🟡 CodeQL default-setup; semgrep not codified.** default-setup works (0 alerts), but a
|
||||
committed `codeql.yml` gives more control; semgrep runs via an external cloud platform, not
|
||||
versioned in the repo.
|
||||
|
||||
---
|
||||
|
||||
## Part 3 — Complete Catalog of Quality Checkpoints (portable)
|
||||
|
||||
The 12 categories below are the "quality system" in reusable form. Each lists the
|
||||
**objective** (what to protect), the **tools we use** and the **tool-agnostic equivalent**
|
||||
to replicate on any stack.
|
||||
|
||||
### 1. Style & formatting (deterministic, fast)
|
||||
|
||||
- **OmniRoute:** Prettier + ESLint via lint-staged (pre-commit), 2-spaces/double-quotes/100col.
|
||||
- **Generic:** one auto-fixable formatter + one linter, running in pre-commit on staged files.
|
||||
|
||||
### 2. Types
|
||||
|
||||
- **OmniRoute:** `typecheck:core` (blocking) + `typecheck:noimplicit:core` (advisory) + `type-coverage` ratchet 92.17% + per-file any-budget.
|
||||
- **Generic:** strict typecheck in CI + ratcheted type-coverage metric + per-file `any`/escape-hatch budget.
|
||||
|
||||
### 3. Tests (intensity)
|
||||
|
||||
- **OmniRoute:** 2 non-overlapping runners (Node native + vitest), 8 shards, global coverage 60/60/60/60 + ratchet ~76% + **8 per-module floors for critical modules** + nightly property tests + **mutation testing** nightly.
|
||||
- **Generic:** test runner(s) + **absolute** coverage floor (anti-zero) + coverage **ratchet** (anti-regression) + **per-module floors for high-risk code** (anti-Goodhart) + property-based for pure logic + **mutation testing** nightly as the real measure of test quality.
|
||||
|
||||
### 4. Test policy (anti-gaming)
|
||||
|
||||
- **OmniRoute:** `pr-test-policy` (prod code requires a test), `check-test-masking` (blocks weakened asserts), `pr-evidence` (success claim requires evidence block), `test-discovery` (every test collected by a runner).
|
||||
- **Generic:** "new code ⇒ new test" gate + assert-removed/tautology detector + evidence requirement (TDD or living test) + guarantee that no test is orphaned outside the globs.
|
||||
|
||||
### 5. Complexity & code health (ratchets)
|
||||
|
||||
- **OmniRoute:** ESLint-warnings (3769↓), jscpd duplication (5.72%↓), cyclomatic+max-lines complexity (1800↓), cognitive complexity sonarjs (753↓), dead-code/unused-exports knip (339↓), per-file file-size (frozen, shrink-only), circular-deps (custom Tarjan, blocking).
|
||||
- **Generic:** ratchet every health metric (warnings, duplication, cyclomatic **and** cognitive complexity, dead code, file size, import cycles). Direction always "don't regress".
|
||||
|
||||
### 6. Static security (SAST + secrets)
|
||||
|
||||
- **OmniRoute:** CodeQL (ratchet alerts = 0), gitleaks (`[extend] useDefault=true` — critical!), SonarQube, custom security rules (public-creds, error-helper, route-guard-membership, route-validation).
|
||||
- **Generic:** SAST (CodeQL/Sonar/semgrep) with alert ratchet + secrets scanner with **inherited default ruleset** (custom config that overrides the default = blind) + project-specific Hard Rule security gates.
|
||||
|
||||
### 7. Supply-chain (dependencies)
|
||||
|
||||
- **OmniRoute:** osv-scanner + npm-audit + Trivy + Dependabot (SCA), license-checker (SPDX allowlist), lockfile-lint (HTTPS+sha512+registry), `check-deps` anti-slopsquatting (allowlist + age ≥72h).
|
||||
- **Generic:** multi-source SCA + license allowlist + lockfile integrity check + dependency allowlist with age/typosquatting check + grouped update bot.
|
||||
|
||||
### 8. Supply-chain (build & release)
|
||||
|
||||
- **OmniRoute:** SBOM (CycloneDX + syft), SLSA provenance (`--provenance`), OpenSSF Scorecard (weekly), workflow hardening (zizmor: artipacked→`persist-credentials:false`, cache-poisoning, token-permissions).
|
||||
- **Generic:** generate SBOM on publish + signed provenance (SLSA L2+) + scheduled Scorecard + harden all workflows (minimum-privilege tokens, no persisted credentials on non-pusher checkout, actions pinned by SHA).
|
||||
|
||||
### 9. Contracts & API
|
||||
|
||||
- **OmniRoute:** oasdiff (breaking-change OpenAPI), schemathesis (contract fuzz nightly), openapi-coverage (% documented routes, ratchet 38.3%), openapi-security-tiers (spec vs route-guard).
|
||||
- **Generic:** breaking-change contract diff (oasdiff/buf) + property-based fuzz against the spec (schemathesis) + ratcheted documentation coverage + spec↔code consistency.
|
||||
|
||||
### 10. Docs & i18n (anti-rot)
|
||||
|
||||
- **OmniRoute:** docs-sync (mirrored versions), docs-counts-sync (numbers in docs vs code), env-doc-sync, doc-links, fabricated-docs, cli-i18n, i18n-ui-coverage (`--threshold=65` + ratchet 80.1%).
|
||||
- **Generic:** sync versions/counts/env-vars between docs and code (gate, not trust) + validate internal links + ratcheted i18n coverage.
|
||||
|
||||
### 11. Anti-hallucination / consistency (the rare category)
|
||||
|
||||
- **OmniRoute:** known-symbols (string dispatch ⇒ living symbol), provider-consistency, fetch-targets (client fetch ⇒ real route), docs-symbols, db-rules (Hard Rules #2/#5), migration-numbering.
|
||||
- **Generic:** for every "duplicated source of truth" (registry, string dispatch, cross-layer references), a gate that proves both sides match. Catches the rot that typecheck/test don't.
|
||||
|
||||
### 12. Resilience & domain (product-specific)
|
||||
|
||||
- **OmniRoute:** chaos (fault-injection), heap-growth (leak), k6 (soak), promptfoo+garak (LLM red-team OWASP LLM Top 10), the 3 resilience laws (circuit-breaker/cooldown/lockout).
|
||||
- **Generic:** identify the failure modes of **your** domain and have a gate (even if nightly) for each. For AI apps: injection red-team. For distributed systems: chaos + leak + soak.
|
||||
|
||||
---
|
||||
|
||||
## Part 4 — Replication Plan for Any Project
|
||||
|
||||
Build in **phases**, each delivering value on its own. Don't try all 12 categories at once —
|
||||
that causes exactly the gate fatigue Part 2 warns about. Every new gate enters **advisory** and
|
||||
becomes **blocking** when stable.
|
||||
|
||||
### The reusable centerpiece: the "anatomy of a ratchet gate"
|
||||
|
||||
The entire system revolves around this 3-file pattern. Copy it first:
|
||||
|
||||
1. **`baseline.json`** — the frozen metric value + `direction` (`up`/`down`) + `eps` (anti-flake) + `tightenSlack` + `dedicatedGate`.
|
||||
2. **`collect-metrics.<ext>`** — runs the tool, extracts the number, writes `metrics.json`.
|
||||
3. **`check-ratchet.<ext>`** — compares `metrics.json` vs `baseline.json`; `exit 1` **only** if regressed beyond `eps`; `exit 0` (graceful skip) if the tool/infra was missing; with `--require-tighten`, `exit 1` if it **improved** without updating the baseline (locks in the gain).
|
||||
|
||||
With this in place, **every** new metric (coverage, complexity, warnings, SAST alerts, bundle size, mutation score…) is just one line in the baseline.
|
||||
|
||||
### Phase 0 — Foundation (week 1)
|
||||
|
||||
CI exists; formatter + linter + typecheck + 1 test runner + **absolute** coverage floor
|
||||
(e.g., 60%). Pre-commit runs fast auto-fixable checks. _Output: no PR breaks the basics._
|
||||
|
||||
### Phase 1 — The ratchet engine (week 2) — **the foundation of everything**
|
||||
|
||||
Implement the 3 files above. Freeze baselines for: warnings, coverage, complexity, duplication,
|
||||
dead code, file size. _Output: the codebase can only improve from here._
|
||||
|
||||
### Phase 2 — Static depth (week 3)
|
||||
|
||||
SAST (CodeQL/Sonar/semgrep) with alert ratchet; secrets scanner (**inherit the default ruleset**);
|
||||
SCA (osv/Dependabot) + license allowlist + lockfile-lint. _Output: known vulnerabilities and
|
||||
leaked secrets don't pass._
|
||||
|
||||
### Phase 3 — Build supply-chain (week 4)
|
||||
|
||||
SBOM on publish + signed provenance (SLSA L2) + scheduled Scorecard + workflow hardening
|
||||
(zizmor: minimum tokens, no persisted credentials, pinned actions). _Output: traceable and
|
||||
tamper-proof releases._
|
||||
|
||||
### Phase 4 — Test intensity (week 5–6)
|
||||
|
||||
2nd runner if useful; **per-module coverage floors for critical modules** (anti-Goodhart);
|
||||
property-based for pure logic; **mutation testing nightly** → when the 1st score arrives, make
|
||||
`mutationScore` a ratchet. _Output: coverage stops being a vanity metric; tests provably catch bugs._
|
||||
|
||||
### Phase 5 — Contract & dynamic (week 7)
|
||||
|
||||
If there's a public API: oasdiff (breaking-change, **blocking**) + schemathesis (nightly fuzz).
|
||||
DAST/red-team nightly as appropriate for the domain. _Output: contracts don't break silently._
|
||||
|
||||
### Phase 6 — Anti-hallucination & domain (week 8)
|
||||
|
||||
One consistency gate for each "duplicated truth" in the project. Domain-specific failure-mode
|
||||
gates (for AI: injection red-team). _Output: structural rot and domain failures have a safety net._
|
||||
|
||||
### Phase 7 — Governance (ongoing)
|
||||
|
||||
- Advisory→blocking cycle for every new gate.
|
||||
- `stale-allowlist`: every suppression has a justification + issue; obsolete suppression is caught.
|
||||
- `evidence-gate`: success claim in a PR requires proof (test or living test).
|
||||
- **Quarterly ROI review per gate** (kill/defund those that don't pay back — fights fatigue).
|
||||
- Promote your project's Hard Rules into executable gates.
|
||||
|
||||
### Cross-cutting principles (non-negotiable)
|
||||
|
||||
- **Ratchet, not absolute.** Gate _non-regression_, not a fixed number (except anti-zero floors).
|
||||
- **Absolute floor + ratchet together.** The floor prevents collapse; the ratchet prevents slow erosion.
|
||||
- **Anti-Goodhart by design.** Every target metric needs a counterweight (coverage ⇒ mutation + anti-masking; per-module floors to force testing the hard code).
|
||||
- **Graceful skip.** Missing infra never blocks; only real regression blocks.
|
||||
- **`dedicatedGate` for expensive metrics.** Metrics that need an external binary get their own script (with skip), outside the synchronous central ratchet.
|
||||
- **Gate where the merge happens.** Don't leave a gap between the fast gate and the actual merge (the lesson from the fast-gates split).
|
||||
- **Few blocking gates, well-chosen.** Sonar/DORA: too many conditions = fatigue. Prefer advisory + ratchet over a wall of blocking gates.
|
||||
|
||||
---
|
||||
|
||||
## Part 5 — Recommended improvements (prioritized, compatible)
|
||||
|
||||
**P0 — highest ROI, almost ready**
|
||||
|
||||
1. **Mutation score ratchet** (after the 1st nightly Stryker produces values). Key antidote against coverage-Goodhart; ~90% done.
|
||||
2. **Close the fast-gates hole** — add typecheck + impacted tests to `quality.yml` (PR→release).
|
||||
3. **Branch-protection on `main`** (owner setting) — boosts Scorecard, closes the DSOMM gap.
|
||||
|
||||
**P1 — valuable** 4. **osv/oasdiff → blocking with the right scope** — osv only CRITICAL+fixable (two-step like Trivy); oasdiff blocks breaking-changes. 5. **`require-tighten` → blocking** (end of cycle) — locks in metric gains. 6. **ROI/timing review per-gate** in `ci-summary` — find and prune slow/low-value gates.
|
||||
|
||||
**P2 — diminishing returns** 7. **SLSA L3** — hermetic/reproducible builder (GitHub SLSA generator) if you want to move up from L2. 8. **Committed CodeQL config + versioned semgrep** — more control/reproducibility. 9. **Per-PR DAST smoke** — fast subset of schemathesis/promptfoo on highest-risk endpoints (not just nightly). 10. **Flakiness dashboard + DORA metrics** — ensure gates aren't eroding speed.
|
||||
|
||||
---
|
||||
|
||||
## Part 6 — Concrete release lessons (gates to add in Phase 9)
|
||||
|
||||
> This section records real incidents from release closures where a gate **was missing**,
|
||||
> with concrete evidence and the proposed gate. Each item is a candidate for Part 5.
|
||||
|
||||
### Lesson v3.8.27 (2026-06-17) — the "fast-gates hole" lets deterministic regressions reach release day
|
||||
|
||||
**What happened.** During the v3.8.27 `/generate-release`, the release PR (`release/v3.8.27` → `main`)
|
||||
was the **first** execution of the full `ci.yml` matrix in the integrated cycle. Result: 12 failures
|
||||
at once — **3 deterministic tests** + ~9 flakes/env. None were live product regressions, but
|
||||
all went unnoticed because cycle PRs enter `release/**` via the **Fast QG
|
||||
(`quality.yml`)**, which does NOT run the full unit suite, nor `pr-test-policy` (test-masking), nor the
|
||||
full integration suite, nor schema parity checking. The 3 deterministic ones:
|
||||
|
||||
1. **Test outdated by UI change** — `permissions modal switch buttons declare button type`:
|
||||
#4034 added a 4th switch (a11y `type="button"` maintained); the test's `=== 3` count became
|
||||
outdated. Static analysis should have caught this in the #4034 PR.
|
||||
2. **Test outdated by packaging change** — `findMissingArtifactPaths ... root runtime files`:
|
||||
`dist/http-method-guard.cjs` became a legitimate required-path; the test's expected list became
|
||||
outdated.
|
||||
3. **Lossy modularization divergence (most serious)** — `settings schemas accept ... unprefixed
|
||||
toggle`: the **modularized** `updateSettingsSchema` (`schemas/settings.ts`, created by #3988) diverged
|
||||
from the canonical one (`settingsSchemas.ts`): **45 fields vs 85 — 40 dropped + 6 divergent (qdrant\*)**. It was
|
||||
**dead-code** (runtime uses the canonical one), so no live impact, but only a hand-written parity
|
||||
test caught it. #4030 restored 16 analogous drops from #3988/#3993, but this one slipped through.
|
||||
|
||||
**Proposed gates (Phase 9):**
|
||||
|
||||
- **G1 — Actually close the fast-gates hole (extends P0 #2).** In `quality.yml` (PR→`release/**`),
|
||||
beyond typecheck + impacted tests, run **`pr-test-policy` (test-masking) + the full deterministic
|
||||
unit suite** (or at least the static/parity files, which are fast and non-flaky).
|
||||
This way, outdated tests and assert removal are caught in the PR that introduces them — not on
|
||||
release day. Keep integration/e2e out (slow/flaky), but the deterministic layer CANNOT stay only
|
||||
in PR→main.
|
||||
- **G2 — Modularization parity gate (NEW, not covered today).** A check that, for each symbol
|
||||
re-exported by a modularized barrel (`src/shared/validation/schemas/*`, `providerRegistry`
|
||||
modules, etc.), compares the **shape** (`z.object` keys, registry entries) against the canonical
|
||||
source and **fails on divergence** (dropped/extra field). Would have caught the 40-field drop from
|
||||
#3988 in that very PR. Generalizes the hand-written parity tests (which only exist where someone
|
||||
remembered to write them). Cheap: imports both and diffs `Object.keys(shape)`.
|
||||
- **G3 — Deterministic flake triage (support).** LiveWS-startup and the integration-combo/breaker
|
||||
tests fail due to server timeout/cascade in CI (env), not logic. Mark these as
|
||||
`known-flaky` (quarantined with issue) so the release-PR red is **only real signals**, not noise
|
||||
masking deterministic regressions in the middle.
|
||||
|
||||
**Principle:** _the gate has to run where the merge happens_ (already in "Cross-cutting principles"). The
|
||||
v3.8.27 incident shows this also applies to the **deterministic test layer**, not just lint/typecheck —
|
||||
otherwise the debt of outdated tests + lossy modularization only appears in PR→main, in batch, at
|
||||
the worst moment.
|
||||
|
||||
---
|
||||
|
||||
## Sources (industry best practices)
|
||||
|
||||
- OWASP DevSecOps Maturity Model (DSOMM) — https://dsomm.owasp.org/about
|
||||
- OpenSSF Scorecard / SLSA — https://openssf.org · https://slsa.dev
|
||||
- SonarQube "Clean as You Code" — https://docs.sonarsource.com/sonarqube-server/latest/user-guide/clean-as-you-code
|
||||
- Quality Ratchets (LeadDev) — https://leaddev.com/software-quality/introducing-quality-ratchets-tool-managing-complex-systems
|
||||
- Continuous Code Improvement Using Ratcheting (Greiner) — https://robertgreiner.com/continuous-code-improvement-using-ratcheting/
|
||||
- DORA 2024 State of DevOps — https://cloud.google.com/blog/products/devops-sre/announcing-the-2024-dora-report
|
||||
- Mutation testing best practices (Stryker) — https://stryker-mutator.io
|
||||
- Coverage as anti-pattern (Goodhart) — https://www.industriallogic.com/blog/code-coverage-complications/
|
||||
- OWASP Top 10 for LLM Applications (2025) — https://owasp.org/www-project-top-10-for-large-language-model-applications/
|
||||
- Contract testing (oasdiff/schemathesis) — https://www.oasdiff.com · https://schemathesis.readthedocs.io
|
||||
@@ -0,0 +1,326 @@
|
||||
---
|
||||
title: "Release Checklist"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Release Checklist
|
||||
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
> Streamlined release flow that leverages Claude Code skills for automation.
|
||||
>
|
||||
> **Keep the queue/branch green between releases:** see [RELEASE_GREEN.md](./RELEASE_GREEN.md)
|
||||
> (`/green-prs` family + `npm run check:release-green` + `/babysit` + nightly). Running
|
||||
> this periodically — and especially **before** this checklist — makes the release PR start green.
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
# 1. Bump version + generate CHANGELOG (skill)
|
||||
/version-bump-cc patch # or minor/major
|
||||
|
||||
# 2. Run quality gate locally
|
||||
npm run check # lint + tests
|
||||
npm run test:coverage # full coverage gate (60/60/60/60)
|
||||
|
||||
# 3. Build & smoke
|
||||
npm run build
|
||||
npm run test:e2e # optional but recommended
|
||||
|
||||
# 4. Generate release (skill)
|
||||
/generate-release-cc
|
||||
|
||||
# 5. Deploy (skill)
|
||||
/deploy-vps-both-cc # or akamai-cc / local-cc
|
||||
|
||||
# 6. Capture release evidences (skill)
|
||||
/capture-release-evidences-cc
|
||||
```
|
||||
|
||||
## Detailed Checklist
|
||||
|
||||
### Pre-release
|
||||
|
||||
- [ ] All PRs targeted to this release are merged to `release/vX.Y.0`
|
||||
- [ ] All open Linear/issue items for this version are closed or pushed to next milestone
|
||||
- [ ] CI green on `release/vX.Y.0` branch
|
||||
- [ ] No `TODO(release)` markers in code: `grep -r "TODO(release)" src/ open-sse/`
|
||||
- [ ] Docker base image up to date (currently `node:24.15.0-trixie-slim`)
|
||||
|
||||
### Version & Changelog
|
||||
|
||||
- [ ] Run `/version-bump-cc <patch|minor|major>` (Claude Code skill)
|
||||
- Bumps `package.json`, `electron/package.json`
|
||||
- Regenerates `CHANGELOG.md` from git commits since last tag
|
||||
- Updates README.md badges
|
||||
- [ ] Manually review CHANGELOG.md and clean up commit messages if needed
|
||||
- [ ] Ensure the latest semver section in `CHANGELOG.md` equals `package.json` version
|
||||
- [ ] Keep `## [Unreleased]` as the first changelog section for upcoming work
|
||||
- [ ] Update `docs/openapi.yaml` → `info.version` must equal `package.json` version
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] `npm run lint` — 0 errors (warnings are pre-existing)
|
||||
- [ ] `npm run typecheck:core` — clean
|
||||
- [ ] `npm run typecheck:noimplicit:core` — clean (strict)
|
||||
- [ ] `npm run check:cycles` — no circular deps
|
||||
- [ ] `npm run check:any-budget:t11` — within budget
|
||||
- [ ] `npm run check:route-validation:t06` — clean
|
||||
- [ ] `npm run check:node-runtime` — supported runtime floor met (`>=22.22.2 <23`, `>=24.0.0 <27`, per `SUPPORTED_NODE_RANGE` in `src/shared/utils/nodeRuntimeSupport.ts`; aligned with `package.json` `engines`)
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] `npm run test:unit` — pass
|
||||
- [ ] `npm run test:vitest` — pass (MCP server, autoCombo, cache)
|
||||
- [ ] `npm run test:coverage` — gate 60/60/60/60 satisfied (statements/lines/functions/branches)
|
||||
- [ ] `npm run test:integration` — pass (if changes touch DB / handlers)
|
||||
- [ ] `npm run test:combo:matrix` — pass (combo strategy matrix: proves all 17 routing strategies' selection decisions deterministically; run when touching combo routing, strategy resolution, or fallback logic)
|
||||
- [ ] `RUN_COMBO_LIVE=1 npm run test:combo:live` — **optional/manual** (gated real-upstream smoke; sources a read-only DB snapshot from VPS `root@192.168.0.15`; hits real providers, costs credits; never runs in CI; skips cleanly without the gate)
|
||||
- [ ] `npm run test:combo:live:vps` — **optional/manual** (Phase-3 VPS live smoke: 7 HTTP scenarios against the live `.15` server via plain Node ESM; requires `ssh root@192.168.0.15`; creates/deletes only `__live_test__*` combos; hits real providers; never runs in CI)
|
||||
- [ ] `npm run test:e2e` — pass (UI changes)
|
||||
- [ ] `npm run test:protocols:e2e` — pass (MCP/A2A changes)
|
||||
- [ ] `npm run test:ecosystem` — pass
|
||||
|
||||
### Hooks (Husky validated)
|
||||
|
||||
Husky hooks live in `.husky/` and run automatically on git operations.
|
||||
|
||||
- **pre-commit:** `npx lint-staged + node scripts/check/check-docs-sync.mjs + npm run check:any-budget:t11`
|
||||
- **pre-push:** fast deterministic gates — `npm run check:any-budget:t11 && npm run check:tracked-artifacts` (activated 2026-06-13). Intentionally excludes `test:unit` (slow; covered by the CI `test-unit` job).
|
||||
- Run `npm run test:unit` manually before pushing release branches.
|
||||
|
||||
If a hook fails: fix the underlying issue, don't bypass with `--no-verify`.
|
||||
|
||||
### Conventional Commits
|
||||
|
||||
All release-bound commits must follow `type(scope): subject` format.
|
||||
|
||||
**Valid types:** `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `style`, `ci`
|
||||
|
||||
**Valid scopes:** `db`, `sse`, `oauth`, `dashboard`, `api`, `cli`, `docker`, `ci`, `mcp`, `a2a`, `memory`, `skills`, `cloud-agent`, `guardrails`, `compression`, `auto-combo`, `resilience`, `providers`, `executors`, `translator`, `domain`, `authz`
|
||||
|
||||
Breaking changes: add `BREAKING CHANGE:` footer or `!` after the scope (e.g. `feat(api)!: drop /v0`).
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] `npm run check:docs-sync` passes (auto-run by pre-commit)
|
||||
- [ ] `npm run check:docs-all` passes (umbrella: docs-sync + docs-counts + env-doc-sync + deprecated-versions + doc-links)
|
||||
- [ ] `npm run check:env-doc-sync` exits 0 — code ↔ `.env.example` ↔ `docs/reference/ENVIRONMENT.md` env contract is intact
|
||||
- [ ] `npm run check:doc-links` exits 0 — no broken internal markdown references after restructuring
|
||||
- [ ] `docs/architecture/ARCHITECTURE.md` reviewed for storage/runtime drift
|
||||
- [ ] `docs/guides/TROUBLESHOOTING.md` reviewed for env var and operational drift
|
||||
- [ ] If `.env.example` changed: `docs/reference/ENVIRONMENT.md` updated
|
||||
- [ ] If new feature has a UI: `docs/guides/USER_GUIDE.md` mentions it
|
||||
- [ ] If new feature has API: `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` updated
|
||||
- [ ] If new feature is a module: dedicated `docs/<MODULE>.md` exists
|
||||
- [ ] If breaking change: `docs/guides/TROUBLESHOOTING.md` has migration note
|
||||
|
||||
### i18n
|
||||
|
||||
- [ ] `npm run i18n:check` exits 0 — translation state (`.i18n-state.json`) in sync with source docs (no drifted sources in strict mode; warn-mode advisory is acceptable for last-minute doc touch-ups, but should be 0 before tagging)
|
||||
- [ ] `npm run i18n:check-ui-coverage` exits 0 — every UI locale at or above the 80% coverage floor
|
||||
- [ ] `npm run i18n:sync-ui:dry` reports 0 missing keys across all 42 locales
|
||||
- [ ] If source English docs changed, run `npm run i18n:run` (requires `OMNIROUTE_TRANSLATION_API_KEY` in `.env`) before tagging
|
||||
- [ ] Translation contributions can be deferred to next release if minor (track in CHANGELOG)
|
||||
|
||||
### Database Migrations
|
||||
|
||||
- [ ] If `src/lib/db/migrations/` has new files:
|
||||
- [ ] Each migration is idempotent (`CREATE TABLE IF NOT EXISTS`, etc.)
|
||||
- [ ] Migrations wrapped in transactions
|
||||
- [ ] Numbered correctly (no gaps in sequence)
|
||||
- [ ] Test on fresh install: delete `~/.omniroute/omniroute.db` and run `npm run dev`
|
||||
- [ ] Test on existing install: backup DB, run migration, verify schema
|
||||
- [ ] WAL files (`-wal`, `-shm`) handled correctly if migration rewrites tables
|
||||
|
||||
### Provider Catalog (Zod-validated)
|
||||
|
||||
- [ ] `src/shared/constants/providers.ts` Zod schema valid at load time
|
||||
- [ ] All providers have required fields (`id`, `label`, `kind`, etc.)
|
||||
- [ ] `freeNote` provided for new free providers
|
||||
- [ ] OAuth providers have `oauthConfig` registered in `src/lib/oauth/constants/oauth.ts`
|
||||
- [ ] If new provider added: corresponding executor in `open-sse/executors/`
|
||||
- [ ] If non-OpenAI format: translator in `open-sse/translator/`
|
||||
- [ ] Models registered in `open-sse/config/providerRegistry.ts`
|
||||
- [ ] Unit tests in `tests/unit/` cover provider classification and routing
|
||||
|
||||
### Desktop (Electron)
|
||||
|
||||
If `electron/` changed:
|
||||
|
||||
- [ ] `npm run electron:smoke:packaged` passes
|
||||
- [ ] Builds tested for at least one of `:win`, `:mac`, `:linux`
|
||||
- [ ] Code signing certs not expired (if signing)
|
||||
- [ ] `electron/package.json` version matches root `package.json`
|
||||
- [ ] Auto-update channel pointer updated if releasing to `stable`
|
||||
|
||||
### Build Layout
|
||||
|
||||
The repository uses three distinct output directories — never mix them up:
|
||||
|
||||
| Directory | Purpose | Tracked? |
|
||||
| --------- | -------------------------------------------------------- | --------------- |
|
||||
| `src/` | Application source (TypeScript / TSX) | Yes |
|
||||
| `.build/` | Build intermediates — `next build` output (`distDir`) | No (gitignored) |
|
||||
| `dist/` | Shippable npm bundle — assembled by `assembleStandalone` | No (gitignored) |
|
||||
|
||||
> **Operator note:** the remote VPS image directory remains `/usr/lib/node_modules/omniroute/app/`.
|
||||
> Only the **in-repo** build output moved (`app/` → `dist/`). The deploy skills rsync
|
||||
> `dist/` contents into the remote `app/` dir — no VPS path changes required.
|
||||
|
||||
**Single-build flow:**
|
||||
|
||||
```
|
||||
npm run build:release
|
||||
└─ rm -rf .build dist (clean)
|
||||
└─ next build → .build/next/ (intermediates)
|
||||
└─ assembleStandalone (copies standalone + static + public + natives → dist/)
|
||||
└─ writes dist/BUILD_SHA (HEAD sentinel)
|
||||
```
|
||||
|
||||
Do NOT run `npm run build` followed by a separate `npm run build:cli` for deploy — use
|
||||
`npm run build:release` which does a clean rebuild + sentinel in one command.
|
||||
|
||||
### Artifact Validation
|
||||
|
||||
- [ ] `npm run build:release` succeeds and `dist/BUILD_SHA` == `git rev-parse --short HEAD`
|
||||
- [ ] `npm run check:pack-artifact` clean — no `app.__qa_backup`, `scripts/scratch`, `package-lock.json`, or other local residue
|
||||
- [ ] `dist/server.js` exists after build
|
||||
|
||||
### Tagging & Release
|
||||
|
||||
- [ ] Run `/generate-release-cc` (Claude Code skill):
|
||||
- Creates tag `vX.Y.Z`
|
||||
- Pushes tag and branch
|
||||
- Opens GitHub Release with changelog body
|
||||
- Attaches Electron installers (if built)
|
||||
- [ ] Or manually:
|
||||
```bash
|
||||
git tag -a vX.Y.Z -m "Release vX.Y.Z"
|
||||
git push origin vX.Y.Z
|
||||
gh release create vX.Y.Z --notes-from-tag
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
Deploy skills use the light rsync flow — no `npm pack`, no `npm i -g`:
|
||||
|
||||
- [ ] Use deploy skill that matches target:
|
||||
- `/deploy-vps-local-cc` — local VPS (192.168.0.15)
|
||||
- `/deploy-vps-akamai-cc` — Akamai VPS (69.164.221.35)
|
||||
- `/deploy-vps-both-cc` — both
|
||||
- [ ] Before deploying, confirm `dist/BUILD_SHA` == `git rev-parse --short HEAD`
|
||||
- [ ] Build must run where `node_modules` is real (main checkout or `npm ci`'d worktree — NOT a symlinked worktree)
|
||||
- [ ] Smoke test deployed instance:
|
||||
- Open `/dashboard/health` → check version string matches release
|
||||
- Run a `/v1/chat/completions` request against a known provider
|
||||
- Verify `/api/monitoring/health` returns `CLOSED` circuit breakers
|
||||
- Confirm MCP transports respond (`/mcp` HTTP, `/mcp-sse` SSE)
|
||||
|
||||
### Post-release
|
||||
|
||||
- [ ] Run `/capture-release-evidences-cc` (Claude Code skill)
|
||||
- Captures WebP screenshots/recordings of new features
|
||||
- Attaches to release notes / blog post
|
||||
- [ ] Update GitHub Discussions / Discord with release announcement
|
||||
- [ ] Open milestone for next version
|
||||
- [ ] If critical: pin discussion or post in `news.json` for in-app banner
|
||||
|
||||
## Embedded Services smoke (v3.8.4+)
|
||||
|
||||
Before shipping any release that includes embedded services changes, verify:
|
||||
|
||||
### Fresh-DB boot (catches migration collisions — added after v3.8.4 hotfix)
|
||||
|
||||
- [ ] `DATA_DIR=$(mktemp -d) npm start &` — wait 10 s for boot
|
||||
- [ ] `curl -s http://127.0.0.1:20128/api/services/9router/status | jq '.tool'` returns `"9router"` (NOT 404, NOT 500). Confirms migration `071_services.sql` applied + row seeded.
|
||||
- [ ] `sqlite3 $DATA_DIR/storage.sqlite "PRAGMA table_info(version_manager);" | grep -E "provider_expose|logs_buffer_path|last_sync_at"` returns 3 rows.
|
||||
- [ ] `sqlite3 $DATA_DIR/storage.sqlite "PRAGMA table_info(webhooks);" | grep -E "kind|metadata_encrypted"` returns 2 rows (validates `070_webhooks_kind_metadata.sql` applied).
|
||||
- [ ] `node --import tsx/esm --test tests/unit/db/no-migration-collisions.test.ts` passes — guards against future collisions.
|
||||
|
||||
### 9Router
|
||||
|
||||
- [ ] `POST /api/services/9router/install` returns 200 with `installedVersion` in under 2 min
|
||||
- [ ] `POST /api/services/9router/start` returns 200 and `state: "running"` in under 30 s
|
||||
- [ ] `GET /api/services/9router/status` reports `health: "healthy"`
|
||||
- [ ] `POST /v1/chat/completions` with `"model": "9router/auto/..."` returns 200 (end-to-end routing through 9Router)
|
||||
- [ ] `GET /dashboard/providers/services/9router/embed/dashboard` renders the 9Router native UI inside the proxy (no direct `127.0.0.1:port` iframe)
|
||||
- [ ] `POST /api/services/9router/rotate-key` returns `{ keyRotated: true }` and service restarts cleanly
|
||||
- [ ] `POST /api/services/9router/stop` returns 200 and `state: "stopped"`
|
||||
- [ ] `GET /api/services/9router/logs?tail=50` returns SSE stream with `snapshot` event containing recent lines
|
||||
- [ ] Install in environment without `npm` in PATH returns 500 with a friendly (non-stack-trace) error message
|
||||
|
||||
### CLIProxyAPI
|
||||
|
||||
- [ ] `POST /api/services/cliproxy/install` returns 200 in under 2 min
|
||||
- [ ] `POST /api/services/cliproxy/start` returns 200 and `state: "running"` in under 30 s
|
||||
- [ ] `GET /api/services/cliproxy/status` reports `health: "healthy"`
|
||||
- [ ] `POST /api/services/cliproxy/stop` returns 200 and `state: "stopped"`
|
||||
- [ ] `GET /api/services/cliproxy/logs?tail=50` returns SSE stream
|
||||
|
||||
### Security regression
|
||||
|
||||
- [ ] `curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:20128/api/services/9router/start` returns `403 LOCAL_ONLY`
|
||||
- [ ] `curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:20128/api/services/cliproxy/start` returns `403 LOCAL_ONLY`
|
||||
- [ ] Error responses from `/api/services/*` do not contain `err.stack` or absolute file paths
|
||||
|
||||
## v3.8.0+ checks
|
||||
|
||||
Before shipping any v3.8.x release, verify these additional items:
|
||||
|
||||
- [ ] `omniroute --tray` boots on macOS (systray2 installed into `~/.omniroute/runtime/`)
|
||||
- [ ] `omniroute --tray` boots on Linux (requires DISPLAY; graceful error if not set)
|
||||
- [ ] `omniroute --tray` boots on Windows (PowerShell NotifyIcon, no extra binaries)
|
||||
- [ ] `omniroute config tray enable` creates autostart entry; disable removes it
|
||||
- [ ] `npm install -g omniroute@<this-version>` runs postinstall without fatal exit
|
||||
- [ ] Update path keeps optional deps: `omniroute update --apply` and the auto-updater
|
||||
run `npm install -g … --include=optional` so `optionalDependencies` (better-sqlite3,
|
||||
keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2`,
|
||||
`@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) survive an update.
|
||||
`@huggingface/transformers` stays optional so its `onnxruntime-node` CUDA provider postinstall
|
||||
cannot abort installation on CUDA 11 hosts. The ultra `modelPath` SLM tier also needs the
|
||||
tinybert model, auto-downloaded to `${DATA_DIR}/models/llmlingua` on first use. Postinstall
|
||||
(`scripts/build/colocateOptionals.mjs`) then co-locates the SLM optional closure into
|
||||
`dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` 3.5.2
|
||||
optional instance — the standalone trace bundles only transformers, not the dynamically-imported
|
||||
optionals, so without this the worker would load llmlingua-2 against the root's transformers
|
||||
and the SLM tier would silently fail-open.
|
||||
- [ ] `omniroute status` works with no `.env` (CLI token path, loopback only)
|
||||
- [ ] `curl http://localhost:20128/api/shutdown` returns 401 (always-protected route)
|
||||
- [ ] `curl -H "host: evil.com" http://localhost:20128/api/mcp/sse` returns 401 (loopback guard)
|
||||
- [ ] SQLite runtime resolves to `bundled` on first run (bundled binary valid for platform)
|
||||
- [ ] SQLite runtime falls back to `runtime` when `node_modules/better-sqlite3` is deleted
|
||||
- [ ] Smart MCP filter compresses real `playwright-mcp browser_snapshot` output (≥50% reduction)
|
||||
- [ ] All 10 `skills/omniroute*/SKILL.md` files are publicly fetchable via raw GitHub URL
|
||||
- [ ] Onboarding wizard shows "How It Works" tier tour step on fresh setup
|
||||
- [ ] Home dashboard tier coverage widget shows configured/active counts
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
If release has critical issue:
|
||||
|
||||
1. `gh release edit vX.Y.Z --prerelease` (marks as not latest)
|
||||
2. `git tag -d vX.Y.Z && git push --delete origin vX.Y.Z` (only if not yet adopted by users)
|
||||
3. Or: hotfix on `release/vX.Y.0` → patch release `vX.Y.(Z+1)`
|
||||
4. Communicate in GitHub Discussions and Discord immediately
|
||||
|
||||
## Hard Rules
|
||||
|
||||
- Never commit directly to `main`
|
||||
- Never use `git push --force` to `main` or `release/*` branches
|
||||
- Never skip Husky hooks (`--no-verify`)
|
||||
- Never commit secrets, credentials, or `.env` files
|
||||
- Coverage must stay ≥60/60/60/60 (statements/lines/functions/branches)
|
||||
- Always include or update tests when changing production code in `src/`, `open-sse/`, `electron/`, or `bin/`
|
||||
|
||||
## Automated Sync Check
|
||||
|
||||
Run the docs sync guard locally before opening a PR:
|
||||
|
||||
```bash
|
||||
npm run check:docs-sync
|
||||
```
|
||||
|
||||
CI also runs this check in `.github/workflows/ci.yml` (lint job).
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: "Release-Green — keeping the queue and release branch green"
|
||||
---
|
||||
|
||||
# Release-Green: keeping the queue and release branch green
|
||||
|
||||
## The problem this solves
|
||||
|
||||
The **full gate** (`.github/workflows/ci.yml` — unit shards, vitest, ratchets,
|
||||
`package-artifact`, SonarQube, E2E) runs **only on the release PR** (PR → `main`). PRs targeting
|
||||
`release/**` receive only the **fast-gates** (`quality.yml`: TIA-impacted tests + typecheck +
|
||||
lint). Consequence: reds accumulate silently on the release branch and **explode in layers
|
||||
of ~40 min** at release time, one at a time.
|
||||
|
||||
The "release-green family" exists to **anticipate** those reds — validate the equivalent of the full
|
||||
gate **locally / outside of release**, at any time, so the release PR is already
|
||||
green on its first CI run.
|
||||
|
||||
> **Non-negotiable principle:** none of this blocks the contributor. We do not add a required
|
||||
> check that fails their PR. The **drift** (ratchets) is for the maintainer to rebaseline at release —
|
||||
> never a contributor concern. No piece **closes** a PR (credit theft) nor
|
||||
> **weakens** a test to pass.
|
||||
|
||||
## The family (4 pieces) — and how each runs independently
|
||||
|
||||
| Piece | What it is | When to run | Scope |
|
||||
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| **`/green-prs`** (Solution A) | On-demand scan by the maintainer of the **queue of open PRs** | **Independently, periodically** — and especially **before** a `/generate-release` | Entire PR queue → `release/**` |
|
||||
| **`/validate-release-green`** (Solution C — `npm run check:release-green`) | Validation engine: reproduces the full gate against a branch OR a merge candidate | Independently, at any time | A specific branch or a merge-PR |
|
||||
| **`/babysit <PR#>`** | Drives **live CI** of **one** PR to green | Independently, per PR | A single PR |
|
||||
| **`nightly-release-green.yml`** (Solution D) | Automated nightly workflow; opens issue on HARD red | Automatic (cron) | The active release branch |
|
||||
|
||||
**Short answer to "is this only for releases?":** **no.** `/green-prs` was designed to
|
||||
run **periodically, between releases**. Running independently is the normal use — release is just
|
||||
the moment when running it yields the most value.
|
||||
|
||||
## Solution C — `npm run check:release-green` (the engine)
|
||||
|
||||
Reproduces release-equivalent validation against the current working tree and classifies each red:
|
||||
|
||||
- **HARD** (typecheck, lint errors, unit, vitest, db-rules, public-creds, optional
|
||||
`package-artifact`) → **real defect**; `exit 1`. Fixed on the source branch (TDD, Rule #18).
|
||||
- **DRIFT** (eslint **warnings**, cognitive-complexity, file-size) → ratchet drift accumulated in
|
||||
the cycle, **not the contributor's fault**; it is only reported and **rebaselined by the maintainer at
|
||||
release**. Drift **never** changes the exit code — so it never blocks anyone.
|
||||
|
||||
```bash
|
||||
npm run check:release-green # current branch (working tree)
|
||||
node scripts/quality/validate-release-green.mjs --json # structured output
|
||||
node scripts/quality/validate-release-green.mjs --quick # skips unit+vitest (drift+typecheck+lint only)
|
||||
node scripts/quality/validate-release-green.mjs --with-build # includes package-artifact (slow)
|
||||
```
|
||||
|
||||
Diagnoses and **reports** only (no auto-fix). The fix-to-green orchestration lives in
|
||||
`/green-prs` and `/review-prs`.
|
||||
|
||||
## Solution A — `/green-prs` (the queue scan)
|
||||
|
||||
Procedure (summary — see the `green-prs` skill for details):
|
||||
|
||||
1. **Inventory** the queue of open PRs against the active release branch.
|
||||
2. **Triage** each PR (viable / reject-worthy / needs-author) — reject/needs-author are
|
||||
**reported, not closed** (the author decides).
|
||||
3. For each viable PR, in an **isolated worktree** (Rule #19), bring the PR to the release tip and run
|
||||
`npm run check:release-green`:
|
||||
- **HARD** → fix **on the contributor's branch** via co-authorship (preserves the author's "Merged" status),
|
||||
re-run until all HARDs are cleared.
|
||||
- **DRIFT** → leave it; it will be rebaselined at release.
|
||||
4. **Report** a PR × (verdict, HARD reds, fixed?, DRIFT, release-green now?) table.
|
||||
|
||||
Can **prepare** the queue without merging; only merges when explicitly requested — and never closes a PR.
|
||||
|
||||
## Recommended cadence
|
||||
|
||||
- Run **`/green-prs` periodically** (e.g., weekly) and **always before a
|
||||
`/generate-release`**.
|
||||
- Keep **`nightly-release-green.yml`** (Solution D) as a continuous signal: when it opens a
|
||||
HARD red issue, it is time for a scan.
|
||||
- Use **`/validate-release-green`** ad-hoc to check a branch or a specific merge candidate.
|
||||
- Use **`/babysit <PR#>`** when a specific PR needs to be driven to green on live CI.
|
||||
|
||||
## Relationship to release
|
||||
|
||||
- `/generate-release` calls validation in **Phase 0 (pre-flight)**: rebaselines DRIFT and fixes
|
||||
HARD before opening the release PR.
|
||||
- `/review-prs` uses the release-green gate at the merge decision step (green-before-merge).
|
||||
|
||||
The goal of all pieces is the same: **a green release PR on the first CI run**, instead of surfing
|
||||
reds in 40-minute layers on release day.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: "SQLite Runtime Resolution"
|
||||
---
|
||||
|
||||
# SQLite Runtime Resolution
|
||||
|
||||
OmniRoute resolves its SQLite driver at startup through a 5-step fallback chain:
|
||||
|
||||
1. **Bundled `better-sqlite3`** (via `dependencies` in `package.json`)
|
||||
— fastest, native binary, installed by `npm install` when build tools are present.
|
||||
|
||||
2. **Runtime-installed `better-sqlite3`** (in `~/.omniroute/runtime/`)
|
||||
— installed lazily on first run **OR** by `scripts/build/postinstall.mjs → scripts/postinstall.mjs`.
|
||||
Validates native `.node` magic bytes (ELF / Mach-O / PE) before loading
|
||||
to guard against corrupt or wrong-platform binaries.
|
||||
|
||||
3. **`node:sqlite`** (Node ≥22.5 stdlib) — no native build needed; used when
|
||||
both better-sqlite3 paths fail. Limited feature set.
|
||||
|
||||
4. **`sql.js`** (WASM) — final fallback. Works everywhere but is slower
|
||||
and writes data on an interval rather than synchronously.
|
||||
|
||||
## Why this complexity?
|
||||
|
||||
- **Windows EBUSY**: `npm install -g omniroute@latest` can fail if the previous
|
||||
version's `better_sqlite3.node` is locked by a running process. The runtime
|
||||
install in `~/.omniroute/runtime/` sidesteps the global npm cache.
|
||||
- **No build tools**: Some environments (corporate Windows without VS Build
|
||||
Tools, minimal Docker images) cannot compile `better-sqlite3`. The runtime
|
||||
installer resolves a pre-built binary from the npm registry; the fallback
|
||||
drivers ensure OmniRoute still boots even if that fails.
|
||||
- **Air-gapped systems**: If the npm registry is unreachable, `node:sqlite`
|
||||
or `sql.js` guarantee baseline functionality.
|
||||
|
||||
## Magic-byte validation
|
||||
|
||||
Before loading a runtime-installed `.node` file, OmniRoute reads the first 8
|
||||
bytes and matches against known platform magics:
|
||||
|
||||
| Platform | Bytes (hex) | Label |
|
||||
| --------------------- | ------------- | ----------- |
|
||||
| Linux | `7F 45 4C 46` | `elf` |
|
||||
| macOS 64-bit BE | `FE ED FA CF` | `macho` |
|
||||
| macOS 64-bit LE | `CF FA ED FE` | `macho-le` |
|
||||
| macOS fat (universal) | `CA FE BA BE` | `macho-fat` |
|
||||
| Windows | `4D 5A` (MZ) | `pe` |
|
||||
|
||||
A mismatched magic → file is ignored, fallback continues to the next step.
|
||||
|
||||
## Checking the active driver
|
||||
|
||||
```typescript
|
||||
import { getDriverInfo } from "@/lib/db/core";
|
||||
|
||||
const info = getDriverInfo();
|
||||
// { source: "bundled" | "runtime" | "runtime-installed-now" | "node-sqlite" | "sql-js",
|
||||
// kind: "better-sqlite3" | "node-sqlite" | "sql-js" }
|
||||
```
|
||||
|
||||
## Manual control
|
||||
|
||||
```bash
|
||||
# Skip postinstall warm-up (for fast CI installs)
|
||||
OMNIROUTE_SKIP_POSTINSTALL=1 npm install -g omniroute
|
||||
|
||||
# Force-reinstall runtime better-sqlite3
|
||||
rm -rf ~/.omniroute/runtime
|
||||
omniroute # will reinstall on next start
|
||||
|
||||
# Check what driver is active
|
||||
omniroute config db-info # (if CLI command exists)
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
Implementation:
|
||||
|
||||
- `bin/cli/runtime/magicBytes.mjs` — binary magic-byte validation helpers
|
||||
- `bin/cli/runtime/sqliteRuntime.mjs` — 5-step runtime resolver + lazy installer
|
||||
- `bin/cli/runtime/index.mjs` — startup orchestrator (`warmUpRuntimes()`)
|
||||
- `scripts/postinstall.mjs` — npm post-install hook (non-fatal warm-up)
|
||||
- `src/lib/db/core.ts` — `ensureDbInitialized()` / `getDriverInfo()` exports
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
title: "Tunnels Guide"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Tunnels Guide
|
||||
|
||||
> **Source of truth:** `src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts`, `src/app/api/tunnels/`
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute can expose its local server (`http://localhost:20128`) to the public
|
||||
internet via three tunnel backends. This is useful for:
|
||||
|
||||
- OAuth callbacks from cloud providers (Antigravity, Gemini, Cursor) that need a
|
||||
publicly reachable redirect URL.
|
||||
- Sharing your local instance with teammates without deploying a VM.
|
||||
- Mobile, remote, or cross-network testing.
|
||||
|
||||
All three backends are managed in-process — OmniRoute starts/stops the underlying
|
||||
binary or SDK from the dashboard or REST API. No reverse-proxy or systemd setup
|
||||
is required.
|
||||
|
||||
## Backends at a glance
|
||||
|
||||
| Backend | Persistence | Cost | Setup |
|
||||
| --------------------------- | ------------------------------------------------------ | ----------------- | ----------------------------------------------- |
|
||||
| **Cloudflare Quick Tunnel** | Ephemeral (URL changes each restart) | Free | Zero — auto-installs `cloudflared` |
|
||||
| **ngrok** | Stable while a paid plan or fixed domain is configured | Free tier + paid | Requires ngrok account + authtoken |
|
||||
| **Tailscale Funnel** | Stable per node within your tailnet | Free for personal | Requires Tailscale install + login + Funnel ACL |
|
||||
|
||||
The implementations live in `src/lib/cloudflaredTunnel.ts`,
|
||||
`src/lib/ngrokTunnel.ts`, and `src/lib/tailscaleTunnel.ts`. All three return a
|
||||
common-shaped `status` object with `phase`, `running`, `publicUrl`, `apiUrl`,
|
||||
`targetUrl`, and `lastError` fields, so the dashboard can render them uniformly.
|
||||
|
||||
## 1. Cloudflare Tunnel (Quick Tunnel)
|
||||
|
||||
`src/lib/cloudflaredTunnel.ts` runs `cloudflared tunnel --url
|
||||
http://localhost:<apiPort>` as a child process and parses the assigned
|
||||
`*.trycloudflare.com` URL from stdout.
|
||||
|
||||
Key behaviors:
|
||||
|
||||
- **Auto-install.** On first use, OmniRoute downloads the latest `cloudflared`
|
||||
binary from the official GitHub releases (managed install lives under
|
||||
`DATA_DIR/cloudflared/`). SHA256 of the downloaded asset is verified against the
|
||||
release manifest before execution.
|
||||
- **Quick-tunnel only.** The current implementation runs only the
|
||||
`--url`-style quick tunnel. Named/persistent tunnels (`cloudflared tunnel
|
||||
login` + `cloudflared tunnel route dns ...`) are not orchestrated by
|
||||
OmniRoute. URLs are ephemeral and will change every restart.
|
||||
- **Process supervision.** The cloudflared PID and resolved URL are persisted to
|
||||
`cloudflared-state.json` so the dashboard can resume status across reloads.
|
||||
|
||||
### Enable / disable via REST
|
||||
|
||||
The endpoint uses an `{action: "enable" | "disable"}` body, not separate
|
||||
`start`/`stop` paths. Management auth (admin session or admin API key) is
|
||||
required.
|
||||
|
||||
```bash
|
||||
# Enable
|
||||
curl -X POST http://localhost:20128/api/tunnels/cloudflared \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-d '{"action":"enable"}'
|
||||
|
||||
# Status
|
||||
curl http://localhost:20128/api/tunnels/cloudflared \
|
||||
-H "Cookie: auth_token=..."
|
||||
|
||||
# Disable
|
||||
curl -X POST http://localhost:20128/api/tunnels/cloudflared \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-d '{"action":"disable"}'
|
||||
```
|
||||
|
||||
Or via dashboard: **Settings → Tunnels → Cloudflare**.
|
||||
|
||||
### Optional env vars
|
||||
|
||||
| Variable | Purpose |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `CLOUDFLARED_BIN` | Override the binary path. If set and valid, OmniRoute uses it instead of downloading. |
|
||||
| `CLOUDFLARED_PROTOCOL` / `TUNNEL_TRANSPORT_PROTOCOL` | Transport protocol (default `http2`). |
|
||||
|
||||
## 2. ngrok
|
||||
|
||||
`src/lib/ngrokTunnel.ts` uses the **`@ngrok/ngrok` SDK** (in-process, no CLI
|
||||
subprocess). The native module is imported lazily on first start so platforms
|
||||
without prebuilt binaries do not break the app at boot.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Sign up at <https://ngrok.com>.
|
||||
2. Copy your authtoken from the ngrok dashboard.
|
||||
3. Provide it either via:
|
||||
- `.env`: `NGROK_AUTHTOKEN=<token>`, or
|
||||
- Dashboard: **Settings → Tunnels → ngrok**, or
|
||||
- REST body (one-shot): `{"action":"enable","authToken":"<token>"}`.
|
||||
|
||||
If neither is configured, status returns `phase: "needs_auth"`.
|
||||
|
||||
### Enable / disable via REST
|
||||
|
||||
```bash
|
||||
# Enable (uses NGROK_AUTHTOKEN from env)
|
||||
curl -X POST http://localhost:20128/api/tunnels/ngrok \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-d '{"action":"enable"}'
|
||||
|
||||
# Enable with inline token
|
||||
curl -X POST http://localhost:20128/api/tunnels/ngrok \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-d '{"action":"enable","authToken":"2abc..."}'
|
||||
|
||||
# Status
|
||||
curl http://localhost:20128/api/tunnels/ngrok \
|
||||
-H "Cookie: auth_token=..."
|
||||
|
||||
# Disable
|
||||
curl -X POST http://localhost:20128/api/tunnels/ngrok \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-d '{"action":"disable"}'
|
||||
```
|
||||
|
||||
The response includes the assigned `publicUrl` (e.g.
|
||||
`https://abcd-1234.ngrok-free.app`). Custom domains, regions, and policy rules
|
||||
must be configured in the ngrok dashboard — OmniRoute itself only forwards the
|
||||
local target URL to the SDK.
|
||||
|
||||
## 3. Tailscale Funnel
|
||||
|
||||
`src/lib/tailscaleTunnel.ts` orchestrates the system `tailscale` CLI to expose
|
||||
the local API port via **Funnel** (Tailscale's public-internet egress for serve).
|
||||
It supports the full lifecycle: install, login, daemon start, enable, disable.
|
||||
|
||||
The implementation invokes `tailscale funnel --bg <port>` (background mode). The
|
||||
public URL has the shape `https://<machine>.<tailnet>.ts.net/`.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Install Tailscale (or let OmniRoute do it — see `install` endpoint below).
|
||||
2. Sign in (`tailscale login` or via OmniRoute's `login` endpoint).
|
||||
3. Enable Funnel for your tailnet in the Tailscale admin console:
|
||||
<https://login.tailscale.com/admin/settings/features>.
|
||||
|
||||
On Linux and macOS the daemon (`tailscaled`) requires `sudo` to control. The
|
||||
POST endpoints accept an optional `sudoPassword` field which is forwarded to
|
||||
OmniRoute's MITM password cache (`getCachedPassword` / `setCachedPassword`) for
|
||||
the duration of the call. Windows uses the default service install at
|
||||
`C:\Program Files\Tailscale\tailscale.exe`.
|
||||
|
||||
### REST endpoints
|
||||
|
||||
Tailscale has a richer surface than the other backends because installation,
|
||||
login, daemon, and tunnel are separate concerns.
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| ------------------------------------- | ------ | --------------------------------------------------------------- |
|
||||
| `/api/tunnels/tailscale` | `GET` | Aggregated tunnel status (`phase`, `tunnelUrl`, `apiUrl`, etc.) |
|
||||
| `/api/tunnels/tailscale/check` | `GET` | Lower-level check: installed? logged in? daemon running? |
|
||||
| `/api/tunnels/tailscale/install` | `POST` | Install Tailscale (SSE-streamed progress events) — Linux/macOS |
|
||||
| `/api/tunnels/tailscale/start-daemon` | `POST` | Start `tailscaled` on Linux/macOS |
|
||||
| `/api/tunnels/tailscale/login` | `POST` | Begin login flow; returns `authUrl` to open in a browser |
|
||||
| `/api/tunnels/tailscale/enable` | `POST` | Start the Funnel for the API port |
|
||||
| `/api/tunnels/tailscale/disable` | `POST` | Stop the Funnel |
|
||||
|
||||
All Tailscale endpoints require management auth (see `routeUtils.ts ::
|
||||
requireTailscaleAuth`).
|
||||
|
||||
Example enable:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:20128/api/tunnels/tailscale/enable \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-d '{"sudoPassword":"<linux-pwd>","port":20128}'
|
||||
```
|
||||
|
||||
If Funnel is not enabled in the admin console, the response includes
|
||||
`funnelNotEnabled: true` plus an `enableUrl` to open in a browser.
|
||||
|
||||
### Optional env vars
|
||||
|
||||
| Variable | Purpose |
|
||||
| --------------- | ------------------------------------ |
|
||||
| `TAILSCALE_BIN` | Override the `tailscale` binary path |
|
||||
|
||||
## Endpoint summary
|
||||
|
||||
| Endpoint | Method | Body | Auth |
|
||||
| ------------------------------------- | ------ | ----------------------------------- | ---------- |
|
||||
| `/api/tunnels/cloudflared` | `GET` | — | management |
|
||||
| `/api/tunnels/cloudflared` | `POST` | `{action: "enable" \| "disable"}` | management |
|
||||
| `/api/tunnels/ngrok` | `GET` | — | management |
|
||||
| `/api/tunnels/ngrok` | `POST` | `{action, authToken?}` | management |
|
||||
| `/api/tunnels/tailscale` | `GET` | — | management |
|
||||
| `/api/tunnels/tailscale/check` | `GET` | — | management |
|
||||
| `/api/tunnels/tailscale/install` | `POST` | `{sudoPassword?}` (SSE) | management |
|
||||
| `/api/tunnels/tailscale/start-daemon` | `POST` | `{sudoPassword?}` | management |
|
||||
| `/api/tunnels/tailscale/login` | `POST` | `{hostname?}` | management |
|
||||
| `/api/tunnels/tailscale/enable` | `POST` | `{sudoPassword?, hostname?, port?}` | management |
|
||||
| `/api/tunnels/tailscale/disable` | `POST` | `{sudoPassword?}` | management |
|
||||
|
||||
There is no central `/api/settings/tunnels` endpoint — each backend is
|
||||
independent.
|
||||
|
||||
## OAuth callback considerations
|
||||
|
||||
When you expose OmniRoute through a tunnel, the dashboard and OAuth flows must
|
||||
build callback URLs against the **public** hostname, not `localhost`. Otherwise
|
||||
the OAuth provider redirects the user back to a URL its servers cannot reach,
|
||||
and the handshake fails.
|
||||
|
||||
Dashboard edits and settings saves do not require pinning the tunnel hostname in
|
||||
`NEXT_PUBLIC_BASE_URL`. The authenticated dashboard sends same-origin unsafe
|
||||
requests with a session-bound CSRF token, so ephemeral Cloudflare Quick Tunnel
|
||||
hosts can still be used for normal UI management after logging in.
|
||||
|
||||
Set:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_BASE_URL=https://<your-tunnel-host>
|
||||
```
|
||||
|
||||
and restart OmniRoute before initiating OAuth. For ephemeral Cloudflare Quick
|
||||
Tunnels the URL changes after every restart, so prefer ngrok with a reserved
|
||||
domain or Tailscale Funnel for production OAuth use.
|
||||
|
||||
## Health and monitoring
|
||||
|
||||
The dashboard surfaces tunnel state under **Settings → Tunnels**:
|
||||
|
||||
- Active backend(s) and current `phase` (`stopped`, `starting`, `running`,
|
||||
`needs_auth`, `error`).
|
||||
- The current public URL and the derived API URL (`<publicUrl>/v1`).
|
||||
- The local target URL the tunnel is forwarding to.
|
||||
- Last error message, if any.
|
||||
|
||||
For programmatic monitoring poll the per-backend `GET` endpoints. Running more
|
||||
than one backend simultaneously is allowed; OmniRoute will track each
|
||||
independently.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "cloudflared binary not found"
|
||||
|
||||
OmniRoute attempts to auto-install on first use. If the install is blocked
|
||||
(restricted network, no GitHub access), download `cloudflared` manually from
|
||||
<https://github.com/cloudflare/cloudflared/releases> and set
|
||||
`CLOUDFLARED_BIN=/path/to/cloudflared`.
|
||||
|
||||
### "ngrok: authtoken required"
|
||||
|
||||
`phase: "needs_auth"` means no authtoken was found. Set `NGROK_AUTHTOKEN` in
|
||||
`.env`, configure it via the dashboard, or pass `authToken` in the enable POST
|
||||
body.
|
||||
|
||||
### "tailscale: funnel not enabled"
|
||||
|
||||
When the enable response includes `funnelNotEnabled: true`, Funnel is disabled
|
||||
for your tailnet. Open the returned `enableUrl` (or the admin console feature
|
||||
page) and toggle Funnel on.
|
||||
|
||||
### Tunnel URL changes break OAuth
|
||||
|
||||
Use ngrok with a reserved domain or Tailscale Funnel (both stable per-node).
|
||||
Cloudflare Quick Tunnels are ephemeral by design and not recommended for
|
||||
long-lived OAuth callbacks.
|
||||
|
||||
### Permission denied on Linux/macOS for Tailscale
|
||||
|
||||
`tailscaled` needs root. Provide `sudoPassword` to the relevant POST endpoint,
|
||||
or run the daemon yourself (`sudo systemctl start tailscaled`).
|
||||
|
||||
## See also
|
||||
|
||||
- [PROXY_GUIDE.md](./PROXY_GUIDE.md) — outbound proxy (1proxy, SOCKS5, HTTP) for
|
||||
egress traffic.
|
||||
- [ENVIRONMENT.md](../reference/ENVIRONMENT.md) — full list of env vars including
|
||||
`NEXT_PUBLIC_BASE_URL`.
|
||||
- [FLY_IO_DEPLOYMENT_GUIDE.md](./FLY_IO_DEPLOYMENT_GUIDE.md),
|
||||
[DOCKER_GUIDE.md](../guides/DOCKER_GUIDE.md) — alternatives to tunneling for stable
|
||||
public hosting.
|
||||
- Source: `src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts`,
|
||||
`src/app/api/tunnels/`.
|
||||
@@ -0,0 +1,424 @@
|
||||
---
|
||||
title: "OmniRoute — Deployment Guide on VM with Cloudflare"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute — Deployment Guide on VM with Cloudflare
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](./VM_DEPLOYMENT_GUIDE.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇪🇸 [Español](../i18n/es/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇫🇷 [Français](../i18n/fr/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇮🇹 [Italiano](../i18n/it/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇷🇺 [Русский](../i18n/ru/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇩🇪 [Deutsch](../i18n/de/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇮🇳 [हिन्दी](../i18n/in/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇹🇭 [ไทย](../i18n/th/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇸🇦 [العربية](../i18n/ar/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇯🇵 [日本語](../i18n/ja/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇧🇬 [Български](../i18n/bg/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇩🇰 [Dansk](../i18n/da/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇫🇮 [Suomi](../i18n/fi/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇮🇱 [עברית](../i18n/he/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇭🇺 [Magyar](../i18n/hu/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇰🇷 [한국어](../i18n/ko/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇳🇴 [Norsk](../i18n/no/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇷🇴 [Română](../i18n/ro/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇵🇱 [Polski](../i18n/pl/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇸🇪 [Svenska](../i18n/sv/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇵🇭 [Filipino](../i18n/phi/docs/ops/VM_DEPLOYMENT_GUIDE.md) | 🇨🇿 [Čeština](../i18n/cs/docs/ops/VM_DEPLOYMENT_GUIDE.md)
|
||||
|
||||
Complete guide to install and configure OmniRoute on a VM (VPS) with domain managed via Cloudflare.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Item | Minimum | Recommended |
|
||||
| ---------- | ------------------------ | ---------------- |
|
||||
| **CPU** | 1 vCPU | 2 vCPU |
|
||||
| **RAM** | 1 GB | 2 GB |
|
||||
| **Disk** | 10 GB SSD | 25 GB SSD |
|
||||
| **OS** | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
|
||||
| **Domain** | Registered on Cloudflare | — |
|
||||
| **Docker** | Docker Engine 24+ | Docker 27+ |
|
||||
|
||||
**Tested providers**: Akamai (Linode), DigitalOcean, Vultr, Hetzner, AWS Lightsail.
|
||||
|
||||
---
|
||||
|
||||
## 1. Configure the VM
|
||||
|
||||
### 1.1 Create the instance
|
||||
|
||||
On your preferred VPS provider:
|
||||
|
||||
- Choose Ubuntu 24.04 LTS
|
||||
- Select the minimum plan (1 vCPU / 1 GB RAM)
|
||||
- Set a strong root password or configure SSH key
|
||||
- Note the **public IP** (e.g., `203.0.113.10`)
|
||||
|
||||
### 1.2 Connect via SSH
|
||||
|
||||
```bash
|
||||
ssh root@203.0.113.10
|
||||
```
|
||||
|
||||
### 1.3 Update the system
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
```
|
||||
|
||||
### 1.4 Install Docker
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
apt install -y ca-certificates curl gnupg
|
||||
|
||||
# Add official Docker repository
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $ (. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt update
|
||||
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
```
|
||||
|
||||
### 1.5 Install nginx
|
||||
|
||||
```bash
|
||||
apt install -y nginx
|
||||
```
|
||||
|
||||
### 1.6 Configure Firewall (UFW)
|
||||
|
||||
```bash
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow 22/tcp # SSH
|
||||
ufw allow 80/tcp # HTTP (redirect)
|
||||
ufw allow 443/tcp # HTTPS
|
||||
ufw enable
|
||||
```
|
||||
|
||||
> **Tip**: For maximum security, restrict ports 80 and 443 to Cloudflare IPs only. See the [Advanced Security](#advanced-security) section.
|
||||
|
||||
---
|
||||
|
||||
## 2. Install OmniRoute
|
||||
|
||||
### 2.1 Create configuration directory
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/omniroute
|
||||
```
|
||||
|
||||
### 2.2 Create environment variables file
|
||||
|
||||
```bash
|
||||
cat > /opt/omniroute/.env << 'EOF'
|
||||
# === Security ===
|
||||
JWT_SECRET=CHANGE-TO-A-UNIQUE-64-CHAR-SECRET-KEY
|
||||
INITIAL_PASSWORD=YourSecurePassword123!
|
||||
API_KEY_SECRET=REPLACE-WITH-ANOTHER-SECRET-KEY
|
||||
STORAGE_ENCRYPTION_KEY=REPLACE-WITH-THIRD-SECRET-KEY
|
||||
STORAGE_ENCRYPTION_KEY_VERSION=v1
|
||||
MACHINE_ID_SALT=CHANGE-TO-A-UNIQUE-SALT
|
||||
OMNIROUTE_WS_BRIDGE_SECRET=REPLACE-WITH-WS-BRIDGE-SECRET # REQUIRED em produção: usado pelo Codex Responses WS bridge
|
||||
|
||||
# === App ===
|
||||
PORT=20128
|
||||
NODE_ENV=production
|
||||
HOSTNAME=0.0.0.0
|
||||
DATA_DIR=/app/data
|
||||
APP_LOG_TO_FILE=true
|
||||
AUTH_COOKIE_SECURE=true
|
||||
REQUIRE_API_KEY=false
|
||||
|
||||
# === URLs (change to your domain) ===
|
||||
# Internal server-to-server base URL for scheduled jobs / self-fetches.
|
||||
BASE_URL=http://127.0.0.1:20128
|
||||
# Browser-facing URL used for OAuth callbacks, dashboard links, and generated public URLs.
|
||||
NEXT_PUBLIC_BASE_URL=https://llms.seudominio.com
|
||||
# Optional explicit public origin override for generated public asset URLs.
|
||||
# OMNIROUTE_PUBLIC_BASE_URL=https://llms.seudominio.com
|
||||
|
||||
# === Cloud Sync (optional) ===
|
||||
# CLOUD_URL=https://cloud.omniroute.online
|
||||
# NEXT_PUBLIC_CLOUD_URL=https://cloud.omniroute.online
|
||||
EOF
|
||||
```
|
||||
|
||||
> ⚠️ **IMPORTANT**: Generate unique secret keys! Use `openssl rand -hex 32` for each key.
|
||||
|
||||
### 2.3 Start the container
|
||||
|
||||
```bash
|
||||
docker pull diegosouzapw/omniroute:latest
|
||||
|
||||
docker run -d \
|
||||
--name omniroute \
|
||||
--restart unless-stopped \
|
||||
--env-file /opt/omniroute/.env \
|
||||
-p 20128:20128 \
|
||||
-v omniroute-data:/app/data \
|
||||
diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
### 2.4 Verify that it is running
|
||||
|
||||
```bash
|
||||
docker ps | grep omniroute
|
||||
docker logs omniroute --tail 20
|
||||
```
|
||||
|
||||
It should display: `[DB] SQLite database ready` and `listening on port 20128`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configure nginx (Reverse Proxy)
|
||||
|
||||
### 3.1 Generate SSL certificate (Cloudflare Origin)
|
||||
|
||||
In the Cloudflare dashboard:
|
||||
|
||||
1. Go to **SSL/TLS → Origin Server**
|
||||
2. Click **Create Certificate**
|
||||
3. Keep the defaults (15 years, \*.yourdomain.com)
|
||||
4. Copy the **Origin Certificate** and the **Private Key**
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/nginx/ssl
|
||||
|
||||
# Paste the certificate
|
||||
nano /etc/nginx/ssl/origin.crt
|
||||
|
||||
# Paste the private key
|
||||
nano /etc/nginx/ssl/origin.key
|
||||
|
||||
chmod 600 /etc/nginx/ssl/origin.key
|
||||
```
|
||||
|
||||
### 3.2 Nginx Configuration
|
||||
|
||||
```bash
|
||||
cat > /etc/nginx/sites-available/omniroute << 'NGINX'
|
||||
# Default server — blocks direct access via IP
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
ssl_certificate /etc/nginx/ssl/origin.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/origin.key;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
|
||||
# OmniRoute — HTTPS
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name llms.yourdomain.com; # Change to your domain
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/origin.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/origin.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:20128;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# SSE (Server-Sent Events) — streaming AI responses
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name llms.yourdomain.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
NGINX
|
||||
```
|
||||
|
||||
Keep reverse-proxy stream timeouts aligned with your OmniRoute timeout env vars. If you raise
|
||||
`FETCH_TIMEOUT_MS` / `STREAM_IDLE_TIMEOUT_MS`, raise `proxy_read_timeout` / `proxy_send_timeout`
|
||||
above the same threshold.
|
||||
|
||||
OmniRoute uses `NEXT_PUBLIC_BASE_URL` as the canonical browser-facing origin for OAuth
|
||||
callbacks and generated public links. Authenticated dashboard writes use same-origin requests
|
||||
plus session-bound CSRF protection, so they do not require a static public base URL. The
|
||||
`X-Forwarded-*` headers above are still useful routing metadata, but they are not a replacement
|
||||
for setting the explicit public URL when OAuth or generated browser links need one. Only enable
|
||||
`OMNIROUTE_TRUST_PROXY` if OmniRoute is not directly reachable by clients and your proxy
|
||||
strips/rebuilds incoming forwarded headers.
|
||||
|
||||
### 3.3 Enable and Test
|
||||
|
||||
```bash
|
||||
# Remove default configuration
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Enable OmniRoute
|
||||
ln -sf /etc/nginx/sites-available/omniroute /etc/nginx/sites-enabled/omniroute
|
||||
|
||||
# Test and reload
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Configure Cloudflare DNS
|
||||
|
||||
### 4.1 Add DNS record
|
||||
|
||||
In the Cloudflare dashboard → DNS:
|
||||
|
||||
| Type | Name | Content | Proxy |
|
||||
| ---- | ------ | ---------------------- | ---------- |
|
||||
| A | `llms` | `203.0.113.10` (VM IP) | ✅ Proxied |
|
||||
|
||||
### 4.2 Configure SSL
|
||||
|
||||
Under **SSL/TLS → Overview**:
|
||||
|
||||
- Mode: **Full (Strict)**
|
||||
|
||||
Under **SSL/TLS → Edge Certificates**:
|
||||
|
||||
- Always Use HTTPS: ✅ On
|
||||
- Minimum TLS Version: TLS 1.2
|
||||
- Automatic HTTPS Rewrites: ✅ On
|
||||
|
||||
### 4.3 Testing
|
||||
|
||||
```bash
|
||||
curl -sI https://llms.seudominio.com/health
|
||||
# Should return HTTP/2 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Operations and Maintenance
|
||||
|
||||
### Upgrade to a new version
|
||||
|
||||
```bash
|
||||
docker pull diegosouzapw/omniroute:latest
|
||||
docker stop omniroute && docker rm omniroute
|
||||
docker run -d --name omniroute --restart unless-stopped \
|
||||
--env-file /opt/omniroute/.env \
|
||||
-p 20128:20128 \
|
||||
-v omniroute-data:/app/data \
|
||||
diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
### View logs
|
||||
|
||||
```bash
|
||||
docker logs -f omniroute # Real-time stream
|
||||
docker logs omniroute --tail 50 # Last 50 lines
|
||||
```
|
||||
|
||||
### Manual database backup
|
||||
|
||||
```bash
|
||||
# Copy data from the volume to the host
|
||||
docker cp omniroute:/app/data ./backup-$(date +%F)
|
||||
|
||||
# Or compress the entire volume
|
||||
docker run --rm -v omniroute-data:/data -v $(pwd):/backup \
|
||||
alpine tar czf /backup/omniroute-data-$(date +%F).tar.gz /data
|
||||
```
|
||||
|
||||
### Restore from backup
|
||||
|
||||
```bash
|
||||
docker stop omniroute
|
||||
docker run --rm -v omniroute-data:/data -v $(pwd):/backup \
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/omniroute-data-YYYY-MM-DD.tar.gz -C /"
|
||||
docker start omniroute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Advanced Security
|
||||
|
||||
### Restrict nginx to Cloudflare IPs
|
||||
|
||||
```bash
|
||||
cat > /etc/nginx/cloudflare-ips.conf << 'CF'
|
||||
# Cloudflare IPv4 ranges — update periodically
|
||||
# https://www.cloudflare.com/ips-v4/
|
||||
set_real_ip_from 173.245.48.0/20;
|
||||
set_real_ip_from 103.21.244.0/22;
|
||||
set_real_ip_from 103.22.200.0/22;
|
||||
set_real_ip_from 103.31.4.0/22;
|
||||
set_real_ip_from 141.101.64.0/18;
|
||||
set_real_ip_from 108.162.192.0/18;
|
||||
set_real_ip_from 190.93.240.0/20;
|
||||
set_real_ip_from 188.114.96.0/20;
|
||||
set_real_ip_from 197.234.240.0/22;
|
||||
set_real_ip_from 198.41.128.0/17;
|
||||
set_real_ip_from 162.158.0.0/15;
|
||||
set_real_ip_from 104.16.0.0/13;
|
||||
set_real_ip_from 104.24.0.0/14;
|
||||
set_real_ip_from 172.64.0.0/13;
|
||||
set_real_ip_from 131.0.72.0/22;
|
||||
real_ip_header CF-Connecting-IP;
|
||||
CF
|
||||
```
|
||||
|
||||
Add the following to `nginx.conf` inside the `http {}` block:
|
||||
|
||||
```nginx
|
||||
include /etc/nginx/cloudflare-ips.conf;
|
||||
```
|
||||
|
||||
### Install fail2ban
|
||||
|
||||
```bash
|
||||
apt install -y fail2ban
|
||||
systemctl enable fail2ban
|
||||
systemctl start fail2ban
|
||||
|
||||
# Check status
|
||||
fail2ban-client status sshd
|
||||
```
|
||||
|
||||
### Block direct access to the Docker port
|
||||
|
||||
```bash
|
||||
# Prevent direct external access to port 20128
|
||||
iptables -I DOCKER-USER -p tcp --dport 20128 -j DROP
|
||||
iptables -I DOCKER-USER -i lo -p tcp --dport 20128 -j ACCEPT
|
||||
|
||||
# Persist the rules
|
||||
apt install -y iptables-persistent
|
||||
netfilter-persistent save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Deploy to Cloudflare Workers (Optional)
|
||||
|
||||
For remote access via Cloudflare Workers (without exposing the VM directly):
|
||||
|
||||
```bash
|
||||
# In the local repository
|
||||
cd omnirouteCloud
|
||||
npm install
|
||||
npx wrangler login
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
See also [TUNNELS_GUIDE.md](./TUNNELS_GUIDE.md) for the in-repo Cloudflare Tunnel walkthrough. The standalone `omnirouteCloud/` worker lives in a separate companion repo.
|
||||
|
||||
---
|
||||
|
||||
## Port Summary
|
||||
|
||||
| Port | Service | Access |
|
||||
| ----- | ----------- | -------------------------- |
|
||||
| 22 | SSH | Public (with fail2ban) |
|
||||
| 80 | nginx HTTP | Redirect → HTTPS |
|
||||
| 443 | nginx HTTPS | Via Cloudflare Proxy |
|
||||
| 20128 | OmniRoute | Localhost only (via nginx) |
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Operations",
|
||||
"pages": [
|
||||
"RELEASE_CHECKLIST",
|
||||
"RELEASE_GREEN",
|
||||
"VM_DEPLOYMENT_GUIDE",
|
||||
"FLY_IO_DEPLOYMENT_GUIDE",
|
||||
"TUNNELS_GUIDE",
|
||||
"PROXY_GUIDE",
|
||||
"SQLITE_RUNTIME",
|
||||
"COVERAGE_PLAN"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user