Files
worldmonitor/settings.html
Elie Habib a388afe400 feat: API key gating for desktop cloud fallback + registration (#215)
* feat: API key gating for desktop cloud fallback + registration system

Gate desktop cloud fallback behind WORLDMONITOR_API_KEY — desktop users
need a valid key for cloud access, otherwise operate local-only (sidecar).
Add email registration system via Convex DB for future key distribution.

Client-side: installRuntimeFetchPatch() checks key presence before
allowing cloud fallback, with secretsReady promise + 2s timeout.
Server-side: origin-aware validation in sebuf gateway — desktop origins
require key, web origins pass through.

- Add WORLDMONITOR_API_KEY to 3-place secret system (Rust, TS, sidecar)
- New "World Monitor" settings tab with key input + registration form
- New api/_api-key.js server-side validation (origin-aware)
- New api/register-interest.js edge function with rate limiting
- Convex DB schema + mutation for email registration storage
- CORS headers updated for X-WorldMonitor-Key + Authorization
- E2E tests for key gate (blocked without key, allowed with key)
- Deployment docs (API_KEY_DEPLOYMENT.md) + updated desktop config docs

* fix: harden worldmonitor key + registration input handling

* fix: show invalid WorldMonitor API key status

* fix: simplify key validation, trim registration checks, add env example vars

- Inline getValidKeys() in _api-key.js
- Remove redundant type checks in register-interest.js
- Simplify WorldMonitorTab status to present/missing
- Add WORLDMONITOR_VALID_KEYS and CONVEX_URL to .env.example

* feat(sidecar): integrate proto gateway bundle into desktop build

The sidecar's buildRouteTable() only discovers .js files, so the proto
gateway at api/[domain]/v1/[rpc].ts was invisible — all 45 sebuf RPCs
returned 404 in the desktop app. Wire the existing build script into
Tauri's build commands and add esbuild as an explicit devDependency.
2026-02-21 10:36:23 +00:00

62 lines
4.1 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>World Monitor Settings</title>
<script>(function(){try{var t=localStorage.getItem('worldmonitor-theme');if(t==='light')document.documentElement.dataset.theme='light';}catch(e){}document.documentElement.classList.add('no-transition');})()</script>
</head>
<body style="background:#1a1c1e;color:#e8eaed;margin:0">
<div class="settings-shell">
<div class="settings-tabs" role="tablist">
<button class="settings-tab active" role="tab" aria-selected="true" aria-controls="tabPanelLLMs" data-tab="llms">LLMs</button>
<button class="settings-tab" role="tab" aria-selected="false" aria-controls="tabPanelKeys" data-tab="keys">API Keys</button>
<button class="settings-tab" role="tab" aria-selected="false" aria-controls="tabPanelDebug" data-tab="debug">Debug &amp; Logs</button>
<button class="settings-tab" role="tab" aria-selected="false" aria-controls="tabPanelWorldMonitor" data-tab="worldmonitor">World Monitor</button>
</div>
<p id="settingsActionStatus" class="settings-action-status" aria-live="polite"></p>
<div class="settings-tab-panels">
<div id="tabPanelLLMs" class="settings-tab-panel active" role="tabpanel">
<main id="llmApp" class="settings-content"><div style="display:flex;align-items:center;justify-content:center;padding:60px 0;color:#9aa0a6;font-size:14px;gap:10px"><svg width="20" height="20" viewBox="0 0 24 24" style="animation:spin 1s linear infinite"><style>@keyframes spin{to{transform:rotate(360deg)}}</style><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="31 31"/></svg>Loading...</div></main>
</div>
<div id="tabPanelKeys" class="settings-tab-panel" role="tabpanel">
<main id="apiKeysApp" class="settings-content"><div style="display:flex;align-items:center;justify-content:center;padding:60px 0;color:#9aa0a6;font-size:14px;gap:10px"><svg width="20" height="20" viewBox="0 0 24 24" style="animation:spin 1s linear infinite"><style>@keyframes spin{to{transform:rotate(360deg)}}</style><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="31 31"/></svg>Loading...</div></main>
</div>
<div id="tabPanelDebug" class="settings-tab-panel" role="tabpanel">
<div class="debug-actions">
<button id="openLogsBtn" type="button">Open Logs Folder</button>
<button id="openSidecarLogBtn" type="button">Open API Log</button>
</div>
<section class="settings-diagnostics" id="diagnosticsSection">
<header class="diag-header">
<h2>Diagnostics</h2>
<div class="diag-toggles">
<label><input type="checkbox" id="verboseApiLog"> Verbose Sidecar Log</label>
<label><input type="checkbox" id="fetchDebugLog"> Frontend Fetch Debug</label>
</div>
</header>
<div class="diag-traffic-bar">
<h3>API Traffic <span id="trafficCount"></span></h3>
<div class="diag-traffic-controls">
<label><input type="checkbox" id="autoRefreshLog" checked> Auto</label>
<button id="refreshLogBtn" type="button">Refresh</button>
<button id="clearLogBtn" type="button">Clear</button>
</div>
</div>
<div id="trafficLog" class="diag-traffic-log"></div>
</section>
</div>
<div id="tabPanelWorldMonitor" class="settings-tab-panel" role="tabpanel">
<main id="worldmonitorApp" class="settings-content"></main>
</div>
</div>
<footer class="settings-footer">
<button id="cancelBtn" type="button" class="settings-btn settings-btn-secondary">Cancel</button>
<button id="okBtn" type="button" class="settings-btn settings-btn-primary">OK</button>
</footer>
</div>
<script type="module" src="/src/settings-main.ts"></script>
</body>
</html>