| 1 | import { SharedAccountError, SharedAccountId, type SharedAccountAccess } from "@t3tools/contracts"; |
| 2 | import * as Context from "effect/Context"; |
| 3 | import * as Crypto from "effect/Crypto"; |
| 4 | import * as DateTime from "effect/DateTime"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Redacted from "effect/Redacted"; |
| 7 | import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Path from "effect/Path"; |
| 10 | import * as Schema from "effect/Schema"; |
| 11 | import * as KeyedLock from "@t3tools/shared/KeyedLock"; |
| 12 | import * as ServerConfig from "../config.ts"; |
| 13 | import * as SharedAccountClient from "./SharedAccountClient.ts"; |
| 14 | |
| 15 | const decodeId = Schema.decodeEffect(SharedAccountId); |
| 16 | const encodeAuth = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
| 17 | const isSharedAccountError = Schema.is(SharedAccountError); |
| 18 | |
| 19 | const CLAUDE_SHARED_DIRECTORIES = [ |
| 20 | "projects", |
| 21 | "tasks", |
| 22 | "todos", |
| 23 | "plans", |
| 24 | "skills", |
| 25 | "plugins", |
| 26 | "cache", |
| 27 | "shell-snapshots", |
| 28 | "session-env", |
| 29 | ]; |
| 30 | |
| 31 | export function sharedProviderHomePaths( |
| 32 | path: Path.Path, |
| 33 | stateDir: string, |
| 34 | driver: SharedAccountAccess["driver"], |
| 35 | id: string, |
| 36 | ) { |
| 37 | const root = path.join(stateDir, "providers", driver); |
| 38 | return { shared: path.join(root, "shared"), account: path.join(root, "accounts", id) }; |
| 39 | } |
| 40 | |
| 41 | export class ManagedProviderHome extends Context.Service< |
| 42 | ManagedProviderHome, |
| 43 | { |
| 44 | readonly paths: ( |
| 45 | driver: SharedAccountAccess["driver"], |
| 46 | id: string, |
| 47 | ) => Effect.Effect< |
| 48 | { |
| 49 | readonly shared: string; |
| 50 | readonly account: string; |
| 51 | }, |
| 52 | SharedAccountError |
| 53 | >; |
| 54 | readonly prepare: ( |
| 55 | driver: SharedAccountAccess["driver"], |
| 56 | id: string, |
| 57 | ) => Effect.Effect<SharedAccountAccess, SharedAccountError>; |
| 58 | } |
| 59 | >()("t3/provider/ManagedProviderHome") {} |
| 60 | |
| 61 | const make = Effect.gen(function* () { |
| 62 | const fs = yield* FileSystem.FileSystem; |
| 63 | const path = yield* Path.Path; |
| 64 | const crypto = yield* Crypto.Crypto; |
| 65 | const { stateDir } = yield* ServerConfig.ServerConfig; |
| 66 | const accounts = yield* SharedAccountClient.SharedAccountClient; |
| 67 | const locks = yield* KeyedLock.make<string>(); |
| 68 | |
| 69 | const paths = Effect.fn("ManagedProviderHome.paths")(function* ( |
| 70 | driver: SharedAccountAccess["driver"], |
| 71 | inputId: string, |
| 72 | ) { |
| 73 | const id = yield* decodeId(inputId).pipe( |
| 74 | Effect.mapError( |
| 75 | (cause) => new SharedAccountError({ reason: "missing", cause: Redacted.make(cause) }), |
| 76 | ), |
| 77 | ); |
| 78 | return sharedProviderHomePaths(path, stateDir, driver, id); |
| 79 | }); |
| 80 | |
| 81 | const prepare = Effect.fn("ManagedProviderHome.prepare")(function* ( |
| 82 | driver: SharedAccountAccess["driver"], |
| 83 | id: string, |
| 84 | ) { |
| 85 | const credentials = yield* accounts.access(id); |
| 86 | if (credentials.driver !== driver) return yield* new SharedAccountError({ reason: "identity" }); |
| 87 | const home = yield* paths(driver, id); |
| 88 | |
| 89 | yield* Effect.gen(function* () { |
| 90 | yield* fs.makeDirectory(home.shared, { recursive: true }); |
| 91 | yield* fs.makeDirectory(home.account, { recursive: true }); |
| 92 | yield* fs.chmod(home.account, 0o700); |
| 93 | if (driver === "claudeAgent") { |
| 94 | for (const entry of CLAUDE_SHARED_DIRECTORIES) { |
| 95 | const target = path.join(home.shared, entry); |
| 96 | const link = path.join(home.account, entry); |
| 97 | yield* fs.makeDirectory(target, { recursive: true }); |
| 98 | if (!(yield* fs.exists(link))) yield* fs.symlink(target, link); |
| 99 | else { |
| 100 | const existing = yield* fs.readLink(link); |
| 101 | if (path.resolve(path.dirname(link), existing) !== target) |
| 102 | return yield* new SharedAccountError({ reason: "storage" }); |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | if (credentials.driver === "codex") { |
| 107 | const json = yield* encodeAuth({ |
| 108 | auth_mode: "chatgptAuthTokens", |
| 109 | tokens: { |
| 110 | access_token: credentials.accessToken, |
| 111 | id_token: credentials.idToken, |
| 112 | account_id: credentials.accountId, |
| 113 | refresh_token: "", |
| 114 | }, |
| 115 | last_refresh: DateTime.formatIso(yield* DateTime.now), |
| 116 | }); |
| 117 | const temporary = path.join(home.account, `${yield* crypto.randomUUIDv4()}.tmp`); |
| 118 | yield* fs.writeFileString(temporary, json, { mode: 0o600 }); |
| 119 | yield* fs.chmod(temporary, 0o600); |
| 120 | yield* fs.rename(temporary, path.join(home.account, "auth.json")); |
| 121 | } |
| 122 | }).pipe( |
| 123 | Effect.mapError((cause) => |
| 124 | isSharedAccountError(cause) |
| 125 | ? cause |
| 126 | : new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 127 | ), |
| 128 | ); |
| 129 | |
| 130 | return credentials; |
| 131 | }); |
| 132 | |
| 133 | return ManagedProviderHome.of({ |
| 134 | paths, |
| 135 | prepare: (driver, id) => locks.withLock(`${driver}:${id}`, prepare(driver, id)), |
| 136 | }); |
| 137 | }); |
| 138 | |
| 139 | export const layer = Layer.effect(ManagedProviderHome, make); |