1import { SharedAccountError, SharedAccountId, type SharedAccountAccess } from "@t3tools/contracts";
2import * as Context from "effect/Context";
3import * as Crypto from "effect/Crypto";
4import * as DateTime from "effect/DateTime";
5import * as Effect from "effect/Effect";
6import * as Redacted from "effect/Redacted";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Path from "effect/Path";
10import * as Schema from "effect/Schema";
11import * as KeyedLock from "@t3tools/shared/KeyedLock";
12import * as ServerConfig from "../config.ts";
13import * as SharedAccountClient from "./SharedAccountClient.ts";
14
15const decodeId = Schema.decodeEffect(SharedAccountId);
16const encodeAuth = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
17const isSharedAccountError = Schema.is(SharedAccountError);
18
19const 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
31export 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
41export 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
61const 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
139export const layer = Layer.effect(ManagedProviderHome, make);