1import {
2 ClaudeSettings,
3 CodexSettings,
4 ProviderDriverKind,
5 ProviderInstanceId,
6 SharedAccountError,
7 SharedAccountId,
8 type SharedProfilesRequest,
9 type SharedProfilesStatus,
10} from "@t3tools/contracts";
11import { decodeJwt } from "jose";
12import * as Context from "effect/Context";
13import * as Effect from "effect/Effect";
14import * as Redacted from "effect/Redacted";
15import * as FileSystem from "effect/FileSystem";
16import * as Layer from "effect/Layer";
17import * as Path from "effect/Path";
18import * as Option from "effect/Option";
19import * as Schema from "effect/Schema";
20import * as Semaphore from "effect/Semaphore";
21import * as ServerSettings from "../serverSettings.ts";
22import { resolveCodexHomeLayout } from "./Drivers/CodexHomeLayout.ts";
23import * as SharedAccountClient from "./SharedAccountClient.ts";
24
25const SharedConfig = Schema.Struct({
26 sharedAccountId: SharedAccountId,
27 sharedAccountRevision: Schema.optionalKey(Schema.Int),
28});
29const 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
40const isSharedConfig = Schema.is(SharedConfig);
41const decodeSharedConfig = Schema.decodeUnknownOption(SharedConfig);
42const decodeCodexSettings = Schema.decodeUnknownEffect(CodexSettings);
43const decodeClaudeSettings = Schema.decodeUnknownEffect(ClaudeSettings);
44const decodeCodexAuth = Schema.decodeEffect(CodexAuthFile);
45const isSharedAccountError = Schema.is(SharedAccountError);
46const readConfig = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown));
47
48export 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
57const 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
237export const layer = Layer.effect(SharedProfiles, make);