1import {
2 SharedAccount,
3 SharedAccountCredentials,
4 SharedAccountError,
5 type SharedAccountRequest,
6 type SharedAccountResponse,
7} from "@t3tools/contracts";
8import * as Clock from "effect/Clock";
9import * as Context from "effect/Context";
10import * as Crypto from "effect/Crypto";
11import * as Effect from "effect/Effect";
12import * as Redacted from "effect/Redacted";
13import * as Layer from "effect/Layer";
14import * as Option from "effect/Option";
15import * as Schema from "effect/Schema";
16import * as Semaphore from "effect/Semaphore";
17import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
18
19const StoredAccount = Schema.Struct({
20 ...SharedAccount.fields,
21 credentials: SharedAccountCredentials,
22 lease: Schema.NullOr(Schema.Struct({ token: Schema.String, expiresAt: Schema.Finite })),
23});
24const StoredAccounts = Schema.fromJsonString(Schema.Array(StoredAccount));
25const decodeAccounts = Schema.decodeEffect(StoredAccounts);
26const encodeAccounts = Schema.encodeEffect(StoredAccounts);
27const sameCredentials = Schema.toEquivalence(SharedAccountCredentials);
28
29export class SharedAccounts extends Context.Service<
30 SharedAccounts,
31 {
32 readonly execute: (
33 request: SharedAccountRequest,
34 ) => Effect.Effect<SharedAccountResponse, SharedAccountError>;
35 }
36>()("t3/provider/SharedAccounts") {}
37
38const make = Effect.gen(function* () {
39 const secrets = yield* ServerSecretStore.ServerSecretStore;
40 const crypto = yield* Crypto.Crypto;
41 const lock = yield* Semaphore.make(1);
42
43 const read = secrets.get("shared-provider-accounts").pipe(
44 Effect.flatMap((bytes) =>
45 Option.isNone(bytes)
46 ? Effect.succeed<ReadonlyArray<typeof StoredAccount.Type>>([])
47 : decodeAccounts(new TextDecoder().decode(bytes.value)),
48 ),
49 Effect.mapError(
50 (cause) => new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }),
51 ),
52 );
53
54 const write = (accounts: ReadonlyArray<typeof StoredAccount.Type>) =>
55 encodeAccounts(accounts).pipe(
56 Effect.flatMap((json) =>
57 secrets.set("shared-provider-accounts", new TextEncoder().encode(json)),
58 ),
59 Effect.mapError(
60 (cause) => new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }),
61 ),
62 );
63
64 const publicAccount = ({ id, label, driver, revision }: typeof StoredAccount.Type) => ({
65 id,
66 label,
67 driver,
68 revision,
69 });
70 const access = (account: typeof StoredAccount.Type): SharedAccountResponse => ({
71 type: "access",
72 account: publicAccount(account),
73 credentials:
74 account.credentials.driver === "codex"
75 ? {
76 driver: "codex",
77 accessToken: account.credentials.accessToken,
78 idToken: account.credentials.idToken,
79 accountId: account.credentials.accountId,
80 expiresAt: account.credentials.expiresAt,
81 }
82 : account.credentials,
83 });
84
85 const execute = Effect.fn("SharedAccounts.execute")(function* (request: SharedAccountRequest) {
86 return yield* lock
87 .withPermits(1)(
88 Effect.gen(function* (): Effect.fn.Return<SharedAccountResponse, SharedAccountError> {
89 const accounts = [...(yield* read)];
90 if (request.type === "list") {
91 return { type: "list", accounts: accounts.map(publicAccount) };
92 }
93 if (request.type === "import") {
94 if (request.replaceId) {
95 const index = accounts.findIndex((account) => account.id === request.replaceId);
96 const previous = accounts[index];
97 if (!previous) return yield* new SharedAccountError({ reason: "missing" });
98 if (
99 previous.driver !== request.credentials.driver ||
100 (previous.credentials.driver === "codex" &&
101 request.credentials.driver === "codex" &&
102 previous.credentials.accountId !== request.credentials.accountId)
103 )
104 return yield* new SharedAccountError({ reason: "identity" });
105 if (sameCredentials(previous.credentials, request.credentials))
106 return { type: "imported", account: publicAccount(previous) };
107 const updated = {
108 ...previous,
109 credentials: request.credentials,
110 revision: previous.revision + 1,
111 lease: null,
112 };
113 accounts[index] = updated;
114 yield* write(accounts);
115 return { type: "imported", account: publicAccount(updated) };
116 }
117 const existing = accounts.find(({ credentials }) =>
118 credentials.driver === "codex" && request.credentials.driver === "codex"
119 ? credentials.accountId === request.credentials.accountId
120 : credentials.driver === "claudeAgent" &&
121 request.credentials.driver === "claudeAgent" &&
122 credentials.accessToken === request.credentials.accessToken,
123 );
124 if (existing) return { type: "imported", account: publicAccount(existing) };
125
126 const id = yield* crypto
127 .randomUUIDv4()
128 .pipe(
129 Effect.mapError(
130 (cause) =>
131 new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }),
132 ),
133 );
134 const account: typeof StoredAccount.Type = {
135 id,
136 label: request.label,
137 driver: request.credentials.driver,
138 revision: 1,
139 credentials: request.credentials,
140 lease: null,
141 };
142 accounts.push(account);
143 yield* write(accounts);
144 return { type: "imported", account: publicAccount(account) };
145 }
146
147 const index = accounts.findIndex((entry) => entry.id === request.id);
148 const account = accounts[index];
149 if (!account) return yield* new SharedAccountError({ reason: "missing" });
150
151 if (request.type === "remove") {
152 accounts.splice(index, 1);
153 yield* write(accounts);
154 return { type: "removed" };
155 }
156 if (request.type === "commit") {
157 if (
158 account.revision === request.revision + 1 &&
159 sameCredentials(account.credentials, request.credentials)
160 )
161 return access(account);
162
163 if (
164 account.revision !== request.revision ||
165 account.lease?.token !== request.leaseToken
166 ) {
167 return yield* new SharedAccountError({ reason: "stale" });
168 }
169 if (
170 account.credentials.driver !== "codex" ||
171 account.credentials.accountId !== request.credentials.accountId
172 ) {
173 return yield* new SharedAccountError({ reason: "identity" });
174 }
175 const updated = {
176 ...account,
177 credentials: request.credentials,
178 revision: account.revision + 1,
179 lease: null,
180 };
181 accounts[index] = updated;
182 yield* write(accounts);
183 return access(updated);
184 }
185
186 const now = yield* Clock.currentTimeMillis;
187 if (
188 account.credentials.driver === "claudeAgent" ||
189 (account.credentials.expiresAt > now + 300_000 &&
190 request.refreshRevision !== account.revision)
191 ) {
192 return access(account);
193 }
194 if (account.lease && account.lease.expiresAt > now) {
195 return { type: "busy", retryAt: account.lease.expiresAt };
196 }
197 const token = yield* crypto
198 .randomUUIDv4()
199 .pipe(
200 Effect.mapError(
201 (cause) =>
202 new SharedAccountError({ reason: "storage", cause: Redacted.make(cause) }),
203 ),
204 );
205 const lease = { token, expiresAt: now + 30_000 };
206 accounts[index] = { ...account, lease };
207 yield* write(accounts);
208 return {
209 type: "refresh",
210 account: publicAccount(account),
211 leaseToken: token,
212 credentials: account.credentials,
213 expiresAt: lease.expiresAt,
214 };
215 }),
216 )
217 .pipe(Effect.uninterruptible);
218 });
219
220 return SharedAccounts.of({ execute });
221});
222
223export const layer = Layer.effect(SharedAccounts, make);