1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import {
4 CodexSettings,
5 ClaudeSettings,
6 ProviderDriverKind,
7 ProviderInstanceId,
8 type SharedAccount,
9 SharedAccountError,
10} from "@t3tools/contracts";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Layer from "effect/Layer";
14import * as Path from "effect/Path";
15import * as Schema from "effect/Schema";
16import { HttpClient, HttpClientResponse } from "effect/http";
17import * as ServerConfig from "../config.ts";
18import * as ServerSettings from "../serverSettings.ts";
19import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
20import * as ManagedProviderHome from "./ManagedProviderHome.ts";
21import * as SharedAccounts from "./SharedAccounts.ts";
22import * as SharedAccountClient from "./SharedAccountClient.ts";
23import * as SharedProfiles from "./SharedProfiles.ts";
24import { codexUsageReader } from "./Drivers/codexUsage.ts";
25import { claudeUsageReader } from "./Drivers/claudeUsage.ts";
26
27const decodeCodexSettings = Schema.decodeUnknownEffect(CodexSettings);
28const decodeClaudeSettings = Schema.decodeUnknownEffect(ClaudeSettings);
29const isSharedAccountError = Schema.is(SharedAccountError);
30
31const fixture = Effect.gen(function* () {
32 const fs = yield* FileSystem.FileSystem;
33 const path = yield* Path.Path;
34 const source = yield* fs.makeTempDirectoryScoped();
35 const tokenFile = path.join(source, "claude-token");
36 yield* fs.writeFileString(tokenFile, "sk-ant-oat01-test-setup-token\n", { mode: 0o600 });
37 const accessToken = `eyJhbGciOiJub25lIn0.${Buffer.from(JSON.stringify({ exp: 3_000_000_000 })).toString("base64url")}.signature`;
38 yield* fs.writeFileString(
39 path.join(source, "auth.json"),
40 JSON.stringify({
41 tokens: {
42 access_token: accessToken,
43 refresh_token: "original-refresh",
44 id_token: "original-id",
45 account_id: "account-a",
46 },
47 }),
48 );
49 const settings = ServerSettings.layerTest({
50 providerInstances: {
51 [ProviderInstanceId.make("personal")]: {
52 driver: ProviderDriverKind.make("codex"),
53 displayName: "Personal",
54 config: { homePath: source },
55 },
56 [ProviderInstanceId.make("claude")]: {
57 driver: ProviderDriverKind.make("claudeAgent"),
58 displayName: "Claude",
59 config: {},
60 },
61 },
62 });
63 const config = ServerConfig.layerTest(process.cwd(), { prefix: "t3-shared-profiles-" });
64 let remoteAccounts: ReadonlyArray<SharedAccount> = [];
65 const layer = SharedProfiles.layer.pipe(
66 Layer.provideMerge(ManagedProviderHome.layer),
67 Layer.provideMerge(SharedAccountClient.layer),
68 Layer.provideMerge(SharedAccounts.layer),
69 Layer.provideMerge(settings),
70 Layer.provideMerge(ServerSecretStore.layer),
71 Layer.provideMerge(config),
72 Layer.provide(
73 Layer.succeed(
74 HttpClient.HttpClient,
75 HttpClient.make((request) =>
76 Effect.succeed(
77 HttpClientResponse.fromWeb(
78 request,
79 Response.json({ type: "list", accounts: remoteAccounts }),
80 ),
81 ),
82 ),
83 ),
84 ),
85 );
86 return {
87 layer,
88 source,
89 tokenFile,
90 accessToken,
91 remoteAccounts: (value: ReadonlyArray<SharedAccount>) => {
92 remoteAccounts = value;
93 },
94 };
95});
96
97it.layer(NodeServices.layer)("managed shared profiles", (it) => {
98 it.effect(
99 "imports into private T3-owned homes without copying refresh credentials into native CLI storage",
100 () =>
101 Effect.gen(function* () {
102 const f = yield* fixture;
103 yield* Effect.gen(function* () {
104 const profiles = yield* SharedProfiles.SharedProfiles;
105 const homes = yield* ManagedProviderHome.ManagedProviderHome;
106 const settings = yield* ServerSettings.ServerSettingsService;
107 const fs = yield* FileSystem.FileSystem;
108 const path = yield* Path.Path;
109 const result = yield* profiles.execute({
110 type: "import",
111 instanceId: ProviderInstanceId.make("personal"),
112 });
113 const account = result.accounts[0];
114 assert.isDefined(account);
115 if (!account) return yield* Effect.die("Expected an account");
116 const instance = (yield* settings.getSettings).providerInstances[
117 ProviderInstanceId.make(`shared-${account.id}`)
118 ];
119 assert.strictEqual(instance?.displayName, "Personal");
120 yield* homes.prepare("codex", account.id);
121 const paths = yield* homes.paths("codex", account.id);
122 const directories = yield* codexUsageReader.directories({
123 instanceId: ProviderInstanceId.make("managed"),
124 config: yield* decodeCodexSettings({
125 sharedAccountId: account.id,
126 homePath: f.source,
127 }),
128 environment: {},
129 configured: true,
130 });
131 assert.deepStrictEqual(directories, [{ dir: path.join(paths.shared, "sessions") }]);
132 const config = yield* ServerConfig.ServerConfig;
133 assert.isTrue(paths.account.startsWith(config.stateDir));
134 const json = yield* fs.readFileString(path.join(paths.account, "auth.json"));
135 assert.isFalse(json.includes("original-refresh"));
136 assert.deepInclude(JSON.parse(json), { auth_mode: "chatgptAuthTokens" });
137 assert.isTrue(
138 (yield* fs.readFileString(path.join(f.source, "auth.json"))).includes(
139 "original-refresh",
140 ),
141 );
142 assert.strictEqual(
143 Number((yield* fs.stat(path.join(paths.account, "auth.json"))).mode) & 0o777,
144 0o600,
145 );
146 yield* profiles.execute({ type: "remove", id: account.id });
147 assert.strictEqual(
148 (yield* settings.getSettings).providerInstances[
149 ProviderInstanceId.make(`shared-${account.id}`)
150 ]?.enabled,
151 false,
152 );
153 assert.strictEqual(
154 (yield* homes.prepare("codex", account.id).pipe(Effect.flip)).reason,
155 "missing",
156 );
157 }).pipe(Effect.provide(f.layer));
158 }),
159 );
160
161 it.effect(
162 "shares Claude session directories between accounts while keeping account configuration separate",
163 () =>
164 Effect.gen(function* () {
165 const f = yield* fixture;
166 yield* Effect.gen(function* () {
167 const profiles = yield* SharedProfiles.SharedProfiles;
168 const homes = yield* ManagedProviderHome.ManagedProviderHome;
169 const hub = yield* SharedAccounts.SharedAccounts;
170 const settings = yield* ServerSettings.ServerSettingsService;
171 const fs = yield* FileSystem.FileSystem;
172 const path = yield* Path.Path;
173 const result = yield* profiles.execute({
174 type: "import",
175 instanceId: ProviderInstanceId.make("claude"),
176 tokenFile: f.tokenFile,
177 });
178 const first = result.accounts[0];
179 const second = yield* hub.execute({
180 type: "import",
181 label: "Second",
182 credentials: { driver: "claudeAgent", accessToken: "second-setup-token" },
183 });
184 if (!first || second.type !== "imported")
185 return yield* Effect.die("Expected two accounts");
186 yield* Effect.all(
187 [
188 homes.prepare("claudeAgent", first.id),
189 homes.prepare("claudeAgent", first.id),
190 homes.prepare("claudeAgent", second.account.id),
191 ],
192 { concurrency: "unbounded" },
193 );
194 const a = yield* homes.paths("claudeAgent", first.id);
195 const b = yield* homes.paths("claudeAgent", second.account.id);
196 yield* fs.writeFileString(
197 path.join(a.account, "projects", "session.jsonl"),
198 "shared-session",
199 );
200 assert.strictEqual(
201 yield* fs.readFileString(path.join(b.account, "projects", "session.jsonl")),
202 "shared-session",
203 );
204 yield* fs.writeFileString(
205 path.join(a.account, ".claude.json"),
206 "private-account-settings",
207 );
208 assert.isFalse(yield* fs.exists(path.join(b.account, ".claude.json")));
209 assert.isFalse(
210 JSON.stringify(yield* settings.getSettings).includes("sk-ant-oat01-test-setup-token"),
211 );
212 const directories = yield* claudeUsageReader.directories({
213 instanceId: ProviderInstanceId.make("managed"),
214 config: yield* decodeClaudeSettings({
215 sharedAccountId: first.id,
216 homePath: f.source,
217 }),
218 environment: {},
219 configured: true,
220 });
221 assert.strictEqual(
222 yield* fs.readFileString(path.join(directories[0]!.dir, "session.jsonl")),
223 "shared-session",
224 );
225 yield* fs.writeFileString(f.tokenFile, "sk-ant-oat01-renewed-token");
226 const renewed = yield* profiles.execute({
227 type: "import",
228 instanceId: ProviderInstanceId.make("claude"),
229 tokenFile: f.tokenFile,
230 replaceId: first.id,
231 });
232 assert.strictEqual(
233 renewed.accounts.find((account) => account.id === first.id)?.revision,
234 2,
235 );
236 const instance = (yield* settings.getSettings).providerInstances[
237 ProviderInstanceId.make(`shared-${first.id}`)
238 ];
239 assert.strictEqual(
240 (yield* decodeClaudeSettings(instance?.config)).sharedAccountRevision,
241 2,
242 );
243 assert.strictEqual(
244 (yield* homes.prepare("claudeAgent", first.id)).accessToken,
245 "sk-ant-oat01-renewed-token",
246 );
247 }).pipe(Effect.provide(f.layer));
248 }),
249 );
250
251 it.effect(
252 "disconnect disables managed instances and explicit reconnect restores them without overriding later manual disables",
253 () =>
254 Effect.gen(function* () {
255 const f = yield* fixture;
256 yield* Effect.gen(function* () {
257 const profiles = yield* SharedProfiles.SharedProfiles;
258 const settings = yield* ServerSettings.ServerSettingsService;
259 const result = yield* profiles.execute({
260 type: "import",
261 instanceId: ProviderInstanceId.make("personal"),
262 });
263 const account = result.accounts[0];
264 if (!account) return yield* Effect.die("Expected an account");
265 f.remoteAccounts(result.accounts);
266 yield* profiles.execute({ type: "disconnect" });
267 const current = yield* settings.getSettings;
268 assert.strictEqual(
269 current.providerInstances[ProviderInstanceId.make(`shared-${account.id}`)]?.enabled,
270 false,
271 );
272 assert.notStrictEqual(
273 current.providerInstances[ProviderInstanceId.make("personal")]?.enabled,
274 false,
275 );
276 const id = ProviderInstanceId.make(`shared-${account.id}`);
277 yield* profiles.execute({
278 type: "connect",
279 url: "https://shared.example.test",
280 tokenFile: f.tokenFile,
281 });
282 assert.strictEqual((yield* settings.getSettings).providerInstances[id]?.enabled, true);
283 const instance = (yield* settings.getSettings).providerInstances[id];
284 if (!instance) return yield* Effect.die("Expected a managed instance");
285 yield* settings.updateProviderInstance({
286 operation: "upsert",
287 instanceId: id,
288 instance: { ...instance, enabled: false },
289 });
290 yield* profiles.execute({ type: "sync" });
291 assert.strictEqual((yield* settings.getSettings).providerInstances[id]?.enabled, false);
292 assert.strictEqual(
293 (yield* profiles
294 .execute({ type: "import", instanceId: ProviderInstanceId.make("missing") })
295 .pipe(Effect.flip)).reason,
296 "setup",
297 );
298 assert.isTrue(
299 isSharedAccountError(
300 yield* (yield* ManagedProviderHome.ManagedProviderHome)
301 .prepare("codex", "../outside")
302 .pipe(Effect.flip),
303 ),
304 );
305 }).pipe(Effect.provide(f.layer));
306 }),
307 );
308});