| 1 | import * as Crypto from "effect/Crypto"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Hex from "effect/encoding/Hex"; |
| 4 | import * as Schema from "effect/Schema"; |
| 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; |
| 7 | |
| 8 | import type { AgentDeviceEndpoint } from "./DeviceHost.ts"; |
| 9 | |
| 10 | const encodeEndpoint = Schema.encodeEffect( |
| 11 | Schema.fromJsonString( |
| 12 | Schema.Struct({ daemonBaseUrl: Schema.String, daemonAuthToken: Schema.String }), |
| 13 | ), |
| 14 | ); |
| 15 | |
| 16 | const key = Effect.fn("AgentDeviceTarget.key")(function* (value: string) { |
| 17 | const crypto = yield* Crypto.Crypto; |
| 18 | const digest = yield* crypto |
| 19 | .digest("SHA-256", new TextEncoder().encode(value)) |
| 20 | .pipe(Effect.orDie); |
| 21 | return Hex.encode(digest).slice(0, 24); |
| 22 | }); |
| 23 | |
| 24 | /** A stable file per host lets forwarded endpoints change without retargeting other commands. */ |
| 25 | export const agentDeviceConfigPath = (stateDir: string, hostId: string, path: Path.Path) => |
| 26 | key(hostId).pipe(Effect.map((hash) => path.join(stateDir, "device", "hosts", `${hash}.json`))); |
| 27 | |
| 28 | export const agentDeviceSession = (threadId: string, hostId: string, deviceId: string) => |
| 29 | key(JSON.stringify([threadId, hostId, deviceId])).pipe(Effect.map((hash) => `t3-${hash}`)); |
| 30 | |
| 31 | export const writeAgentDeviceConfig = Effect.fn("AgentDeviceTarget.writeConfig")(function* ( |
| 32 | file: string, |
| 33 | endpoint: AgentDeviceEndpoint, |
| 34 | ) { |
| 35 | const fs = yield* FileSystem.FileSystem; |
| 36 | const path = yield* Path.Path; |
| 37 | yield* fs.makeDirectory(path.dirname(file), { recursive: true }); |
| 38 | const content = yield* encodeEndpoint({ |
| 39 | daemonBaseUrl: endpoint.baseUrl, |
| 40 | daemonAuthToken: endpoint.token, |
| 41 | }); |
| 42 | if ((yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => ""))) === content) return; |
| 43 | const temporary = yield* fs.makeTempFile({ directory: path.dirname(file), prefix: ".endpoint-" }); |
| 44 | yield* Effect.gen(function* () { |
| 45 | yield* fs.chmod(temporary, 0o600); |
| 46 | yield* fs.writeFileString(temporary, content); |
| 47 | yield* fs.rename(temporary, file); |
| 48 | }).pipe(Effect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.ignore))); |
| 49 | }); |