Skip to content

Commit df67cc0

Browse files
committed
fix(server): preserve encrypted peer routes when merging main
2 parents ef3f23e + 7c22e24 commit df67cc0

747 files changed

Lines changed: 27134 additions & 8706 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/test-t3-app/SKILL.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ description: Test T3 Code's web and desktop UI through its built-in Browser pane
55

66
# Test T3 web and desktop
77

8-
Use T3's built-in Browser panel for verification. If its tools are absent or
9-
the panel reports unavailable, explain the blocker and stop verification.
10-
Do not install or switch to another automation system. For native mobile
11-
testing, use [test-t3-mobile](../test-t3-mobile/SKILL.md).
8+
Use T3's built-in Browser panel for verification. Follow the recovery steps
9+
below before declaring it unavailable. Do not install or switch to another
10+
automation system. For native mobile testing, use
11+
[test-t3-mobile](../test-t3-mobile/SKILL.md).
1212

1313
## Start the app
1414

@@ -36,8 +36,13 @@ model, such as Haiku for Claude or Luna for Codex.
3636

3737
## Use the Browser panel
3838

39-
Call `preview_status`, then `preview_open` if the Browser panel is
40-
closed. Navigate to the complete startup pairing URL once with
39+
Call `preview_status`. If no tab can be automated, call `preview_open`, including
40+
when status reports `available: false` or `tabId: null`. Status can report
41+
unavailable simply because no tab exists yet; opening a tab needs no user action.
42+
Stop verification and explain the blocker only if the tools are absent or
43+
`preview_open` reports unavailable or fails.
44+
45+
Navigate to the complete startup pairing URL once with
4146
`preview_navigate`, then use `preview_snapshot` and T3's interaction tools.
4247
If the token was consumed or expired, run `node apps/server/src/bin.ts pair`
4348
for a fresh one. Keep using the same tab.
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { assert, it } from "@effect/vitest";
2+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
3+
import * as Deferred from "effect/Deferred";
4+
import * as Effect from "effect/Effect";
5+
import * as Layer from "effect/Layer";
6+
import * as FileSystem from "effect/FileSystem";
7+
import * as Path from "effect/Path";
8+
import * as HttpClient from "effect/http/HttpClient";
9+
import * as NetService from "@t3tools/shared/Net";
10+
import * as Fiber from "effect/Fiber";
11+
import * as Option from "effect/Option";
12+
import * as Ref from "effect/Ref";
13+
import * as Stream from "effect/Stream";
14+
import { vi } from "vite-plus/test";
15+
16+
vi.mock("electron", () => ({}));
17+
vi.mock("@clerk/electron", () => ({ createClerkBridge: vi.fn() }));
18+
vi.mock("@clerk/electron/storage", () => ({ storage: vi.fn() }));
19+
vi.mock("../ipc/DesktopIpcHandlers.ts", () => ({
20+
installDesktopIpcHandlers: () => Effect.void,
21+
}));
22+
vi.mock("../updates/DesktopRemoteUpdates.ts", () => ({ listen: Effect.void }));
23+
vi.mock("./DesktopStartupRecovery.ts", () => ({ listen: Effect.void }));
24+
25+
import * as DesktopApp from "./DesktopApp.ts";
26+
import * as DesktopAppActivation from "./DesktopAppActivation.ts";
27+
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
28+
import * as DesktopClerk from "./DesktopClerk.ts";
29+
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
30+
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
31+
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
32+
import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
33+
import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";
34+
import * as DesktopShutdown from "./DesktopShutdown.ts";
35+
import * as DesktopState from "./DesktopState.ts";
36+
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
37+
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
38+
import * as ElectronApp from "../electron/ElectronApp.ts";
39+
import * as ElectronDialog from "../electron/ElectronDialog.ts";
40+
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
41+
import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
42+
import * as PreviewPasskeys from "../preview/Passkeys.ts";
43+
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
44+
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
45+
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
46+
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
47+
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
48+
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
49+
import * as DesktopWindow from "../window/DesktopWindow.ts";
50+
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
51+
52+
import * as BrowserImport from "../preview/BrowserImport/BrowserImport.ts";
53+
import * as DesktopBackendConfiguration from "../backend/DesktopBackendConfiguration.ts";
54+
import * as DesktopCliCommand from "./DesktopCliCommand.ts";
55+
import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";
56+
import * as DesktopConnectionCatalogStore from "./DesktopConnectionCatalogStore.ts";
57+
import * as DesktopIpc from "../ipc/DesktopIpc.ts";
58+
import * as DesktopLocalEnvironmentAuth from "../backend/DesktopLocalEnvironmentAuth.ts";
59+
import * as DesktopManagedPolicy from "./DesktopManagedPolicy.ts";
60+
import * as DesktopSshEnvironment from "../ssh/DesktopSshEnvironment.ts";
61+
import * as DesktopSshPasswordPrompts from "../ssh/DesktopSshPasswordPrompts.ts";
62+
import * as DesktopTelemetryPublisher from "../telemetry/DesktopTelemetryPublisher.ts";
63+
import * as DesktopWebLinks from "./DesktopWebLinks.ts";
64+
import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts";
65+
import * as ElectronMenu from "../electron/ElectronMenu.ts";
66+
import * as ElectronShell from "../electron/ElectronShell.ts";
67+
import * as ElectronTheme from "../electron/ElectronTheme.ts";
68+
import * as ElectronWindow from "../electron/ElectronWindow.ts";
69+
import * as MacPermissions from "../permissions/MacPermissions.ts";
70+
import * as PreviewManager from "../preview/Manager.ts";
71+
72+
it.effect(
73+
"starts the backend and window while Linux registration is stalled and cancels it on shutdown",
74+
() =>
75+
Effect.gen(function* () {
76+
const registrationStarted = yield* Deferred.make<void>();
77+
const releaseRegistration = yield* Deferred.make<void>();
78+
const registrationStopped = yield* Deferred.make<void>();
79+
const windowStarted = yield* Deferred.make<void>();
80+
const quitting = yield* Ref.make(false);
81+
const events: string[] = [];
82+
const record = (event: string) =>
83+
Effect.sync(() => {
84+
events.push(event);
85+
});
86+
const runApp = DesktopApp.program
87+
.pipe(
88+
Effect.provide(NodeCrypto.layer),
89+
Effect.provideService(DesktopAppIdentity.DesktopAppIdentity, {
90+
resolveUserDataPath: Effect.succeed("/isolated/userdata"),
91+
configure: record("identity"),
92+
} as DesktopAppIdentity.DesktopAppIdentity["Service"]),
93+
Effect.provideService(ElectronApp.ElectronApp, {
94+
setPath: () => Effect.void,
95+
whenReady: record("ready"),
96+
} as unknown as ElectronApp.ElectronApp["Service"]),
97+
Effect.provideService(ElectronDialog.ElectronDialog, {
98+
showErrorBox: () => Effect.die("unexpected fatal startup error"),
99+
} as unknown as ElectronDialog.ElectronDialog["Service"]),
100+
Effect.provideService(DesktopEnvironment.DesktopEnvironment, {
101+
platform: "linux",
102+
isDevelopment: false,
103+
isPackaged: true,
104+
configuredBackendPort: Option.some(3773),
105+
defaultDesktopSettings: { serverExposureMode: "local-only" },
106+
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]),
107+
Effect.provideService(DesktopPreReadyPlatform.DesktopPreReadyElectronOptions, {
108+
linux: null,
109+
linuxPasswordStoreCommandLine: "basic",
110+
}),
111+
Effect.provideService(DesktopShellEnvironment.DesktopShellEnvironment, {
112+
installIntoProcess: Effect.succeed(true),
113+
awaitDiscovery: Effect.succeed(true),
114+
updates: Stream.empty,
115+
}),
116+
Effect.provideService(DesktopAppSettings.DesktopAppSettings, {
117+
load: Effect.void,
118+
get: Effect.succeed({
119+
localEnvironmentEnabled: true,
120+
serverExposureMode: "local-only",
121+
}),
122+
} as unknown as DesktopAppSettings.DesktopAppSettings["Service"]),
123+
Effect.provideService(DesktopLifecycle.DesktopLifecycle, {
124+
register: Effect.void,
125+
relaunch: () => Effect.void,
126+
}),
127+
Effect.provideService(DesktopClerk.DesktopClerk, {
128+
configure: Effect.void,
129+
} as DesktopClerk.DesktopClerk["Service"]),
130+
Effect.provideService(ElectronSafeStorage.ElectronSafeStorage, {
131+
selectedStorageBackend: Effect.succeedNone,
132+
} as unknown as ElectronSafeStorage.ElectronSafeStorage["Service"]),
133+
Effect.provideService(PreviewPasskeys.PreviewPasskeys, {
134+
configure: Effect.void,
135+
} as PreviewPasskeys.PreviewPasskeys["Service"]),
136+
Effect.provideService(DesktopApplicationMenu.DesktopApplicationMenu, {
137+
configure: Effect.void,
138+
}),
139+
Effect.provideService(DesktopUpdates.DesktopUpdates, {
140+
configure: Effect.void,
141+
} as unknown as DesktopUpdates.DesktopUpdates["Service"]),
142+
Effect.provideService(DesktopLinuxUrlHandler.DesktopLinuxUrlHandler, {
143+
register: Deferred.succeed(registrationStarted, undefined).pipe(
144+
Effect.andThen(Deferred.await(releaseRegistration)),
145+
Effect.ensuring(Deferred.succeed(registrationStopped, undefined)),
146+
),
147+
}),
148+
)
149+
.pipe(
150+
Effect.provideService(ElectronProtocol.ElectronProtocol, {
151+
registerDesktopProtocol: () => Effect.void,
152+
} as unknown as ElectronProtocol.ElectronProtocol["Service"]),
153+
Effect.provideService(DesktopLegacyLocalStorage.DesktopLegacyLocalStorage, {
154+
load: () => Effect.void,
155+
} as unknown as DesktopLegacyLocalStorage.DesktopLegacyLocalStorage["Service"]),
156+
Effect.provideService(DesktopSnapShot.DesktopSnapShot, {
157+
initialize: Effect.void,
158+
} as DesktopSnapShot.DesktopSnapShot["Service"]),
159+
Effect.provideService(DesktopState.DesktopState, {
160+
quitting,
161+
} as DesktopState.DesktopState["Service"]),
162+
Effect.provideService(DesktopWindow.DesktopWindow, {
163+
ensureMain: record("window").pipe(
164+
Effect.andThen(Deferred.succeed(windowStarted, undefined)),
165+
),
166+
} as unknown as DesktopWindow.DesktopWindow["Service"]),
167+
Effect.provideService(DesktopBackendPool.DesktopBackendPool, {
168+
primary: Effect.succeed({
169+
start: record("backend"),
170+
currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }),
171+
}),
172+
list: Effect.succeed([]),
173+
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]),
174+
Effect.provideService(DesktopServerExposure.DesktopServerExposure, {
175+
configureFromSettings: () => Effect.succeed({ mode: "local-only" }),
176+
backendConfig: Effect.succeed({ httpBaseUrl: new URL("http://127.0.0.1:3773") }),
177+
} as unknown as DesktopServerExposure.DesktopServerExposure["Service"]),
178+
Effect.provideService(DesktopWslBackend.DesktopWslBackend, {
179+
reconcile: Effect.void,
180+
} as DesktopWslBackend.DesktopWslBackend["Service"]),
181+
Effect.provideService(DesktopAppActivation.DesktopAppActivation, {
182+
start: Effect.void,
183+
} as unknown as DesktopAppActivation.DesktopAppActivation["Service"]),
184+
Effect.provideService(DesktopRendererHistory.DesktopRendererHistory, {
185+
shutdown: Effect.void,
186+
} as DesktopRendererHistory.DesktopRendererHistory["Service"]),
187+
);
188+
const app = yield* runApp.pipe(
189+
Effect.provide(
190+
Layer.mergeAll(
191+
Layer.mock(BrowserImport.BrowserImport, {}),
192+
Layer.mock(DesktopBackendConfiguration.DesktopBackendConfiguration, {}),
193+
Layer.mock(DesktopCliCommand.DesktopCliCommand, {}),
194+
Layer.mock(DesktopClientSettings.DesktopClientSettings, {}),
195+
Layer.mock(DesktopConnectionCatalogStore.DesktopConnectionCatalogStore, {}),
196+
Layer.mock(DesktopIpc.DesktopIpc, {}),
197+
Layer.mock(DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth, {}),
198+
Layer.mock(DesktopManagedPolicy.DesktopManagedPolicy, {}),
199+
Layer.mock(DesktopSshEnvironment.DesktopSshEnvironment, {}),
200+
Layer.mock(DesktopSshPasswordPrompts.DesktopSshPasswordPrompts, {}),
201+
Layer.mock(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {}),
202+
Layer.mock(DesktopWebLinks.DesktopWebLinks, {}),
203+
Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {}),
204+
Layer.mock(ElectronMenu.ElectronMenu, {}),
205+
Layer.mock(ElectronShell.ElectronShell, {}),
206+
Layer.mock(ElectronTheme.ElectronTheme, {}),
207+
Layer.mock(ElectronWindow.ElectronWindow, {}),
208+
Layer.mock(MacPermissions.MacPermissions, {}),
209+
Layer.mock(PreviewManager.PreviewManager, {
210+
isBrowserPartition: () => {
211+
throw new Error("unexpected browser partition lookup");
212+
},
213+
}),
214+
FileSystem.layerNoop({}),
215+
Layer.mock(NetService.NetService, {}),
216+
Path.layer,
217+
Layer.succeed(
218+
HttpClient.HttpClient,
219+
HttpClient.make(() => Effect.die("unexpected HTTP request")),
220+
),
221+
),
222+
),
223+
Effect.forkScoped,
224+
);
225+
yield* Deferred.await(registrationStarted);
226+
yield* Deferred.await(windowStarted);
227+
assert.deepEqual(events, ["identity", "ready", "identity", "backend", "window"]);
228+
assert.isFalse(yield* Deferred.isDone(registrationStopped));
229+
yield* (yield* DesktopShutdown.DesktopShutdown).request;
230+
yield* Fiber.join(app);
231+
assert.isTrue(yield* Deferred.isDone(registrationStopped));
232+
}).pipe(Effect.provide(DesktopShutdown.layer)),
233+
);

‎apps/desktop/src/app/DesktopApp.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as Ref from "effect/Ref";
66
import * as Schema from "effect/Schema";
77

88
import * as NetService from "@t3tools/shared/Net";
9-
import * as Crypto from "effect/Crypto";
9+
import * as SecureRandom from "effect/crypto/SecureRandom";
1010
import * as ElectronApp from "../electron/ElectronApp.ts";
1111
import * as ElectronDialog from "../electron/ElectronDialog.ts";
1212
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
@@ -42,8 +42,8 @@ const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
4242
const MAX_TCP_PORT = 65_535;
4343
const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const;
4444

45-
const makeDesktopRunId = Crypto.Crypto.pipe(
46-
Effect.flatMap((crypto) => crypto.randomUUIDv4()),
45+
const makeDesktopRunId = SecureRandom.SecureRandom.pipe(
46+
Effect.flatMap((random) => random.uuidV4()),
4747
Effect.map((value) => value.replaceAll("-", "").slice(0, 12)),
4848
);
4949

@@ -337,7 +337,8 @@ const startup = Effect.gen(function* () {
337337
yield* updates.configure;
338338
yield* DesktopRemoteUpdates.listen;
339339
yield* DesktopStartupRecovery.listen;
340-
yield* linuxUrlHandler.register;
340+
// Portal identity is prepared pre-ready; MIME helpers must not gate the backend or window.
341+
yield* linuxUrlHandler.register.pipe(Effect.forkScoped);
341342
yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));
342343
}).pipe(Effect.withSpan("desktop.startup"));
343344

‎apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12
import * as NodeServices from "@effect/platform-node/NodeServices";
23
import { assert, describe, it } from "@effect/vitest";
34
import { ConnectionCatalogDocument } from "@t3tools/client-runtime/platform";
@@ -75,6 +76,7 @@ function layerFor(
7576
layerEnvironment,
7677
layerSafeStorage,
7778
NodeServices.layer,
79+
NodeCrypto.layer,
7880
fileSystemLayer,
7981
);
8082
const layerSavedEnvironments = DesktopSavedEnvironments.layer.pipe(

‎apps/desktop/src/app/DesktopConnectionCatalogStore.ts‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import type { PersistedSavedEnvironmentRecord } from "@t3tools/contracts";
1414
import { fromLenientJson } from "@t3tools/shared/schemaJson";
1515
import * as Context from "effect/Context";
16-
import * as Crypto from "effect/Crypto";
16+
import * as SecureRandom from "effect/crypto/SecureRandom";
1717
import * as Effect from "effect/Effect";
1818
import * as Base64 from "effect/encoding/Base64";
1919
import * as FileSystem from "effect/FileSystem";
@@ -381,7 +381,7 @@ export const make = Effect.gen(function* () {
381381
const fileSystem = yield* FileSystem.FileSystem;
382382
const path = yield* Path.Path;
383383
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
384-
const crypto = yield* Crypto.Crypto;
384+
const random = yield* SecureRandom.SecureRandom;
385385
const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
386386
const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
387387
const encryptionAvailable = safeStorage.isEncryptionAvailable.pipe(
@@ -410,16 +410,7 @@ export const make = Effect.gen(function* () {
410410
),
411411
),
412412
);
413-
const suffix = (yield* crypto.randomUUIDv4().pipe(
414-
Effect.mapError(
415-
(cause) =>
416-
new DesktopConnectionCatalogStoreWriteError({
417-
operation: "create-temporary-file-name",
418-
path: catalogPath,
419-
cause,
420-
}),
421-
),
422-
)).replace(/-/g, "");
413+
const suffix = (yield* random.uuidV4()).replace(/-/g, "");
423414
yield* writeDocument({
424415
fileSystem,
425416
path,

‎apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ describe("DesktopLinuxUrlHandler", () => {
381381
if (!ChildProcess.isStandardCommand(command)) {
382382
return yield* Effect.die("expected a single registration command");
383383
}
384-
recorded.commands.push({ command: command.command, args: command.args });
384+
recorded.commands.push({ command: command.command, args: command.args.map(String) });
385385
if (command.command !== stalledHelper) return mockProcess(0);
386386
const child = yield* realSpawner.spawn(
387387
ChildProcess.make(

‎apps/desktop/src/app/DesktopStartupRecovery.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ describe("resolveStartupRecoveryOffer", () => {
7474
).toEqual({ _tag: "Reinstall", version: "0.1.2" });
7575
});
7676

77+
it("blames the managed policy unless a newer version can read it", () => {
78+
expect(
79+
resolveStartupRecoveryOffer(
80+
updateState({ status: "available", availableVersion: "0.1.4" }),
81+
"0.1.2",
82+
true,
83+
),
84+
).toEqual({ _tag: "Update", version: "0.1.4" });
85+
expect(
86+
resolveStartupRecoveryOffer(updateState({ status: "up-to-date" }), "0.1.2", true),
87+
).toEqual({ _tag: "ManagedPolicy" });
88+
});
89+
7790
it("falls back to the releases page when this version never started", () => {
7891
expect(resolveStartupRecoveryOffer(updateState({ status: "disabled" }), "0.1.3")).toEqual({
7992
_tag: "Releases",
@@ -168,6 +181,7 @@ it.effect.each([
168181
yield* SubscriptionRef.set(state.primaryBackendStartup, {
169182
_tag: "Failing",
170183
reason: "Server failed before readiness",
184+
managedPolicy: false,
171185
output: "Failure details",
172186
});
173187
yield* Deferred.await(checked);

0 commit comments

Comments
 (0)