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 @@
11import * as NodeVM from "node:vm";
2import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
23import { it as effectIt } from "@effect/vitest";
34import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts";
45import type {
......@@ -279,6 +280,7 @@ const layer = PreviewManager.layer.pipe(
279280 Layer.provideMerge(layerEnvironment),
280281 Layer.provideMerge(layerFileSystem),
281282 Layer.provideMerge(Path.layer),
283 Layer.provideMerge(NodeCrypto.layer),
282284 Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")),
283285);
284286const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError);
apps/desktop/src/preview/Manager.ts+9-9
......@@ -5,7 +5,6 @@
55 * elements live in the renderer; we only attach listeners and forward state
66 * here). Single layer-scoped browser session partition.
77 */
8import * as NodeCrypto from "node:crypto";
98import {
109 DesktopPreviewRecordingInputSchema,
1110 DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER,
......@@ -49,6 +48,7 @@ import {
4948import * as Cause from "effect/Cause";
5049import * as Clock from "effect/Clock";
5150import * as Context from "effect/Context";
51import * as Crypto from "effect/Crypto";
5252import * as DateTime from "effect/DateTime";
5353import * as Deferred from "effect/Deferred";
5454import * as Effect from "effect/Effect";
......@@ -643,6 +643,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
643643 const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;
644644 const hostPlatform = yield* HostProcessPlatform;
645645 const path = yield* Path.Path;
646 const crypto = yield* Crypto.Crypto;
646647 const parentScope = yield* Scope.Scope;
647648 const context = yield* Effect.context<never>();
648649 const runFork = Effect.runForkWith(context);
......@@ -1657,9 +1658,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
16571658 returnByValue: boolean,
16581659 awaitPromise = true,
16591660 ): Effect.Effect<A, PreviewManagerError> =>
1660 Effect.suspend(() => {
1661 const objectGroup = `t3-evaluation-${NodeCrypto.randomUUID()}`;
1662 return send("Runtime.evaluate", {
1661 Effect.gen(function* () {
1662 const objectGroup = `t3-evaluation-${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`;
1663 return yield* send("Runtime.evaluate", {
16631664 expression,
16641665 awaitPromise,
16651666 returnByValue,
......@@ -4186,10 +4187,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
41864187 const context = { operation: "automationPress.awaitNativeKey", tabId, webContentsId: wc.id };
41874188 const evaluate = (frame: Electron.WebFrameMain, expression: string) =>
41884189 attemptPromise(context, () => frame.executeJavaScript(expression));
4190 const receiptId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
41894191 const { frames, receiptKey } = yield* Effect.acquireRelease(
41904192 attempt(context, () => ({
41914193 frames: wc.mainFrame.framesInSubtree,
4192 receiptKey: JSON.stringify(`__t3NativeKey_${NodeCrypto.randomUUID()}`),
4194 receiptKey: JSON.stringify(`__t3NativeKey_${receiptId}`),
41934195 })),
41944196 ({ frames, receiptKey }) =>
41954197 Effect.forEach(
......@@ -4445,10 +4447,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
44454447 keySequence,
44464448 clipboardData,
44474449 );
4448 const selectionKey = yield* encodeJson(
4449 context,
4450 `__t3EditingSelection_${NodeCrypto.randomUUID()}`,
4451 );
4450 const selectionId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
4451 const selectionKey = yield* encodeJson(context, `__t3EditingSelection_${selectionId}`);
44524452 // Editing requires an active document. Preserve the target
44534453 // and selection across focus handlers without focusing the desktop.
44544454 yield* Effect.acquireUseRelease(
apps/server/scripts/measure-pr-preview.ts+1
......@@ -50,6 +50,7 @@ const layerMeasuredProcess = Layer.effect(
5050const layerServices = GitHubPullRequestCli.layer.pipe(
5151 Layer.provide(GitHubCli.layer),
5252 Layer.provideMerge(layerMeasuredProcess),
53 Layer.provide(NodeServices.layer),
5354);
5455
5556const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));
apps/server/src/assets/AttachmentUpload.ts+4-3
......@@ -1,11 +1,10 @@
1import * as NodeCrypto from "node:crypto";
2
31import {
42 ATTACHMENT_UPLOAD_URL_TTL_MS,
53 type AttachmentCreateUploadUrlInput,
64 AttachmentUploadSigningKeyError,
75} from "@t3tools/contracts";
86import * as Clock from "effect/Clock";
7import * as Crypto from "effect/Crypto";
98import * as Effect from "effect/Effect";
109import * as FileSystem from "effect/FileSystem";
1110import * as Option from "effect/Option";
......@@ -170,9 +169,11 @@ export const storeAttachmentUpload = Effect.fn("AttachmentUpload.store")(functio
170169 attachmentsDir: config.attachmentsDir,
171170 relativePath,
172171 });
172 const crypto = yield* Crypto.Crypto;
173 const partId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
173174 const partPath = resolveAttachmentRelativePath({
174175 attachmentsDir: config.attachmentsDir,
175 relativePath: `${relativePath}.${NodeCrypto.randomUUID()}.part`,
176 relativePath: `${relativePath}.${partId}.part`,
176177 });
177178 if (!finalPath || !partPath) {
178179 return { ok: false, status: 500, detail: "Failed to resolve attachment path." };
apps/server/src/assets/NativeAppIconResolver.ts+10-5
......@@ -1,7 +1,7 @@
1import * as NodeCrypto from "node:crypto";
21import type { ToolActivityNativeAppReference } from "@t3tools/contracts";
32import * as Cache from "effect/Cache";
43import * as Cause from "effect/Cause";
4import * as Crypto from "effect/Crypto";
55import * as Effect from "effect/Effect";
66import * as Clock from "effect/Clock";
77import * as Context from "effect/Context";
......@@ -13,6 +13,7 @@ import * as Option from "effect/Option";
1313import * as Path from "effect/Path";
1414import * as PlatformError from "effect/PlatformError";
1515import * as Semaphore from "effect/Semaphore";
16import * as Hex from "effect/encoding/Hex";
1617import { ChildProcess, ChildProcessSpawner } from "effect/process";
1718import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1819
......@@ -175,9 +176,13 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc
175176 const appVersion =
176177 (yield* plistValue(infoPlistPath, "CFBundleVersion")) ||
177178 (yield* plistValue(infoPlistPath, "CFBundleShortVersionString"));
178 const cacheKey = NodeCrypto.createHash("sha256")
179 .update(`${canonicalAppPath}\0${appVersion}\0${sourceIconPath}`)
180 .digest("hex");
179 const crypto = yield* Crypto.Crypto;
180 const cacheKey = yield* crypto
181 .digest(
182 "SHA-256",
183 new TextEncoder().encode(`${canonicalAppPath}\0${appVersion}\0${sourceIconPath}`),
184 )
185 .pipe(Effect.map(Hex.encode), Effect.orDie);
181186 const cacheDirectory = path.join(config.providerStatusCacheDir, "native-app-icons");
182187 const cachePath = path.join(cacheDirectory, `${cacheKey}.png`);
183188 if (yield* existingFile(cachePath)) return cachePath;
......@@ -185,7 +190,7 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc
185190 yield* fileSystem.makeDirectory(cacheDirectory, { recursive: true });
186191 const temporaryPath = path.join(
187192 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`,
189194 );
190195 yield* commandOutput("/usr/bin/sips", [
191196 "-z",
apps/server/src/cli/app.ts+4-2
......@@ -1,5 +1,4 @@
11// @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";
32import * as NodeNet from "node:net";
43import * as NodeOS from "node:os";
54
......@@ -18,6 +17,7 @@ import {
1817} from "@t3tools/shared/hostProcess";
1918import * as Config from "effect/Config";
2019import * as Console from "effect/Console";
20import * as Crypto from "effect/Crypto";
2121import * as Effect from "effect/Effect";
2222import * as Option from "effect/Option";
2323import * as Path from "effect/Path";
......@@ -212,9 +212,11 @@ const runAppCommand = Effect.fn("cli.app")(function* (flags: {
212212 userId,
213213 joinPath: path.join,
214214 }).address;
215 const crypto = yield* Crypto.Crypto;
216 const requestId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
215217 const request: DesktopAppActivationRequest = {
216218 version: DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,
217 requestId: NodeCrypto.randomUUID(),
219 requestId,
218220 type: "open-workspace",
219221 workspaceRoot,
220222 platform: hostPlatform,
apps/server/src/device/SshDeviceHost.ts+10-5
......@@ -1,4 +1,3 @@
1import * as NodeCrypto from "node:crypto";
21import {
32 type DeviceHostSummary,
43 DevicePlatformAvailability,
......@@ -9,6 +8,7 @@ import {
98import { runSshCommand, baseSshArgs, resolveSshCommand } from "@t3tools/ssh/command";
109import * as NetService from "@t3tools/shared/Net";
1110import { waitForHttpReady } from "@t3tools/shared/httpReadiness";
11import * as Crypto from "effect/Crypto";
1212import * as Exit from "effect/Exit";
1313import * as Effect from "effect/Effect";
1414import * as FileSystem from "effect/FileSystem";
......@@ -17,6 +17,7 @@ import * as Scope from "effect/Scope";
1717import * as Semaphore from "effect/Semaphore";
1818import * as Schema from "effect/Schema";
1919import * as Stream from "effect/Stream";
20import * as Hex from "effect/encoding/Hex";
2021import * as HttpClient from "effect/http/HttpClient";
2122import * as ChildProcess from "effect/process/ChildProcess";
2223import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
......@@ -79,10 +80,11 @@ const ownerFor = Effect.fn("SshDeviceHost.ownerFor")(function* (hostId: string)
7980 const environmentId = yield* fs
8081 .readFileString(server.environmentIdPath)
8182 .pipe(Effect.orElseSucceed(() => server.stateDir));
82 return NodeCrypto.createHash("sha256")
83 .update(`${environmentId}\0${server.stateDir}\0${hostId}`)
84 .digest("hex")
85 .slice(0, 24);
83 const crypto = yield* Crypto.Crypto;
84 const owner = yield* crypto
85 .digest("SHA-256", new TextEncoder().encode(`${environmentId}\0${server.stateDir}\0${hostId}`))
86 .pipe(Effect.map(Hex.encode), Effect.orDie);
87 return owner.slice(0, 24);
8688});
8789
8890export const probe = Effect.fn("SshDeviceHost.probe")(function* (
......@@ -125,6 +127,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
125127 const net = yield* NetService.NetService;
126128 const http = yield* HttpClient.HttpClient;
127129 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
130 const crypto = yield* Crypto.Crypto;
128131 const parentScope = yield* Scope.Scope;
129132 const ssh = yield* resolveSshCommand;
130133 const owner = yield* ownerFor(config.id);
......@@ -135,6 +138,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
135138 | FileSystem.FileSystem
136139 | Path.Path
137140 | ChildProcessSpawner.ChildProcessSpawner
141 | Crypto.Crypto
138142 | ServerConfig.ServerConfig
139143 >,
140144 ) =>
......@@ -143,6 +147,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
143147 Effect.provideService(Path.Path, path),
144148 Effect.provideService(ServerConfig.ServerConfig, server),
145149 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
150 Effect.provideService(Crypto.Crypto, crypto),
146151 );
147152 const lock = yield* Semaphore.make(1);
148153 let stopped = false;
apps/server/src/mcp/McpHttpServer.ts+5-3
......@@ -1,7 +1,7 @@
1import * as NodeCrypto from "node:crypto";
21import * as Cause from "effect/Cause";
32import * as Clock from "effect/Clock";
43import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
55import * as Effect from "effect/Effect";
66import * as FileSystem from "effect/FileSystem";
77import * as Layer from "effect/Layer";
......@@ -334,8 +334,10 @@ const saveScreenshot = Effect.fn("McpHttpServer.saveScreenshot")(function* (
334334 const fileSystem = yield* FileSystem.FileSystem;
335335 const path = yield* Path.Path;
336336 const millis = yield* Clock.currentTimeMillis;
337 const crypto = yield* Crypto.Crypto;
337338 // 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`;
339341 const screenshotPath = path.join(config.browserArtifactsDir, fileName);
340342 yield* fileSystem.makeDirectory(config.browserArtifactsDir, { recursive: true }).pipe(
341343 Effect.andThen(fileSystem.writeFile(screenshotPath, data)),
......@@ -387,7 +389,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
387389 const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
388390 // The MCP tool runner only supplies the client, so hand the save path its services here.
389391 const saveServices = yield* Effect.context<
390 ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path
392 ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path | Crypto.Crypto
391393 >();
392394 const built = yield* PreviewSnapshotToolkit;
393395 const tool = PreviewSnapshotTool;
apps/server/src/mcp/toolkits/previewControls/handlers.test.ts+2-1
......@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12import { expect, it } from "@effect/vitest";
23import {
34 DEFAULT_SERVER_SETTINGS,
......@@ -45,7 +46,7 @@ it.effect.each([
4546 capabilities: new Set(effective.enableAgentBrowserAccess ? ["preview"] : []),
4647 issuedAt: 0,
4748 };
48 const manager = yield* Preview.make;
49 const manager = yield* Preview.make.pipe(Effect.provide(NodeCrypto.layer));
4950 const tab = yield* manager.open({ threadId, url: "http://localhost:3000" });
5051 const layerDependencies = Layer.mergeAll(
5152 Layer.succeed(Preview.PreviewManager, manager),
apps/server/src/preview/Manager.test.ts+5-2
......@@ -1,7 +1,10 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12import { it } from "@effect/vitest";
23import { type PreviewEvent, ThreadId } from "@t3tools/contracts";
34import { 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";
58import { expect } from "vite-plus/test";
69
710import * as PreviewManager from "./Manager.ts";
......@@ -35,7 +38,7 @@ const collectEvents = Effect.gen(function* () {
3538 return collector;
3639}).pipe(Effect.withSpan("preview.test.collectEvents"));
3740
38it.layer(PreviewManager.layer)("PreviewManager", (it) => {
41it.layer(PreviewManager.layer.pipe(Layer.provide(NodeCrypto.layer)))("PreviewManager", (it) => {
3942 it.effect("opens a session and emits opened with normalized URL", () =>
4043 Effect.gen(function* () {
4144 const threadId = freshThreadId();
apps/server/src/preview/Manager.ts+3-2
......@@ -31,8 +31,8 @@ import {
3131 newPreviewTabId,
3232 normalizePreviewUrl,
3333} from "@t3tools/shared/preview";
34import * as NodeCrypto from "node:crypto";
3534import * as Context from "effect/Context";
35import * as Crypto from "effect/Crypto";
3636import * as DateTime from "effect/DateTime";
3737import * as Effect from "effect/Effect";
3838import * as Layer from "effect/Layer";
......@@ -155,7 +155,8 @@ const buildIdleSnapshot = (input: {
155155
156156/** @public Service construction is part of the canonical Effect module API. */
157157export 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);
159160 const stateRef = yield* SynchronizedRef.make<ManagerState>(initialState);
160161 // Unbounded PubSub is fine here — events are tiny and we don't want to
161162 // block publishers if a subscriber is slow. WS clients backpressure on
apps/server/src/project/ProjectSetupScriptRunner.test.ts+2
......@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12import { assert, it, vi } from "@effect/vitest";
23import { ProjectId } from "@t3tools/contracts";
34import * as Effect from "effect/Effect";
......@@ -64,6 +65,7 @@ it.effect("resolves setup scripts through the standalone project service", () =>
6465 }),
6566 Layer.mock(TerminalManager.TerminalManager)({ open, write, subscribe }),
6667 ServerSettings.layerTest(),
68 NodeCrypto.layer,
6769 ),
6870 ),
6971 );
apps/server/src/project/ProjectSetupScriptRunner.ts+5-2
......@@ -5,10 +5,10 @@ import {
55 resolveProjectScripts,
66 setupProjectScript,
77} from "@t3tools/shared/projectScripts";
8import * as NodeCrypto from "node:crypto";
98
109import * as Clock from "effect/Clock";
1110import * as Context from "effect/Context";
11import * as Crypto from "effect/Crypto";
1212import * as Deferred from "effect/Deferred";
1313import * as Effect from "effect/Effect";
1414import * as Layer from "effect/Layer";
......@@ -201,6 +201,7 @@ export const make = Effect.gen(function* () {
201201 const projects = yield* ProjectService.ProjectService;
202202 const terminalManager = yield* TerminalManager.TerminalManager;
203203 const serverSettings = yield* ServerSettings.ServerSettingsService;
204 const crypto = yield* Crypto.Crypto;
204205 const completionShell = resolveCompletionShell(
205206 yield* HostProcessPlatform,
206207 yield* HostProcessEnvironment,
......@@ -367,7 +368,9 @@ export const make = Effect.gen(function* () {
367368 COLORTERM: "",
368369 };
369370 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;
371374 const commandLine =
372375 observe && completionToken
373376 ? wrapCommandForCompletion(
apps/server/src/provider/CodexChatGptAuth.ts+6-1
......@@ -12,6 +12,7 @@ import { codexCallbackUrl } from "@t3tools/shared/codexAuthHandoff";
1212import * as Clock from "effect/Clock";
1313import * as Cause from "effect/Cause";
1414import * as AnalyticsService from "../telemetry/AnalyticsService.ts";
15import * as Crypto from "effect/Crypto";
1516import * as Exit from "effect/Exit";
1617import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts";
1718import * as Effect from "effect/Effect";
......@@ -158,6 +159,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
158159 options.instanceId,
159160 );
160161 const secrets = yield* ServerSecretStore.ServerSecretStore;
162 const crypto = yield* Crypto.Crypto;
161163 const environmentLocks = sessionLocks.get(secrets) ?? new Map<string, Semaphore.Semaphore>();
162164 sessionLocks.set(secrets, environmentLocks);
163165 const lock = environmentLocks.get(store.binding.key) ?? (yield* Semaphore.make(1));
......@@ -306,7 +308,10 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
306308 savedConnectionCount: savedConnections.size,
307309 unidentifiedConnectedConnectionCount,
308310 };
309 }).pipe(Effect.provideService(ServerSecretStore.ServerSecretStore, secrets));
311 }).pipe(
312 Effect.provideService(ServerSecretStore.ServerSecretStore, secrets),
313 Effect.provideService(Crypto.Crypto, crypto),
314 );
310315 const saveRegistration = Effect.fnUntraced(function* (profile: typeof Registration.Type) {
311316 const saved = yield* readRegistrations;
312317 profile = {
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2
......@@ -199,6 +199,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
199199 const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime;
200200 const serverConfig = yield* ServerConfig.ServerConfig;
201201 const httpClient = yield* HttpClient.HttpClient;
202 const crypto = yield* Crypto.Crypto;
202203 const serverSettings = yield* ServerSettings.ServerSettingsService;
203204 const processEnv = mergeProviderInstanceEnvironment(environment);
204205 const continuationIdentity = defaultProviderContinuationIdentity({
......@@ -388,6 +389,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
388389 Effect.provideService(FileSystem.FileSystem, fileSystem),
389390 Effect.provideService(Path.Path, pathService),
390391 Effect.provideService(HttpClient.HttpClient, httpClient),
392 Effect.provideService(Crypto.Crypto, crypto),
391393 Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner),
392394 Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime),
393395 );
apps/server/src/provider/ProviderCredentialStore.test.ts+7-1
......@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12import { assert, it } from "@effect/vitest";
23import * as Effect from "effect/Effect";
34import * as Option from "effect/Option";
......@@ -35,6 +36,11 @@ it.effect("isolates provider bindings and preserves opaque credentials", () =>
3536 assert.isTrue(Option.isNone(yield* b.get));
3637 assert.isTrue(Option.isNone(yield* c.get));
3738 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 );
3844 const long = yield* ProviderCredentialStore.make("a".repeat(64), "b".repeat(64)).pipe(
3945 Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore),
4046 );
......@@ -45,5 +51,5 @@ it.effect("isolates provider bindings and preserves opaque credentials", () =>
4551 assert.notStrictEqual(delimiter.binding.key, a.binding.key);
4652 yield* a.remove;
4753 assert.isTrue(Option.isNone(yield* a.get));
48 }),
54 }).pipe(Effect.provide(NodeCrypto.layer)),
4955);
apps/server/src/provider/ProviderCredentialStore.ts+7-4
......@@ -1,5 +1,6 @@
1import * as NodeCrypto from "node:crypto";
1import * as Crypto from "effect/Crypto";
22import * as Effect from "effect/Effect";
3import * as Hex from "effect/encoding/Hex";
34import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
45
56/** 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* (
89 bindingId: string,
910) {
1011 const secrets = yield* ServerSecretStore.ServerSecretStore;
12 const crypto = yield* Crypto.Crypto;
1113 // Hash the tuple so arbitrary bindings cannot escape or exceed a filename.
12 const key = `provider-auth-${NodeCrypto.createHash("sha256")
13 .update(`${driver.length}:${driver}${bindingId}`)
14 .digest("hex")}`;
14 const bindingHash = yield* crypto
15 .digest("SHA-256", new TextEncoder().encode(`${driver.length}:${driver}${bindingId}`))
16 .pipe(Effect.map(Hex.encode), Effect.orDie);
17 const key = `provider-auth-${bindingHash}`;
1518 return {
1619 binding: { owner: "t3" as const, key },
1720 get: secrets.get(key),
apps/server/src/provider/openCodeUsageLimits.ts+10-8
......@@ -1,13 +1,14 @@
11import * as NodeOS from "node:os";
2import * as NodeCrypto from "node:crypto";
32
43import type { ServerProviderUsageWindow } from "@t3tools/contracts";
4import * as Crypto from "effect/Crypto";
55import * as DateTime from "effect/DateTime";
66import * as Effect from "effect/Effect";
77import * as FileSystem from "effect/FileSystem";
88import * as Option from "effect/Option";
99import * as Path from "effect/Path";
1010import * as Schema from "effect/Schema";
11import * as Hex from "effect/encoding/Hex";
1112import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
1213
1314import {
......@@ -96,15 +97,16 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
9697 resetsAt: DateTime.formatIso(body.usage.monthly.resetsAt),
9798 },
9899 ];
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);
99107 return {
100108 ...makeUsageLimits({ checkedAt, windows }),
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 credentialFingerprint: NodeCrypto.createHash("sha256")
105 .update("opencode-go\0")
106 .update(apiKey)
107 .digest("hex"),
109 credentialFingerprint,
108110 };
109111 }).pipe(
110112 Effect.timeout("5 seconds"),
apps/server/src/pullRequest/GitHubPullRequestCli.test.ts+3-1
......@@ -1,3 +1,4 @@
1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12import { afterEach, assert, expect, it, vi } from "@effect/vitest";
23import * as Effect from "effect/Effect";
34import * as Clock from "effect/Clock";
......@@ -65,6 +66,7 @@ const layer = it.layer(
6566 }),
6667 ),
6768 Layer.provideMerge(GitHubGraphQlBudget.layer),
69 Layer.provide(NodeCrypto.layer),
6870 ),
6971);
7072
......@@ -247,7 +249,7 @@ it.effect(
247249 );
248250 const cli = yield* GitHubPullRequestCli.make.pipe(
249251 Effect.provideService(GitHubCli.GitHubCli, github),
250 Effect.provide(GitHubGraphQlBudget.layer),
252 Effect.provide(Layer.merge(GitHubGraphQlBudget.layer, NodeCrypto.layer)),
251253 );
252254 const input = { cwd: "/repo", host: "github.com" };
253255 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
44import * as Cause from "effect/Cause";
55import * as Context from "effect/Context";
66import * as Clock from "effect/Clock";
7import * as NodeCrypto from "node:crypto";
7import * as Crypto from "effect/Crypto";
88import * as Effect from "effect/Effect";
99import * as Layer from "effect/Layer";
1010import * as Exit from "effect/Exit";
......@@ -15,6 +15,7 @@ import * as RequestResolver from "effect/RequestResolver";
1515import * as Result from "effect/Result";
1616import * as Schema from "effect/Schema";
1717import * as Semaphore from "effect/Semaphore";
18import * as Hex from "effect/encoding/Hex";
1819import {
1920 resolvePullRequestAuthorFilter,
2021 PositiveInt,
......@@ -1148,6 +1149,7 @@ function actionArgs(
11481149export const make = Effect.gen(function* () {
11491150 const github = yield* GitHubCli.GitHubCli;
11501151 const graphQlBudget = yield* GitHubGraphQlBudget.GitHubGraphQlBudget;
1152 const crypto = yield* Crypto.Crypto;
11511153 const revalidateChecks = yield* makeChecksRevalidator;
11521154 const routingIdentities = new Map<
11531155 string,
......@@ -1184,7 +1186,10 @@ export const make = Effect.gen(function* () {
11841186 })
11851187 .pipe(Effect.mapError(unavailable))).stdout.trim();
11861188 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}`;
11881193 const credential = { host, token: Redacted.make(token), credentialFingerprint: key };
11891194 // A cold page may ask several times. Wait per credential and check again after the
11901195 // first verification; cancellation releases the next waiter without losing its request.
apps/server/src/vcs/GitVcsDriver.ts+4-5
......@@ -1,7 +1,7 @@
1import * as NodeCrypto from "node:crypto";
21import * as NodeBuffer from "node:buffer";
32
43import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
55import * as DateTime from "effect/DateTime";
66import * as Effect from "effect/Effect";
77import * as FileSystem from "effect/FileSystem";
......@@ -536,6 +536,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
536536 const fileSystem = yield* FileSystem.FileSystem;
537537 const path = yield* Path.Path;
538538 const vcsProcess = yield* VcsProcess.VcsProcess;
539 const crypto = yield* Crypto.Crypto;
539540 const capabilities = {
540541 kind: "git" as const,
541542 supportsWorktrees: true,
......@@ -811,10 +812,8 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
811812 "sparse.expectFilesOutsideOfPatterns=false",
812813 ];
813814 const gitCommonDir = yield* resolveGitCommonDir(input.cwd);
814 const tempIndexPath = path.join(
815 gitCommonDir,
816 `t3-checkpoint-index-${NodeCrypto.randomUUID()}`,
817 );
815 const indexId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
816 const tempIndexPath = path.join(gitCommonDir, `t3-checkpoint-index-${indexId}`);
818817 const commitEnv: NodeJS.ProcessEnv = {
819818 ...process.env,
820819 GIT_INDEX_FILE: tempIndexPath,
apps/server/src/ws.ts+1-2
......@@ -1,7 +1,6 @@
11import { OrchestrationDispatchCommandError } from "@t3tools/contracts";
22import * as Crypto from "effect/Crypto";
33import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
4import * as NodeCrypto from "node:crypto";
54
65import * as DateTime from "effect/DateTime";
76import * as Duration from "effect/Duration";
......@@ -1469,7 +1468,7 @@ const layerWsRpc = (
14691468 provider?.models.find((candidate) => candidate.isDefault)?.slug ??
14701469 provider?.models[0]?.slug ??
14711470 "default";
1472 const commandId = CommandId.make(NodeCrypto.randomUUID());
1471 const commandId = CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
14731472 const launched = yield* Effect.result(
14741473 startup.enqueueCommand(
14751474 threadLaunch.launch({