mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
fix(oauth): route zed-hosted native-app callback back to the dashboard port (#10517)
* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * Hide health-check excluded models from /v1/models catalog (#10026) Mirror the request-time exclusion rule (provider_specific_data.excludedModels) in the unified catalog builder: a model is hidden when its provider has connections but none of them is eligible for it. Applied across the PROVIDER_MODELS, synced, custom, alias-backed, and managed-fallback loops so ghost models no longer appear as available. Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com> * fix(models): memoize getModelsDevPricing (event loop / healthz) (#10055) * fix(models): memoize getModelsDevPricing for /v1/models catalog resolveCatalogPricing called getModelsDevPricing once per model while building GET /v1/models. Each call re-scanned models_dev_pricing and JSON.parsed every row (~10k SQL scans + multi-GB parse work), pegging the event loop so even /healthz timed out (#9685, #10052). Memoize the parsed map until saveModelsDevPricing / clearModelsDevPricing and add a unit test for invalidation. Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> * fix(db): invalidate modelsDevPricing cache on DB reset (#10055) Copilot review fixes: 1. Register invalidateModelsDevPricingCache() with DB state reset system so resetDbInstance() clears the process-local memo, preventing stale pricing data from surviving across DB reset/restore operations. 2. Add test assertion verifying DB reset bypasses the memo (Copilot #10055). The process-local memo at modelsDevSync.ts:204 caches getModelsDevPricing() results until saveModelsDevPricing()/clearModelsDevPricing() to avoid re-scanning all pricing rows on every /v1/models request. Without this hook, backup restore and test DB resets would serve stale cached data from the previous connection. Tests: npm run test:unit:serial -- tests/unit/modelsDevSync-extended.test.ts --------- Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> * fix(oauth): route Zed hosted sign-in callback back to the dashboard port Zed's native-app sign-in always redirects the browser to the loopback port sent as native_app_port (hardcoded default 58443), where nothing listens: the browser shows "site can't be reached" and the login looks broken even though the token is in the URL. The manual paste fallback was broken too - handleManualSubmit requires a ?code= param that Zed's callback (user_id + access_token) never carries, so the flow could never complete. - zed-hosted: derive native_app_port from the dashboard's own loopback port so the redirect lands back on OmniRoute; remote/LAN origins keep the old default port and the paste flow - app root: forward ?user_id=...&access_token=... to the /callback relay instead of dropping the query string on the /dashboard redirect - /callback relay: recognize the Zed payload (no code param) and relay the full URL as the exchange payload; allow postMessage to both loopback spellings (localhost/127.0.0.1) of the same port - OAuthModal: zed-hosted popup auto-completes on true localhost; the manual paste path passes the full URL through to the exchange instead of erroring with "No authorization code found" - manual input panel: zed-hosted-specific placeholder and hint - tests: extend the postMessage scope guard with the loopback same-port trusted origins * changelog: fragment for #10517 * fix(oauth): derive Zed native_app_port from server config, not browser scheme/port resolveDashboardLoopbackPort() previously re-derived the dashboard's loopback port from the browser-supplied redirectUri (window.location.port || protocol === "https:" ? "443" : "80"), which produced http://127.0.0.1:443/ native-app redirects when the dashboard was reached over HTTPS on its implicit default port (e.g. behind a local TLS-terminating reverse proxy) - a scheme/port mismatch, since Zed's own redirect is always plain http and nothing serves plain HTTP on 443 in that scenario. This code runs server-side (in the OAuth authorize API route), so once the redirect URI's hostname is confirmed loopback it now uses the OmniRoute process's own authoritative listening port via getRuntimePorts() (OMNIROUTE_PORT/PORT/DASHBOARD_PORT) instead of re-deriving it from the browser-observed scheme/port. Non-loopback (remote/LAN) redirect URIs still return null and fall back to the manual paste flow. Adds tests/unit/zed-hosted-loopback-port-derivation.test.ts (8 cases) covering the port-derivation logic directly, including the HTTPS-default-port mismatch scenario that motivated this fix, env-var precedence, IPv6 loopback, non-loopback/remote fallback, and buildAuthUrl's native_app_port wiring. Also rebaselines config/quality/file-size-baseline.json for OAuthModal.tsx's own growth from this PR's earlier commit (1134->1149 gate units) - legitimate zed-hosted callback wiring at the existing provider-switch chokepoint, not extractable without a broader modal decomposition (tracked in #3501). The live Zed OAuth handshake itself (root -> /callback -> OAuthModal exchange against the real zed.dev endpoint) still needs a documented VPS smoke test per Hard Rule #18; this fix covers the TDD-able port-derivation logic that motivated the change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: ritheshcn25 <rithesh.chandran@snb.ca> Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com> Co-authored-by: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -456,11 +456,17 @@ export default function OAuthModal({
|
||||
// Claude Code and Cline OAuth flows can finish on provider-hosted pages that
|
||||
// show an auth code instead of redirecting back to OmniRoute.
|
||||
// Start directly in manual mode so users always have an input to paste code/url.
|
||||
// zed-hosted's native-app sign-in always redirects the browser to a local
|
||||
// 127.0.0.1:<port> callback that OmniRoute never listens on (the port is
|
||||
// arbitrary and unrelated to the dashboard's own port) — nothing can
|
||||
// auto-close the popup, so always show the manual paste-URL input.
|
||||
if (provider === "claude" || provider === "cline" || provider === "zed-hosted") {
|
||||
// zed-hosted's native-app sign-in redirects the browser to a local
|
||||
// 127.0.0.1:<native_app_port> callback. On true localhost that port IS the
|
||||
// dashboard's own (buildAuthUrl reuses it), so the redirect lands on the
|
||||
// /callback relay and the popup flow auto-completes. Elsewhere (LAN/remote)
|
||||
// the port is unreachable — nothing can auto-close the popup, so always
|
||||
// show the manual paste-URL input.
|
||||
if (
|
||||
provider === "claude" ||
|
||||
provider === "cline" ||
|
||||
(provider === "zed-hosted" && !isTrueLocalhost)
|
||||
) {
|
||||
forceManual = true;
|
||||
}
|
||||
|
||||
@@ -880,6 +886,17 @@ export default function OAuthModal({
|
||||
}
|
||||
|
||||
const input = callbackUrl.trim();
|
||||
|
||||
// zed-hosted: the native-app callback (http://127.0.0.1:<port>/?user_id=...&access_token=...)
|
||||
// carries no ?code= param — the FULL pasted URL (or JSON/query blob) is the
|
||||
// payload. zed-hosted's exchangeToken parses user_id/access_token out of it
|
||||
// and RSA-decrypts the token with the private key held in codeVerifier, so
|
||||
// skip the generic code/state extraction below.
|
||||
if (provider === "zed-hosted") {
|
||||
await exchangeTokens(input, authData?.state || null);
|
||||
return;
|
||||
}
|
||||
|
||||
let code = null;
|
||||
let state = authData?.state || null;
|
||||
let errorParam = null;
|
||||
|
||||
@@ -364,13 +364,23 @@ export function OAuthManualInputPanel({
|
||||
code: (chunks) => <code className="font-mono">{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
{provider === "zed-hosted" && (
|
||||
<p className="text-xs text-amber-500 mb-2">
|
||||
After signing in, Zed redirects to a local address like{" "}
|
||||
<code className="font-mono">http://127.0.0.1:<port>/?user_id=...</code> which the
|
||||
browser may show as unreachable — that is expected. Copy the FULL URL from the
|
||||
browser address bar (the access token is inside it) and paste it above.
|
||||
</p>
|
||||
)}
|
||||
<Input
|
||||
value={callbackUrl}
|
||||
onChange={(event) => onCallbackUrlChange(event.target.value)}
|
||||
placeholder={
|
||||
provider === "claude" || provider === "cline"
|
||||
? "code#state or /callback?code=..."
|
||||
: placeholderUrl
|
||||
: provider === "zed-hosted"
|
||||
? "http://127.0.0.1:<port>/?user_id=...&access_token=..."
|
||||
: placeholderUrl
|
||||
}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user