From b113623f71bfe44678ee7c60c91f4302fca7b2a5 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Sat, 20 Jun 2026 10:56:28 +0200 Subject: [PATCH 1/9] fix: stabilize local desktop startup without clerk config Gate Clerk-dependent renderer and desktop bridge setup when local config is absent, register the desktop schemes independently, proxy custom-scheme requests through the app protocol, and load the diff worker via the portable package worker entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/desktop/src/app/DesktopClerk.test.ts | 20 ++++ apps/desktop/src/app/DesktopClerk.ts | 104 ++++++++++-------- .../src/electron/ElectronProtocol.test.ts | 75 +++++++++++-- apps/desktop/src/electron/ElectronProtocol.ts | 62 ++++++++++- apps/desktop/src/main.ts | 8 +- apps/web/src/authBootstrap.test.ts | 17 +++ .../web/src/cloud/ConfiguredCloudAuthRoot.tsx | 25 +++++ .../src/components/DiffWorkerPoolProvider.tsx | 9 +- .../T3ConnectSidebarSignIn.configured.tsx | 56 ++++++++++ .../clerk/T3ConnectSidebarSignIn.tsx | 74 ++++--------- .../environments/primary/bootstrap.test.ts | 26 +++++ apps/web/src/environments/primary/target.ts | 71 ++++++++---- apps/web/src/main.tsx | 19 +--- apps/web/src/routes/settings.connections.tsx | 17 ++- apps/web/vite.config.ts | 4 +- pnpm-lock.yaml | 1 - pnpm-workspace.yaml | 1 - 17 files changed, 430 insertions(+), 159 deletions(-) create mode 100644 apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx create mode 100644 apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx diff --git a/apps/desktop/src/app/DesktopClerk.test.ts b/apps/desktop/src/app/DesktopClerk.test.ts index 9b5ed56d1f3..5b13e82ddfc 100644 --- a/apps/desktop/src/app/DesktopClerk.test.ts +++ b/apps/desktop/src/app/DesktopClerk.test.ts @@ -53,6 +53,26 @@ describe("DesktopClerk", () => { assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname("invalid"), undefined); }); + it.effect("skips acquiring the SDK bridge when Clerk is disabled", () => { + const environment = DesktopEnvironment.DesktopEnvironment.of({ + stateDir: "/tmp/t3-state", + isDevelopment: true, + } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); + + return Effect.gen(function* () { + yield* Effect.scoped( + Layer.build( + DesktopClerk.makeDesktopClerkLayer(false).pipe( + Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)), + ), + ), + ); + + assert.deepEqual(storageMock.mock.calls, []); + assert.deepEqual(createClerkBridgeMock.mock.calls, []); + }); + }); + it.effect("acquires and releases the SDK bridge with the layer", () => { const cleanup = vi.fn(); storageMock.mockReturnValue(storageAdapter); diff --git a/apps/desktop/src/app/DesktopClerk.ts b/apps/desktop/src/app/DesktopClerk.ts index 0e283f8dd0c..7ed97461eeb 100644 --- a/apps/desktop/src/app/DesktopClerk.ts +++ b/apps/desktop/src/app/DesktopClerk.ts @@ -71,6 +71,8 @@ export const desktopClerkFrontendApiHostname = resolveDesktopClerkFrontendApiHos : __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__, ); +export const desktopClerkBridgeEnabled = Boolean(desktopClerkFrontendApiHostname); + export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolean) { return createClerkBridge({ storage: storage({ path: stateDir }), @@ -82,54 +84,60 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea }); } -export const make = Effect.gen(function* () { - const environment = yield* DesktopEnvironment.DesktopEnvironment; - yield* Effect.acquireRelease( - Effect.try({ - try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment), - catch: (cause) => - new DesktopClerkBridgeInitializationError({ - stateDir: environment.stateDir, - isDevelopment: environment.isDevelopment, - cause, +export function makeDesktopClerkLayer(enabled = desktopClerkBridgeEnabled) { + const make = Effect.gen(function* () { + const environment = yield* DesktopEnvironment.DesktopEnvironment; + if (enabled) { + yield* Effect.acquireRelease( + Effect.try({ + try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment), + catch: (cause) => + new DesktopClerkBridgeInitializationError({ + stateDir: environment.stateDir, + isDevelopment: environment.isDevelopment, + cause, + }), }), - }), - (bridge) => - Effect.try({ - try: () => bridge.cleanup(), - catch: (cause) => - new DesktopClerkBridgeCleanupError({ - stateDir: environment.stateDir, - isDevelopment: environment.isDevelopment, - cause, - }), - }).pipe(Effect.orDie), - ); - - return DesktopClerk.of({ - configure: Effect.gen(function* () { - const electronApp = yield* ElectronApp.ElectronApp; - const electronWindow = yield* ElectronWindow.ElectronWindow; - const context = yield* Effect.context(); - const runPromise = Effect.runPromiseWith(context); - - if (!(yield* electronApp.requestSingleInstanceLock)) { - yield* electronApp.quit; - return yield* Effect.interrupt; - } - - yield* electronApp.on("second-instance", () => { - void runPromise( - Effect.gen(function* () { - const mainWindow = yield* electronWindow.currentMainOrFirst; - if (Option.isSome(mainWindow)) { - yield* electronWindow.reveal(mainWindow.value); - } - }), - ); - }); - }).pipe(Effect.withSpan("desktop.clerk.configure")), + (bridge) => + Effect.try({ + try: () => bridge.cleanup(), + catch: (cause) => + new DesktopClerkBridgeCleanupError({ + stateDir: environment.stateDir, + isDevelopment: environment.isDevelopment, + cause, + }), + }).pipe(Effect.orDie), + ); + } + + return DesktopClerk.of({ + configure: Effect.gen(function* () { + const electronApp = yield* ElectronApp.ElectronApp; + const electronWindow = yield* ElectronWindow.ElectronWindow; + const context = yield* Effect.context(); + const runPromise = Effect.runPromiseWith(context); + + if (!(yield* electronApp.requestSingleInstanceLock)) { + yield* electronApp.quit; + return yield* Effect.interrupt; + } + + yield* electronApp.on("second-instance", () => { + void runPromise( + Effect.gen(function* () { + const mainWindow = yield* electronWindow.currentMainOrFirst; + if (Option.isSome(mainWindow)) { + yield* electronWindow.reveal(mainWindow.value); + } + }), + ); + }); + }).pipe(Effect.withSpan("desktop.clerk.configure")), + }); }); -}); -export const layer = Layer.effect(DesktopClerk, make); + return Layer.effect(DesktopClerk, make); +} + +export const layer = makeDesktopClerkLayer(); diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index 56fe009fee2..24433549672 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -1,35 +1,79 @@ import { assert, describe, it } from "@effect/vitest"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { beforeEach, vi } from "vite-plus/test"; -const { handleMock, netFetchMock, unhandleMock } = vi.hoisted(() => ({ +const { handleMock, registerSchemesAsPrivilegedMock, unhandleMock } = vi.hoisted(() => ({ handleMock: vi.fn(), - netFetchMock: vi.fn(), + registerSchemesAsPrivilegedMock: vi.fn(), unhandleMock: vi.fn(), })); vi.mock("electron", () => ({ - net: { fetch: netFetchMock }, - protocol: { handle: handleMock, unhandle: unhandleMock }, + protocol: { + handle: handleMock, + registerSchemesAsPrivileged: registerSchemesAsPrivilegedMock, + unhandle: unhandleMock, + }, })); import * as ElectronProtocol from "./ElectronProtocol.ts"; +function makeHttpClientLayer( + handler: ( + request: HttpClientRequest.HttpClientRequest, + ) => Effect.Effect, +) { + return Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => handler(request)), + ); +} + describe("ElectronProtocol", () => { beforeEach(() => { handleMock.mockReset(); - netFetchMock.mockReset(); + registerSchemesAsPrivilegedMock.mockReset(); unhandleMock.mockReset(); }); + it("registers the desktop scheme as a secure standard scheme", () => { + ElectronProtocol.registerDesktopSchemePrivileges(true); + + assert.deepEqual(registerSchemesAsPrivilegedMock.mock.calls, [ + [ + [ + { + scheme: "t3code-dev", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + stream: true, + }, + }, + ], + ], + ]); + }); + it.effect("proxies the stable renderer origin to the current app server", () => Effect.gen(function* () { let handler: ((request: Request) => Promise) | undefined; + const requestUrls: string[] = []; handleMock.mockImplementation((_scheme, nextHandler) => { handler = nextHandler; }); - netFetchMock.mockResolvedValue(new Response("ok")); + const httpClientLayer = makeHttpClientLayer((request) => + Effect.gen(function* () { + const webRequest = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie); + requestUrls.push(webRequest.url); + return HttpClientResponse.fromWeb(request, new Response("ok")); + }), + ); yield* Effect.scoped( Effect.gen(function* () { @@ -63,23 +107,30 @@ describe("ElectronProtocol", () => { "font-src 'self' t3code-dev: data:", ); }), - ); + ).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer)))); assert.deepEqual( handleMock.mock.calls.map((call) => call[0]), ["t3code-dev"], ); - assert.equal(netFetchMock.mock.calls[0]?.[0], "http://127.0.0.1:3773/api/health?verbose=1"); + assert.equal(requestUrls[0], "http://127.0.0.1:3773/api/health?verbose=1"); assert.deepEqual(unhandleMock.mock.calls, [["t3code-dev"]]); - }).pipe(Effect.provide(ElectronProtocol.layer)), + }), ); it.effect("rejects custom protocol requests for another host", () => Effect.gen(function* () { let handler: ((request: Request) => Promise) | undefined; + const requests: HttpClientRequest.HttpClientRequest[] = []; handleMock.mockImplementation((_scheme, nextHandler) => { handler = nextHandler; }); + const httpClientLayer = makeHttpClientLayer((request) => + Effect.sync(() => { + requests.push(request); + return HttpClientResponse.fromWeb(request, new Response("unexpected")); + }), + ); const response = yield* Effect.scoped( Effect.gen(function* () { @@ -92,11 +143,11 @@ describe("ElectronProtocol", () => { }); return yield* Effect.promise(() => handler!(new Request("t3code://other/"))); }), - ); + ).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer)))); assert.equal(response.status, 404); - assert.equal(netFetchMock.mock.calls.length, 0); - }).pipe(Effect.provide(ElectronProtocol.layer)), + assert.equal(requests.length, 0); + }), ); it.effect("preserves protocol registration failures", () => diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index 757c26178d0..5bacf52503e 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; +import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import * as Electron from "electron"; @@ -23,6 +24,21 @@ export function getDesktopUrl(isDevelopment: boolean): string { return `${getDesktopOrigin(isDevelopment)}/`; } +export function registerDesktopSchemePrivileges(isDevelopment: boolean): void { + Electron.protocol.registerSchemesAsPrivileged([ + { + scheme: getDesktopScheme(isDevelopment), + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + stream: true, + }, + }, + ]); +} + export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass()( "ElectronProtocolRegistrationError", { @@ -103,10 +119,33 @@ function withContentSecurityPolicy(response: Response, policy: string): Response }); } +const hopByHopRequestHeaders = new Set([ + "connection", + "host", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]); + +function makeProxyRequestHeaders(requestHeaders: Headers): Headers { + const headers = new Headers(); + for (const [name, value] of requestHeaders) { + if (!hopByHopRequestHeaders.has(name.toLowerCase())) { + headers.set(name, value); + } + } + return headers; +} + async function proxyRequest( request: Request, targetOrigin: URL, contentSecurityPolicy: string, + httpClient: HttpClient.HttpClient, ): Promise { const requestUrl = new URL(request.url); if (requestUrl.host !== DESKTOP_HOST) { @@ -115,19 +154,34 @@ async function proxyRequest( const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, targetOrigin); const init: RequestInit = { + cache: "no-store", method: request.method, - headers: request.headers, + headers: makeProxyRequestHeaders(request.headers), }; if (request.method !== "GET" && request.method !== "HEAD") { init.body = request.body; (init as RequestInit & { duplex: "half" }).duplex = "half"; } - const response = await Electron.net.fetch(targetUrl.toString(), init); - return withContentSecurityPolicy(response, contentSecurityPolicy); + + const effect = Effect.gen(function* () { + const clientRequest = HttpClientRequest.fromWeb(new Request(targetUrl, init)); + const clientResponse = yield* httpClient.execute(clientRequest); + const body = request.method === "HEAD" ? null : yield* clientResponse.arrayBuffer; + return withContentSecurityPolicy( + new Response(body, { + status: clientResponse.status, + headers: clientResponse.headers, + }), + contentSecurityPolicy, + ); + }); + + return Effect.runPromise(effect); } export const make = Effect.gen(function* () { const registered = yield* Ref.make(false); + const httpClient = yield* HttpClient.HttpClient; const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")( function* (input: DesktopProtocolRegistrationInput) { @@ -139,7 +193,7 @@ export const make = Effect.gen(function* () { Effect.try({ try: () => { Electron.protocol.handle(input.scheme, (request) => - proxyRequest(request, input.targetOrigin, contentSecurityPolicy), + proxyRequest(request, input.targetOrigin, contentSecurityPolicy, httpClient), ); }, catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }), diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b88eb18e57f..b307e858771 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -51,6 +51,12 @@ import * as BrowserSession from "./preview/BrowserSession.ts"; import * as PreviewManager from "./preview/Manager.ts"; import * as DesktopWindow from "./window/DesktopWindow.ts"; +const isDesktopDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); + +if (!DesktopClerk.desktopClerkBridgeEnabled) { + ElectronProtocol.registerDesktopSchemePrivileges(isDesktopDevelopment); +} + const desktopEnvironmentLayer = Layer.unwrap( Effect.gen(function* () { const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe( @@ -105,7 +111,7 @@ const electronLayer = Layer.mergeAll( ElectronApp.layer, ElectronDialog.layer, ElectronMenu.layer, - ElectronProtocol.layer, + ElectronProtocol.layer.pipe(Layer.provide(NodeHttpClient.layerUndici)), ElectronSafeStorage.layer, ElectronShell.layer, ElectronTheme.layer, diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts index ced16c15f4e..fc8a876eaaf 100644 --- a/apps/web/src/authBootstrap.test.ts +++ b/apps/web/src/authBootstrap.test.ts @@ -211,6 +211,23 @@ describe("resolveInitialServerAuthGateState", () => { ); }); + it("uses the desktop custom scheme proxy for desktop-managed auth requests", async () => { + const testWindow = installTestBrowser("t3code-dev://app/"); + testWindow.desktopBridge = { + getLocalEnvironmentBootstrap: () => ({ + label: "Local environment", + httpBaseUrl: "http://127.0.0.1:3773", + wsBaseUrl: "ws://127.0.0.1:3773", + }), + } as DesktopBridge; + + const { resolvePrimaryEnvironmentHttpUrl } = await import("./environments/primary"); + + expect(resolvePrimaryEnvironmentHttpUrl("/api/auth/session")).toBe( + "t3code-dev://app/api/auth/session", + ); + }); + it("returns a requires-auth state instead of throwing when no bootstrap credential exists", async () => { await installAuthApi({ session: () => unauthenticatedSession(LOOPBACK_AUTH) }); const { resolveInitialServerAuthGateState } = await import("./environments/primary"); diff --git a/apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx b/apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx new file mode 100644 index 00000000000..955c2509959 --- /dev/null +++ b/apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx @@ -0,0 +1,25 @@ +import { ClerkProvider } from "@clerk/react"; +import { passkeys } from "@clerk/electron/passkeys"; +import { ClerkProvider as ElectronClerkProvider } from "@clerk/electron/react"; +import type { ReactNode } from "react"; + +import { isElectron } from "../env"; +import { ManagedRelayAuthProvider } from "./managedAuth"; + +export default function ConfiguredCloudAuthRoot({ + children, + publishableKey, +}: { + readonly children: ReactNode; + readonly publishableKey: string; +}) { + return isElectron ? ( + + {children} + + ) : ( + + {children} + + ); +} diff --git a/apps/web/src/components/DiffWorkerPoolProvider.tsx b/apps/web/src/components/DiffWorkerPoolProvider.tsx index 3ec748c6bcb..8228a9f1fcb 100644 --- a/apps/web/src/components/DiffWorkerPoolProvider.tsx +++ b/apps/web/src/components/DiffWorkerPoolProvider.tsx @@ -1,5 +1,4 @@ import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react"; -import DiffsWorker from "@pierre/diffs/worker/worker.js?worker"; import * as Schema from "effect/Schema"; import { useEffect, useMemo, type ReactNode } from "react"; import { useTheme } from "../hooks/useTheme"; @@ -15,6 +14,12 @@ export class DiffWorkerError extends Schema.TaggedErrorClass()( } } +function createDiffsWorker() { + return new Worker(new URL("@pierre/diffs/worker/worker-portable.js", import.meta.url), { + type: "module", + }); +} + function DiffWorkerThemeSync({ themeName }: { themeName: DiffThemeName }) { const workerPool = useWorkerPool(); @@ -59,7 +64,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { poolOptions={{ workerFactory: () => { try { - return new DiffsWorker(); + return createDiffsWorker(); } catch (cause) { throw new DiffWorkerError({ operation: "create-worker", diff --git a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx b/apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx new file mode 100644 index 00000000000..1cf3d1a8203 --- /dev/null +++ b/apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx @@ -0,0 +1,56 @@ +import { UserButton, useAuth } from "@clerk/react"; +import { LogInIcon, SmartphoneIcon } from "lucide-react"; + +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar"; +import { MobileClientsUserProfilePage } from "./MobileClientsUserProfilePage"; +import { useT3ConnectAuthPrompt } from "./useT3ConnectAuthPrompt"; + +export function ConfiguredT3ConnectSidebarAvatar() { + const { isLoaded, isSignedIn } = useAuth(); + + if (!isLoaded || !isSignedIn) return null; + + return ( + + } + url="mobile-clients" + > + + + + ); +} + +export function ConfiguredT3ConnectSidebarSignIn() { + const { isLoaded, isSignedIn } = useAuth(); + const { authPrompt, openAuthPrompt } = useT3ConnectAuthPrompt(); + + if (!isLoaded || isSignedIn) return null; + + return ( + <> + + + + + Sign in to T3 Connect + + + + {authPrompt} + + ); +} diff --git a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx b/apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx index 45477ee1b7e..cf1c0d0d21d 100644 --- a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx +++ b/apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx @@ -1,69 +1,35 @@ -import { UserButton, useAuth } from "@clerk/react"; -import { LogInIcon, SmartphoneIcon } from "lucide-react"; +import { lazy, Suspense } from "react"; import { hasCloudPublicConfig } from "../../cloud/publicConfig"; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar"; -import { MobileClientsUserProfilePage } from "./MobileClientsUserProfilePage"; -import { useT3ConnectAuthPrompt } from "./useT3ConnectAuthPrompt"; -export function T3ConnectSidebarSignIn() { - if (!hasCloudPublicConfig()) return null; +const ConfiguredT3ConnectSidebarSignIn = lazy(() => + import("./T3ConnectSidebarSignIn.configured").then((module) => ({ + default: module.ConfiguredT3ConnectSidebarSignIn, + })), +); - return ; -} +const ConfiguredT3ConnectSidebarAvatar = lazy(() => + import("./T3ConnectSidebarSignIn.configured").then((module) => ({ + default: module.ConfiguredT3ConnectSidebarAvatar, + })), +); -export function T3ConnectSidebarAvatar() { +export function T3ConnectSidebarSignIn() { if (!hasCloudPublicConfig()) return null; - return ; -} - -function ConfiguredT3ConnectSidebarAvatar() { - const { isLoaded, isSignedIn } = useAuth(); - - if (!isLoaded || !isSignedIn) return null; - return ( - - } - url="mobile-clients" - > - - - + + + ); } -function ConfiguredT3ConnectSidebarSignIn() { - const { isLoaded, isSignedIn } = useAuth(); - const { authPrompt, openAuthPrompt } = useT3ConnectAuthPrompt(); - - if (!isLoaded || isSignedIn) return null; +export function T3ConnectSidebarAvatar() { + if (!hasCloudPublicConfig()) return null; return ( - <> - - - - - Sign in to T3 Connect - - - - {authPrompt} - + + + ); } diff --git a/apps/web/src/environments/primary/bootstrap.test.ts b/apps/web/src/environments/primary/bootstrap.test.ts index e8333c2e078..8e85cffa0ae 100644 --- a/apps/web/src/environments/primary/bootstrap.test.ts +++ b/apps/web/src/environments/primary/bootstrap.test.ts @@ -251,4 +251,30 @@ describe("environmentBootstrap", () => { message: "The window-origin primary environment target uses unsupported protocol file:.", }); }); + + it("uses the desktop custom scheme proxy for desktop-managed descriptor requests", async () => { + writePrimaryEnvironmentDescriptor(BASE_ENVIRONMENT); + vi.stubGlobal("window", { + location: new URL("t3code-dev://app/"), + history: { + replaceState: vi.fn(), + }, + desktopBridge: { + getLocalEnvironmentBootstrap: () => ({ + label: "Local environment", + httpBaseUrl: "http://127.0.0.1:3773", + wsBaseUrl: "ws://127.0.0.1:3773", + bootstrapToken: "desktop-bootstrap-token", + }), + }, + }); + + expect(resolvePrimaryEnvironmentHttpUrl("/.well-known/t3/environment")).toBe( + "t3code-dev://app/.well-known/t3/environment", + ); + expect(getPrimaryKnownEnvironment()?.target).toEqual({ + httpBaseUrl: "t3code-dev://app/", + wsBaseUrl: "ws://127.0.0.1:3773/", + }); + }); }); diff --git a/apps/web/src/environments/primary/target.ts b/apps/web/src/environments/primary/target.ts index a14a99ec4dc..f311d1b51a7 100644 --- a/apps/web/src/environments/primary/target.ts +++ b/apps/web/src/environments/primary/target.ts @@ -98,6 +98,10 @@ function parseTargetUrl(input: { } } +function currentWindowBaseUrl(): string { + return window.location.origin === "null" ? window.location.href : window.location.origin; +} + function normalizeBaseUrl( rawValue: string, source: PrimaryEnvironmentTargetSource, @@ -105,7 +109,7 @@ function normalizeBaseUrl( ): string { return parseTargetUrl({ rawValue, - baseUrl: window.location.origin, + baseUrl: currentWindowBaseUrl(), source, urlKind, }).toString(); @@ -139,11 +143,17 @@ export function isLoopbackHostname(hostname: string): boolean { function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string { const httpBaseUrl = primaryTarget.target.httpBaseUrl; - const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim(); - if (!configuredDevServerUrl) { - return httpBaseUrl; - } +function effectiveUrlOrigin(url: URL): string { + return url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin; +} + +function originBaseUrl(origin: string): string { + return origin.endsWith("/") ? origin : `${origin}/`; +} + +function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string { + const httpBaseUrl = primaryTarget.target.httpBaseUrl; const currentUrl = parseTargetUrl({ rawValue: window.location.href, source: "window-origin", @@ -154,27 +164,38 @@ function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): str source: primaryTarget.source, urlKind: "http-base-url", }); + const currentOrigin = effectiveUrlOrigin(currentUrl); + const isCurrentOriginDesktopDevApp = + currentUrl.protocol === "t3code-dev:" && currentUrl.host === "app"; + + if (currentOrigin === targetUrl.origin || !isLoopbackHostname(targetUrl.hostname)) { + return httpBaseUrl; + } + + if (isCurrentOriginDesktopDevApp) { + return originBaseUrl(currentOrigin); + } + + const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim(); + if (!configuredDevServerUrl) { + return httpBaseUrl; + } + const devServerUrl = parseTargetUrl({ rawValue: configuredDevServerUrl, - baseUrl: currentUrl.origin, + baseUrl: originBaseUrl(currentOrigin), source: "configured", urlKind: "development-server-url", }); const isCurrentOriginDevServer = (currentUrl.protocol === "http:" || currentUrl.protocol === "https:") && - currentUrl.origin === devServerUrl.origin; - - if ( - !isCurrentOriginDevServer || - currentUrl.origin === targetUrl.origin || - !isLoopbackHostname(currentUrl.hostname) || - !isLoopbackHostname(targetUrl.hostname) - ) { + currentOrigin === devServerUrl.origin; + if (!isCurrentOriginDevServer || !isLoopbackHostname(currentUrl.hostname)) { return httpBaseUrl; } - return currentUrl.origin; + return originBaseUrl(currentOrigin); } function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null { @@ -249,11 +270,21 @@ function resolveDesktopPrimaryTarget(): PrimaryEnvironmentTarget | null { return { source: "desktop-managed", target: { - httpBaseUrl: normalizeBaseUrl( - desktopBootstrap.httpBaseUrl, - "desktop-managed", - "http-base-url", - ), + httpBaseUrl: resolveHttpRequestBaseUrl({ + source: "desktop-managed", + target: { + httpBaseUrl: normalizeBaseUrl( + desktopBootstrap.httpBaseUrl, + "desktop-managed", + "http-base-url", + ), + wsBaseUrl: normalizeBaseUrl( + desktopBootstrap.wsBaseUrl, + "desktop-managed", + "websocket-base-url", + ), + }, + }), wsBaseUrl: normalizeBaseUrl( desktopBootstrap.wsBaseUrl, "desktop-managed", diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 453649bfdc5..5caf960dcfb 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,8 +1,5 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { ClerkProvider } from "@clerk/react"; -import { passkeys } from "@clerk/electron/passkeys"; -import { ClerkProvider as ElectronClerkProvider } from "@clerk/electron/react"; import { createHashHistory, createBrowserHistory } from "@tanstack/react-router"; import "@fontsource-variable/dm-sans/index.css"; @@ -12,7 +9,6 @@ import "@xterm/xterm/css/xterm.css"; import "./index.css"; import { isElectron } from "./env"; -import { ManagedRelayAuthProvider } from "./cloud/managedAuth"; import { hasCloudPublicConfig } from "./cloud/publicConfig"; import { getRouter } from "./router"; import { syncDocumentWindowControlsOverlayClass } from "./lib/windowControlsOverlay"; @@ -28,21 +24,18 @@ if (isElectron) { } const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; +const ConfiguredCloudAuthRoot = React.lazy(() => import("./cloud/ConfiguredCloudAuthRoot")); const app = ; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( {clerkPublishableKey && hasCloudPublicConfig() ? ( - isElectron ? ( - - {app} - - ) : ( - - {app} - - ) + + + {app} + + ) : ( app )} diff --git a/apps/web/src/routes/settings.connections.tsx b/apps/web/src/routes/settings.connections.tsx index 275eda6c516..e1490d19f3b 100644 --- a/apps/web/src/routes/settings.connections.tsx +++ b/apps/web/src/routes/settings.connections.tsx @@ -1,7 +1,20 @@ import { createFileRoute } from "@tanstack/react-router"; +import { lazy, Suspense } from "react"; -import { ConnectionsSettings } from "../components/settings/ConnectionsSettings"; +const ConnectionsSettings = lazy(() => + import("../components/settings/ConnectionsSettings").then((module) => ({ + default: module.ConnectionsSettings, + })), +); + +function ConnectionsSettingsRoute() { + return ( + + + + ); +} export const Route = createFileRoute("/settings/connections")({ - component: ConnectionsSettings, + component: ConnectionsSettingsRoute, }); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 8f984c850dc..ba532796537 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -14,6 +14,7 @@ Object.assign(process.env, repoEnv); const port = Number(process.env.PORT ?? 5733); const host = process.env.HOST?.trim() || "localhost"; +const configuredDevServerUrl = process.env.VITE_DEV_SERVER_URL?.trim(); const configuredWsUrl = process.env.VITE_WS_URL?.trim(); const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; @@ -103,7 +104,6 @@ export default defineConfig(() => { "@pierre/diffs", "@pierre/diffs/editor", "@pierre/diffs/react", - "@pierre/diffs/worker/worker.js", "effect/Array", "effect/Order", "react-dom/client", @@ -111,6 +111,7 @@ export default defineConfig(() => { }, define: { // In dev mode, tell the web app where the WebSocket server lives + "import.meta.env.VITE_DEV_SERVER_URL": JSON.stringify(configuredDevServerUrl ?? ""), "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl), "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey), @@ -156,6 +157,7 @@ export default defineConfig(() => { // connection logs — enable "Verbose" in DevTools to see them. protocol: "ws", host, + clientPort: port, }, }, build: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61fa9c92146..f99a9f8565f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,6 @@ overrides: '@effect/vitest>@vitest/runner': '-' '@effect/vitest>vitest': '-' '@expo/metro-config': 56.0.13 - '@pierre/diffs>@shikijs/transformers': ^4.2.0 '@types/node': 24.12.4 effect: 4.0.0-beta.78 vite: npm:@voidzero-dev/vite-plus-core@0.1.24 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8096d3a0a3a..b8618329c23 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -73,7 +73,6 @@ overrides: "@effect/vitest>@vitest/runner": "-" "@effect/vitest>vitest": "-" "@expo/metro-config": 56.0.13 - "@pierre/diffs>@shikijs/transformers": ^4.2.0 "@types/node": "catalog:" effect: "catalog:" vite: "catalog:" From 98d85b14d44da1769d6d6b90e41c711b653cfa21 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Sat, 20 Jun 2026 11:17:53 +0200 Subject: [PATCH 2/9] fix: harden local dev pairing bootstrap Route configured loopback HTTP requests through the Vite origin so browser-session cookies are scoped to the page origin, while preserving direct WebSocket targets. Dedupe same-token pairing submits and tolerate stale one-time-token retries when a session is already authenticated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/web/src/authBootstrap.test.ts | 43 +++++++++++++++++- apps/web/src/environments/primary/auth.ts | 45 ++++++++++++++++++- .../environments/primary/bootstrap.test.ts | 17 +++++++ .../environments/primary/httpLayer.test.ts | 25 +++++++++++ apps/web/src/environments/primary/target.ts | 11 +++-- 5 files changed, 135 insertions(+), 6 deletions(-) diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts index fc8a876eaaf..f992ec94ea6 100644 --- a/apps/web/src/authBootstrap.test.ts +++ b/apps/web/src/authBootstrap.test.ts @@ -186,6 +186,25 @@ describe("resolveInitialServerAuthGateState", () => { ); }); + it("uses the vite proxy for configured loopback auth requests during local dev", async () => { + await installAuthApi({ session: () => unauthenticatedSession(LOOPBACK_AUTH) }); + vi.stubEnv("VITE_HTTP_URL", "http://localhost:13773"); + vi.stubEnv("VITE_WS_URL", "ws://localhost:13773"); + vi.stubEnv("VITE_DEV_SERVER_URL", "http://localhost:5733"); + installTestBrowser("http://localhost: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://localhost: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"); @@ -307,7 +326,7 @@ describe("resolveInitialServerAuthGateState", () => { status: "authenticated", }); expect(testApi.calls.browserSession).toEqual([{ credential: "retry-token" }]); - expect(testApi.calls.session).toBe(2); + expect(testApi.calls.session).toBe(1); }); it("rejects a blank pairing token with a structured validation error", async () => { @@ -378,6 +397,28 @@ describe("resolveInitialServerAuthGateState", () => { expect(error.message).not.toContain(cause.message); }); + it("accepts an already-established session after a duplicate one-time token submit", async () => { + const testWindow = installTestBrowser("http://localhost/pair#token=already-used-token"); + const testApi = await installAuthApi({ + session: () => authenticatedSession(LOOPBACK_AUTH), + browserSession: () => + Effect.fail( + new EnvironmentAuthInvalidError({ + code: "auth_invalid", + reason: "invalid_credential", + traceId: "trace-invalid-credential", + }), + ), + }); + + const { submitServerAuthCredential } = await import("./environments/primary"); + + await expect(submitServerAuthCredential("already-used-token")).resolves.toBeUndefined(); + expect(testWindow.location.hash).toBe(""); + expect(testApi.calls.browserSession).toEqual([{ credential: "already-used-token" }]); + expect(testApi.calls.session).toBe(1); + }); + it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => { vi.useFakeTimers(); const nextSession = sequence( diff --git a/apps/web/src/environments/primary/auth.ts b/apps/web/src/environments/primary/auth.ts index 96814b92b79..ec6d7272d8f 100644 --- a/apps/web/src/environments/primary/auth.ts +++ b/apps/web/src/environments/primary/auth.ts @@ -150,6 +150,10 @@ type ServerAuthGateState = let bootstrapPromise: Promise | null = null; let resolvedAuthenticatedGateState: ServerAuthGateState | null = null; +let credentialSubmitPromise: { + readonly credential: string; + readonly promise: Promise; +} | null = null; const AUTH_SESSION_ESTABLISH_TIMEOUT_MS = 2_000; const AUTH_SESSION_ESTABLISH_STEP_MS = 100; @@ -346,8 +350,46 @@ export async function submitServerAuthCredential(credential: string): Promise { + if (resolvedAuthenticatedGateState?.status === "authenticated") { + bootstrapPromise = null; + stripPairingTokenFromUrl(); + return; + } resolvedAuthenticatedGateState = null; - await exchangeBootstrapCredential(trimmedCredential); + try { + await exchangeBootstrapCredential(trimmedCredential); + } catch (error) { + if (isBootstrapHttpError(error) && error.status === 401) { + const currentSession = await fetchSessionState().catch(() => null); + if (currentSession?.authenticated) { + resolvedAuthenticatedGateState = { status: "authenticated" }; + bootstrapPromise = null; + stripPairingTokenFromUrl(); + return; + } + } + throw error; + } + resolvedAuthenticatedGateState = { status: "authenticated" }; bootstrapPromise = null; stripPairingTokenFromUrl(); } @@ -529,4 +571,5 @@ export async function resolveInitialServerAuthGateState(): Promise { ); }); + it("uses the vite proxy for configured loopback descriptor requests during local dev", async () => { + vi.stubEnv("VITE_HTTP_URL", "http://localhost:13773"); + vi.stubEnv("VITE_WS_URL", "ws://localhost:13773"); + vi.stubEnv("VITE_DEV_SERVER_URL", "http://localhost:5733"); + installTestBrowser("http://localhost:5733/"); + await installDescriptorApi(); + + await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT); + expect(resolvePrimaryEnvironmentHttpUrl("/.well-known/t3/environment")).toBe( + "http://localhost:5733/.well-known/t3/environment", + ); + expect(getPrimaryKnownEnvironment()?.target).toEqual({ + httpBaseUrl: "http://localhost:5733/", + wsBaseUrl: "ws://localhost:13773/", + }); + }); + 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.stubGlobal("window", { diff --git a/apps/web/src/environments/primary/httpLayer.test.ts b/apps/web/src/environments/primary/httpLayer.test.ts index 5bc1ef01da1..292f53a6c34 100644 --- a/apps/web/src/environments/primary/httpLayer.test.ts +++ b/apps/web/src/environments/primary/httpLayer.test.ts @@ -35,6 +35,31 @@ describe.sequential("primary environment HTTP layer", () => { }).pipe(Effect.provide(makePrimaryEnvironmentHttpLayer())); }); + it.effect("uses cookie credentials for vite-proxied configured loopback environments", () => { + const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); + vi.stubEnv("VITE_HTTP_URL", "http://localhost:13773"); + vi.stubEnv("VITE_WS_URL", "ws://localhost:13773"); + vi.stubEnv("VITE_DEV_SERVER_URL", "http://localhost:5733"); + vi.stubGlobal("fetch", fetchMock); + Object.defineProperty(globalThis, "window", { + configurable: true, + value: { + location: { + href: "http://localhost:5733/pair", + origin: "http://localhost:5733", + }, + }, + }); + + return Effect.gen(function* () { + yield* HttpClient.get("http://localhost: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); diff --git a/apps/web/src/environments/primary/target.ts b/apps/web/src/environments/primary/target.ts index f311d1b51a7..a38bf824a12 100644 --- a/apps/web/src/environments/primary/target.ts +++ b/apps/web/src/environments/primary/target.ts @@ -141,9 +141,6 @@ export function isLoopbackHostname(hostname: string): boolean { return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname)); } -function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string { - const httpBaseUrl = primaryTarget.target.httpBaseUrl; - function effectiveUrlOrigin(url: URL): string { return url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin; } @@ -220,7 +217,13 @@ function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null { return { source: "configured", target: { - httpBaseUrl: normalizeBaseUrl(resolvedHttpBaseUrl, "configured", "http-base-url"), + httpBaseUrl: resolveHttpRequestBaseUrl({ + source: "configured", + target: { + httpBaseUrl: normalizeBaseUrl(resolvedHttpBaseUrl, "configured", "http-base-url"), + wsBaseUrl: normalizeBaseUrl(resolvedWsBaseUrl, "configured", "websocket-base-url"), + }, + }), wsBaseUrl: normalizeBaseUrl(resolvedWsBaseUrl, "configured", "websocket-base-url"), }, }; From 36dc4070a1602bc58cc09a253d563b45f2f36fb3 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Sat, 20 Jun 2026 11:46:20 +0200 Subject: [PATCH 3/9] fix: address local dev startup review feedback Stream desktop protocol proxy responses instead of buffering complete bodies, and wait for browser sessions to become observable after manual pairing exchanges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/electron/ElectronProtocol.test.ts | 49 +++++++++++++++++++ apps/desktop/src/electron/ElectronProtocol.ts | 13 +++-- apps/web/src/authBootstrap.test.ts | 2 +- apps/web/src/environments/primary/auth.ts | 1 + 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index 24433549672..5b0c6754953 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -118,6 +118,55 @@ describe("ElectronProtocol", () => { }), ); + it.effect("returns proxied response bodies without buffering them first", () => + Effect.gen(function* () { + let handler: ((request: Request) => Promise) | undefined; + handleMock.mockImplementation((_scheme, nextHandler) => { + handler = nextHandler; + }); + const chunk = new TextEncoder().encode("streamed"); + const httpClientLayer = makeHttpClientLayer((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(chunk); + }, + }), + ), + ), + ), + ); + + const response = yield* Effect.scoped( + Effect.gen(function* () { + const protocol = yield* ElectronProtocol.ElectronProtocol; + yield* protocol.registerDesktopProtocol({ + scheme: "t3code-dev", + targetOrigin: new URL("http://127.0.0.1:3773/"), + backendOrigin: new URL("http://127.0.0.1:3774/"), + clerkFrontendApiHostname: undefined, + }); + assert.isDefined(handler); + return yield* Effect.raceFirst( + Effect.promise(() => handler!(new Request("t3code-dev://app/large-asset"))), + Effect.sleep("250 millis").pipe( + Effect.andThen(Effect.die(new Error("proxy response was buffered"))), + ), + ); + }), + ).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer)))); + + const reader = response.body?.getReader(); + assert.isDefined(reader); + const firstChunk = yield* Effect.promise(() => reader!.read()); + assert.deepEqual(firstChunk.value, chunk); + yield* Effect.promise(() => reader!.cancel()); + }), + ); + it.effect("rejects custom protocol requests for another host", () => Effect.gen(function* () { let handler: ((request: Request) => Promise) | undefined; diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index 5bacf52503e..81d773bac04 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; +import * as Stream from "effect/Stream"; import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import * as Electron from "electron"; @@ -166,12 +167,14 @@ async function proxyRequest( const effect = Effect.gen(function* () { const clientRequest = HttpClientRequest.fromWeb(new Request(targetUrl, init)); const clientResponse = yield* httpClient.execute(clientRequest); - const body = request.method === "HEAD" ? null : yield* clientResponse.arrayBuffer; return withContentSecurityPolicy( - new Response(body, { - status: clientResponse.status, - headers: clientResponse.headers, - }), + new Response( + request.method === "HEAD" ? null : Stream.toReadableStream(clientResponse.stream), + { + status: clientResponse.status, + headers: clientResponse.headers, + }, + ), contentSecurityPolicy, ); }); diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts index f992ec94ea6..a6e758ea52d 100644 --- a/apps/web/src/authBootstrap.test.ts +++ b/apps/web/src/authBootstrap.test.ts @@ -326,7 +326,7 @@ describe("resolveInitialServerAuthGateState", () => { status: "authenticated", }); expect(testApi.calls.browserSession).toEqual([{ credential: "retry-token" }]); - expect(testApi.calls.session).toBe(1); + expect(testApi.calls.session).toBe(2); }); it("rejects a blank pairing token with a structured validation error", async () => { diff --git a/apps/web/src/environments/primary/auth.ts b/apps/web/src/environments/primary/auth.ts index ec6d7272d8f..935fbdb8a37 100644 --- a/apps/web/src/environments/primary/auth.ts +++ b/apps/web/src/environments/primary/auth.ts @@ -377,6 +377,7 @@ async function submitServerAuthCredentialOnce(trimmedCredential: string): Promis resolvedAuthenticatedGateState = null; try { await exchangeBootstrapCredential(trimmedCredential); + await waitForAuthenticatedSessionAfterBootstrap(); } catch (error) { if (isBootstrapHttpError(error) && error.status === 401) { const currentSession = await fetchSessionState().catch(() => null); From 5aab1098558e10e50587ec5218f67216557cdf14 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Sat, 20 Jun 2026 11:54:45 +0200 Subject: [PATCH 4/9] chore: trigger ci Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 55f4c2f4d2c0515afe734a9267754b9ba7b4e02f Mon Sep 17 00:00:00 2001 From: huxcrux Date: Sun, 21 Jun 2026 11:46:56 +0200 Subject: [PATCH 5/9] fix: resolve latest main rebase conflicts --- apps/desktop/src/app/DesktopClerk.test.ts | 2 +- apps/desktop/src/electron/ElectronProtocol.test.ts | 8 ++++++-- apps/web/src/environments/primary/auth.ts | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/app/DesktopClerk.test.ts b/apps/desktop/src/app/DesktopClerk.test.ts index 5b13e82ddfc..c51a815388f 100644 --- a/apps/desktop/src/app/DesktopClerk.test.ts +++ b/apps/desktop/src/app/DesktopClerk.test.ts @@ -31,7 +31,7 @@ const makeDesktopClerkLayer = (isDevelopment = true) => { isDevelopment, } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); - return DesktopClerk.layer.pipe( + return DesktopClerk.makeDesktopClerkLayer(true).pipe( Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)), ); }; diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index 5b0c6754953..406873856da 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -32,6 +32,10 @@ function makeHttpClientLayer( ); } +const unexpectedHttpClientLayer = makeHttpClientLayer(() => + Effect.die("unexpected Electron protocol proxy request"), +); + describe("ElectronProtocol", () => { beforeEach(() => { handleMock.mockReset(); @@ -220,7 +224,7 @@ describe("ElectronProtocol", () => { assert.equal(error.scheme, "t3code-dev"); assert.strictEqual(error.cause, cause); assert.equal(error.message, 'Failed to register Electron protocol scheme "t3code-dev".'); - }).pipe(Effect.provide(ElectronProtocol.layer)), + }).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(unexpectedHttpClientLayer)))), ); it.effect("preserves protocol unregistration failures", () => @@ -250,7 +254,7 @@ describe("ElectronProtocol", () => { assert.strictEqual(error.cause, cause); assert.equal(error.message, 'Failed to unregister Electron protocol scheme "t3code".'); } - }).pipe(Effect.provide(ElectronProtocol.layer)), + }).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(unexpectedHttpClientLayer)))), ); it("keeps executable sources host-restricted while allowing runtime network resources", () => { diff --git a/apps/web/src/environments/primary/auth.ts b/apps/web/src/environments/primary/auth.ts index 935fbdb8a37..23259def1b0 100644 --- a/apps/web/src/environments/primary/auth.ts +++ b/apps/web/src/environments/primary/auth.ts @@ -379,7 +379,10 @@ async function submitServerAuthCredentialOnce(trimmedCredential: string): Promis await exchangeBootstrapCredential(trimmedCredential); await waitForAuthenticatedSessionAfterBootstrap(); } catch (error) { - if (isBootstrapHttpError(error) && error.status === 401) { + if ( + (isPrimaryEnvironmentRequestError(error) && error.status === 401) || + isPrimaryEnvironmentPairingCredentialRejectedError(error) + ) { const currentSession = await fetchSessionState().catch(() => null); if (currentSession?.authenticated) { resolvedAuthenticatedGateState = { status: "authenticated" }; From e62429679a08a36316d730e0f891b348ef59cbcc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 22 Jun 2026 15:41:00 -0700 Subject: [PATCH 6/9] some web changes --- apps/web/src/AppRoot.test.tsx | 22 --------- apps/web/src/AppRoot.tsx | 19 -------- .../web/src/cloud/ConfiguredCloudAuthRoot.tsx | 25 ----------- .../clerk/useT3ConnectAuthPrompt.tsx | 9 ---- ...MobileClientsUserProfilePage.logic.test.ts | 0 .../MobileClientsUserProfilePage.logic.ts | 0 .../MobileClientsUserProfilePage.tsx | 0 .../RelayClientInstallDialog.tsx | 1 + .../T3ConnectClerkProvider.electron.tsx | 18 ++++++++ .../connect/T3ConnectClerkProvider.web.tsx | 17 +++++++ .../T3ConnectSidebarSignIn.configured.tsx | 32 ++++++------- .../T3ConnectSidebarSignIn.tsx | 14 +++--- .../settings/SettingsSidebarNav.tsx | 2 +- apps/web/src/main.tsx | 45 ++++++++++++------- apps/web/src/routes/__root.tsx | 2 +- 15 files changed, 87 insertions(+), 119 deletions(-) delete mode 100644 apps/web/src/AppRoot.test.tsx delete mode 100644 apps/web/src/AppRoot.tsx delete mode 100644 apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx delete mode 100644 apps/web/src/components/clerk/useT3ConnectAuthPrompt.tsx rename apps/web/src/components/{clerk => connect}/MobileClientsUserProfilePage.logic.test.ts (100%) rename apps/web/src/components/{clerk => connect}/MobileClientsUserProfilePage.logic.ts (100%) rename apps/web/src/components/{clerk => connect}/MobileClientsUserProfilePage.tsx (100%) rename apps/web/src/components/{cloud => connect}/RelayClientInstallDialog.tsx (99%) create mode 100644 apps/web/src/components/connect/T3ConnectClerkProvider.electron.tsx create mode 100644 apps/web/src/components/connect/T3ConnectClerkProvider.web.tsx rename apps/web/src/components/{clerk => connect}/T3ConnectSidebarSignIn.configured.tsx (60%) rename apps/web/src/components/{clerk => connect}/T3ConnectSidebarSignIn.tsx (70%) diff --git a/apps/web/src/AppRoot.test.tsx b/apps/web/src/AppRoot.test.tsx deleted file mode 100644 index 9112e31cb86..00000000000 --- a/apps/web/src/AppRoot.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Children, isValidElement, type ReactElement, type ReactNode } from "react"; -import { RouterProvider } from "@tanstack/react-router"; -import { describe, expect, it } from "vite-plus/test"; - -import { ElectronBrowserHost } from "./browser/ElectronBrowserHost"; -import { AppAtomRegistryProvider } from "./rpc/atomRegistry"; -import type { AppRouter } from "./router"; -import { AppRoot } from "./AppRoot"; - -describe("AppRoot", () => { - it("shares the application atom registry with routed UI and the Electron browser host", () => { - const root = AppRoot({ router: {} as AppRouter }); - - expect(root.type).toBe(AppAtomRegistryProvider); - const children = Children.toArray( - (root as ReactElement<{ readonly children: ReactNode }>).props.children, - ); - expect(children).toHaveLength(2); - expect(isValidElement(children[0]) && children[0].type).toBe(RouterProvider); - expect(isValidElement(children[1]) && children[1].type).toBe(ElectronBrowserHost); - }); -}); diff --git a/apps/web/src/AppRoot.tsx b/apps/web/src/AppRoot.tsx deleted file mode 100644 index 1ecb9f6b7b6..00000000000 --- a/apps/web/src/AppRoot.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { RouterProvider } from "@tanstack/react-router"; - -import { ElectronBrowserHost } from "./browser/ElectronBrowserHost"; -import { AppAtomRegistryProvider } from "./rpc/atomRegistry"; -import type { AppRouter } from "./router"; - -/** - * Owns renderer-wide providers. The Electron browser host intentionally sits - * outside the router so its webviews survive route transitions, but it must - * share the same atom registry as routed UI. - */ -export function AppRoot({ router }: { readonly router: AppRouter }) { - return ( - - - - - ); -} diff --git a/apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx b/apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx deleted file mode 100644 index 955c2509959..00000000000 --- a/apps/web/src/cloud/ConfiguredCloudAuthRoot.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ClerkProvider } from "@clerk/react"; -import { passkeys } from "@clerk/electron/passkeys"; -import { ClerkProvider as ElectronClerkProvider } from "@clerk/electron/react"; -import type { ReactNode } from "react"; - -import { isElectron } from "../env"; -import { ManagedRelayAuthProvider } from "./managedAuth"; - -export default function ConfiguredCloudAuthRoot({ - children, - publishableKey, -}: { - readonly children: ReactNode; - readonly publishableKey: string; -}) { - return isElectron ? ( - - {children} - - ) : ( - - {children} - - ); -} diff --git a/apps/web/src/components/clerk/useT3ConnectAuthPrompt.tsx b/apps/web/src/components/clerk/useT3ConnectAuthPrompt.tsx deleted file mode 100644 index 05fa8250b30..00000000000 --- a/apps/web/src/components/clerk/useT3ConnectAuthPrompt.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useClerk } from "@clerk/react"; - -export function useT3ConnectAuthPrompt() { - const clerk = useClerk(); - const openAuthPrompt = () => { - clerk.openWaitlist(); - }; - return { authPrompt: null, openAuthPrompt }; -} diff --git a/apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.test.ts b/apps/web/src/components/connect/MobileClientsUserProfilePage.logic.test.ts similarity index 100% rename from apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.test.ts rename to apps/web/src/components/connect/MobileClientsUserProfilePage.logic.test.ts diff --git a/apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.ts b/apps/web/src/components/connect/MobileClientsUserProfilePage.logic.ts similarity index 100% rename from apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.ts rename to apps/web/src/components/connect/MobileClientsUserProfilePage.logic.ts diff --git a/apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx b/apps/web/src/components/connect/MobileClientsUserProfilePage.tsx similarity index 100% rename from apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx rename to apps/web/src/components/connect/MobileClientsUserProfilePage.tsx diff --git a/apps/web/src/components/cloud/RelayClientInstallDialog.tsx b/apps/web/src/components/connect/RelayClientInstallDialog.tsx similarity index 99% rename from apps/web/src/components/cloud/RelayClientInstallDialog.tsx rename to apps/web/src/components/connect/RelayClientInstallDialog.tsx index 78282f65915..5782978bec0 100644 --- a/apps/web/src/components/cloud/RelayClientInstallDialog.tsx +++ b/apps/web/src/components/connect/RelayClientInstallDialog.tsx @@ -18,6 +18,7 @@ import { DialogPopup, DialogTitle, } from "../ui/dialog"; + const installSteps: ReadonlyArray<{ readonly stage: RelayClientInstallProgressStage; readonly label: string; diff --git a/apps/web/src/components/connect/T3ConnectClerkProvider.electron.tsx b/apps/web/src/components/connect/T3ConnectClerkProvider.electron.tsx new file mode 100644 index 00000000000..0034efc9125 --- /dev/null +++ b/apps/web/src/components/connect/T3ConnectClerkProvider.electron.tsx @@ -0,0 +1,18 @@ +import { passkeys } from "@clerk/electron/passkeys"; +import { ClerkProvider as ClerkElectronProvider } from "@clerk/electron/react"; + +import { ManagedRelayAuthProvider } from "../../cloud/managedAuth"; + +export default function ClerkProvider({ + children, + publishableKey, +}: { + readonly children: React.ReactNode; + readonly publishableKey: string; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/components/connect/T3ConnectClerkProvider.web.tsx b/apps/web/src/components/connect/T3ConnectClerkProvider.web.tsx new file mode 100644 index 00000000000..fc9ff3e2ba1 --- /dev/null +++ b/apps/web/src/components/connect/T3ConnectClerkProvider.web.tsx @@ -0,0 +1,17 @@ +import { ClerkProvider as ClerkReactProvider } from "@clerk/react"; + +import { ManagedRelayAuthProvider } from "../../cloud/managedAuth"; + +export default function ClerkProvider({ + children, + publishableKey, +}: { + readonly children: React.ReactNode; + readonly publishableKey: string; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx b/apps/web/src/components/connect/T3ConnectSidebarSignIn.configured.tsx similarity index 60% rename from apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx rename to apps/web/src/components/connect/T3ConnectSidebarSignIn.configured.tsx index 1cf3d1a8203..331cd51ec45 100644 --- a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.configured.tsx +++ b/apps/web/src/components/connect/T3ConnectSidebarSignIn.configured.tsx @@ -1,9 +1,8 @@ -import { UserButton, useAuth } from "@clerk/react"; +import { UserButton, useAuth, useClerk } from "@clerk/react"; import { LogInIcon, SmartphoneIcon } from "lucide-react"; import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar"; import { MobileClientsUserProfilePage } from "./MobileClientsUserProfilePage"; -import { useT3ConnectAuthPrompt } from "./useT3ConnectAuthPrompt"; export function ConfiguredT3ConnectSidebarAvatar() { const { isLoaded, isSignedIn } = useAuth(); @@ -32,25 +31,22 @@ export function ConfiguredT3ConnectSidebarAvatar() { export function ConfiguredT3ConnectSidebarSignIn() { const { isLoaded, isSignedIn } = useAuth(); - const { authPrompt, openAuthPrompt } = useT3ConnectAuthPrompt(); + const clerk = useClerk(); if (!isLoaded || isSignedIn) return null; return ( - <> - - - - - Sign in to T3 Connect - - - - {authPrompt} - + + + clerk.openWaitlist()} + > + + Sign in to T3 Connect + + + ); } diff --git a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx b/apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx similarity index 70% rename from apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx rename to apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx index cf1c0d0d21d..2a80714c7fd 100644 --- a/apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx +++ b/apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx @@ -1,14 +1,14 @@ -import { lazy, Suspense } from "react"; +import * as React from "react"; import { hasCloudPublicConfig } from "../../cloud/publicConfig"; -const ConfiguredT3ConnectSidebarSignIn = lazy(() => +const ConfiguredT3ConnectSidebarSignIn = React.lazy(() => import("./T3ConnectSidebarSignIn.configured").then((module) => ({ default: module.ConfiguredT3ConnectSidebarSignIn, })), ); -const ConfiguredT3ConnectSidebarAvatar = lazy(() => +const ConfiguredT3ConnectSidebarAvatar = React.lazy(() => import("./T3ConnectSidebarSignIn.configured").then((module) => ({ default: module.ConfiguredT3ConnectSidebarAvatar, })), @@ -18,9 +18,9 @@ export function T3ConnectSidebarSignIn() { if (!hasCloudPublicConfig()) return null; return ( - + - + ); } @@ -28,8 +28,8 @@ export function T3ConnectSidebarAvatar() { if (!hasCloudPublicConfig()) return null; return ( - + - + ); } diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx index 6774b6f333f..3759a6feb91 100644 --- a/apps/web/src/components/settings/SettingsSidebarNav.tsx +++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx @@ -20,7 +20,7 @@ import { SidebarSeparator, useSidebar, } from "../ui/sidebar"; -import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn"; +import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../connect/T3ConnectSidebarSignIn"; export type SettingsSectionPath = | "/settings/general" diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 5caf960dcfb..b8ff588f8b0 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import { createHashHistory, createBrowserHistory } from "@tanstack/react-router"; +import * as React from "react"; +import * as ReactDOM from "react-dom/client"; +import { createHashHistory, createBrowserHistory, RouterProvider } from "@tanstack/react-router"; import "@fontsource-variable/dm-sans/index.css"; import "@fontsource/jetbrains-mono/400.css"; @@ -9,10 +9,11 @@ import "@xterm/xterm/css/xterm.css"; import "./index.css"; import { isElectron } from "./env"; -import { hasCloudPublicConfig } from "./cloud/publicConfig"; +import { hasCloudPublicConfig, resolveCloudPublicConfig } from "./cloud/publicConfig"; import { getRouter } from "./router"; import { syncDocumentWindowControlsOverlayClass } from "./lib/windowControlsOverlay"; -import { AppRoot } from "./AppRoot"; +import { AppAtomRegistryProvider } from "./rpc/atomRegistry"; +import { ElectronBrowserHost } from "./browser/ElectronBrowserHost"; // Electron loads the app from a file-backed shell, so hash history avoids path resolution issues. const history = isElectron ? createHashHistory() : createBrowserHistory(); @@ -23,21 +24,31 @@ if (isElectron) { syncDocumentWindowControlsOverlayClass(); } -const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; -const ConfiguredCloudAuthRoot = React.lazy(() => import("./cloud/ConfiguredCloudAuthRoot")); +const clerkPublishableKey = resolveCloudPublicConfig().clerkPublishableKey; +const T3ConnectClerkProvider = React.lazy(() => + isElectron + ? import("./components/connect/T3ConnectClerkProvider.electron") + : import("./components/connect/T3ConnectClerkProvider.web"), +); -const app = ; +const AuthWrapper = (props: { children: React.ReactNode }) => + clerkPublishableKey && hasCloudPublicConfig() ? ( + + + {props.children} + + + ) : ( + props.children + ); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - {clerkPublishableKey && hasCloudPublicConfig() ? ( - - - {app} - - - ) : ( - app - )} + + + + + + , ); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 36de3b95706..4c1f9200051 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -14,7 +14,7 @@ import { APP_BASE_NAME, APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../branding"; import { resolveServerBackedAppDisplayName } from "../branding.logic"; import { AppSidebarLayout } from "../components/AppSidebarLayout"; import { CommandPalette } from "../components/CommandPalette"; -import { RelayClientInstallDialog } from "../components/cloud/RelayClientInstallDialog"; +import { RelayClientInstallDialog } from "../components/connect/RelayClientInstallDialog"; import { SshPasswordPromptDialog } from "../components/desktop/SshPasswordPromptDialog"; import { ProviderUpdateLaunchNotification } from "../components/ProviderUpdateLaunchNotification"; import { SlowRpcRequestToastCoordinator } from "../components/SlowRpcRequestToastCoordinator"; From 63d0d6a9d1d8a92d2e55272064e793b92f854a54 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Tue, 23 Jun 2026 02:07:54 +0200 Subject: [PATCH 7/9] fix: avoid local desktop clerk startup without config --- apps/desktop/src/app/DesktopApp.ts | 14 +- apps/desktop/src/app/DesktopClerk.test.ts | 41 +++--- .../src/electron/ElectronProtocol.test.ts | 132 ++---------------- apps/desktop/src/electron/ElectronProtocol.ts | 65 +-------- apps/desktop/src/main.ts | 8 +- apps/desktop/src/preload.ts | 15 +- apps/desktop/src/window/DesktopWindow.test.ts | 2 +- apps/desktop/src/window/DesktopWindow.ts | 6 +- apps/web/src/authBootstrap.test.ts | 58 -------- .../src/components/DiffWorkerPoolProvider.tsx | 9 +- .../connect/T3ConnectSidebarSignIn.tsx | 30 +--- apps/web/src/environments/primary/auth.ts | 49 +------ .../environments/primary/bootstrap.test.ts | 43 ------ .../environments/primary/httpLayer.test.ts | 25 ---- apps/web/src/environments/primary/target.ts | 78 +++-------- apps/web/src/main.tsx | 39 +++--- apps/web/src/routes/settings.connections.tsx | 17 +-- apps/web/vite.config.ts | 4 +- pnpm-lock.yaml | 1 + pnpm-workspace.yaml | 1 + 20 files changed, 125 insertions(+), 512 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 214fd383e04..a721b883a7a 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -170,12 +170,14 @@ const bootstrap = Effect.gen(function* () { const rendererTarget = environment.isDevelopment ? Option.getOrThrow(environment.devServerUrl) : backendConfig.httpBaseUrl; - yield* electronProtocol.registerDesktopProtocol({ - scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment), - targetOrigin: rendererTarget, - backendOrigin: backendConfig.httpBaseUrl, - clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname, - }); + if (!environment.isDevelopment || DesktopClerk.desktopClerkBridgeEnabled) { + yield* electronProtocol.registerDesktopProtocol({ + scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment), + targetOrigin: rendererTarget, + backendOrigin: backendConfig.httpBaseUrl, + clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname, + }); + } yield* logBootstrapInfo("bootstrap resolved backend endpoint", { baseUrl: backendConfig.httpBaseUrl.href, }); diff --git a/apps/desktop/src/app/DesktopClerk.test.ts b/apps/desktop/src/app/DesktopClerk.test.ts index c51a815388f..6e8b7355d64 100644 --- a/apps/desktop/src/app/DesktopClerk.test.ts +++ b/apps/desktop/src/app/DesktopClerk.test.ts @@ -53,26 +53,6 @@ describe("DesktopClerk", () => { assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname("invalid"), undefined); }); - it.effect("skips acquiring the SDK bridge when Clerk is disabled", () => { - const environment = DesktopEnvironment.DesktopEnvironment.of({ - stateDir: "/tmp/t3-state", - isDevelopment: true, - } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); - - return Effect.gen(function* () { - yield* Effect.scoped( - Layer.build( - DesktopClerk.makeDesktopClerkLayer(false).pipe( - Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)), - ), - ), - ); - - assert.deepEqual(storageMock.mock.calls, []); - assert.deepEqual(createClerkBridgeMock.mock.calls, []); - }); - }); - it.effect("acquires and releases the SDK bridge with the layer", () => { const cleanup = vi.fn(); storageMock.mockReturnValue(storageAdapter); @@ -96,6 +76,27 @@ describe("DesktopClerk", () => { }); }); + it.effect("skips the SDK bridge when the build has no Clerk publishable key", () => { + storageMock.mockReturnValue(storageAdapter); + createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn() }); + + return Effect.gen(function* () { + yield* Effect.scoped( + Layer.build(DesktopClerk.makeDesktopClerkLayer(false)).pipe( + Effect.provide( + Layer.succeed(DesktopEnvironment.DesktopEnvironment, { + stateDir: "/tmp/t3-state", + isDevelopment: true, + } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]), + ), + ), + ); + + assert.deepEqual(storageMock.mock.calls, []); + assert.deepEqual(createClerkBridgeMock.mock.calls, []); + }); + }); + it.effect("preserves bridge initialization failures", () => { const cause = new Error("bridge initialization failed"); storageMock.mockReturnValue(storageAdapter); diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index 406873856da..56fe009fee2 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -1,83 +1,35 @@ import { assert, describe, it } from "@effect/vitest"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { beforeEach, vi } from "vite-plus/test"; -const { handleMock, registerSchemesAsPrivilegedMock, unhandleMock } = vi.hoisted(() => ({ +const { handleMock, netFetchMock, unhandleMock } = vi.hoisted(() => ({ handleMock: vi.fn(), - registerSchemesAsPrivilegedMock: vi.fn(), + netFetchMock: vi.fn(), unhandleMock: vi.fn(), })); vi.mock("electron", () => ({ - protocol: { - handle: handleMock, - registerSchemesAsPrivileged: registerSchemesAsPrivilegedMock, - unhandle: unhandleMock, - }, + net: { fetch: netFetchMock }, + protocol: { handle: handleMock, unhandle: unhandleMock }, })); import * as ElectronProtocol from "./ElectronProtocol.ts"; -function makeHttpClientLayer( - handler: ( - request: HttpClientRequest.HttpClientRequest, - ) => Effect.Effect, -) { - return Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => handler(request)), - ); -} - -const unexpectedHttpClientLayer = makeHttpClientLayer(() => - Effect.die("unexpected Electron protocol proxy request"), -); - describe("ElectronProtocol", () => { beforeEach(() => { handleMock.mockReset(); - registerSchemesAsPrivilegedMock.mockReset(); + netFetchMock.mockReset(); unhandleMock.mockReset(); }); - it("registers the desktop scheme as a secure standard scheme", () => { - ElectronProtocol.registerDesktopSchemePrivileges(true); - - assert.deepEqual(registerSchemesAsPrivilegedMock.mock.calls, [ - [ - [ - { - scheme: "t3code-dev", - privileges: { - standard: true, - secure: true, - supportFetchAPI: true, - corsEnabled: true, - stream: true, - }, - }, - ], - ], - ]); - }); - it.effect("proxies the stable renderer origin to the current app server", () => Effect.gen(function* () { let handler: ((request: Request) => Promise) | undefined; - const requestUrls: string[] = []; handleMock.mockImplementation((_scheme, nextHandler) => { handler = nextHandler; }); - const httpClientLayer = makeHttpClientLayer((request) => - Effect.gen(function* () { - const webRequest = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie); - requestUrls.push(webRequest.url); - return HttpClientResponse.fromWeb(request, new Response("ok")); - }), - ); + netFetchMock.mockResolvedValue(new Response("ok")); yield* Effect.scoped( Effect.gen(function* () { @@ -111,79 +63,23 @@ describe("ElectronProtocol", () => { "font-src 'self' t3code-dev: data:", ); }), - ).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer)))); + ); assert.deepEqual( handleMock.mock.calls.map((call) => call[0]), ["t3code-dev"], ); - assert.equal(requestUrls[0], "http://127.0.0.1:3773/api/health?verbose=1"); + assert.equal(netFetchMock.mock.calls[0]?.[0], "http://127.0.0.1:3773/api/health?verbose=1"); assert.deepEqual(unhandleMock.mock.calls, [["t3code-dev"]]); - }), - ); - - it.effect("returns proxied response bodies without buffering them first", () => - Effect.gen(function* () { - let handler: ((request: Request) => Promise) | undefined; - handleMock.mockImplementation((_scheme, nextHandler) => { - handler = nextHandler; - }); - const chunk = new TextEncoder().encode("streamed"); - const httpClientLayer = makeHttpClientLayer((request) => - Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response( - new ReadableStream({ - start(controller) { - controller.enqueue(chunk); - }, - }), - ), - ), - ), - ); - - const response = yield* Effect.scoped( - Effect.gen(function* () { - const protocol = yield* ElectronProtocol.ElectronProtocol; - yield* protocol.registerDesktopProtocol({ - scheme: "t3code-dev", - targetOrigin: new URL("http://127.0.0.1:3773/"), - backendOrigin: new URL("http://127.0.0.1:3774/"), - clerkFrontendApiHostname: undefined, - }); - assert.isDefined(handler); - return yield* Effect.raceFirst( - Effect.promise(() => handler!(new Request("t3code-dev://app/large-asset"))), - Effect.sleep("250 millis").pipe( - Effect.andThen(Effect.die(new Error("proxy response was buffered"))), - ), - ); - }), - ).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer)))); - - const reader = response.body?.getReader(); - assert.isDefined(reader); - const firstChunk = yield* Effect.promise(() => reader!.read()); - assert.deepEqual(firstChunk.value, chunk); - yield* Effect.promise(() => reader!.cancel()); - }), + }).pipe(Effect.provide(ElectronProtocol.layer)), ); it.effect("rejects custom protocol requests for another host", () => Effect.gen(function* () { let handler: ((request: Request) => Promise) | undefined; - const requests: HttpClientRequest.HttpClientRequest[] = []; handleMock.mockImplementation((_scheme, nextHandler) => { handler = nextHandler; }); - const httpClientLayer = makeHttpClientLayer((request) => - Effect.sync(() => { - requests.push(request); - return HttpClientResponse.fromWeb(request, new Response("unexpected")); - }), - ); const response = yield* Effect.scoped( Effect.gen(function* () { @@ -196,11 +92,11 @@ describe("ElectronProtocol", () => { }); return yield* Effect.promise(() => handler!(new Request("t3code://other/"))); }), - ).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer)))); + ); assert.equal(response.status, 404); - assert.equal(requests.length, 0); - }), + assert.equal(netFetchMock.mock.calls.length, 0); + }).pipe(Effect.provide(ElectronProtocol.layer)), ); it.effect("preserves protocol registration failures", () => @@ -224,7 +120,7 @@ describe("ElectronProtocol", () => { assert.equal(error.scheme, "t3code-dev"); assert.strictEqual(error.cause, cause); assert.equal(error.message, 'Failed to register Electron protocol scheme "t3code-dev".'); - }).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(unexpectedHttpClientLayer)))), + }).pipe(Effect.provide(ElectronProtocol.layer)), ); it.effect("preserves protocol unregistration failures", () => @@ -254,7 +150,7 @@ describe("ElectronProtocol", () => { assert.strictEqual(error.cause, cause); assert.equal(error.message, 'Failed to unregister Electron protocol scheme "t3code".'); } - }).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(unexpectedHttpClientLayer)))), + }).pipe(Effect.provide(ElectronProtocol.layer)), ); it("keeps executable sources host-restricted while allowing runtime network resources", () => { diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index 81d773bac04..757c26178d0 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -4,8 +4,6 @@ import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import * as Electron from "electron"; @@ -25,21 +23,6 @@ export function getDesktopUrl(isDevelopment: boolean): string { return `${getDesktopOrigin(isDevelopment)}/`; } -export function registerDesktopSchemePrivileges(isDevelopment: boolean): void { - Electron.protocol.registerSchemesAsPrivileged([ - { - scheme: getDesktopScheme(isDevelopment), - privileges: { - standard: true, - secure: true, - supportFetchAPI: true, - corsEnabled: true, - stream: true, - }, - }, - ]); -} - export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass()( "ElectronProtocolRegistrationError", { @@ -120,33 +103,10 @@ function withContentSecurityPolicy(response: Response, policy: string): Response }); } -const hopByHopRequestHeaders = new Set([ - "connection", - "host", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "te", - "trailer", - "transfer-encoding", - "upgrade", -]); - -function makeProxyRequestHeaders(requestHeaders: Headers): Headers { - const headers = new Headers(); - for (const [name, value] of requestHeaders) { - if (!hopByHopRequestHeaders.has(name.toLowerCase())) { - headers.set(name, value); - } - } - return headers; -} - async function proxyRequest( request: Request, targetOrigin: URL, contentSecurityPolicy: string, - httpClient: HttpClient.HttpClient, ): Promise { const requestUrl = new URL(request.url); if (requestUrl.host !== DESKTOP_HOST) { @@ -155,36 +115,19 @@ async function proxyRequest( const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, targetOrigin); const init: RequestInit = { - cache: "no-store", method: request.method, - headers: makeProxyRequestHeaders(request.headers), + headers: request.headers, }; if (request.method !== "GET" && request.method !== "HEAD") { init.body = request.body; (init as RequestInit & { duplex: "half" }).duplex = "half"; } - - const effect = Effect.gen(function* () { - const clientRequest = HttpClientRequest.fromWeb(new Request(targetUrl, init)); - const clientResponse = yield* httpClient.execute(clientRequest); - return withContentSecurityPolicy( - new Response( - request.method === "HEAD" ? null : Stream.toReadableStream(clientResponse.stream), - { - status: clientResponse.status, - headers: clientResponse.headers, - }, - ), - contentSecurityPolicy, - ); - }); - - return Effect.runPromise(effect); + const response = await Electron.net.fetch(targetUrl.toString(), init); + return withContentSecurityPolicy(response, contentSecurityPolicy); } export const make = Effect.gen(function* () { const registered = yield* Ref.make(false); - const httpClient = yield* HttpClient.HttpClient; const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")( function* (input: DesktopProtocolRegistrationInput) { @@ -196,7 +139,7 @@ export const make = Effect.gen(function* () { Effect.try({ try: () => { Electron.protocol.handle(input.scheme, (request) => - proxyRequest(request, input.targetOrigin, contentSecurityPolicy, httpClient), + proxyRequest(request, input.targetOrigin, contentSecurityPolicy), ); }, catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }), diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b307e858771..b88eb18e57f 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -51,12 +51,6 @@ import * as BrowserSession from "./preview/BrowserSession.ts"; import * as PreviewManager from "./preview/Manager.ts"; import * as DesktopWindow from "./window/DesktopWindow.ts"; -const isDesktopDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); - -if (!DesktopClerk.desktopClerkBridgeEnabled) { - ElectronProtocol.registerDesktopSchemePrivileges(isDesktopDevelopment); -} - const desktopEnvironmentLayer = Layer.unwrap( Effect.gen(function* () { const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe( @@ -111,7 +105,7 @@ const electronLayer = Layer.mergeAll( ElectronApp.layer, ElectronDialog.layer, ElectronMenu.layer, - ElectronProtocol.layer.pipe(Layer.provide(NodeHttpClient.layerUndici)), + ElectronProtocol.layer, ElectronSafeStorage.layer, ElectronShell.layer, ElectronTheme.layer, diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 6f126f41334..617b74eaf6b 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -4,12 +4,23 @@ import type { DesktopPreviewRecordingFrame, DesktopPreviewTabState, } from "@t3tools/contracts"; -import { exposeClerkBridge } from "@clerk/electron/preload"; import { contextBridge, ipcRenderer } from "electron"; import * as IpcChannels from "./ipc/channels.ts"; -exposeClerkBridge({ passkeys: true }); +declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; + +const desktopClerkPreloadEnabled = Boolean( + typeof __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__ === "undefined" + ? undefined + : __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__.trim(), +); + +if (desktopClerkPreloadEnabled) { + const { exposeClerkBridge } = + require("@clerk/electron/preload") as typeof import("@clerk/electron/preload"); + exposeClerkBridge({ passkeys: true }); +} function unwrapEnsureSshEnvironmentResult(result: unknown) { if ( diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 76413dd0b55..efc57720b92 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -231,7 +231,7 @@ describe("DesktopWindow", () => { assert.equal(yield* Ref.get(createCount), 1); assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor); assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]); - assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]); + assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["http://127.0.0.1:5733/"]); assert.equal(fakeWindow.openDevTools.mock.calls.length, 1); }).pipe(Effect.provide(layer)); }), diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index e6cfce3c54f..c8e3d7292fb 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -7,6 +7,7 @@ import * as Ref from "effect/Ref"; import type * as Electron from "electron"; import * as DesktopAssets from "../app/DesktopAssets.ts"; +import * as DesktopClerk from "../app/DesktopClerk.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import { makeComponentLogger } from "../app/DesktopObservability.ts"; import * as DesktopState from "../app/DesktopState.ts"; @@ -159,7 +160,10 @@ export const make = Effect.gen(function* () { DesktopWindowError > { yield* previewManager.getBrowserSession(); - const applicationUrl = getDesktopUrl(environment.isDevelopment); + const applicationUrl = + environment.isDevelopment && !DesktopClerk.desktopClerkBridgeEnabled + ? Option.getOrThrow(environment.devServerUrl).href + : getDesktopUrl(environment.isDevelopment); const iconPaths = yield* assets.iconPaths; const iconOption = getIconOption(iconPaths, environment.platform); const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts index a6e758ea52d..ced16c15f4e 100644 --- a/apps/web/src/authBootstrap.test.ts +++ b/apps/web/src/authBootstrap.test.ts @@ -186,25 +186,6 @@ describe("resolveInitialServerAuthGateState", () => { ); }); - it("uses the vite proxy for configured loopback auth requests during local dev", async () => { - await installAuthApi({ session: () => unauthenticatedSession(LOOPBACK_AUTH) }); - vi.stubEnv("VITE_HTTP_URL", "http://localhost:13773"); - vi.stubEnv("VITE_WS_URL", "ws://localhost:13773"); - vi.stubEnv("VITE_DEV_SERVER_URL", "http://localhost:5733"); - installTestBrowser("http://localhost: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://localhost: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"); @@ -230,23 +211,6 @@ describe("resolveInitialServerAuthGateState", () => { ); }); - it("uses the desktop custom scheme proxy for desktop-managed auth requests", async () => { - const testWindow = installTestBrowser("t3code-dev://app/"); - testWindow.desktopBridge = { - getLocalEnvironmentBootstrap: () => ({ - label: "Local environment", - httpBaseUrl: "http://127.0.0.1:3773", - wsBaseUrl: "ws://127.0.0.1:3773", - }), - } as DesktopBridge; - - const { resolvePrimaryEnvironmentHttpUrl } = await import("./environments/primary"); - - expect(resolvePrimaryEnvironmentHttpUrl("/api/auth/session")).toBe( - "t3code-dev://app/api/auth/session", - ); - }); - it("returns a requires-auth state instead of throwing when no bootstrap credential exists", async () => { await installAuthApi({ session: () => unauthenticatedSession(LOOPBACK_AUTH) }); const { resolveInitialServerAuthGateState } = await import("./environments/primary"); @@ -397,28 +361,6 @@ describe("resolveInitialServerAuthGateState", () => { expect(error.message).not.toContain(cause.message); }); - it("accepts an already-established session after a duplicate one-time token submit", async () => { - const testWindow = installTestBrowser("http://localhost/pair#token=already-used-token"); - const testApi = await installAuthApi({ - session: () => authenticatedSession(LOOPBACK_AUTH), - browserSession: () => - Effect.fail( - new EnvironmentAuthInvalidError({ - code: "auth_invalid", - reason: "invalid_credential", - traceId: "trace-invalid-credential", - }), - ), - }); - - const { submitServerAuthCredential } = await import("./environments/primary"); - - await expect(submitServerAuthCredential("already-used-token")).resolves.toBeUndefined(); - expect(testWindow.location.hash).toBe(""); - expect(testApi.calls.browserSession).toEqual([{ credential: "already-used-token" }]); - expect(testApi.calls.session).toBe(1); - }); - it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => { vi.useFakeTimers(); const nextSession = sequence( diff --git a/apps/web/src/components/DiffWorkerPoolProvider.tsx b/apps/web/src/components/DiffWorkerPoolProvider.tsx index 8228a9f1fcb..3ec748c6bcb 100644 --- a/apps/web/src/components/DiffWorkerPoolProvider.tsx +++ b/apps/web/src/components/DiffWorkerPoolProvider.tsx @@ -1,4 +1,5 @@ import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react"; +import DiffsWorker from "@pierre/diffs/worker/worker.js?worker"; import * as Schema from "effect/Schema"; import { useEffect, useMemo, type ReactNode } from "react"; import { useTheme } from "../hooks/useTheme"; @@ -14,12 +15,6 @@ export class DiffWorkerError extends Schema.TaggedErrorClass()( } } -function createDiffsWorker() { - return new Worker(new URL("@pierre/diffs/worker/worker-portable.js", import.meta.url), { - type: "module", - }); -} - function DiffWorkerThemeSync({ themeName }: { themeName: DiffThemeName }) { const workerPool = useWorkerPool(); @@ -64,7 +59,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { poolOptions={{ workerFactory: () => { try { - return createDiffsWorker(); + return new DiffsWorker(); } catch (cause) { throw new DiffWorkerError({ operation: "create-worker", diff --git a/apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx b/apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx index 2a80714c7fd..4aaae8ba8d1 100644 --- a/apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx +++ b/apps/web/src/components/connect/T3ConnectSidebarSignIn.tsx @@ -1,35 +1,17 @@ -import * as React from "react"; - import { hasCloudPublicConfig } from "../../cloud/publicConfig"; - -const ConfiguredT3ConnectSidebarSignIn = React.lazy(() => - import("./T3ConnectSidebarSignIn.configured").then((module) => ({ - default: module.ConfiguredT3ConnectSidebarSignIn, - })), -); - -const ConfiguredT3ConnectSidebarAvatar = React.lazy(() => - import("./T3ConnectSidebarSignIn.configured").then((module) => ({ - default: module.ConfiguredT3ConnectSidebarAvatar, - })), -); +import { + ConfiguredT3ConnectSidebarAvatar, + ConfiguredT3ConnectSidebarSignIn, +} from "./T3ConnectSidebarSignIn.configured"; export function T3ConnectSidebarSignIn() { if (!hasCloudPublicConfig()) return null; - return ( - - - - ); + return ; } export function T3ConnectSidebarAvatar() { if (!hasCloudPublicConfig()) return null; - return ( - - - - ); + return ; } diff --git a/apps/web/src/environments/primary/auth.ts b/apps/web/src/environments/primary/auth.ts index 23259def1b0..96814b92b79 100644 --- a/apps/web/src/environments/primary/auth.ts +++ b/apps/web/src/environments/primary/auth.ts @@ -150,10 +150,6 @@ type ServerAuthGateState = let bootstrapPromise: Promise | null = null; let resolvedAuthenticatedGateState: ServerAuthGateState | null = null; -let credentialSubmitPromise: { - readonly credential: string; - readonly promise: Promise; -} | null = null; const AUTH_SESSION_ESTABLISH_TIMEOUT_MS = 2_000; const AUTH_SESSION_ESTABLISH_STEP_MS = 100; @@ -350,50 +346,8 @@ export async function submitServerAuthCredential(credential: string): Promise { - if (resolvedAuthenticatedGateState?.status === "authenticated") { - bootstrapPromise = null; - stripPairingTokenFromUrl(); - return; - } resolvedAuthenticatedGateState = null; - try { - await exchangeBootstrapCredential(trimmedCredential); - await waitForAuthenticatedSessionAfterBootstrap(); - } catch (error) { - if ( - (isPrimaryEnvironmentRequestError(error) && error.status === 401) || - isPrimaryEnvironmentPairingCredentialRejectedError(error) - ) { - const currentSession = await fetchSessionState().catch(() => null); - if (currentSession?.authenticated) { - resolvedAuthenticatedGateState = { status: "authenticated" }; - bootstrapPromise = null; - stripPairingTokenFromUrl(); - return; - } - } - throw error; - } - resolvedAuthenticatedGateState = { status: "authenticated" }; + await exchangeBootstrapCredential(trimmedCredential); bootstrapPromise = null; stripPairingTokenFromUrl(); } @@ -575,5 +529,4 @@ export async function resolveInitialServerAuthGateState(): Promise { ); }); - it("uses the vite proxy for configured loopback descriptor requests during local dev", async () => { - vi.stubEnv("VITE_HTTP_URL", "http://localhost:13773"); - vi.stubEnv("VITE_WS_URL", "ws://localhost:13773"); - vi.stubEnv("VITE_DEV_SERVER_URL", "http://localhost:5733"); - installTestBrowser("http://localhost:5733/"); - await installDescriptorApi(); - - await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT); - expect(resolvePrimaryEnvironmentHttpUrl("/.well-known/t3/environment")).toBe( - "http://localhost:5733/.well-known/t3/environment", - ); - expect(getPrimaryKnownEnvironment()?.target).toEqual({ - httpBaseUrl: "http://localhost:5733/", - wsBaseUrl: "ws://localhost:13773/", - }); - }); - 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.stubGlobal("window", { @@ -268,30 +251,4 @@ describe("environmentBootstrap", () => { message: "The window-origin primary environment target uses unsupported protocol file:.", }); }); - - it("uses the desktop custom scheme proxy for desktop-managed descriptor requests", async () => { - writePrimaryEnvironmentDescriptor(BASE_ENVIRONMENT); - vi.stubGlobal("window", { - location: new URL("t3code-dev://app/"), - history: { - replaceState: vi.fn(), - }, - desktopBridge: { - getLocalEnvironmentBootstrap: () => ({ - label: "Local environment", - httpBaseUrl: "http://127.0.0.1:3773", - wsBaseUrl: "ws://127.0.0.1:3773", - bootstrapToken: "desktop-bootstrap-token", - }), - }, - }); - - expect(resolvePrimaryEnvironmentHttpUrl("/.well-known/t3/environment")).toBe( - "t3code-dev://app/.well-known/t3/environment", - ); - expect(getPrimaryKnownEnvironment()?.target).toEqual({ - httpBaseUrl: "t3code-dev://app/", - wsBaseUrl: "ws://127.0.0.1:3773/", - }); - }); }); diff --git a/apps/web/src/environments/primary/httpLayer.test.ts b/apps/web/src/environments/primary/httpLayer.test.ts index 292f53a6c34..5bc1ef01da1 100644 --- a/apps/web/src/environments/primary/httpLayer.test.ts +++ b/apps/web/src/environments/primary/httpLayer.test.ts @@ -35,31 +35,6 @@ describe.sequential("primary environment HTTP layer", () => { }).pipe(Effect.provide(makePrimaryEnvironmentHttpLayer())); }); - it.effect("uses cookie credentials for vite-proxied configured loopback environments", () => { - const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); - vi.stubEnv("VITE_HTTP_URL", "http://localhost:13773"); - vi.stubEnv("VITE_WS_URL", "ws://localhost:13773"); - vi.stubEnv("VITE_DEV_SERVER_URL", "http://localhost:5733"); - vi.stubGlobal("fetch", fetchMock); - Object.defineProperty(globalThis, "window", { - configurable: true, - value: { - location: { - href: "http://localhost:5733/pair", - origin: "http://localhost:5733", - }, - }, - }); - - return Effect.gen(function* () { - yield* HttpClient.get("http://localhost: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); diff --git a/apps/web/src/environments/primary/target.ts b/apps/web/src/environments/primary/target.ts index a38bf824a12..a14a99ec4dc 100644 --- a/apps/web/src/environments/primary/target.ts +++ b/apps/web/src/environments/primary/target.ts @@ -98,10 +98,6 @@ function parseTargetUrl(input: { } } -function currentWindowBaseUrl(): string { - return window.location.origin === "null" ? window.location.href : window.location.origin; -} - function normalizeBaseUrl( rawValue: string, source: PrimaryEnvironmentTargetSource, @@ -109,7 +105,7 @@ function normalizeBaseUrl( ): string { return parseTargetUrl({ rawValue, - baseUrl: currentWindowBaseUrl(), + baseUrl: window.location.origin, source, urlKind, }).toString(); @@ -141,16 +137,13 @@ export function isLoopbackHostname(hostname: string): boolean { return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname)); } -function effectiveUrlOrigin(url: URL): string { - return url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin; -} - -function originBaseUrl(origin: string): string { - return origin.endsWith("/") ? origin : `${origin}/`; -} - function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string { const httpBaseUrl = primaryTarget.target.httpBaseUrl; + const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim(); + if (!configuredDevServerUrl) { + return httpBaseUrl; + } + const currentUrl = parseTargetUrl({ rawValue: window.location.href, source: "window-origin", @@ -161,38 +154,27 @@ function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): str source: primaryTarget.source, urlKind: "http-base-url", }); - const currentOrigin = effectiveUrlOrigin(currentUrl); - const isCurrentOriginDesktopDevApp = - currentUrl.protocol === "t3code-dev:" && currentUrl.host === "app"; - - if (currentOrigin === targetUrl.origin || !isLoopbackHostname(targetUrl.hostname)) { - return httpBaseUrl; - } - - if (isCurrentOriginDesktopDevApp) { - return originBaseUrl(currentOrigin); - } - - const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim(); - if (!configuredDevServerUrl) { - return httpBaseUrl; - } - const devServerUrl = parseTargetUrl({ rawValue: configuredDevServerUrl, - baseUrl: originBaseUrl(currentOrigin), + baseUrl: currentUrl.origin, source: "configured", urlKind: "development-server-url", }); const isCurrentOriginDevServer = (currentUrl.protocol === "http:" || currentUrl.protocol === "https:") && - currentOrigin === devServerUrl.origin; - if (!isCurrentOriginDevServer || !isLoopbackHostname(currentUrl.hostname)) { + currentUrl.origin === devServerUrl.origin; + + if ( + !isCurrentOriginDevServer || + currentUrl.origin === targetUrl.origin || + !isLoopbackHostname(currentUrl.hostname) || + !isLoopbackHostname(targetUrl.hostname) + ) { return httpBaseUrl; } - return originBaseUrl(currentOrigin); + return currentUrl.origin; } function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null { @@ -217,13 +199,7 @@ function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null { return { source: "configured", target: { - httpBaseUrl: resolveHttpRequestBaseUrl({ - source: "configured", - target: { - httpBaseUrl: normalizeBaseUrl(resolvedHttpBaseUrl, "configured", "http-base-url"), - wsBaseUrl: normalizeBaseUrl(resolvedWsBaseUrl, "configured", "websocket-base-url"), - }, - }), + httpBaseUrl: normalizeBaseUrl(resolvedHttpBaseUrl, "configured", "http-base-url"), wsBaseUrl: normalizeBaseUrl(resolvedWsBaseUrl, "configured", "websocket-base-url"), }, }; @@ -273,21 +249,11 @@ function resolveDesktopPrimaryTarget(): PrimaryEnvironmentTarget | null { return { source: "desktop-managed", target: { - httpBaseUrl: resolveHttpRequestBaseUrl({ - source: "desktop-managed", - target: { - httpBaseUrl: normalizeBaseUrl( - desktopBootstrap.httpBaseUrl, - "desktop-managed", - "http-base-url", - ), - wsBaseUrl: normalizeBaseUrl( - desktopBootstrap.wsBaseUrl, - "desktop-managed", - "websocket-base-url", - ), - }, - }), + httpBaseUrl: normalizeBaseUrl( + desktopBootstrap.httpBaseUrl, + "desktop-managed", + "http-base-url", + ), wsBaseUrl: normalizeBaseUrl( desktopBootstrap.wsBaseUrl, "desktop-managed", diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index b8ff588f8b0..749a00941b6 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,6 +1,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom/client"; import { createHashHistory, createBrowserHistory, RouterProvider } from "@tanstack/react-router"; +import { ClerkProvider } from "@clerk/react"; +import { passkeys } from "@clerk/electron/passkeys"; +import { ClerkProvider as ElectronClerkProvider } from "@clerk/electron/react"; import "@fontsource-variable/dm-sans/index.css"; import "@fontsource/jetbrains-mono/400.css"; @@ -9,7 +12,8 @@ import "@xterm/xterm/css/xterm.css"; import "./index.css"; import { isElectron } from "./env"; -import { hasCloudPublicConfig, resolveCloudPublicConfig } from "./cloud/publicConfig"; +import { ManagedRelayAuthProvider } from "./cloud/managedAuth"; +import { hasCloudPublicConfig } from "./cloud/publicConfig"; import { getRouter } from "./router"; import { syncDocumentWindowControlsOverlayClass } from "./lib/windowControlsOverlay"; import { AppAtomRegistryProvider } from "./rpc/atomRegistry"; @@ -24,31 +28,32 @@ if (isElectron) { syncDocumentWindowControlsOverlayClass(); } -const clerkPublishableKey = resolveCloudPublicConfig().clerkPublishableKey; -const T3ConnectClerkProvider = React.lazy(() => - isElectron - ? import("./components/connect/T3ConnectClerkProvider.electron") - : import("./components/connect/T3ConnectClerkProvider.web"), +const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; + +const app = ( + + + + ); const AuthWrapper = (props: { children: React.ReactNode }) => clerkPublishableKey && hasCloudPublicConfig() ? ( - - - {props.children} - - + isElectron ? ( + + {props.children} + + ) : ( + + {props.children} + + ) ) : ( props.children ); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - - + {app} , ); diff --git a/apps/web/src/routes/settings.connections.tsx b/apps/web/src/routes/settings.connections.tsx index e1490d19f3b..275eda6c516 100644 --- a/apps/web/src/routes/settings.connections.tsx +++ b/apps/web/src/routes/settings.connections.tsx @@ -1,20 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; -import { lazy, Suspense } from "react"; -const ConnectionsSettings = lazy(() => - import("../components/settings/ConnectionsSettings").then((module) => ({ - default: module.ConnectionsSettings, - })), -); - -function ConnectionsSettingsRoute() { - return ( - - - - ); -} +import { ConnectionsSettings } from "../components/settings/ConnectionsSettings"; export const Route = createFileRoute("/settings/connections")({ - component: ConnectionsSettingsRoute, + component: ConnectionsSettings, }); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index ba532796537..8f984c850dc 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -14,7 +14,6 @@ Object.assign(process.env, repoEnv); const port = Number(process.env.PORT ?? 5733); const host = process.env.HOST?.trim() || "localhost"; -const configuredDevServerUrl = process.env.VITE_DEV_SERVER_URL?.trim(); const configuredWsUrl = process.env.VITE_WS_URL?.trim(); const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; @@ -104,6 +103,7 @@ export default defineConfig(() => { "@pierre/diffs", "@pierre/diffs/editor", "@pierre/diffs/react", + "@pierre/diffs/worker/worker.js", "effect/Array", "effect/Order", "react-dom/client", @@ -111,7 +111,6 @@ export default defineConfig(() => { }, define: { // In dev mode, tell the web app where the WebSocket server lives - "import.meta.env.VITE_DEV_SERVER_URL": JSON.stringify(configuredDevServerUrl ?? ""), "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl), "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey), @@ -157,7 +156,6 @@ export default defineConfig(() => { // connection logs — enable "Verbose" in DevTools to see them. protocol: "ws", host, - clientPort: port, }, }, build: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f99a9f8565f..61fa9c92146 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,7 @@ overrides: '@effect/vitest>@vitest/runner': '-' '@effect/vitest>vitest': '-' '@expo/metro-config': 56.0.13 + '@pierre/diffs>@shikijs/transformers': ^4.2.0 '@types/node': 24.12.4 effect: 4.0.0-beta.78 vite: npm:@voidzero-dev/vite-plus-core@0.1.24 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b8618329c23..8096d3a0a3a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -73,6 +73,7 @@ overrides: "@effect/vitest>@vitest/runner": "-" "@effect/vitest>vitest": "-" "@expo/metro-config": 56.0.13 + "@pierre/diffs>@shikijs/transformers": ^4.2.0 "@types/node": "catalog:" effect: "catalog:" vite: "catalog:" From b976775bd84af81ba5cb866940a4da13730fd6f3 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Tue, 23 Jun 2026 02:39:42 +0200 Subject: [PATCH 8/9] fix: align desktop clerk bridge config --- apps/desktop/src/app/DesktopApp.ts | 4 +-- apps/desktop/src/app/DesktopClerk.ts | 35 +++++++--------------- apps/desktop/src/app/DesktopClerkConfig.ts | 30 +++++++++++++++++++ apps/desktop/src/preload.ts | 11 ++----- apps/desktop/src/window/DesktopWindow.ts | 2 +- apps/desktop/vite.config.ts | 5 +++- 6 files changed, 50 insertions(+), 37 deletions(-) create mode 100644 apps/desktop/src/app/DesktopClerkConfig.ts diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index a721b883a7a..c1a88ef301a 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -170,12 +170,12 @@ const bootstrap = Effect.gen(function* () { const rendererTarget = environment.isDevelopment ? Option.getOrThrow(environment.devServerUrl) : backendConfig.httpBaseUrl; - if (!environment.isDevelopment || DesktopClerk.desktopClerkBridgeEnabled) { + if (!environment.isDevelopment || DesktopClerk.isDesktopClerkBridgeEnabled()) { yield* electronProtocol.registerDesktopProtocol({ scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment), targetOrigin: rendererTarget, backendOrigin: backendConfig.httpBaseUrl, - clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname, + clerkFrontendApiHostname: DesktopClerk.getDesktopClerkFrontendApiHostname(), }); } yield* logBootstrapInfo("bootstrap resolved backend endpoint", { diff --git a/apps/desktop/src/app/DesktopClerk.ts b/apps/desktop/src/app/DesktopClerk.ts index 7ed97461eeb..8974fa9619b 100644 --- a/apps/desktop/src/app/DesktopClerk.ts +++ b/apps/desktop/src/app/DesktopClerk.ts @@ -7,13 +7,21 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; -import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; import * as ElectronApp from "../electron/ElectronApp.ts"; import * as ElectronProtocol from "../electron/ElectronProtocol.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; +import { + getDesktopClerkFrontendApiHostname, + isDesktopClerkBridgeEnabled, + resolveDesktopClerkFrontendApiHostname, +} from "./DesktopClerkConfig.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; -declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; +export { + getDesktopClerkFrontendApiHostname, + isDesktopClerkBridgeEnabled, + resolveDesktopClerkFrontendApiHostname, +} from "./DesktopClerkConfig.ts"; export class DesktopClerkBridgeInitializationError extends Schema.TaggedErrorClass()( "DesktopClerkBridgeInitializationError", @@ -52,27 +60,6 @@ export class DesktopClerk extends Context.Service< } >()("@t3tools/desktop/app/DesktopClerk") {} -export function resolveDesktopClerkFrontendApiHostname( - publishableKey: string | undefined, -): string | undefined { - const normalizedKey = publishableKey?.trim(); - if (!normalizedKey) return undefined; - - try { - return clerkFrontendApiHostnameFromPublishableKey(normalizedKey); - } catch { - return undefined; - } -} - -export const desktopClerkFrontendApiHostname = resolveDesktopClerkFrontendApiHostname( - typeof __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__ === "undefined" - ? undefined - : __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__, -); - -export const desktopClerkBridgeEnabled = Boolean(desktopClerkFrontendApiHostname); - export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolean) { return createClerkBridge({ storage: storage({ path: stateDir }), @@ -84,7 +71,7 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea }); } -export function makeDesktopClerkLayer(enabled = desktopClerkBridgeEnabled) { +export function makeDesktopClerkLayer(enabled = isDesktopClerkBridgeEnabled()) { const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; if (enabled) { diff --git a/apps/desktop/src/app/DesktopClerkConfig.ts b/apps/desktop/src/app/DesktopClerkConfig.ts new file mode 100644 index 00000000000..ef33e5051ce --- /dev/null +++ b/apps/desktop/src/app/DesktopClerkConfig.ts @@ -0,0 +1,30 @@ +import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; + +declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; + +export function resolveDesktopClerkFrontendApiHostname( + publishableKey: string | undefined, +): string | undefined { + const normalizedKey = publishableKey?.trim(); + if (!normalizedKey) return undefined; + + try { + return clerkFrontendApiHostnameFromPublishableKey(normalizedKey); + } catch { + return undefined; + } +} + +const desktopClerkFrontendApiHostname = resolveDesktopClerkFrontendApiHostname( + typeof __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__ === "undefined" + ? undefined + : __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__, +); + +export function getDesktopClerkFrontendApiHostname(): string | undefined { + return desktopClerkFrontendApiHostname; +} + +export function isDesktopClerkBridgeEnabled(): boolean { + return Boolean(desktopClerkFrontendApiHostname); +} diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 617b74eaf6b..71cd63f3f59 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -6,17 +6,10 @@ import type { } from "@t3tools/contracts"; import { contextBridge, ipcRenderer } from "electron"; +import { isDesktopClerkBridgeEnabled } from "./app/DesktopClerkConfig.ts"; import * as IpcChannels from "./ipc/channels.ts"; -declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; - -const desktopClerkPreloadEnabled = Boolean( - typeof __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__ === "undefined" - ? undefined - : __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__.trim(), -); - -if (desktopClerkPreloadEnabled) { +if (isDesktopClerkBridgeEnabled()) { const { exposeClerkBridge } = require("@clerk/electron/preload") as typeof import("@clerk/electron/preload"); exposeClerkBridge({ passkeys: true }); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index c8e3d7292fb..74228649817 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -161,7 +161,7 @@ export const make = Effect.gen(function* () { > { yield* previewManager.getBrowserSession(); const applicationUrl = - environment.isDevelopment && !DesktopClerk.desktopClerkBridgeEnabled + environment.isDevelopment && !DesktopClerk.isDesktopClerkBridgeEnabled() ? Option.getOrThrow(environment.devServerUrl).href : getDesktopUrl(environment.isDevelopment); const iconPaths = yield* assets.iconPaths; diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 96e089b9183..f47456dbaa4 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -60,7 +60,10 @@ export default defineConfig({ // Sandboxed Electron preloads cannot reliably resolve package imports // from inside the packaged ASAR. Bundle Clerk's preload bridge into the // preload artifact instead of leaving a runtime require() behind. - alwaysBundle: (id) => id === "@clerk/electron" || id.startsWith("@clerk/electron/"), + alwaysBundle: (id) => + id === "@clerk/electron" || + id.startsWith("@clerk/electron/") || + id.startsWith("@t3tools/"), }, }, { From c5d62140b61dde493a9c3ce9d081fb76890c2180 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Tue, 23 Jun 2026 02:49:27 +0200 Subject: [PATCH 9/9] fix: keep desktop preload config sandbox-safe --- apps/desktop/src/app/DesktopClerkConfig.ts | 8 +++++--- apps/desktop/vite.config.ts | 5 +---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/app/DesktopClerkConfig.ts b/apps/desktop/src/app/DesktopClerkConfig.ts index ef33e5051ce..548bd5ea3ac 100644 --- a/apps/desktop/src/app/DesktopClerkConfig.ts +++ b/apps/desktop/src/app/DesktopClerkConfig.ts @@ -1,5 +1,3 @@ -import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; - declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; export function resolveDesktopClerkFrontendApiHostname( @@ -9,7 +7,11 @@ export function resolveDesktopClerkFrontendApiHostname( if (!normalizedKey) return undefined; try { - return clerkFrontendApiHostnameFromPublishableKey(normalizedKey); + const encodedFrontendApi = normalizedKey.split("_").slice(2).join("_"); + const frontendApi = globalThis.atob(encodedFrontendApi).replace(/\$$/u, ""); + if (frontendApi.length === 0 || frontendApi.includes("/")) return undefined; + + return new URL(`https://${frontendApi}`).hostname; } catch { return undefined; } diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index f47456dbaa4..96e089b9183 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -60,10 +60,7 @@ export default defineConfig({ // Sandboxed Electron preloads cannot reliably resolve package imports // from inside the packaged ASAR. Bundle Clerk's preload bridge into the // preload artifact instead of leaving a runtime require() behind. - alwaysBundle: (id) => - id === "@clerk/electron" || - id.startsWith("@clerk/electron/") || - id.startsWith("@t3tools/"), + alwaysBundle: (id) => id === "@clerk/electron" || id.startsWith("@clerk/electron/"), }, }, {