authorgravatar for ethorelli@gmail.comErik Thorelli <ethorelli@gmail.com> 2026-10-05 23:10:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 23:10:39-07:00
log83b982a85c87b4c0eda08beb9028e3a73f7c89b6
tree524afd394866f0c7d03bcb60262d8b26e7d0f5cb
parent8f75697b9e022033f2615cb32e3c6ddc61c52353
signature Signed by PGP key B5690EEEBB952194

refactor: Effect code gets UUIDs and SHA-256 from Effect's Crypto (#16377)


22 files changed, 109 insertions(+), 58 deletions(-)

apps/desktop/src/preview/Manager.test.ts+2
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import * as NodeVM from "node:vm";1import * as NodeVM from "node:vm";
2import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import { it as effectIt } from "@effect/vitest";3import { it as effectIt } from "@effect/vitest";
3import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts";4import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts";
4import type {5import type {
...@@ -279,6 +280,7 @@ const layer = PreviewManager.layer.pipe(...@@ -279,6 +280,7 @@ const layer = PreviewManager.layer.pipe(
279 Layer.provideMerge(layerEnvironment),280 Layer.provideMerge(layerEnvironment),
280 Layer.provideMerge(layerFileSystem),281 Layer.provideMerge(layerFileSystem),
281 Layer.provideMerge(Path.layer),282 Layer.provideMerge(Path.layer),
283 Layer.provideMerge(NodeCrypto.layer),
282 Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")),284 Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")),
283);285);
284const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError);286const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError);
apps/desktop/src/preview/Manager.ts+9-9
...@@ -5,7 +5,6 @@...@@ -5,7 +5,6 @@
5 * elements live in the renderer; we only attach listeners and forward state5 * elements live in the renderer; we only attach listeners and forward state
6 * here). Single layer-scoped browser session partition.6 * here). Single layer-scoped browser session partition.
7 */7 */
8import * as NodeCrypto from "node:crypto";
9import {8import {
10 DesktopPreviewRecordingInputSchema,9 DesktopPreviewRecordingInputSchema,
11 DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER,10 DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER,
...@@ -49,6 +48,7 @@ import {...@@ -49,6 +48,7 @@ import {
49import * as Cause from "effect/Cause";48import * as Cause from "effect/Cause";
50import * as Clock from "effect/Clock";49import * as Clock from "effect/Clock";
51import * as Context from "effect/Context";50import * as Context from "effect/Context";
51import * as Crypto from "effect/Crypto";
52import * as DateTime from "effect/DateTime";52import * as DateTime from "effect/DateTime";
53import * as Deferred from "effect/Deferred";53import * as Deferred from "effect/Deferred";
54import * as Effect from "effect/Effect";54import * as Effect from "effect/Effect";
...@@ -643,6 +643,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -643,6 +643,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
643 const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;643 const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;
644 const hostPlatform = yield* HostProcessPlatform;644 const hostPlatform = yield* HostProcessPlatform;
645 const path = yield* Path.Path;645 const path = yield* Path.Path;
646 const crypto = yield* Crypto.Crypto;
646 const parentScope = yield* Scope.Scope;647 const parentScope = yield* Scope.Scope;
647 const context = yield* Effect.context<never>();648 const context = yield* Effect.context<never>();
648 const runFork = Effect.runForkWith(context);649 const runFork = Effect.runForkWith(context);
...@@ -1657,9 +1658,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -1657,9 +1658,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
1657 returnByValue: boolean,1658 returnByValue: boolean,
1658 awaitPromise = true,1659 awaitPromise = true,
1659 ): Effect.Effect<A, PreviewManagerError> =>1660 ): Effect.Effect<A, PreviewManagerError> =>
1660 Effect.suspend(() => {1661 Effect.gen(function* () {
1661 const objectGroup = `t3-evaluation-${NodeCrypto.randomUUID()}`;1662 const objectGroup = `t3-evaluation-${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`;
1662 return send("Runtime.evaluate", {1663 return yield* send("Runtime.evaluate", {
1663 expression,1664 expression,
1664 awaitPromise,1665 awaitPromise,
1665 returnByValue,1666 returnByValue,
...@@ -4186,10 +4187,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -4186,10 +4187,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
4186 const context = { operation: "automationPress.awaitNativeKey", tabId, webContentsId: wc.id };4187 const context = { operation: "automationPress.awaitNativeKey", tabId, webContentsId: wc.id };
4187 const evaluate = (frame: Electron.WebFrameMain, expression: string) =>4188 const evaluate = (frame: Electron.WebFrameMain, expression: string) =>
4188 attemptPromise(context, () => frame.executeJavaScript(expression));4189 attemptPromise(context, () => frame.executeJavaScript(expression));
4190 const receiptId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
4189 const { frames, receiptKey } = yield* Effect.acquireRelease(4191 const { frames, receiptKey } = yield* Effect.acquireRelease(
4190 attempt(context, () => ({4192 attempt(context, () => ({
4191 frames: wc.mainFrame.framesInSubtree,4193 frames: wc.mainFrame.framesInSubtree,
4192 receiptKey: JSON.stringify(`__t3NativeKey_${NodeCrypto.randomUUID()}`),4194 receiptKey: JSON.stringify(`__t3NativeKey_${receiptId}`),
4193 })),4195 })),
4194 ({ frames, receiptKey }) =>4196 ({ frames, receiptKey }) =>
4195 Effect.forEach(4197 Effect.forEach(
...@@ -4445,10 +4447,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -4445,10 +4447,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
4445 keySequence,4447 keySequence,
4446 clipboardData,4448 clipboardData,
4447 );4449 );
4448 const selectionKey = yield* encodeJson(4450 const selectionId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
4449 context,4451 const selectionKey = yield* encodeJson(context, `__t3EditingSelection_${selectionId}`);
4450 `__t3EditingSelection_${NodeCrypto.randomUUID()}`,
4451 );
4452 // Editing requires an active document. Preserve the target4452 // Editing requires an active document. Preserve the target
4453 // and selection across focus handlers without focusing the desktop.4453 // and selection across focus handlers without focusing the desktop.
4454 yield* Effect.acquireUseRelease(4454 yield* Effect.acquireUseRelease(
apps/server/scripts/measure-pr-preview.ts+1
...@@ -50,6 +50,7 @@ const layerMeasuredProcess = Layer.effect(...@@ -50,6 +50,7 @@ const layerMeasuredProcess = Layer.effect(
50const layerServices = GitHubPullRequestCli.layer.pipe(50const layerServices = GitHubPullRequestCli.layer.pipe(
51 Layer.provide(GitHubCli.layer),51 Layer.provide(GitHubCli.layer),
52 Layer.provideMerge(layerMeasuredProcess),52 Layer.provideMerge(layerMeasuredProcess),
53 Layer.provide(NodeServices.layer),
53);54);
5455
55const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));56const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));
apps/server/src/assets/AttachmentUpload.ts+4-3
...@@ -1,11 +1,10 @@...@@ -1,11 +1,10 @@
1import * as NodeCrypto from "node:crypto";
2
3import {1import {
4 ATTACHMENT_UPLOAD_URL_TTL_MS,2 ATTACHMENT_UPLOAD_URL_TTL_MS,
5 type AttachmentCreateUploadUrlInput,3 type AttachmentCreateUploadUrlInput,
6 AttachmentUploadSigningKeyError,4 AttachmentUploadSigningKeyError,
7} from "@t3tools/contracts";5} from "@t3tools/contracts";
8import * as Clock from "effect/Clock";6import * as Clock from "effect/Clock";
7import * as Crypto from "effect/Crypto";
9import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";9import * as FileSystem from "effect/FileSystem";
11import * as Option from "effect/Option";10import * as Option from "effect/Option";
...@@ -170,9 +169,11 @@ export const storeAttachmentUpload = Effect.fn("AttachmentUpload.store")(functio...@@ -170,9 +169,11 @@ export const storeAttachmentUpload = Effect.fn("AttachmentUpload.store")(functio
170 attachmentsDir: config.attachmentsDir,169 attachmentsDir: config.attachmentsDir,
171 relativePath,170 relativePath,
172 });171 });
172 const crypto = yield* Crypto.Crypto;
173 const partId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
173 const partPath = resolveAttachmentRelativePath({174 const partPath = resolveAttachmentRelativePath({
174 attachmentsDir: config.attachmentsDir,175 attachmentsDir: config.attachmentsDir,
175 relativePath: `${relativePath}.${NodeCrypto.randomUUID()}.part`,176 relativePath: `${relativePath}.${partId}.part`,
176 });177 });
177 if (!finalPath || !partPath) {178 if (!finalPath || !partPath) {
178 return { ok: false, status: 500, detail: "Failed to resolve attachment path." };179 return { ok: false, status: 500, detail: "Failed to resolve attachment path." };
apps/server/src/assets/NativeAppIconResolver.ts+10-5
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as NodeCrypto from "node:crypto";
2import type { ToolActivityNativeAppReference } from "@t3tools/contracts";1import type { ToolActivityNativeAppReference } from "@t3tools/contracts";
3import * as Cache from "effect/Cache";2import * as Cache from "effect/Cache";
4import * as Cause from "effect/Cause";3import * as Cause from "effect/Cause";
4import * as Crypto from "effect/Crypto";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Clock from "effect/Clock";6import * as Clock from "effect/Clock";
7import * as Context from "effect/Context";7import * as Context from "effect/Context";
...@@ -13,6 +13,7 @@ import * as Option from "effect/Option";...@@ -13,6 +13,7 @@ import * as Option from "effect/Option";
13import * as Path from "effect/Path";13import * as Path from "effect/Path";
14import * as PlatformError from "effect/PlatformError";14import * as PlatformError from "effect/PlatformError";
15import * as Semaphore from "effect/Semaphore";15import * as Semaphore from "effect/Semaphore";
16import * as Hex from "effect/encoding/Hex";
16import { ChildProcess, ChildProcessSpawner } from "effect/process";17import { ChildProcess, ChildProcessSpawner } from "effect/process";
17import { HostProcessPlatform } from "@t3tools/shared/hostProcess";18import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1819
...@@ -175,9 +176,13 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc...@@ -175,9 +176,13 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc
175 const appVersion =176 const appVersion =
176 (yield* plistValue(infoPlistPath, "CFBundleVersion")) ||177 (yield* plistValue(infoPlistPath, "CFBundleVersion")) ||
177 (yield* plistValue(infoPlistPath, "CFBundleShortVersionString"));178 (yield* plistValue(infoPlistPath, "CFBundleShortVersionString"));
178 const cacheKey = NodeCrypto.createHash("sha256")179 const crypto = yield* Crypto.Crypto;
179 .update(`${canonicalAppPath}\0${appVersion}\0${sourceIconPath}`)180 const cacheKey = yield* crypto
180 .digest("hex");181 .digest(
182 "SHA-256",
183 new TextEncoder().encode(`${canonicalAppPath}\0${appVersion}\0${sourceIconPath}`),
184 )
185 .pipe(Effect.map(Hex.encode), Effect.orDie);
181 const cacheDirectory = path.join(config.providerStatusCacheDir, "native-app-icons");186 const cacheDirectory = path.join(config.providerStatusCacheDir, "native-app-icons");
182 const cachePath = path.join(cacheDirectory, `${cacheKey}.png`);187 const cachePath = path.join(cacheDirectory, `${cacheKey}.png`);
183 if (yield* existingFile(cachePath)) return cachePath;188 if (yield* existingFile(cachePath)) return cachePath;
...@@ -185,7 +190,7 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc...@@ -185,7 +190,7 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc
185 yield* fileSystem.makeDirectory(cacheDirectory, { recursive: true });190 yield* fileSystem.makeDirectory(cacheDirectory, { recursive: true });
186 const temporaryPath = path.join(191 const temporaryPath = path.join(
187 cacheDirectory,192 cacheDirectory,
188 `.${cacheKey}-${process.pid}-${(yield* Clock.currentTimeMillis).toString(36)}-${NodeCrypto.randomUUID()}.png`,193 `.${cacheKey}-${process.pid}-${(yield* Clock.currentTimeMillis).toString(36)}-${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}.png`,
189 );194 );
190 yield* commandOutput("/usr/bin/sips", [195 yield* commandOutput("/usr/bin/sips", [
191 "-z",196 "-z",
apps/server/src/cli/app.ts+4-2
...@@ -1,5 +1,4 @@...@@ -1,5 +1,4 @@
1// @effect-diagnostics globalTimers:off -- The Node socket client owns its response deadline and clears it on every completion path.1// @effect-diagnostics globalTimers:off -- The Node socket client owns its response deadline and clears it on every completion path.
2import * as NodeCrypto from "node:crypto";
3import * as NodeNet from "node:net";2import * as NodeNet from "node:net";
4import * as NodeOS from "node:os";3import * as NodeOS from "node:os";
54
...@@ -18,6 +17,7 @@ import {...@@ -18,6 +17,7 @@ import {
18} from "@t3tools/shared/hostProcess";17} from "@t3tools/shared/hostProcess";
19import * as Config from "effect/Config";18import * as Config from "effect/Config";
20import * as Console from "effect/Console";19import * as Console from "effect/Console";
20import * as Crypto from "effect/Crypto";
21import * as Effect from "effect/Effect";21import * as Effect from "effect/Effect";
22import * as Option from "effect/Option";22import * as Option from "effect/Option";
23import * as Path from "effect/Path";23import * as Path from "effect/Path";
...@@ -212,9 +212,11 @@ const runAppCommand = Effect.fn("cli.app")(function* (flags: {...@@ -212,9 +212,11 @@ const runAppCommand = Effect.fn("cli.app")(function* (flags: {
212 userId,212 userId,
213 joinPath: path.join,213 joinPath: path.join,
214 }).address;214 }).address;
215 const crypto = yield* Crypto.Crypto;
216 const requestId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
215 const request: DesktopAppActivationRequest = {217 const request: DesktopAppActivationRequest = {
216 version: DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,218 version: DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,
217 requestId: NodeCrypto.randomUUID(),219 requestId,
218 type: "open-workspace",220 type: "open-workspace",
219 workspaceRoot,221 workspaceRoot,
220 platform: hostPlatform,222 platform: hostPlatform,
apps/server/src/device/SshDeviceHost.ts+10-5
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import * as NodeCrypto from "node:crypto";
2import {1import {
3 type DeviceHostSummary,2 type DeviceHostSummary,
4 DevicePlatformAvailability,3 DevicePlatformAvailability,
...@@ -9,6 +8,7 @@ import {...@@ -9,6 +8,7 @@ import {
9import { runSshCommand, baseSshArgs, resolveSshCommand } from "@t3tools/ssh/command";8import { runSshCommand, baseSshArgs, resolveSshCommand } from "@t3tools/ssh/command";
10import * as NetService from "@t3tools/shared/Net";9import * as NetService from "@t3tools/shared/Net";
11import { waitForHttpReady } from "@t3tools/shared/httpReadiness";10import { waitForHttpReady } from "@t3tools/shared/httpReadiness";
11import * as Crypto from "effect/Crypto";
12import * as Exit from "effect/Exit";12import * as Exit from "effect/Exit";
13import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14import * as FileSystem from "effect/FileSystem";14import * as FileSystem from "effect/FileSystem";
...@@ -17,6 +17,7 @@ import * as Scope from "effect/Scope";...@@ -17,6 +17,7 @@ import * as Scope from "effect/Scope";
17import * as Semaphore from "effect/Semaphore";17import * as Semaphore from "effect/Semaphore";
18import * as Schema from "effect/Schema";18import * as Schema from "effect/Schema";
19import * as Stream from "effect/Stream";19import * as Stream from "effect/Stream";
20import * as Hex from "effect/encoding/Hex";
20import * as HttpClient from "effect/http/HttpClient";21import * as HttpClient from "effect/http/HttpClient";
21import * as ChildProcess from "effect/process/ChildProcess";22import * as ChildProcess from "effect/process/ChildProcess";
22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";23import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
...@@ -79,10 +80,11 @@ const ownerFor = Effect.fn("SshDeviceHost.ownerFor")(function* (hostId: string)...@@ -79,10 +80,11 @@ const ownerFor = Effect.fn("SshDeviceHost.ownerFor")(function* (hostId: string)
79 const environmentId = yield* fs80 const environmentId = yield* fs
80 .readFileString(server.environmentIdPath)81 .readFileString(server.environmentIdPath)
81 .pipe(Effect.orElseSucceed(() => server.stateDir));82 .pipe(Effect.orElseSucceed(() => server.stateDir));
82 return NodeCrypto.createHash("sha256")83 const crypto = yield* Crypto.Crypto;
83 .update(`${environmentId}\0${server.stateDir}\0${hostId}`)84 const owner = yield* crypto
84 .digest("hex")85 .digest("SHA-256", new TextEncoder().encode(`${environmentId}\0${server.stateDir}\0${hostId}`))
85 .slice(0, 24);86 .pipe(Effect.map(Hex.encode), Effect.orDie);
87 return owner.slice(0, 24);
86});88});
8789
88export const probe = Effect.fn("SshDeviceHost.probe")(function* (90export const probe = Effect.fn("SshDeviceHost.probe")(function* (
...@@ -125,6 +127,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (...@@ -125,6 +127,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
125 const net = yield* NetService.NetService;127 const net = yield* NetService.NetService;
126 const http = yield* HttpClient.HttpClient;128 const http = yield* HttpClient.HttpClient;
127 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;129 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
130 const crypto = yield* Crypto.Crypto;
128 const parentScope = yield* Scope.Scope;131 const parentScope = yield* Scope.Scope;
129 const ssh = yield* resolveSshCommand;132 const ssh = yield* resolveSshCommand;
130 const owner = yield* ownerFor(config.id);133 const owner = yield* ownerFor(config.id);
...@@ -135,6 +138,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (...@@ -135,6 +138,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
135 | FileSystem.FileSystem138 | FileSystem.FileSystem
136 | Path.Path139 | Path.Path
137 | ChildProcessSpawner.ChildProcessSpawner140 | ChildProcessSpawner.ChildProcessSpawner
141 | Crypto.Crypto
138 | ServerConfig.ServerConfig142 | ServerConfig.ServerConfig
139 >,143 >,
140 ) =>144 ) =>
...@@ -143,6 +147,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (...@@ -143,6 +147,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
143 Effect.provideService(Path.Path, path),147 Effect.provideService(Path.Path, path),
144 Effect.provideService(ServerConfig.ServerConfig, server),148 Effect.provideService(ServerConfig.ServerConfig, server),
145 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),149 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
150 Effect.provideService(Crypto.Crypto, crypto),
146 );151 );
147 const lock = yield* Semaphore.make(1);152 const lock = yield* Semaphore.make(1);
148 let stopped = false;153 let stopped = false;
apps/server/src/mcp/McpHttpServer.ts+5-3
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as NodeCrypto from "node:crypto";
2import * as Cause from "effect/Cause";1import * as Cause from "effect/Cause";
3import * as Clock from "effect/Clock";2import * as Clock from "effect/Clock";
4import * as Context from "effect/Context";3import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
...@@ -334,8 +334,10 @@ const saveScreenshot = Effect.fn("McpHttpServer.saveScreenshot")(function* (...@@ -334,8 +334,10 @@ const saveScreenshot = Effect.fn("McpHttpServer.saveScreenshot")(function* (
334 const fileSystem = yield* FileSystem.FileSystem;334 const fileSystem = yield* FileSystem.FileSystem;
335 const path = yield* Path.Path;335 const path = yield* Path.Path;
336 const millis = yield* Clock.currentTimeMillis;336 const millis = yield* Clock.currentTimeMillis;
337 const crypto = yield* Crypto.Crypto;
337 // Two saves in the same millisecond must not overwrite each other.338 // Two saves in the same millisecond must not overwrite each other.
338 const fileName = `browser-screenshot-${screenshotSiteSlug(pageUrl)}-${millis.toString(36)}-${NodeCrypto.randomUUID().slice(0, 8)}.png`;339 const unique = (yield* crypto.randomUUIDv4.pipe(Effect.orDie)).slice(0, 8);
340 const fileName = `browser-screenshot-${screenshotSiteSlug(pageUrl)}-${millis.toString(36)}-${unique}.png`;
339 const screenshotPath = path.join(config.browserArtifactsDir, fileName);341 const screenshotPath = path.join(config.browserArtifactsDir, fileName);
340 yield* fileSystem.makeDirectory(config.browserArtifactsDir, { recursive: true }).pipe(342 yield* fileSystem.makeDirectory(config.browserArtifactsDir, { recursive: true }).pipe(
341 Effect.andThen(fileSystem.writeFile(screenshotPath, data)),343 Effect.andThen(fileSystem.writeFile(screenshotPath, data)),
...@@ -387,7 +389,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot...@@ -387,7 +389,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
387 const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;389 const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
388 // The MCP tool runner only supplies the client, so hand the save path its services here.390 // The MCP tool runner only supplies the client, so hand the save path its services here.
389 const saveServices = yield* Effect.context<391 const saveServices = yield* Effect.context<
390 ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path392 ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path | Crypto.Crypto
391 >();393 >();
392 const built = yield* PreviewSnapshotToolkit;394 const built = yield* PreviewSnapshotToolkit;
393 const tool = PreviewSnapshotTool;395 const tool = PreviewSnapshotTool;
apps/server/src/mcp/toolkits/previewControls/handlers.test.ts+2-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
1import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
2import {3import {
3 DEFAULT_SERVER_SETTINGS,4 DEFAULT_SERVER_SETTINGS,
...@@ -45,7 +46,7 @@ it.effect.each([...@@ -45,7 +46,7 @@ it.effect.each([
45 capabilities: new Set(effective.enableAgentBrowserAccess ? ["preview"] : []),46 capabilities: new Set(effective.enableAgentBrowserAccess ? ["preview"] : []),
46 issuedAt: 0,47 issuedAt: 0,
47 };48 };
48 const manager = yield* Preview.make;49 const manager = yield* Preview.make.pipe(Effect.provide(NodeCrypto.layer));
49 const tab = yield* manager.open({ threadId, url: "http://localhost:3000" });50 const tab = yield* manager.open({ threadId, url: "http://localhost:3000" });
50 const layerDependencies = Layer.mergeAll(51 const layerDependencies = Layer.mergeAll(
51 Layer.succeed(Preview.PreviewManager, manager),52 Layer.succeed(Preview.PreviewManager, manager),
apps/server/src/preview/Manager.test.ts+5-2
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
1import { it } from "@effect/vitest";2import { it } from "@effect/vitest";
2import { type PreviewEvent, ThreadId } from "@t3tools/contracts";3import { type PreviewEvent, ThreadId } from "@t3tools/contracts";
3import { PreviewUrlNormalizationError } from "@t3tools/shared/preview";4import { PreviewUrlNormalizationError } from "@t3tools/shared/preview";
4import { Effect, PubSub } from "effect";5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as PubSub from "effect/PubSub";
5import { expect } from "vite-plus/test";8import { expect } from "vite-plus/test";
69
7import * as PreviewManager from "./Manager.ts";10import * as PreviewManager from "./Manager.ts";
...@@ -35,7 +38,7 @@ const collectEvents = Effect.gen(function* () {...@@ -35,7 +38,7 @@ const collectEvents = Effect.gen(function* () {
35 return collector;38 return collector;
36}).pipe(Effect.withSpan("preview.test.collectEvents"));39}).pipe(Effect.withSpan("preview.test.collectEvents"));
3740
38it.layer(PreviewManager.layer)("PreviewManager", (it) => {41it.layer(PreviewManager.layer.pipe(Layer.provide(NodeCrypto.layer)))("PreviewManager", (it) => {
39 it.effect("opens a session and emits opened with normalized URL", () =>42 it.effect("opens a session and emits opened with normalized URL", () =>
40 Effect.gen(function* () {43 Effect.gen(function* () {
41 const threadId = freshThreadId();44 const threadId = freshThreadId();
apps/server/src/preview/Manager.ts+3-2
...@@ -31,8 +31,8 @@ import {...@@ -31,8 +31,8 @@ import {
31 newPreviewTabId,31 newPreviewTabId,
32 normalizePreviewUrl,32 normalizePreviewUrl,
33} from "@t3tools/shared/preview";33} from "@t3tools/shared/preview";
34import * as NodeCrypto from "node:crypto";
35import * as Context from "effect/Context";34import * as Context from "effect/Context";
35import * as Crypto from "effect/Crypto";
36import * as DateTime from "effect/DateTime";36import * as DateTime from "effect/DateTime";
37import * as Effect from "effect/Effect";37import * as Effect from "effect/Effect";
38import * as Layer from "effect/Layer";38import * as Layer from "effect/Layer";
...@@ -155,7 +155,8 @@ const buildIdleSnapshot = (input: {...@@ -155,7 +155,8 @@ const buildIdleSnapshot = (input: {
155155
156/** @public Service construction is part of the canonical Effect module API. */156/** @public Service construction is part of the canonical Effect module API. */
157export const make = Effect.gen(function* PreviewManagerMake() {157export const make = Effect.gen(function* PreviewManagerMake() {
158 const serverEpoch = NodeCrypto.randomUUID();158 const crypto = yield* Crypto.Crypto;
159 const serverEpoch = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
159 const stateRef = yield* SynchronizedRef.make<ManagerState>(initialState);160 const stateRef = yield* SynchronizedRef.make<ManagerState>(initialState);
160 // Unbounded PubSub is fine here — events are tiny and we don't want to161 // Unbounded PubSub is fine here — events are tiny and we don't want to
161 // block publishers if a subscriber is slow. WS clients backpressure on162 // block publishers if a subscriber is slow. WS clients backpressure on
apps/server/src/project/ProjectSetupScriptRunner.test.ts+2
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
1import { assert, it, vi } from "@effect/vitest";2import { assert, it, vi } from "@effect/vitest";
2import { ProjectId } from "@t3tools/contracts";3import { ProjectId } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
...@@ -64,6 +65,7 @@ it.effect("resolves setup scripts through the standalone project service", () =>...@@ -64,6 +65,7 @@ it.effect("resolves setup scripts through the standalone project service", () =>
64 }),65 }),
65 Layer.mock(TerminalManager.TerminalManager)({ open, write, subscribe }),66 Layer.mock(TerminalManager.TerminalManager)({ open, write, subscribe }),
66 ServerSettings.layerTest(),67 ServerSettings.layerTest(),
68 NodeCrypto.layer,
67 ),69 ),
68 ),70 ),
69 );71 );
apps/server/src/project/ProjectSetupScriptRunner.ts+5-2
...@@ -5,10 +5,10 @@ import {...@@ -5,10 +5,10 @@ import {
5 resolveProjectScripts,5 resolveProjectScripts,
6 setupProjectScript,6 setupProjectScript,
7} from "@t3tools/shared/projectScripts";7} from "@t3tools/shared/projectScripts";
8import * as NodeCrypto from "node:crypto";
98
10import * as Clock from "effect/Clock";9import * as Clock from "effect/Clock";
11import * as Context from "effect/Context";10import * as Context from "effect/Context";
11import * as Crypto from "effect/Crypto";
12import * as Deferred from "effect/Deferred";12import * as Deferred from "effect/Deferred";
13import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
...@@ -201,6 +201,7 @@ export const make = Effect.gen(function* () {...@@ -201,6 +201,7 @@ export const make = Effect.gen(function* () {
201 const projects = yield* ProjectService.ProjectService;201 const projects = yield* ProjectService.ProjectService;
202 const terminalManager = yield* TerminalManager.TerminalManager;202 const terminalManager = yield* TerminalManager.TerminalManager;
203 const serverSettings = yield* ServerSettings.ServerSettingsService;203 const serverSettings = yield* ServerSettings.ServerSettingsService;
204 const crypto = yield* Crypto.Crypto;
204 const completionShell = resolveCompletionShell(205 const completionShell = resolveCompletionShell(
205 yield* HostProcessPlatform,206 yield* HostProcessPlatform,
206 yield* HostProcessEnvironment,207 yield* HostProcessEnvironment,
...@@ -367,7 +368,9 @@ export const make = Effect.gen(function* () {...@@ -367,7 +368,9 @@ export const make = Effect.gen(function* () {
367 COLORTERM: "",368 COLORTERM: "",
368 };369 };
369 const observe = input.observeCompletion;370 const observe = input.observeCompletion;
370 const completionToken = observe ? NodeCrypto.randomUUID().replaceAll("-", "") : null;371 const completionToken = observe
372 ? (yield* crypto.randomUUIDv4.pipe(Effect.orDie)).replaceAll("-", "")
373 : null;
371 const commandLine =374 const commandLine =
372 observe && completionToken375 observe && completionToken
373 ? wrapCommandForCompletion(376 ? wrapCommandForCompletion(
apps/server/src/provider/CodexChatGptAuth.ts+6-1
...@@ -12,6 +12,7 @@ import { codexCallbackUrl } from "@t3tools/shared/codexAuthHandoff";...@@ -12,6 +12,7 @@ import { codexCallbackUrl } from "@t3tools/shared/codexAuthHandoff";
12import * as Clock from "effect/Clock";12import * as Clock from "effect/Clock";
13import * as Cause from "effect/Cause";13import * as Cause from "effect/Cause";
14import * as AnalyticsService from "../telemetry/AnalyticsService.ts";14import * as AnalyticsService from "../telemetry/AnalyticsService.ts";
15import * as Crypto from "effect/Crypto";
15import * as Exit from "effect/Exit";16import * as Exit from "effect/Exit";
16import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts";17import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts";
17import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
...@@ -158,6 +159,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*...@@ -158,6 +159,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
158 options.instanceId,159 options.instanceId,
159 );160 );
160 const secrets = yield* ServerSecretStore.ServerSecretStore;161 const secrets = yield* ServerSecretStore.ServerSecretStore;
162 const crypto = yield* Crypto.Crypto;
161 const environmentLocks = sessionLocks.get(secrets) ?? new Map<string, Semaphore.Semaphore>();163 const environmentLocks = sessionLocks.get(secrets) ?? new Map<string, Semaphore.Semaphore>();
162 sessionLocks.set(secrets, environmentLocks);164 sessionLocks.set(secrets, environmentLocks);
163 const lock = environmentLocks.get(store.binding.key) ?? (yield* Semaphore.make(1));165 const lock = environmentLocks.get(store.binding.key) ?? (yield* Semaphore.make(1));
...@@ -306,7 +308,10 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*...@@ -306,7 +308,10 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
306 savedConnectionCount: savedConnections.size,308 savedConnectionCount: savedConnections.size,
307 unidentifiedConnectedConnectionCount,309 unidentifiedConnectedConnectionCount,
308 };310 };
309 }).pipe(Effect.provideService(ServerSecretStore.ServerSecretStore, secrets));311 }).pipe(
312 Effect.provideService(ServerSecretStore.ServerSecretStore, secrets),
313 Effect.provideService(Crypto.Crypto, crypto),
314 );
310 const saveRegistration = Effect.fnUntraced(function* (profile: typeof Registration.Type) {315 const saveRegistration = Effect.fnUntraced(function* (profile: typeof Registration.Type) {
311 const saved = yield* readRegistrations;316 const saved = yield* readRegistrations;
312 profile = {317 profile = {
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2
...@@ -199,6 +199,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>...@@ -199,6 +199,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
199 const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime;199 const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime;
200 const serverConfig = yield* ServerConfig.ServerConfig;200 const serverConfig = yield* ServerConfig.ServerConfig;
201 const httpClient = yield* HttpClient.HttpClient;201 const httpClient = yield* HttpClient.HttpClient;
202 const crypto = yield* Crypto.Crypto;
202 const serverSettings = yield* ServerSettings.ServerSettingsService;203 const serverSettings = yield* ServerSettings.ServerSettingsService;
203 const processEnv = mergeProviderInstanceEnvironment(environment);204 const processEnv = mergeProviderInstanceEnvironment(environment);
204 const continuationIdentity = defaultProviderContinuationIdentity({205 const continuationIdentity = defaultProviderContinuationIdentity({
...@@ -388,6 +389,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>...@@ -388,6 +389,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
388 Effect.provideService(FileSystem.FileSystem, fileSystem),389 Effect.provideService(FileSystem.FileSystem, fileSystem),
389 Effect.provideService(Path.Path, pathService),390 Effect.provideService(Path.Path, pathService),
390 Effect.provideService(HttpClient.HttpClient, httpClient),391 Effect.provideService(HttpClient.HttpClient, httpClient),
392 Effect.provideService(Crypto.Crypto, crypto),
391 Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner),393 Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner),
392 Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime),394 Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime),
393 );395 );
apps/server/src/provider/ProviderCredentialStore.test.ts+7-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
1import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";4import * as Option from "effect/Option";
...@@ -35,6 +36,11 @@ it.effect("isolates provider bindings and preserves opaque credentials", () =>...@@ -35,6 +36,11 @@ it.effect("isolates provider bindings and preserves opaque credentials", () =>
35 assert.isTrue(Option.isNone(yield* b.get));36 assert.isTrue(Option.isNone(yield* b.get));
36 assert.isTrue(Option.isNone(yield* c.get));37 assert.isTrue(Option.isNone(yield* c.get));
37 assert.isFalse(a.binding.key.includes("/"));38 assert.isFalse(a.binding.key.includes("/"));
39 // Stored credentials are found by this name, so it must not change between releases.
40 assert.strictEqual(
41 a.binding.key,
42 "provider-auth-db3cc9436c8272847fe0c152ba8b0d4695af90acd916f555649e17ba6efe6094",
43 );
38 const long = yield* ProviderCredentialStore.make("a".repeat(64), "b".repeat(64)).pipe(44 const long = yield* ProviderCredentialStore.make("a".repeat(64), "b".repeat(64)).pipe(
39 Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore),45 Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore),
40 );46 );
...@@ -45,5 +51,5 @@ it.effect("isolates provider bindings and preserves opaque credentials", () =>...@@ -45,5 +51,5 @@ it.effect("isolates provider bindings and preserves opaque credentials", () =>
45 assert.notStrictEqual(delimiter.binding.key, a.binding.key);51 assert.notStrictEqual(delimiter.binding.key, a.binding.key);
46 yield* a.remove;52 yield* a.remove;
47 assert.isTrue(Option.isNone(yield* a.get));53 assert.isTrue(Option.isNone(yield* a.get));
48 }),54 }).pipe(Effect.provide(NodeCrypto.layer)),
49);55);
apps/server/src/provider/ProviderCredentialStore.ts+7-4
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import * as NodeCrypto from "node:crypto";1import * as Crypto from "effect/Crypto";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Hex from "effect/encoding/Hex";
3import * as ServerSecretStore from "../auth/ServerSecretStore.ts";4import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
45
5/** A provider binding stores opaque bytes; only its adapter decodes or refreshes them. */6/** A provider binding stores opaque bytes; only its adapter decodes or refreshes them. */
...@@ -8,10 +9,12 @@ export const make = Effect.fn("ProviderCredentialStore.make")(function* (...@@ -8,10 +9,12 @@ export const make = Effect.fn("ProviderCredentialStore.make")(function* (
8 bindingId: string,9 bindingId: string,
9) {10) {
10 const secrets = yield* ServerSecretStore.ServerSecretStore;11 const secrets = yield* ServerSecretStore.ServerSecretStore;
12 const crypto = yield* Crypto.Crypto;
11 // Hash the tuple so arbitrary bindings cannot escape or exceed a filename.13 // Hash the tuple so arbitrary bindings cannot escape or exceed a filename.
12 const key = `provider-auth-${NodeCrypto.createHash("sha256")14 const bindingHash = yield* crypto
13 .update(`${driver.length}:${driver}${bindingId}`)15 .digest("SHA-256", new TextEncoder().encode(`${driver.length}:${driver}${bindingId}`))
14 .digest("hex")}`;16 .pipe(Effect.map(Hex.encode), Effect.orDie);
17 const key = `provider-auth-${bindingHash}`;
15 return {18 return {
16 binding: { owner: "t3" as const, key },19 binding: { owner: "t3" as const, key },
17 get: secrets.get(key),20 get: secrets.get(key),
apps/server/src/provider/openCodeUsageLimits.ts+10-8
...@@ -1,13 +1,14 @@...@@ -1,13 +1,14 @@
1import * as NodeOS from "node:os";1import * as NodeOS from "node:os";
2import * as NodeCrypto from "node:crypto";
32
4import type { ServerProviderUsageWindow } from "@t3tools/contracts";3import type { ServerProviderUsageWindow } from "@t3tools/contracts";
4import * as Crypto from "effect/Crypto";
5import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";7import * as FileSystem from "effect/FileSystem";
8import * as Option from "effect/Option";8import * as Option from "effect/Option";
9import * as Path from "effect/Path";9import * as Path from "effect/Path";
10import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
11import * as Hex from "effect/encoding/Hex";
11import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";12import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
1213
13import {14import {
...@@ -96,15 +97,16 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(...@@ -96,15 +97,16 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
96 resetsAt: DateTime.formatIso(body.usage.monthly.resetsAt),97 resetsAt: DateTime.formatIso(body.usage.monthly.resetsAt),
97 },98 },
98 ];99 ];
100 const crypto = yield* Crypto.Crypto;
101 // Go's usage response has no account ID. An unkeyed hash matches across
102 // environments without a shared secret. It permits offline guesses, but
103 // Go keys are randomly generated.
104 const credentialFingerprint = yield* crypto
105 .digest("SHA-256", new TextEncoder().encode(`opencode-go\0${apiKey}`))
106 .pipe(Effect.map(Hex.encode), Effect.orDie);
99 return {107 return {
100 ...makeUsageLimits({ checkedAt, windows }),108 ...makeUsageLimits({ checkedAt, windows }),
101 // Go's usage response has no account ID. An unkeyed hash matches across109 credentialFingerprint,
102 // environments without a shared secret. It permits offline guesses, but
103 // Go keys are randomly generated.
104 credentialFingerprint: NodeCrypto.createHash("sha256")
105 .update("opencode-go\0")
106 .update(apiKey)
107 .digest("hex"),
108 };110 };
109 }).pipe(111 }).pipe(
110 Effect.timeout("5 seconds"),112 Effect.timeout("5 seconds"),
apps/server/src/pullRequest/GitHubPullRequestCli.test.ts+3-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
1import { afterEach, assert, expect, it, vi } from "@effect/vitest";2import { afterEach, assert, expect, it, vi } from "@effect/vitest";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as Clock from "effect/Clock";4import * as Clock from "effect/Clock";
...@@ -65,6 +66,7 @@ const layer = it.layer(...@@ -65,6 +66,7 @@ const layer = it.layer(
65 }),66 }),
66 ),67 ),
67 Layer.provideMerge(GitHubGraphQlBudget.layer),68 Layer.provideMerge(GitHubGraphQlBudget.layer),
69 Layer.provide(NodeCrypto.layer),
68 ),70 ),
69);71);
7072
...@@ -247,7 +249,7 @@ it.effect(...@@ -247,7 +249,7 @@ it.effect(
247 );249 );
248 const cli = yield* GitHubPullRequestCli.make.pipe(250 const cli = yield* GitHubPullRequestCli.make.pipe(
249 Effect.provideService(GitHubCli.GitHubCli, github),251 Effect.provideService(GitHubCli.GitHubCli, github),
250 Effect.provide(GitHubGraphQlBudget.layer),252 Effect.provide(Layer.merge(GitHubGraphQlBudget.layer, NodeCrypto.layer)),
251 );253 );
252 const input = { cwd: "/repo", host: "github.com" };254 const input = { cwd: "/repo", host: "github.com" };
253 const first = yield* cli.withVerifiedCredential(input, (identity) =>255 const first = yield* cli.withVerifiedCredential(input, (identity) =>
apps/server/src/pullRequest/GitHubPullRequestCli.ts+7-2
...@@ -4,7 +4,7 @@ import { runGitHubStackAction, type GitHubStackActionError } from "./githubStack...@@ -4,7 +4,7 @@ import { runGitHubStackAction, type GitHubStackActionError } from "./githubStack
4import * as Cause from "effect/Cause";4import * as Cause from "effect/Cause";
5import * as Context from "effect/Context";5import * as Context from "effect/Context";
6import * as Clock from "effect/Clock";6import * as Clock from "effect/Clock";
7import * as NodeCrypto from "node:crypto";7import * as Crypto from "effect/Crypto";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
10import * as Exit from "effect/Exit";10import * as Exit from "effect/Exit";
...@@ -15,6 +15,7 @@ import * as RequestResolver from "effect/RequestResolver";...@@ -15,6 +15,7 @@ import * as RequestResolver from "effect/RequestResolver";
15import * as Result from "effect/Result";15import * as Result from "effect/Result";
16import * as Schema from "effect/Schema";16import * as Schema from "effect/Schema";
17import * as Semaphore from "effect/Semaphore";17import * as Semaphore from "effect/Semaphore";
18import * as Hex from "effect/encoding/Hex";
18import {19import {
19 resolvePullRequestAuthorFilter,20 resolvePullRequestAuthorFilter,
20 PositiveInt,21 PositiveInt,
...@@ -1148,6 +1149,7 @@ function actionArgs(...@@ -1148,6 +1149,7 @@ function actionArgs(
1148export const make = Effect.gen(function* () {1149export const make = Effect.gen(function* () {
1149 const github = yield* GitHubCli.GitHubCli;1150 const github = yield* GitHubCli.GitHubCli;
1150 const graphQlBudget = yield* GitHubGraphQlBudget.GitHubGraphQlBudget;1151 const graphQlBudget = yield* GitHubGraphQlBudget.GitHubGraphQlBudget;
1152 const crypto = yield* Crypto.Crypto;
1151 const revalidateChecks = yield* makeChecksRevalidator;1153 const revalidateChecks = yield* makeChecksRevalidator;
1152 const routingIdentities = new Map<1154 const routingIdentities = new Map<
1153 string,1155 string,
...@@ -1184,7 +1186,10 @@ export const make = Effect.gen(function* () {...@@ -1184,7 +1186,10 @@ export const make = Effect.gen(function* () {
1184 })1186 })
1185 .pipe(Effect.mapError(unavailable))).stdout.trim();1187 .pipe(Effect.mapError(unavailable))).stdout.trim();
1186 if (!token) return yield* unavailable();1188 if (!token) return yield* unavailable();
1187 const key = `${host}:${NodeCrypto.createHash("sha256").update(token).digest("hex")}`;1189 const tokenHash = yield* crypto
1190 .digest("SHA-256", new TextEncoder().encode(token))
1191 .pipe(Effect.map(Hex.encode), Effect.orDie);
1192 const key = `${host}:${tokenHash}`;
1188 const credential = { host, token: Redacted.make(token), credentialFingerprint: key };1193 const credential = { host, token: Redacted.make(token), credentialFingerprint: key };
1189 // A cold page may ask several times. Wait per credential and check again after the1194 // A cold page may ask several times. Wait per credential and check again after the
1190 // first verification; cancellation releases the next waiter without losing its request.1195 // first verification; cancellation releases the next waiter without losing its request.
apps/server/src/vcs/GitVcsDriver.ts+4-5
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as NodeCrypto from "node:crypto";
2import * as NodeBuffer from "node:buffer";1import * as NodeBuffer from "node:buffer";
32
4import * as Context from "effect/Context";3import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
5import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";7import * as FileSystem from "effect/FileSystem";
...@@ -536,6 +536,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (...@@ -536,6 +536,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
536 const fileSystem = yield* FileSystem.FileSystem;536 const fileSystem = yield* FileSystem.FileSystem;
537 const path = yield* Path.Path;537 const path = yield* Path.Path;
538 const vcsProcess = yield* VcsProcess.VcsProcess;538 const vcsProcess = yield* VcsProcess.VcsProcess;
539 const crypto = yield* Crypto.Crypto;
539 const capabilities = {540 const capabilities = {
540 kind: "git" as const,541 kind: "git" as const,
541 supportsWorktrees: true,542 supportsWorktrees: true,
...@@ -811,10 +812,8 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (...@@ -811,10 +812,8 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
811 "sparse.expectFilesOutsideOfPatterns=false",812 "sparse.expectFilesOutsideOfPatterns=false",
812 ];813 ];
813 const gitCommonDir = yield* resolveGitCommonDir(input.cwd);814 const gitCommonDir = yield* resolveGitCommonDir(input.cwd);
814 const tempIndexPath = path.join(815 const indexId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
815 gitCommonDir,816 const tempIndexPath = path.join(gitCommonDir, `t3-checkpoint-index-${indexId}`);
816 `t3-checkpoint-index-${NodeCrypto.randomUUID()}`,
817 );
818 const commitEnv: NodeJS.ProcessEnv = {817 const commitEnv: NodeJS.ProcessEnv = {
819 ...process.env,818 ...process.env,
820 GIT_INDEX_FILE: tempIndexPath,819 GIT_INDEX_FILE: tempIndexPath,
apps/server/src/ws.ts+1-2
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import { OrchestrationDispatchCommandError } from "@t3tools/contracts";1import { OrchestrationDispatchCommandError } from "@t3tools/contracts";
2import * as Crypto from "effect/Crypto";2import * as Crypto from "effect/Crypto";
3import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";3import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
4import * as NodeCrypto from "node:crypto";
54
6import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
7import * as Duration from "effect/Duration";6import * as Duration from "effect/Duration";
...@@ -1469,7 +1468,7 @@ const layerWsRpc = (...@@ -1469,7 +1468,7 @@ const layerWsRpc = (
1469 provider?.models.find((candidate) => candidate.isDefault)?.slug ??1468 provider?.models.find((candidate) => candidate.isDefault)?.slug ??
1470 provider?.models[0]?.slug ??1469 provider?.models[0]?.slug ??
1471 "default";1470 "default";
1472 const commandId = CommandId.make(NodeCrypto.randomUUID());1471 const commandId = CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
1473 const launched = yield* Effect.result(1472 const launched = yield* Effect.result(
1474 startup.enqueueCommand(1473 startup.enqueueCommand(
1475 threadLaunch.launch({1474 threadLaunch.launch({