Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/src/authBootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ describe("resolveInitialServerAuthGateState", () => {
);
});

it("uses the LAN dev proxy for same-origin auth requests", async () => {
await installAuthApi({ session: () => unauthenticatedSession(LOOPBACK_AUTH) });
vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733");
vi.stubEnv("VITE_HTTP_URL", "http://127.0.0.1:3773");
vi.stubEnv("VITE_WS_URL", "ws://127.0.0.1:3773");
installTestBrowser("http://192.168.1.20:5733/");

const { resolveInitialServerAuthGateState, resolvePrimaryEnvironmentHttpUrl } =
await import("./environments/primary");

await expect(resolveInitialServerAuthGateState()).resolves.toEqual({
status: "requires-auth",
auth: LOOPBACK_AUTH,
});
expect(resolvePrimaryEnvironmentHttpUrl("/api/auth/session")).toBe(
"http://192.168.1.20:5733/api/auth/session",
);
});

it("uses the vite proxy for desktop-managed loopback auth requests during local dev", async () => {
await installAuthApi({ session: () => unauthenticatedSession(DESKTOP_AUTH) });
vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733");
Expand Down
49 changes: 48 additions & 1 deletion apps/web/src/environments/primary/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ describe("environmentBootstrap", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
vi.stubEnv("VITE_HTTP_URL", "");
vi.stubEnv("VITE_WS_URL", "");
vi.stubEnv("VITE_DEV_SERVER_URL", "");
installTestBrowser("http://localhost/");
});

Expand Down Expand Up @@ -165,8 +168,45 @@ describe("environmentBootstrap", () => {
);
});

it("uses the LAN dev proxy for HTTP and preserves the configured websocket port", async () => {
vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733");
vi.stubEnv("VITE_HTTP_URL", "http://127.0.0.1:3773");
vi.stubEnv("VITE_WS_URL", "ws://127.0.0.1:3774");
installTestBrowser("http://192.168.1.20:5733/");
await installDescriptorApi();

await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT);
expect(resolvePrimaryEnvironmentHttpUrl("/.well-known/t3/environment")).toBe(
"http://192.168.1.20:5733/.well-known/t3/environment",
);
expect(readPrimaryEnvironmentTarget()).toEqual({
source: "configured",
target: {
httpBaseUrl: "http://192.168.1.20:5733/",
wsBaseUrl: "ws://192.168.1.20:3774/",
},
});
});

it("preserves an explicitly configured non-loopback interface", () => {
vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733");
vi.stubEnv("VITE_HTTP_URL", "http://10.0.0.12:3773");
vi.stubEnv("VITE_WS_URL", "ws://10.0.0.12:3774");
installTestBrowser("http://192.168.1.20:5733/");

expect(readPrimaryEnvironmentTarget()).toEqual({
source: "configured",
target: {
httpBaseUrl: "http://10.0.0.12:3773/",
wsBaseUrl: "ws://10.0.0.12:3774/",
},
});
});

it("uses the vite proxy for desktop-managed loopback descriptor requests during local dev", async () => {
vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733");
vi.stubEnv("VITE_HTTP_URL", "http://127.0.0.1:4773");
vi.stubEnv("VITE_WS_URL", "ws://127.0.0.1:4773");
vi.stubGlobal("window", {
location: new URL("http://127.0.0.1:5733/"),
history: {
Expand All @@ -190,6 +230,13 @@ describe("environmentBootstrap", () => {
expect(resolvePrimaryEnvironmentHttpUrl("/.well-known/t3/environment")).toBe(
"http://127.0.0.1:5733/.well-known/t3/environment",
);
expect(readPrimaryEnvironmentTarget()).toEqual({
source: "desktop-managed",
target: {
httpBaseUrl: "http://127.0.0.1:3773/",
wsBaseUrl: "ws://127.0.0.1:3773/",
},
});
});

it("retains the URL parser cause without exposing the configured URL in its message", () => {
Expand All @@ -212,7 +259,7 @@ describe("environmentBootstrap", () => {

it("describes which desktop bootstrap endpoint is missing", () => {
vi.stubGlobal("window", {
location: new URL("http://127.0.0.1:5733/"),
location: new URL("t3code-dev://app/"),
history: { replaceState: vi.fn() },
desktopBridge: {
getLocalEnvironmentBootstraps: () => [
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/environments/primary/httpLayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe.sequential("primary environment HTTP layer", () => {
afterEach(() => {
__resetDesktopPrimaryAuthForTests();
Reflect.deleteProperty(globalThis, "window");
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});

Expand All @@ -35,6 +36,28 @@ describe.sequential("primary environment HTTP layer", () => {
}).pipe(Effect.provide(makePrimaryEnvironmentHttpLayer()));
});

it.effect("uses cookie credentials through the LAN dev proxy", () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733");
vi.stubEnv("VITE_HTTP_URL", "http://127.0.0.1:3773");
vi.stubEnv("VITE_WS_URL", "ws://127.0.0.1:3773");
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {
location: new URL("http://192.168.1.20:5733/settings"),
},
});

return Effect.gen(function* () {
yield* HttpClient.get("http://192.168.1.20:5733/api/auth/session");

const request = new Request(fetchMock.mock.calls[0]?.[0], fetchMock.mock.calls[0]?.[1]);
expect(request.credentials).toBe("include");
expect(request.headers.get("authorization")).toBeNull();
}).pipe(Effect.provide(makePrimaryEnvironmentHttpLayer()));
});

it.effect("uses bearer auth without cookies for desktop-managed primaries", () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
Expand Down
118 changes: 95 additions & 23 deletions apps/web/src/environments/primary/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,44 +141,56 @@ export function isLoopbackHostname(hostname: string): boolean {
return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
}

function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string {
const httpBaseUrl = primaryTarget.target.httpBaseUrl;
function resolveCurrentDevServerOrigin(): string | null {
const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim();
if (!configuredDevServerUrl) {
return httpBaseUrl;
return null;
}

const currentUrl = parseTargetUrl({
rawValue: window.location.href,
source: "window-origin",
urlKind: "window-location-url",
});
const targetUrl = parseTargetUrl({
rawValue: httpBaseUrl,
source: primaryTarget.source,
urlKind: "http-base-url",
});
const devServerUrl = parseTargetUrl({
rawValue: configuredDevServerUrl,
baseUrl: currentUrl.origin,
source: "configured",
urlKind: "development-server-url",
});

const isCurrentOriginDevServer =
(currentUrl.protocol === "http:" || currentUrl.protocol === "https:") &&
currentUrl.origin === devServerUrl.origin;
if (currentUrl.protocol !== "http:" && currentUrl.protocol !== "https:") {
return null;
}

const isExactDevServerOrigin = currentUrl.origin === devServerUrl.origin;
const isLanAliasForLoopbackDevServer =
!isLoopbackHostname(currentUrl.hostname) &&
isLoopbackHostname(devServerUrl.hostname) &&
currentUrl.protocol === devServerUrl.protocol &&
currentUrl.port === devServerUrl.port;

return isExactDevServerOrigin || isLanAliasForLoopbackDevServer ? currentUrl.origin : null;
}

function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string {
const httpBaseUrl = primaryTarget.target.httpBaseUrl;
const devServerOrigin = resolveCurrentDevServerOrigin();
if (!devServerOrigin) {
return httpBaseUrl;
}

if (
!isCurrentOriginDevServer ||
currentUrl.origin === targetUrl.origin ||
!isLoopbackHostname(currentUrl.hostname) ||
!isLoopbackHostname(targetUrl.hostname)
) {
const targetUrl = parseTargetUrl({
rawValue: httpBaseUrl,
source: primaryTarget.source,
urlKind: "http-base-url",
});

if (devServerOrigin === targetUrl.origin || !isLoopbackHostname(targetUrl.hostname)) {
return httpBaseUrl;
}

return currentUrl.origin;
return devServerOrigin;
}

function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null {
Expand Down Expand Up @@ -267,6 +279,60 @@ function resolveDesktopPrimaryTarget(): PrimaryEnvironmentTarget | null {
};
}

function isHttpWindowOrigin(): boolean {
return window.location.protocol === "http:" || window.location.protocol === "https:";
}

function replaceLoopbackHostname(
rawValue: string,
hostname: string,
urlKind: "http-base-url" | "websocket-base-url",
): string {
const url = parseTargetUrl({
rawValue,
source: "configured",
urlKind,
});
if (isLoopbackHostname(url.hostname)) {
url.hostname = hostname;
}
return url.toString();
}

function resolveConfiguredTargetForWindow(
target: PrimaryEnvironmentTarget,
): PrimaryEnvironmentTarget {
if (!isHttpWindowOrigin() || isLoopbackHostname(window.location.hostname)) {
return target;
}

const devServerOrigin = resolveCurrentDevServerOrigin();
if (!devServerOrigin) {
return target;
}

const configuredHttpUrl = parseTargetUrl({
rawValue: target.target.httpBaseUrl,
source: "configured",
urlKind: "http-base-url",
});
const httpBaseUrl = isLoopbackHostname(configuredHttpUrl.hostname)
? new URL(devServerOrigin).toString()
: configuredHttpUrl.toString();

return {
source: target.source,
target: {
httpBaseUrl,
wsBaseUrl: replaceLoopbackHostname(
target.target.wsBaseUrl,
window.location.hostname,
"websocket-base-url",
),
},
};
}

export function resolvePrimaryEnvironmentHttpUrl(
pathname: string,
searchParams?: Record<string, string>,
Expand All @@ -286,9 +352,15 @@ export function resolvePrimaryEnvironmentHttpUrl(
}

export function readPrimaryEnvironmentTarget(): PrimaryEnvironmentTarget {
return (
resolveDesktopPrimaryTarget() ??
resolveConfiguredPrimaryTarget() ??
resolveWindowOriginPrimaryTarget()
);
const desktopTarget = resolveDesktopPrimaryTarget();
if (desktopTarget) {
return desktopTarget;
}

const configuredTarget = resolveConfiguredPrimaryTarget();
if (configuredTarget) {
return resolveConfiguredTargetForWindow(configuredTarget);
}
Comment thread
cursor[bot] marked this conversation as resolved.

return resolveWindowOriginPrimaryTarget();
}
Loading