Files
OmniRoute/tests/unit/zed-hosted-loopback-port-derivation.test.ts
phatchau036 72eff76910 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>
2026-08-18 11:56:00 -03:00

129 lines
5.5 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
/**
* Regression coverage for #10517.
*
* Zed's native-app sign-in always redirects the browser to
* `http://127.0.0.1:<native_app_port>/`, ignoring any path/redirect_uri we send.
* `zed-hosted.ts::buildAuthUrl` reuses the dashboard's own loopback port as
* native_app_port so that redirect lands back on OmniRoute instead of a dead
* "site can't be reached" page.
*
* Before this fix, the port was re-derived from the browser-supplied
* `redirectUri` string (`OAuthModal.tsx`'s `window.location.port ||
* (protocol === "https:" ? "443" : "80")` fallback), which produced
* `http://127.0.0.1:443/` when the dashboard was reached over HTTPS on its
* default port (e.g. behind a local TLS-terminating reverse proxy) — a scheme
* mismatch, since nothing serves plain HTTP on 443 and Zed's redirect is
* always plain http regardless of how the browser reached the dashboard.
*
* The fix runs server-side (this code executes in the Next.js API route, not
* the browser) and derives the port from the OmniRoute process's own
* authoritative listening port (`getRuntimePorts()`, sourced from
* OMNIROUTE_PORT/PORT/DASHBOARD_PORT) once the redirect URI's hostname is
* confirmed loopback — no longer trusting the browser-observed scheme/port.
*/
const originalEnv = {
OMNIROUTE_PORT: process.env.OMNIROUTE_PORT,
PORT: process.env.PORT,
DASHBOARD_PORT: process.env.DASHBOARD_PORT,
};
function resetPortEnv() {
delete process.env.OMNIROUTE_PORT;
delete process.env.PORT;
delete process.env.DASHBOARD_PORT;
}
test.after(() => {
resetPortEnv();
for (const [key, value] of Object.entries(originalEnv)) {
if (value !== undefined) process.env[key] = value;
}
});
const { __test__ } = await import("../../src/lib/oauth/providers/zed-hosted.ts");
const { resolveDashboardLoopbackPort } = __test__;
test("resolveDashboardLoopbackPort: loopback hostname over HTTPS on the default port resolves via server config, not a guessed 443", () => {
resetPortEnv();
process.env.OMNIROUTE_PORT = "20128";
// This is the exact shape OAuthModal.tsx's buggy fallback used to produce
// for the true-localhost + default-port case (scheme hardcoded to "http"
// regardless of the real protocol, port guessed from the protocol default).
// Even with a scheme/port combination that does not reflect reality, the
// hostname alone is enough — the real port comes from server config.
const port = resolveDashboardLoopbackPort("http://localhost:443/callback");
assert.equal(port, 20128, "must use the server's own configured port, never the guessed 443");
});
test("resolveDashboardLoopbackPort: respects OMNIROUTE_PORT override", () => {
resetPortEnv();
process.env.OMNIROUTE_PORT = "31415";
assert.equal(resolveDashboardLoopbackPort("http://127.0.0.1:20128/callback"), 31415);
assert.equal(resolveDashboardLoopbackPort("http://localhost/callback"), 31415);
});
test("resolveDashboardLoopbackPort: falls back to PORT then DASHBOARD_PORT precedence like getRuntimePorts", () => {
resetPortEnv();
process.env.PORT = "9000";
assert.equal(resolveDashboardLoopbackPort("http://localhost:20128/callback"), 9000);
resetPortEnv();
process.env.DASHBOARD_PORT = "9500";
assert.equal(resolveDashboardLoopbackPort("http://127.0.0.1:20128/callback"), 9500);
});
test("resolveDashboardLoopbackPort: IPv6 loopback literal resolves to the server port", () => {
resetPortEnv();
process.env.OMNIROUTE_PORT = "20128";
assert.equal(resolveDashboardLoopbackPort("http://[::1]:20128/callback"), 20128);
});
test("resolveDashboardLoopbackPort: non-loopback (remote/LAN) redirect URIs return null", () => {
resetPortEnv();
process.env.OMNIROUTE_PORT = "20128";
assert.equal(resolveDashboardLoopbackPort("https://omniroute.example.com/callback"), null);
assert.equal(resolveDashboardLoopbackPort("http://192.168.1.50:20128/callback"), null);
});
test("resolveDashboardLoopbackPort: malformed/missing redirect URIs return null", () => {
resetPortEnv();
assert.equal(resolveDashboardLoopbackPort(undefined), null);
assert.equal(resolveDashboardLoopbackPort("not a url"), null);
});
test("zedHosted.buildAuthUrl: reuses the server's configured port as native_app_port for a loopback redirect, regardless of the browser-observed scheme", async () => {
resetPortEnv();
process.env.OMNIROUTE_PORT = "20128";
const { zedHosted } = await import("../../src/lib/oauth/providers/zed-hosted.ts");
const { ZED_HOSTED_CONFIG } = await import("../../src/lib/oauth/constants/oauth.ts");
// Simulate the redirect URI OAuthModal.tsx sends when the dashboard is
// reached over HTTPS on its implicit default port (window.location.port is
// empty): hostname is loopback, but scheme/port do not reflect the real
// OmniRoute listener.
const built = zedHosted.buildAuthUrl(ZED_HOSTED_CONFIG, "http://localhost:443/callback");
assert.equal(built.redirectUri, "http://127.0.0.1:20128/");
const url = new URL(built.authUrl);
assert.equal(url.searchParams.get("native_app_port"), "20128");
});
test("zedHosted.buildAuthUrl: remote/LAN redirect URIs keep the configured default native app port", async () => {
resetPortEnv();
process.env.OMNIROUTE_PORT = "20128";
const { zedHosted } = await import("../../src/lib/oauth/providers/zed-hosted.ts");
const { ZED_HOSTED_CONFIG } = await import("../../src/lib/oauth/constants/oauth.ts");
const built = zedHosted.buildAuthUrl(ZED_HOSTED_CONFIG, "https://omniroute.example.com/callback");
assert.equal(built.redirectUri, `http://127.0.0.1:${ZED_HOSTED_CONFIG.defaultNativeAppPort}/`);
});