| 1 | import { |
| 2 | ClaudeSettings, |
| 3 | CodexSettings, |
| 4 | ProviderDriverKind, |
| 5 | ProviderInstanceId, |
| 6 | SharedAccountError, |
| 7 | SharedAccountId, |
| 8 | type SharedProfilesRequest, |
| 9 | type SharedProfilesStatus, |
| 10 | } from "@t3tools/contracts"; |
| 11 | import { decodeJwt } from "jose"; |
| 12 | import * as Context from "effect/Context"; |
| 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Redacted from "effect/Redacted"; |
| 15 | import * as FileSystem from "effect/FileSystem"; |
| 16 | import * as Layer from "effect/Layer"; |
| 17 | import * as Path from "effect/Path"; |
| 18 | import * as Option from "effect/Option"; |
| 19 | import * as Schema from "effect/Schema"; |
| 20 | import * as Semaphore from "effect/Semaphore"; |
| 21 | import * as ServerSettings from "../serverSettings.ts"; |
| 22 | import { resolveCodexHomeLayout } from "./Drivers/CodexHomeLayout.ts"; |
| 23 | import * as SharedAccountClient from "./SharedAccountClient.ts"; |
| 24 | |
| 25 | const SharedConfig = Schema.Struct({ |
| 26 | sharedAccountId: SharedAccountId, |
| 27 | sharedAccountRevision: Schema.optionalKey(Schema.Int), |
| 28 | }); |
| 29 | const CodexAuthFile = Schema.fromJsonString( |
| 30 | Schema.Struct({ |
| 31 | tokens: Schema.Struct({ |
| 32 | access_token: Schema.NonEmptyString, |
| 33 | refresh_token: Schema.NonEmptyString, |
| 34 | id_token: Schema.NonEmptyString, |
| 35 | account_id: Schema.NonEmptyString, |
| 36 | }), |
| 37 | }), |
| 38 | ); |
| 39 | |
| 40 | const isSharedConfig = Schema.is(SharedConfig); |
| 41 | const decodeSharedConfig = Schema.decodeUnknownOption(SharedConfig); |
| 42 | const decodeCodexSettings = Schema.decodeUnknownEffect(CodexSettings); |
| 43 | const decodeClaudeSettings = Schema.decodeUnknownEffect(ClaudeSettings); |
| 44 | const decodeCodexAuth = Schema.decodeEffect(CodexAuthFile); |
| 45 | const isSharedAccountError = Schema.is(SharedAccountError); |
| 46 | const readConfig = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown)); |
| 47 | |
| 48 | export class SharedProfiles extends Context.Service< |
| 49 | SharedProfiles, |
| 50 | { |
| 51 | readonly execute: ( |
| 52 | request: SharedProfilesRequest, |
| 53 | ) => Effect.Effect<SharedProfilesStatus, SharedAccountError>; |
| 54 | } |
| 55 | >()("t3/provider/SharedProfiles") {} |
| 56 | |
| 57 | const make = Effect.gen(function* () { |
| 58 | const accounts = yield* SharedAccountClient.SharedAccountClient; |
| 59 | const settings = yield* ServerSettings.ServerSettingsService; |
| 60 | const fs = yield* FileSystem.FileSystem; |
| 61 | const path = yield* Path.Path; |
| 62 | const lock = yield* Semaphore.make(1); |
| 63 | |
| 64 | const sync = (enable = false) => |
| 65 | Effect.gen(function* () { |
| 66 | const response = yield* accounts.execute({ type: "list" }); |
| 67 | if (response.type !== "list") return yield* new SharedAccountError({ reason: "connection" }); |
| 68 | const current = yield* settings.getSettings.pipe( |
| 69 | Effect.mapError( |
| 70 | (cause) => new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 71 | ), |
| 72 | ); |
| 73 | for (const account of response.accounts) { |
| 74 | const instanceId = ProviderInstanceId.make(`shared-${account.id}`); |
| 75 | const previous = current.providerInstances[instanceId]; |
| 76 | const config = |
| 77 | account.driver === "codex" |
| 78 | ? { |
| 79 | sharedAccountId: account.id, |
| 80 | setupMode: "existing", |
| 81 | } |
| 82 | : { sharedAccountId: account.id, sharedAccountRevision: account.revision }; |
| 83 | const previousShared = decodeSharedConfig(previous?.config); |
| 84 | if ( |
| 85 | previous?.displayName === account.label && |
| 86 | previousShared._tag === "Some" && |
| 87 | (!enable || previous.enabled !== false) && |
| 88 | (account.driver !== "claudeAgent" || |
| 89 | previousShared.value.sharedAccountRevision === account.revision) |
| 90 | ) |
| 91 | continue; |
| 92 | const previousConfig = Option.getOrElse(readConfig(previous?.config), () => ({})); |
| 93 | yield* settings |
| 94 | .updateProviderInstance({ |
| 95 | operation: "upsert", |
| 96 | instanceId, |
| 97 | instance: { |
| 98 | ...previous, |
| 99 | ...(enable ? { enabled: true } : {}), |
| 100 | driver: ProviderDriverKind.make(account.driver), |
| 101 | displayName: account.label, |
| 102 | config: { |
| 103 | ...previousConfig, |
| 104 | ...config, |
| 105 | }, |
| 106 | }, |
| 107 | }) |
| 108 | .pipe( |
| 109 | Effect.mapError( |
| 110 | (cause) => new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 111 | ), |
| 112 | ); |
| 113 | } |
| 114 | const ids = new Set(response.accounts.map((account) => account.id)); |
| 115 | for (const [key, instance] of Object.entries(current.providerInstances)) { |
| 116 | const shared = decodeSharedConfig(instance.config); |
| 117 | if ( |
| 118 | shared._tag === "Some" && |
| 119 | !ids.has(shared.value.sharedAccountId) && |
| 120 | instance.enabled !== false |
| 121 | ) { |
| 122 | yield* settings |
| 123 | .updateProviderInstance({ |
| 124 | operation: "upsert", |
| 125 | instanceId: ProviderInstanceId.make(key), |
| 126 | instance: { ...instance, enabled: false }, |
| 127 | }) |
| 128 | .pipe( |
| 129 | Effect.mapError( |
| 130 | (cause) => |
| 131 | new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 132 | ), |
| 133 | ); |
| 134 | } |
| 135 | } |
| 136 | return { connected: yield* accounts.configured, accounts: response.accounts }; |
| 137 | }); |
| 138 | |
| 139 | const execute = Effect.fn("SharedProfiles.execute")(function* (request: SharedProfilesRequest) { |
| 140 | return yield* lock.withPermit( |
| 141 | Effect.gen(function* () { |
| 142 | if (request.type === "disconnect") { |
| 143 | const current = yield* settings.getSettings.pipe( |
| 144 | Effect.mapError( |
| 145 | (cause) => new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 146 | ), |
| 147 | ); |
| 148 | for (const [id, instance] of Object.entries(current.providerInstances)) { |
| 149 | if (!isSharedConfig(instance.config) || instance.enabled === false) continue; |
| 150 | yield* settings |
| 151 | .updateProviderInstance({ |
| 152 | operation: "upsert", |
| 153 | instanceId: ProviderInstanceId.make(id), |
| 154 | instance: { ...instance, enabled: false }, |
| 155 | }) |
| 156 | .pipe( |
| 157 | Effect.mapError( |
| 158 | (cause) => |
| 159 | new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 160 | ), |
| 161 | ); |
| 162 | } |
| 163 | yield* accounts.disconnect; |
| 164 | return { connected: false, accounts: [] }; |
| 165 | } |
| 166 | if (request.type === "connect") { |
| 167 | const token = yield* fs.readFileString(request.tokenFile).pipe( |
| 168 | Effect.map((value) => value.trim()), |
| 169 | Effect.mapError( |
| 170 | (cause) => new SharedAccountError({ reason: "setup", cause: Redacted.make(cause) }), |
| 171 | ), |
| 172 | ); |
| 173 | yield* accounts.configure({ url: request.url, token }); |
| 174 | } |
| 175 | if (request.type === "remove") yield* accounts.execute(request); |
| 176 | if (request.type === "import") { |
| 177 | const current = yield* settings.getSettings.pipe( |
| 178 | Effect.mapError( |
| 179 | (cause) => new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }), |
| 180 | ), |
| 181 | ); |
| 182 | const source = current.providerInstances[request.instanceId]; |
| 183 | if (!source || isSharedConfig(source.config)) |
| 184 | return yield* new SharedAccountError({ reason: "setup" }); |
| 185 | const credentials = yield* Effect.gen(function* () { |
| 186 | if (source.driver === "claudeAgent") { |
| 187 | if (!request.tokenFile) return yield* new SharedAccountError({ reason: "setup" }); |
| 188 | yield* decodeClaudeSettings(source.config ?? {}); |
| 189 | const accessToken = (yield* fs.readFileString(request.tokenFile)).trim(); |
| 190 | if (!accessToken.startsWith("sk-ant-oat")) |
| 191 | return yield* new SharedAccountError({ reason: "setup" }); |
| 192 | return { driver: "claudeAgent" as const, accessToken }; |
| 193 | } |
| 194 | if (source.driver !== "codex") |
| 195 | return yield* new SharedAccountError({ reason: "setup" }); |
| 196 | const config = yield* decodeCodexSettings(source.config ?? {}); |
| 197 | if (config.setupMode === "managed") |
| 198 | return yield* new SharedAccountError({ reason: "setup" }); |
| 199 | const home = yield* resolveCodexHomeLayout(config).pipe( |
| 200 | Effect.provideService(Path.Path, path), |
| 201 | ); |
| 202 | const auth = yield* fs |
| 203 | .readFileString(path.join(home.effectiveHomePath ?? home.sharedHomePath, "auth.json")) |
| 204 | .pipe(Effect.flatMap(decodeCodexAuth)); |
| 205 | const exp = yield* Effect.try(() => decodeJwt(auth.tokens.access_token).exp); |
| 206 | if (typeof exp !== "number") return yield* new SharedAccountError({ reason: "setup" }); |
| 207 | return { |
| 208 | driver: "codex" as const, |
| 209 | accessToken: auth.tokens.access_token, |
| 210 | refreshToken: auth.tokens.refresh_token, |
| 211 | idToken: auth.tokens.id_token, |
| 212 | accountId: auth.tokens.account_id, |
| 213 | expiresAt: exp * 1000, |
| 214 | }; |
| 215 | }).pipe( |
| 216 | Effect.mapError((cause) => |
| 217 | isSharedAccountError(cause) |
| 218 | ? cause |
| 219 | : new SharedAccountError({ reason: "setup", cause: Redacted.make(cause) }), |
| 220 | ), |
| 221 | ); |
| 222 | yield* accounts.execute({ |
| 223 | type: "import", |
| 224 | label: source.displayName ?? request.instanceId, |
| 225 | credentials, |
| 226 | ...(request.replaceId ? { replaceId: request.replaceId } : {}), |
| 227 | }); |
| 228 | } |
| 229 | return yield* sync(request.type === "connect"); |
| 230 | }), |
| 231 | ); |
| 232 | }); |
| 233 | |
| 234 | return SharedProfiles.of({ execute }); |
| 235 | }); |
| 236 | |
| 237 | export const layer = Layer.effect(SharedProfiles, make); |