| 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,4 +1,5 @@ |
| 1 | import * as NodeVM from "node:vm"; | 1 | import * as NodeVM from "node:vm"; |
| 2 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 2 | import { it as effectIt } from "@effect/vitest"; | 3 | import { it as effectIt } from "@effect/vitest"; |
| 3 | import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts"; | 4 | import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts"; |
| 4 | import type { | 5 | import 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 | ); |
| 284 | const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError); | 286 | const 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 state | 5 | * 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 | */ |
| 8 | import * as NodeCrypto from "node:crypto"; | ||
| 9 | import { | 8 | import { |
| 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 { |
| 49 | import * as Cause from "effect/Cause"; | 48 | import * as Cause from "effect/Cause"; |
| 50 | import * as Clock from "effect/Clock"; | 49 | import * as Clock from "effect/Clock"; |
| 51 | import * as Context from "effect/Context"; | 50 | import * as Context from "effect/Context"; |
| 51 | import * as Crypto from "effect/Crypto"; | ||
| 52 | import * as DateTime from "effect/DateTime"; | 52 | import * as DateTime from "effect/DateTime"; |
| 53 | import * as Deferred from "effect/Deferred"; | 53 | import * as Deferred from "effect/Deferred"; |
| 54 | import * as Effect from "effect/Effect"; | 54 | import * 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 target | 4452 | // 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( |
| 50 | const layerServices = GitHubPullRequestCli.layer.pipe( | 50 | const 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 | ); |
| 54 | 55 | ||
| 55 | const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown)); | 56 | const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown)); |
apps/server/src/assets/AttachmentUpload.ts+4-3| ... | @@ -1,11 +1,10 @@ | ... | @@ -1,11 +1,10 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | |||
| 3 | import { | 1 | import { |
| 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"; |
| 8 | import * as Clock from "effect/Clock"; | 6 | import * as Clock from "effect/Clock"; |
| 7 | import * as Crypto from "effect/Crypto"; | ||
| 9 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 10 | import * as FileSystem from "effect/FileSystem"; | 9 | import * as FileSystem from "effect/FileSystem"; |
| 11 | import * as Option from "effect/Option"; | 10 | import * 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 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import type { ToolActivityNativeAppReference } from "@t3tools/contracts"; | 1 | import type { ToolActivityNativeAppReference } from "@t3tools/contracts"; |
| 3 | import * as Cache from "effect/Cache"; | 2 | import * as Cache from "effect/Cache"; |
| 4 | import * as Cause from "effect/Cause"; | 3 | import * as Cause from "effect/Cause"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Clock from "effect/Clock"; | 6 | import * as Clock from "effect/Clock"; |
| 7 | import * as Context from "effect/Context"; | 7 | import * 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"; |
| 13 | import * as Path from "effect/Path"; | 13 | import * as Path from "effect/Path"; |
| 14 | import * as PlatformError from "effect/PlatformError"; | 14 | import * as PlatformError from "effect/PlatformError"; |
| 15 | import * as Semaphore from "effect/Semaphore"; | 15 | import * as Semaphore from "effect/Semaphore"; |
| 16 | import * as Hex from "effect/encoding/Hex"; | ||
| 16 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; | 17 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 17 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 18 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 18 | 19 | ||
| ... | @@ -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. |
| 2 | import * as NodeCrypto from "node:crypto"; | ||
| 3 | import * as NodeNet from "node:net"; | 2 | import * as NodeNet from "node:net"; |
| 4 | import * as NodeOS from "node:os"; | 3 | import * as NodeOS from "node:os"; |
| 5 | 4 | ||
| ... | @@ -18,6 +17,7 @@ import { | ... | @@ -18,6 +17,7 @@ import { |
| 18 | } from "@t3tools/shared/hostProcess"; | 17 | } from "@t3tools/shared/hostProcess"; |
| 19 | import * as Config from "effect/Config"; | 18 | import * as Config from "effect/Config"; |
| 20 | import * as Console from "effect/Console"; | 19 | import * as Console from "effect/Console"; |
| 20 | import * as Crypto from "effect/Crypto"; | ||
| 21 | import * as Effect from "effect/Effect"; | 21 | import * as Effect from "effect/Effect"; |
| 22 | import * as Option from "effect/Option"; | 22 | import * as Option from "effect/Option"; |
| 23 | import * as Path from "effect/Path"; | 23 | import * 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 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import { | 1 | import { |
| 3 | type DeviceHostSummary, | 2 | type DeviceHostSummary, |
| 4 | DevicePlatformAvailability, | 3 | DevicePlatformAvailability, |
| ... | @@ -9,6 +8,7 @@ import { | ... | @@ -9,6 +8,7 @@ import { |
| 9 | import { runSshCommand, baseSshArgs, resolveSshCommand } from "@t3tools/ssh/command"; | 8 | import { runSshCommand, baseSshArgs, resolveSshCommand } from "@t3tools/ssh/command"; |
| 10 | import * as NetService from "@t3tools/shared/Net"; | 9 | import * as NetService from "@t3tools/shared/Net"; |
| 11 | import { waitForHttpReady } from "@t3tools/shared/httpReadiness"; | 10 | import { waitForHttpReady } from "@t3tools/shared/httpReadiness"; |
| 11 | import * as Crypto from "effect/Crypto"; | ||
| 12 | import * as Exit from "effect/Exit"; | 12 | import * as Exit from "effect/Exit"; |
| 13 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as FileSystem from "effect/FileSystem"; | 14 | import * 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"; |
| 17 | import * as Semaphore from "effect/Semaphore"; | 17 | import * as Semaphore from "effect/Semaphore"; |
| 18 | import * as Schema from "effect/Schema"; | 18 | import * as Schema from "effect/Schema"; |
| 19 | import * as Stream from "effect/Stream"; | 19 | import * as Stream from "effect/Stream"; |
| 20 | import * as Hex from "effect/encoding/Hex"; | ||
| 20 | import * as HttpClient from "effect/http/HttpClient"; | 21 | import * as HttpClient from "effect/http/HttpClient"; |
| 21 | import * as ChildProcess from "effect/process/ChildProcess"; | 22 | import * as ChildProcess from "effect/process/ChildProcess"; |
| 22 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; | 23 | import * 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* fs | 80 | 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 | }); |
| 87 | 89 | ||
| 88 | export const probe = Effect.fn("SshDeviceHost.probe")(function* ( | 90 | export 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.FileSystem | 138 | | FileSystem.FileSystem |
| 136 | | Path.Path | 139 | | Path.Path |
| 137 | | ChildProcessSpawner.ChildProcessSpawner | 140 | | ChildProcessSpawner.ChildProcessSpawner |
| 141 | | Crypto.Crypto | ||
| 138 | | ServerConfig.ServerConfig | 142 | | 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 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import * as Cause from "effect/Cause"; | 1 | import * as Cause from "effect/Cause"; |
| 3 | import * as Clock from "effect/Clock"; | 2 | import * as Clock from "effect/Clock"; |
| 4 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Layer from "effect/Layer"; | 7 | import * 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.Path | 392 | 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 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { expect, it } from "@effect/vitest"; | 2 | import { expect, it } from "@effect/vitest"; |
| 2 | import { | 3 | import { |
| 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 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { it } from "@effect/vitest"; | 2 | import { it } from "@effect/vitest"; |
| 2 | import { type PreviewEvent, ThreadId } from "@t3tools/contracts"; | 3 | import { type PreviewEvent, ThreadId } from "@t3tools/contracts"; |
| 3 | import { PreviewUrlNormalizationError } from "@t3tools/shared/preview"; | 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 | import { expect } from "vite-plus/test"; | 8 | import { expect } from "vite-plus/test"; |
| 6 | 9 | ||
| 7 | import * as PreviewManager from "./Manager.ts"; | 10 | import * 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")); |
| 37 | 40 | ||
| 38 | it.layer(PreviewManager.layer)("PreviewManager", (it) => { | 41 | it.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"; |
| 34 | import * as NodeCrypto from "node:crypto"; | ||
| 35 | import * as Context from "effect/Context"; | 34 | import * as Context from "effect/Context"; |
| 35 | import * as Crypto from "effect/Crypto"; | ||
| 36 | import * as DateTime from "effect/DateTime"; | 36 | import * as DateTime from "effect/DateTime"; |
| 37 | import * as Effect from "effect/Effect"; | 37 | import * as Effect from "effect/Effect"; |
| 38 | import * as Layer from "effect/Layer"; | 38 | import * as Layer from "effect/Layer"; |
| ... | @@ -155,7 +155,8 @@ const buildIdleSnapshot = (input: { | ... | @@ -155,7 +155,8 @@ const buildIdleSnapshot = (input: { |
| 155 | 155 | ||
| 156 | /** @public Service construction is part of the canonical Effect module API. */ | 156 | /** @public Service construction is part of the canonical Effect module API. */ |
| 157 | export const make = Effect.gen(function* PreviewManagerMake() { | 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 | 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 to | 161 | // 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 on | 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,3 +1,4 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { assert, it, vi } from "@effect/vitest"; | 2 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | import { ProjectId } from "@t3tools/contracts"; | 3 | import { ProjectId } from "@t3tools/contracts"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * 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"; |
| 8 | import * as NodeCrypto from "node:crypto"; | ||
| 9 | 8 | ||
| 10 | import * as Clock from "effect/Clock"; | 9 | import * as Clock from "effect/Clock"; |
| 11 | import * as Context from "effect/Context"; | 10 | import * as Context from "effect/Context"; |
| 11 | import * as Crypto from "effect/Crypto"; | ||
| 12 | import * as Deferred from "effect/Deferred"; | 12 | import * as Deferred from "effect/Deferred"; |
| 13 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Layer from "effect/Layer"; | 14 | import * 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 && completionToken | 375 | 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"; |
| 12 | import * as Clock from "effect/Clock"; | 12 | import * as Clock from "effect/Clock"; |
| 13 | import * as Cause from "effect/Cause"; | 13 | import * as Cause from "effect/Cause"; |
| 14 | import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; | 14 | import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; |
| 15 | import * as Crypto from "effect/Crypto"; | ||
| 15 | import * as Exit from "effect/Exit"; | 16 | import * as Exit from "effect/Exit"; |
| 16 | import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts"; | 17 | import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts"; |
| 17 | import * as Effect from "effect/Effect"; | 18 | import * 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 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | 4 | import * 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 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | 1 | import * as Crypto from "effect/Crypto"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Hex from "effect/encoding/Hex"; | ||
| 3 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 4 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 4 | 5 | ||
| 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 @@ |
| 1 | import * as NodeOS from "node:os"; | 1 | import * as NodeOS from "node:os"; |
| 2 | import * as NodeCrypto from "node:crypto"; | ||
| 3 | 2 | ||
| 4 | import type { ServerProviderUsageWindow } from "@t3tools/contracts"; | 3 | import type { ServerProviderUsageWindow } from "@t3tools/contracts"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import * as DateTime from "effect/DateTime"; | 5 | import * as DateTime from "effect/DateTime"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as FileSystem from "effect/FileSystem"; | 7 | import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as Option from "effect/Option"; | 8 | import * as Option from "effect/Option"; |
| 9 | import * as Path from "effect/Path"; | 9 | import * as Path from "effect/Path"; |
| 10 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| 11 | import * as Hex from "effect/encoding/Hex"; | ||
| 11 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; | 12 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; |
| 12 | 13 | ||
| 13 | import { | 14 | import { |
| ... | @@ -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 across | 109 | 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 @@ |
| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; | ||
| 1 | import { afterEach, assert, expect, it, vi } from "@effect/vitest"; | 2 | import { afterEach, assert, expect, it, vi } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Clock from "effect/Clock"; | 4 | import * 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 | ); |
| 70 | 72 | ||
| ... | @@ -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 |
| 4 | import * as Cause from "effect/Cause"; | 4 | import * as Cause from "effect/Cause"; |
| 5 | import * as Context from "effect/Context"; | 5 | import * as Context from "effect/Context"; |
| 6 | import * as Clock from "effect/Clock"; | 6 | import * as Clock from "effect/Clock"; |
| 7 | import * as NodeCrypto from "node:crypto"; | 7 | import * as Crypto from "effect/Crypto"; |
| 8 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Layer from "effect/Layer"; | 9 | import * as Layer from "effect/Layer"; |
| 10 | import * as Exit from "effect/Exit"; | 10 | import * 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"; |
| 15 | import * as Result from "effect/Result"; | 15 | import * as Result from "effect/Result"; |
| 16 | import * as Schema from "effect/Schema"; | 16 | import * as Schema from "effect/Schema"; |
| 17 | import * as Semaphore from "effect/Semaphore"; | 17 | import * as Semaphore from "effect/Semaphore"; |
| 18 | import * as Hex from "effect/encoding/Hex"; | ||
| 18 | import { | 19 | import { |
| 19 | resolvePullRequestAuthorFilter, | 20 | resolvePullRequestAuthorFilter, |
| 20 | PositiveInt, | 21 | PositiveInt, |
| ... | @@ -1148,6 +1149,7 @@ function actionArgs( | ... | @@ -1148,6 +1149,7 @@ function actionArgs( |
| 1148 | export const make = Effect.gen(function* () { | 1149 | export 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 the | 1194 | // 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 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import * as NodeBuffer from "node:buffer"; | 1 | import * as NodeBuffer from "node:buffer"; |
| 3 | 2 | ||
| 4 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import * as DateTime from "effect/DateTime"; | 5 | import * as DateTime from "effect/DateTime"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as FileSystem from "effect/FileSystem"; | 7 | import * 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 @@ |
| 1 | import { OrchestrationDispatchCommandError } from "@t3tools/contracts"; | 1 | import { OrchestrationDispatchCommandError } from "@t3tools/contracts"; |
| 2 | import * as Crypto from "effect/Crypto"; | 2 | import * as Crypto from "effect/Crypto"; |
| 3 | import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; | 3 | import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; |
| 4 | import * as NodeCrypto from "node:crypto"; | ||
| 5 | 4 | ||
| 6 | import * as DateTime from "effect/DateTime"; | 5 | import * as DateTime from "effect/DateTime"; |
| 7 | import * as Duration from "effect/Duration"; | 6 | import * 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({ |