| author | |
| committer | |
| log | 83b982a85c87b4c0eda08beb9028e3a73f7c89b6 |
| tree | 524afd394866f0c7d03bcb60262d8b26e7d0f5cb |
| parent | 8f75697b9e022033f2615cb32e3c6ddc61c52353 |
| signature | Signed by PGP key B5690EEEBB952194 |
22 files changed, 109 insertions(+), 58 deletions(-)
apps/desktop/src/preview/Manager.test.ts+2| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import * as NodeVM from "node:vm"; |
| 2 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | |
| 2 | 3 | import { it as effectIt } from "@effect/vitest"; |
| 3 | 4 | import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts"; |
| 4 | 5 | import type { |
| ... | ... | @@ -279,6 +280,7 @@ const layer = PreviewManager.layer.pipe( |
| 279 | 280 | Layer.provideMerge(layerEnvironment), |
| 280 | 281 | Layer.provideMerge(layerFileSystem), |
| 281 | 282 | Layer.provideMerge(Path.layer), |
| 283 | Layer.provideMerge(NodeCrypto.layer), | |
| 282 | 284 | Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")), |
| 283 | 285 | ); |
| 284 | 286 | const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError); |
apps/desktop/src/preview/Manager.ts+9-9| ... | ... | @@ -5,7 +5,6 @@ |
| 5 | 5 | * elements live in the renderer; we only attach listeners and forward state |
| 6 | 6 | * here). Single layer-scoped browser session partition. |
| 7 | 7 | */ |
| 8 | import * as NodeCrypto from "node:crypto"; | |
| 9 | 8 | import { |
| 10 | 9 | DesktopPreviewRecordingInputSchema, |
| 11 | 10 | DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER, |
| ... | ... | @@ -49,6 +48,7 @@ import { |
| 49 | 48 | import * as Cause from "effect/Cause"; |
| 50 | 49 | import * as Clock from "effect/Clock"; |
| 51 | 50 | import * as Context from "effect/Context"; |
| 51 | import * as Crypto from "effect/Crypto"; | |
| 52 | 52 | import * as DateTime from "effect/DateTime"; |
| 53 | 53 | import * as Deferred from "effect/Deferred"; |
| 54 | 54 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -643,6 +643,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 643 | 643 | const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory; |
| 644 | 644 | const hostPlatform = yield* HostProcessPlatform; |
| 645 | 645 | const path = yield* Path.Path; |
| 646 | const crypto = yield* Crypto.Crypto; | |
| 646 | 647 | const parentScope = yield* Scope.Scope; |
| 647 | 648 | const context = yield* Effect.context<never>(); |
| 648 | 649 | const runFork = Effect.runForkWith(context); |
| ... | ... | @@ -1657,9 +1658,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1657 | 1658 | returnByValue: boolean, |
| 1658 | 1659 | awaitPromise = true, |
| 1659 | 1660 | ): 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", { | |
| 1663 | 1664 | expression, |
| 1664 | 1665 | awaitPromise, |
| 1665 | 1666 | returnByValue, |
| ... | ... | @@ -4186,10 +4187,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 4186 | 4187 | const context = { operation: "automationPress.awaitNativeKey", tabId, webContentsId: wc.id }; |
| 4187 | 4188 | const evaluate = (frame: Electron.WebFrameMain, expression: string) => |
| 4188 | 4189 | attemptPromise(context, () => frame.executeJavaScript(expression)); |
| 4190 | const receiptId = yield* crypto.randomUUIDv4.pipe(Effect.orDie); | |
| 4189 | 4191 | const { frames, receiptKey } = yield* Effect.acquireRelease( |
| 4190 | 4192 | attempt(context, () => ({ |
| 4191 | 4193 | frames: wc.mainFrame.framesInSubtree, |
| 4192 | receiptKey: JSON.stringify(`__t3NativeKey_${NodeCrypto.randomUUID()}`), | |
| 4194 | receiptKey: JSON.stringify(`__t3NativeKey_${receiptId}`), | |
| 4193 | 4195 | })), |
| 4194 | 4196 | ({ frames, receiptKey }) => |
| 4195 | 4197 | Effect.forEach( |
| ... | ... | @@ -4445,10 +4447,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 4445 | 4447 | keySequence, |
| 4446 | 4448 | clipboardData, |
| 4447 | 4449 | ); |
| 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}`); | |
| 4452 | 4452 | // Editing requires an active document. Preserve the target |
| 4453 | 4453 | // and selection across focus handlers without focusing the desktop. |
| 4454 | 4454 | yield* Effect.acquireUseRelease( |
apps/server/scripts/measure-pr-preview.ts+1| ... | ... | @@ -50,6 +50,7 @@ const layerMeasuredProcess = Layer.effect( |
| 50 | 50 | const layerServices = GitHubPullRequestCli.layer.pipe( |
| 51 | 51 | Layer.provide(GitHubCli.layer), |
| 52 | 52 | Layer.provideMerge(layerMeasuredProcess), |
| 53 | Layer.provide(NodeServices.layer), | |
| 53 | 54 | ); |
| 54 | 55 | |
| 55 | 56 | const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown)); |
apps/server/src/assets/AttachmentUpload.ts+4-3| ... | ... | @@ -1,11 +1,10 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | |
| 2 | ||
| 3 | 1 | import { |
| 4 | 2 | ATTACHMENT_UPLOAD_URL_TTL_MS, |
| 5 | 3 | type AttachmentCreateUploadUrlInput, |
| 6 | 4 | AttachmentUploadSigningKeyError, |
| 7 | 5 | } from "@t3tools/contracts"; |
| 8 | 6 | import * as Clock from "effect/Clock"; |
| 7 | import * as Crypto from "effect/Crypto"; | |
| 9 | 8 | import * as Effect from "effect/Effect"; |
| 10 | 9 | import * as FileSystem from "effect/FileSystem"; |
| 11 | 10 | import * as Option from "effect/Option"; |
| ... | ... | @@ -170,9 +169,11 @@ export const storeAttachmentUpload = Effect.fn("AttachmentUpload.store")(functio |
| 170 | 169 | attachmentsDir: config.attachmentsDir, |
| 171 | 170 | relativePath, |
| 172 | 171 | }); |
| 172 | const crypto = yield* Crypto.Crypto; | |
| 173 | const partId = yield* crypto.randomUUIDv4.pipe(Effect.orDie); | |
| 173 | 174 | const partPath = resolveAttachmentRelativePath({ |
| 174 | 175 | attachmentsDir: config.attachmentsDir, |
| 175 | relativePath: `${relativePath}.${NodeCrypto.randomUUID()}.part`, | |
| 176 | relativePath: `${relativePath}.${partId}.part`, | |
| 176 | 177 | }); |
| 177 | 178 | if (!finalPath || !partPath) { |
| 178 | 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 | import * as NodeCrypto from "node:crypto"; | |
| 2 | 1 | import type { ToolActivityNativeAppReference } from "@t3tools/contracts"; |
| 3 | 2 | import * as Cache from "effect/Cache"; |
| 4 | 3 | import * as Cause from "effect/Cause"; |
| 4 | import * as Crypto from "effect/Crypto"; | |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | import * as Clock from "effect/Clock"; |
| 7 | 7 | import * as Context from "effect/Context"; |
| ... | ... | @@ -13,6 +13,7 @@ import * as Option from "effect/Option"; |
| 13 | 13 | import * as Path from "effect/Path"; |
| 14 | 14 | import * as PlatformError from "effect/PlatformError"; |
| 15 | 15 | import * as Semaphore from "effect/Semaphore"; |
| 16 | import * as Hex from "effect/encoding/Hex"; | |
| 16 | 17 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 17 | 18 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 18 | 19 | |
| ... | ... | @@ -175,9 +176,13 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc |
| 175 | 176 | const appVersion = |
| 176 | 177 | (yield* plistValue(infoPlistPath, "CFBundleVersion")) || |
| 177 | 178 | (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); | |
| 181 | 186 | const cacheDirectory = path.join(config.providerStatusCacheDir, "native-app-icons"); |
| 182 | 187 | const cachePath = path.join(cacheDirectory, `${cacheKey}.png`); |
| 183 | 188 | if (yield* existingFile(cachePath)) return cachePath; |
| ... | ... | @@ -185,7 +190,7 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc |
| 185 | 190 | yield* fileSystem.makeDirectory(cacheDirectory, { recursive: true }); |
| 186 | 191 | const temporaryPath = path.join( |
| 187 | 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 | 195 | yield* commandOutput("/usr/bin/sips", [ |
| 191 | 196 | "-z", |
apps/server/src/cli/app.ts+4-2| ... | ... | @@ -1,5 +1,4 @@ |
| 1 | 1 | // @effect-diagnostics globalTimers:off -- The Node socket client owns its response deadline and clears it on every completion path. |
| 2 | import * as NodeCrypto from "node:crypto"; | |
| 3 | 2 | import * as NodeNet from "node:net"; |
| 4 | 3 | import * as NodeOS from "node:os"; |
| 5 | 4 | |
| ... | ... | @@ -18,6 +17,7 @@ import { |
| 18 | 17 | } from "@t3tools/shared/hostProcess"; |
| 19 | 18 | import * as Config from "effect/Config"; |
| 20 | 19 | import * as Console from "effect/Console"; |
| 20 | import * as Crypto from "effect/Crypto"; | |
| 21 | 21 | import * as Effect from "effect/Effect"; |
| 22 | 22 | import * as Option from "effect/Option"; |
| 23 | 23 | import * as Path from "effect/Path"; |
| ... | ... | @@ -212,9 +212,11 @@ const runAppCommand = Effect.fn("cli.app")(function* (flags: { |
| 212 | 212 | userId, |
| 213 | 213 | joinPath: path.join, |
| 214 | 214 | }).address; |
| 215 | const crypto = yield* Crypto.Crypto; | |
| 216 | const requestId = yield* crypto.randomUUIDv4.pipe(Effect.orDie); | |
| 215 | 217 | const request: DesktopAppActivationRequest = { |
| 216 | 218 | version: DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION, |
| 217 | requestId: NodeCrypto.randomUUID(), | |
| 219 | requestId, | |
| 218 | 220 | type: "open-workspace", |
| 219 | 221 | workspaceRoot, |
| 220 | 222 | platform: hostPlatform, |
apps/server/src/device/SshDeviceHost.ts+10-5| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | |
| 2 | 1 | import { |
| 3 | 2 | type DeviceHostSummary, |
| 4 | 3 | DevicePlatformAvailability, |
| ... | ... | @@ -9,6 +8,7 @@ import { |
| 9 | 8 | import { runSshCommand, baseSshArgs, resolveSshCommand } from "@t3tools/ssh/command"; |
| 10 | 9 | import * as NetService from "@t3tools/shared/Net"; |
| 11 | 10 | import { waitForHttpReady } from "@t3tools/shared/httpReadiness"; |
| 11 | import * as Crypto from "effect/Crypto"; | |
| 12 | 12 | import * as Exit from "effect/Exit"; |
| 13 | 13 | import * as Effect from "effect/Effect"; |
| 14 | 14 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -17,6 +17,7 @@ import * as Scope from "effect/Scope"; |
| 17 | 17 | import * as Semaphore from "effect/Semaphore"; |
| 18 | 18 | import * as Schema from "effect/Schema"; |
| 19 | 19 | import * as Stream from "effect/Stream"; |
| 20 | import * as Hex from "effect/encoding/Hex"; | |
| 20 | 21 | import * as HttpClient from "effect/http/HttpClient"; |
| 21 | 22 | import * as ChildProcess from "effect/process/ChildProcess"; |
| 22 | 23 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| ... | ... | @@ -79,10 +80,11 @@ const ownerFor = Effect.fn("SshDeviceHost.ownerFor")(function* (hostId: string) |
| 79 | 80 | const environmentId = yield* fs |
| 80 | 81 | .readFileString(server.environmentIdPath) |
| 81 | 82 | .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); | |
| 86 | 88 | }); |
| 87 | 89 | |
| 88 | 90 | export const probe = Effect.fn("SshDeviceHost.probe")(function* ( |
| ... | ... | @@ -125,6 +127,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* ( |
| 125 | 127 | const net = yield* NetService.NetService; |
| 126 | 128 | const http = yield* HttpClient.HttpClient; |
| 127 | 129 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 130 | const crypto = yield* Crypto.Crypto; | |
| 128 | 131 | const parentScope = yield* Scope.Scope; |
| 129 | 132 | const ssh = yield* resolveSshCommand; |
| 130 | 133 | const owner = yield* ownerFor(config.id); |
| ... | ... | @@ -135,6 +138,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* ( |
| 135 | 138 | | FileSystem.FileSystem |
| 136 | 139 | | Path.Path |
| 137 | 140 | | ChildProcessSpawner.ChildProcessSpawner |
| 141 | | Crypto.Crypto | |
| 138 | 142 | | ServerConfig.ServerConfig |
| 139 | 143 | >, |
| 140 | 144 | ) => |
| ... | ... | @@ -143,6 +147,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* ( |
| 143 | 147 | Effect.provideService(Path.Path, path), |
| 144 | 148 | Effect.provideService(ServerConfig.ServerConfig, server), |
| 145 | 149 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 150 | Effect.provideService(Crypto.Crypto, crypto), | |
| 146 | 151 | ); |
| 147 | 152 | const lock = yield* Semaphore.make(1); |
| 148 | 153 | let stopped = false; |
apps/server/src/mcp/McpHttpServer.ts+5-3| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | |
| 2 | 1 | import * as Cause from "effect/Cause"; |
| 3 | 2 | import * as Clock from "effect/Clock"; |
| 4 | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | 7 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -334,8 +334,10 @@ const saveScreenshot = Effect.fn("McpHttpServer.saveScreenshot")(function* ( |
| 334 | 334 | const fileSystem = yield* FileSystem.FileSystem; |
| 335 | 335 | const path = yield* Path.Path; |
| 336 | 336 | const millis = yield* Clock.currentTimeMillis; |
| 337 | const crypto = yield* Crypto.Crypto; | |
| 337 | 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 | 341 | const screenshotPath = path.join(config.browserArtifactsDir, fileName); |
| 340 | 342 | yield* fileSystem.makeDirectory(config.browserArtifactsDir, { recursive: true }).pipe( |
| 341 | 343 | Effect.andThen(fileSystem.writeFile(screenshotPath, data)), |
| ... | ... | @@ -387,7 +389,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot |
| 387 | 389 | const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; |
| 388 | 390 | // The MCP tool runner only supplies the client, so hand the save path its services here. |
| 389 | 391 | const saveServices = yield* Effect.context< |
| 390 | ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path | |
| 392 | ServerConfig.ServerConfig | FileSystem.FileSystem | Path.Path | Crypto.Crypto | |
| 391 | 393 | >(); |
| 392 | 394 | const built = yield* PreviewSnapshotToolkit; |
| 393 | 395 | const tool = PreviewSnapshotTool; |
apps/server/src/mcp/toolkits/previewControls/handlers.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | |
| 1 | 2 | import { expect, it } from "@effect/vitest"; |
| 2 | 3 | import { |
| 3 | 4 | DEFAULT_SERVER_SETTINGS, |
| ... | ... | @@ -45,7 +46,7 @@ it.effect.each([ |
| 45 | 46 | capabilities: new Set(effective.enableAgentBrowserAccess ? ["preview"] : []), |
| 46 | 47 | issuedAt: 0, |
| 47 | 48 | }; |
| 48 | const manager = yield* Preview.make; | |
| 49 | const manager = yield* Preview.make.pipe(Effect.provide(NodeCrypto.layer)); | |
| 49 | 50 | const tab = yield* manager.open({ threadId, url: "http://localhost:3000" }); |
| 50 | 51 | const layerDependencies = Layer.mergeAll( |
| 51 | 52 | Layer.succeed(Preview.PreviewManager, manager), |
apps/server/src/preview/Manager.test.ts+5-2| ... | ... | @@ -1,7 +1,10 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | |
| 1 | 2 | import { it } from "@effect/vitest"; |
| 2 | 3 | import { type PreviewEvent, ThreadId } from "@t3tools/contracts"; |
| 3 | 4 | import { PreviewUrlNormalizationError } from "@t3tools/shared/preview"; |
| 4 | import { Effect, PubSub } from "effect"; | |
| 5 | import * as Effect from "effect/Effect"; | |
| 6 | import * as Layer from "effect/Layer"; | |
| 7 | import * as PubSub from "effect/PubSub"; | |
| 5 | 8 | import { expect } from "vite-plus/test"; |
| 6 | 9 | |
| 7 | 10 | import * as PreviewManager from "./Manager.ts"; |
| ... | ... | @@ -35,7 +38,7 @@ const collectEvents = Effect.gen(function* () { |
| 35 | 38 | return collector; |
| 36 | 39 | }).pipe(Effect.withSpan("preview.test.collectEvents")); |
| 37 | 40 | |
| 38 | it.layer(PreviewManager.layer)("PreviewManager", (it) => { | |
| 41 | it.layer(PreviewManager.layer.pipe(Layer.provide(NodeCrypto.layer)))("PreviewManager", (it) => { | |
| 39 | 42 | it.effect("opens a session and emits opened with normalized URL", () => |
| 40 | 43 | Effect.gen(function* () { |
| 41 | 44 | const threadId = freshThreadId(); |
apps/server/src/preview/Manager.ts+3-2| ... | ... | @@ -31,8 +31,8 @@ import { |
| 31 | 31 | newPreviewTabId, |
| 32 | 32 | normalizePreviewUrl, |
| 33 | 33 | } from "@t3tools/shared/preview"; |
| 34 | import * as NodeCrypto from "node:crypto"; | |
| 35 | 34 | import * as Context from "effect/Context"; |
| 35 | import * as Crypto from "effect/Crypto"; | |
| 36 | 36 | import * as DateTime from "effect/DateTime"; |
| 37 | 37 | import * as Effect from "effect/Effect"; |
| 38 | 38 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -155,7 +155,8 @@ const buildIdleSnapshot = (input: { |
| 155 | 155 | |
| 156 | 156 | /** @public Service construction is part of the canonical Effect module API. */ |
| 157 | 157 | export 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 | 160 | const stateRef = yield* SynchronizedRef.make<ManagerState>(initialState); |
| 160 | 161 | // Unbounded PubSub is fine here — events are tiny and we don't want to |
| 161 | 162 | // block publishers if a subscriber is slow. WS clients backpressure on |
apps/server/src/project/ProjectSetupScriptRunner.test.ts+2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | |
| 1 | 2 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | 3 | import { ProjectId } from "@t3tools/contracts"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -64,6 +65,7 @@ it.effect("resolves setup scripts through the standalone project service", () => |
| 64 | 65 | }), |
| 65 | 66 | Layer.mock(TerminalManager.TerminalManager)({ open, write, subscribe }), |
| 66 | 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 | 5 | resolveProjectScripts, |
| 6 | 6 | setupProjectScript, |
| 7 | 7 | } from "@t3tools/shared/projectScripts"; |
| 8 | import * as NodeCrypto from "node:crypto"; | |
| 9 | 8 | |
| 10 | 9 | import * as Clock from "effect/Clock"; |
| 11 | 10 | import * as Context from "effect/Context"; |
| 11 | import * as Crypto from "effect/Crypto"; | |
| 12 | 12 | import * as Deferred from "effect/Deferred"; |
| 13 | 13 | import * as Effect from "effect/Effect"; |
| 14 | 14 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -201,6 +201,7 @@ export const make = Effect.gen(function* () { |
| 201 | 201 | const projects = yield* ProjectService.ProjectService; |
| 202 | 202 | const terminalManager = yield* TerminalManager.TerminalManager; |
| 203 | 203 | const serverSettings = yield* ServerSettings.ServerSettingsService; |
| 204 | const crypto = yield* Crypto.Crypto; | |
| 204 | 205 | const completionShell = resolveCompletionShell( |
| 205 | 206 | yield* HostProcessPlatform, |
| 206 | 207 | yield* HostProcessEnvironment, |
| ... | ... | @@ -367,7 +368,9 @@ export const make = Effect.gen(function* () { |
| 367 | 368 | COLORTERM: "", |
| 368 | 369 | }; |
| 369 | 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 | 374 | const commandLine = |
| 372 | 375 | observe && completionToken |
| 373 | 376 | ? wrapCommandForCompletion( |
apps/server/src/provider/CodexChatGptAuth.ts+6-1| ... | ... | @@ -12,6 +12,7 @@ import { codexCallbackUrl } from "@t3tools/shared/codexAuthHandoff"; |
| 12 | 12 | import * as Clock from "effect/Clock"; |
| 13 | 13 | import * as Cause from "effect/Cause"; |
| 14 | 14 | import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; |
| 15 | import * as Crypto from "effect/Crypto"; | |
| 15 | 16 | import * as Exit from "effect/Exit"; |
| 16 | 17 | import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts"; |
| 17 | 18 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -158,6 +159,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* |
| 158 | 159 | options.instanceId, |
| 159 | 160 | ); |
| 160 | 161 | const secrets = yield* ServerSecretStore.ServerSecretStore; |
| 162 | const crypto = yield* Crypto.Crypto; | |
| 161 | 163 | const environmentLocks = sessionLocks.get(secrets) ?? new Map<string, Semaphore.Semaphore>(); |
| 162 | 164 | sessionLocks.set(secrets, environmentLocks); |
| 163 | 165 | const lock = environmentLocks.get(store.binding.key) ?? (yield* Semaphore.make(1)); |
| ... | ... | @@ -306,7 +308,10 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* |
| 306 | 308 | savedConnectionCount: savedConnections.size, |
| 307 | 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 | 315 | const saveRegistration = Effect.fnUntraced(function* (profile: typeof Registration.Type) { |
| 311 | 316 | const saved = yield* readRegistrations; |
| 312 | 317 | profile = { |
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2| ... | ... | @@ -199,6 +199,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> |
| 199 | 199 | const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime; |
| 200 | 200 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 201 | 201 | const httpClient = yield* HttpClient.HttpClient; |
| 202 | const crypto = yield* Crypto.Crypto; | |
| 202 | 203 | const serverSettings = yield* ServerSettings.ServerSettingsService; |
| 203 | 204 | const processEnv = mergeProviderInstanceEnvironment(environment); |
| 204 | 205 | const continuationIdentity = defaultProviderContinuationIdentity({ |
| ... | ... | @@ -388,6 +389,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> |
| 388 | 389 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 389 | 390 | Effect.provideService(Path.Path, pathService), |
| 390 | 391 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 392 | Effect.provideService(Crypto.Crypto, crypto), | |
| 391 | 393 | Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner), |
| 392 | 394 | Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime), |
| 393 | 395 | ); |
apps/server/src/provider/ProviderCredentialStore.test.ts+7-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Option from "effect/Option"; |
| ... | ... | @@ -35,6 +36,11 @@ it.effect("isolates provider bindings and preserves opaque credentials", () => |
| 35 | 36 | assert.isTrue(Option.isNone(yield* b.get)); |
| 36 | 37 | assert.isTrue(Option.isNone(yield* c.get)); |
| 37 | 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 | 44 | const long = yield* ProviderCredentialStore.make("a".repeat(64), "b".repeat(64)).pipe( |
| 39 | 45 | Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore), |
| 40 | 46 | ); |
| ... | ... | @@ -45,5 +51,5 @@ it.effect("isolates provider bindings and preserves opaque credentials", () => |
| 45 | 51 | assert.notStrictEqual(delimiter.binding.key, a.binding.key); |
| 46 | 52 | yield* a.remove; |
| 47 | 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 | import * as NodeCrypto from "node:crypto"; | |
| 1 | import * as Crypto from "effect/Crypto"; | |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Hex from "effect/encoding/Hex"; | |
| 3 | 4 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 4 | 5 | |
| 5 | 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 | 9 | bindingId: string, |
| 9 | 10 | ) { |
| 10 | 11 | const secrets = yield* ServerSecretStore.ServerSecretStore; |
| 12 | const crypto = yield* Crypto.Crypto; | |
| 11 | 13 | // 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}`; | |
| 15 | 18 | return { |
| 16 | 19 | binding: { owner: "t3" as const, key }, |
| 17 | 20 | get: secrets.get(key), |
apps/server/src/provider/openCodeUsageLimits.ts+10-8| ... | ... | @@ -1,13 +1,14 @@ |
| 1 | 1 | import * as NodeOS from "node:os"; |
| 2 | import * as NodeCrypto from "node:crypto"; | |
| 3 | 2 | |
| 4 | 3 | import type { ServerProviderUsageWindow } from "@t3tools/contracts"; |
| 4 | import * as Crypto from "effect/Crypto"; | |
| 5 | 5 | import * as DateTime from "effect/DateTime"; |
| 6 | 6 | import * as Effect from "effect/Effect"; |
| 7 | 7 | import * as FileSystem from "effect/FileSystem"; |
| 8 | 8 | import * as Option from "effect/Option"; |
| 9 | 9 | import * as Path from "effect/Path"; |
| 10 | 10 | import * as Schema from "effect/Schema"; |
| 11 | import * as Hex from "effect/encoding/Hex"; | |
| 11 | 12 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; |
| 12 | 13 | |
| 13 | 14 | import { |
| ... | ... | @@ -96,15 +97,16 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( |
| 96 | 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 | 107 | return { |
| 100 | 108 | ...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, | |
| 108 | 110 | }; |
| 109 | 111 | }).pipe( |
| 110 | 112 | Effect.timeout("5 seconds"), |
apps/server/src/pullRequest/GitHubPullRequestCli.test.ts+3-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | |
| 1 | 2 | import { afterEach, assert, expect, it, vi } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Clock from "effect/Clock"; |
| ... | ... | @@ -65,6 +66,7 @@ const layer = it.layer( |
| 65 | 66 | }), |
| 66 | 67 | ), |
| 67 | 68 | Layer.provideMerge(GitHubGraphQlBudget.layer), |
| 69 | Layer.provide(NodeCrypto.layer), | |
| 68 | 70 | ), |
| 69 | 71 | ); |
| 70 | 72 | |
| ... | ... | @@ -247,7 +249,7 @@ it.effect( |
| 247 | 249 | ); |
| 248 | 250 | const cli = yield* GitHubPullRequestCli.make.pipe( |
| 249 | 251 | Effect.provideService(GitHubCli.GitHubCli, github), |
| 250 | Effect.provide(GitHubGraphQlBudget.layer), | |
| 252 | Effect.provide(Layer.merge(GitHubGraphQlBudget.layer, NodeCrypto.layer)), | |
| 251 | 253 | ); |
| 252 | 254 | const input = { cwd: "/repo", host: "github.com" }; |
| 253 | 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 | 4 | import * as Cause from "effect/Cause"; |
| 5 | 5 | import * as Context from "effect/Context"; |
| 6 | 6 | import * as Clock from "effect/Clock"; |
| 7 | import * as NodeCrypto from "node:crypto"; | |
| 7 | import * as Crypto from "effect/Crypto"; | |
| 8 | 8 | import * as Effect from "effect/Effect"; |
| 9 | 9 | import * as Layer from "effect/Layer"; |
| 10 | 10 | import * as Exit from "effect/Exit"; |
| ... | ... | @@ -15,6 +15,7 @@ import * as RequestResolver from "effect/RequestResolver"; |
| 15 | 15 | import * as Result from "effect/Result"; |
| 16 | 16 | import * as Schema from "effect/Schema"; |
| 17 | 17 | import * as Semaphore from "effect/Semaphore"; |
| 18 | import * as Hex from "effect/encoding/Hex"; | |
| 18 | 19 | import { |
| 19 | 20 | resolvePullRequestAuthorFilter, |
| 20 | 21 | PositiveInt, |
| ... | ... | @@ -1148,6 +1149,7 @@ function actionArgs( |
| 1148 | 1149 | export const make = Effect.gen(function* () { |
| 1149 | 1150 | const github = yield* GitHubCli.GitHubCli; |
| 1150 | 1151 | const graphQlBudget = yield* GitHubGraphQlBudget.GitHubGraphQlBudget; |
| 1152 | const crypto = yield* Crypto.Crypto; | |
| 1151 | 1153 | const revalidateChecks = yield* makeChecksRevalidator; |
| 1152 | 1154 | const routingIdentities = new Map< |
| 1153 | 1155 | string, |
| ... | ... | @@ -1184,7 +1186,10 @@ export const make = Effect.gen(function* () { |
| 1184 | 1186 | }) |
| 1185 | 1187 | .pipe(Effect.mapError(unavailable))).stdout.trim(); |
| 1186 | 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 | 1193 | const credential = { host, token: Redacted.make(token), credentialFingerprint: key }; |
| 1189 | 1194 | // A cold page may ask several times. Wait per credential and check again after the |
| 1190 | 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 | import * as NodeCrypto from "node:crypto"; | |
| 2 | 1 | import * as NodeBuffer from "node:buffer"; |
| 3 | 2 | |
| 4 | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | |
| 5 | 5 | import * as DateTime from "effect/DateTime"; |
| 6 | 6 | import * as Effect from "effect/Effect"; |
| 7 | 7 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -536,6 +536,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 536 | 536 | const fileSystem = yield* FileSystem.FileSystem; |
| 537 | 537 | const path = yield* Path.Path; |
| 538 | 538 | const vcsProcess = yield* VcsProcess.VcsProcess; |
| 539 | const crypto = yield* Crypto.Crypto; | |
| 539 | 540 | const capabilities = { |
| 540 | 541 | kind: "git" as const, |
| 541 | 542 | supportsWorktrees: true, |
| ... | ... | @@ -811,10 +812,8 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 811 | 812 | "sparse.expectFilesOutsideOfPatterns=false", |
| 812 | 813 | ]; |
| 813 | 814 | 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}`); | |
| 818 | 817 | const commitEnv: NodeJS.ProcessEnv = { |
| 819 | 818 | ...process.env, |
| 820 | 819 | GIT_INDEX_FILE: tempIndexPath, |
apps/server/src/ws.ts+1-2| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import { OrchestrationDispatchCommandError } from "@t3tools/contracts"; |
| 2 | 2 | import * as Crypto from "effect/Crypto"; |
| 3 | 3 | import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; |
| 4 | import * as NodeCrypto from "node:crypto"; | |
| 5 | 4 | |
| 6 | 5 | import * as DateTime from "effect/DateTime"; |
| 7 | 6 | import * as Duration from "effect/Duration"; |
| ... | ... | @@ -1469,7 +1468,7 @@ const layerWsRpc = ( |
| 1469 | 1468 | provider?.models.find((candidate) => candidate.isDefault)?.slug ?? |
| 1470 | 1469 | provider?.models[0]?.slug ?? |
| 1471 | 1470 | "default"; |
| 1472 | const commandId = CommandId.make(NodeCrypto.randomUUID()); | |
| 1471 | const commandId = CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)); | |
| 1473 | 1472 | const launched = yield* Effect.result( |
| 1474 | 1473 | startup.enqueueCommand( |
| 1475 | 1474 | threadLaunch.launch({ |