@eddyskywalker/dsh-chatgpt-subscription 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +93 -38
  3. package/lib/client.js +2126 -1693
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +7597 -2781
  6. package/lib/types/client/claude/ClaudeComposerQuota.d.ts +33 -0
  7. package/lib/types/client/claude/ClaudeComposerQuota.d.ts.map +1 -0
  8. package/lib/types/client/{zhipu/ZhipuSection.d.ts → claude/ClaudeSection.d.ts} +2 -2
  9. package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -0
  10. package/lib/types/client/claude/locales.d.ts +444 -0
  11. package/lib/types/client/claude/locales.d.ts.map +1 -0
  12. package/lib/types/client/claude/styles.d.ts +15 -0
  13. package/lib/types/client/claude/styles.d.ts.map +1 -0
  14. package/lib/types/client/index.d.ts +1 -1
  15. package/lib/types/client/index.d.ts.map +1 -1
  16. package/lib/types/host/claude/__probe-assert.d.ts +2 -0
  17. package/lib/types/host/claude/__probe-assert.d.ts.map +1 -0
  18. package/lib/types/host/claude/account-pool.d.ts +580 -0
  19. package/lib/types/host/claude/account-pool.d.ts.map +1 -0
  20. package/lib/types/host/claude/adapter.d.ts +267 -0
  21. package/lib/types/host/claude/adapter.d.ts.map +1 -0
  22. package/lib/types/host/claude/adopt.d.ts +292 -0
  23. package/lib/types/host/claude/adopt.d.ts.map +1 -0
  24. package/lib/types/host/claude/client.d.ts +513 -0
  25. package/lib/types/host/claude/client.d.ts.map +1 -0
  26. package/lib/types/host/claude/mapper.d.ts +510 -0
  27. package/lib/types/host/claude/mapper.d.ts.map +1 -0
  28. package/lib/types/host/claude/model-catalog.d.ts +365 -0
  29. package/lib/types/host/claude/model-catalog.d.ts.map +1 -0
  30. package/lib/types/host/claude/oauth.d.ts +393 -0
  31. package/lib/types/host/claude/oauth.d.ts.map +1 -0
  32. package/lib/types/host/claude/routes.d.ts +128 -0
  33. package/lib/types/host/claude/routes.d.ts.map +1 -0
  34. package/lib/types/host/claude/token-store.d.ts +312 -0
  35. package/lib/types/host/claude/token-store.d.ts.map +1 -0
  36. package/lib/types/host/claude/types.d.ts +237 -0
  37. package/lib/types/host/claude/types.d.ts.map +1 -0
  38. package/lib/types/index.d.ts +10 -7
  39. package/lib/types/index.d.ts.map +1 -1
  40. package/lib/types/shared/__probe-claude-import.d.ts +2 -0
  41. package/lib/types/shared/__probe-claude-import.d.ts.map +1 -0
  42. package/lib/types/shared/claude-contracts.d.ts +337 -0
  43. package/lib/types/shared/claude-contracts.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/lib/types/client/zhipu/ZhipuComposerQuota.d.ts +0 -26
  46. package/lib/types/client/zhipu/ZhipuComposerQuota.d.ts.map +0 -1
  47. package/lib/types/client/zhipu/ZhipuSection.d.ts.map +0 -1
  48. package/lib/types/client/zhipu/locales.d.ts +0 -275
  49. package/lib/types/client/zhipu/locales.d.ts.map +0 -1
  50. package/lib/types/client/zhipu/styles.d.ts +0 -12
  51. package/lib/types/client/zhipu/styles.d.ts.map +0 -1
  52. package/lib/types/host/zhipu/account-pool.d.ts +0 -43
  53. package/lib/types/host/zhipu/account-pool.d.ts.map +0 -1
  54. package/lib/types/host/zhipu/adapter.d.ts +0 -93
  55. package/lib/types/host/zhipu/adapter.d.ts.map +0 -1
  56. package/lib/types/host/zhipu/client.d.ts +0 -153
  57. package/lib/types/host/zhipu/client.d.ts.map +0 -1
  58. package/lib/types/host/zhipu/mapper.d.ts +0 -111
  59. package/lib/types/host/zhipu/mapper.d.ts.map +0 -1
  60. package/lib/types/host/zhipu/model-catalog.d.ts +0 -73
  61. package/lib/types/host/zhipu/model-catalog.d.ts.map +0 -1
  62. package/lib/types/host/zhipu/routes.d.ts +0 -33
  63. package/lib/types/host/zhipu/routes.d.ts.map +0 -1
  64. package/lib/types/host/zhipu/token-store.d.ts +0 -112
  65. package/lib/types/host/zhipu/token-store.d.ts.map +0 -1
  66. package/lib/types/host/zhipu/types.d.ts +0 -125
  67. package/lib/types/host/zhipu/types.d.ts.map +0 -1
  68. package/lib/types/shared/zhipu-contracts.d.ts +0 -164
  69. package/lib/types/shared/zhipu-contracts.d.ts.map +0 -1
@@ -0,0 +1,580 @@
1
+ /**
2
+ * The Claude subscription line's account pool.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * TWO KINDS OF ACCOUNT, AND ONLY ONE OF THEM MAY BE REFRESHED
6
+ * ---------------------------------------------------------------------------
7
+ *
8
+ * A row in this pool is one of:
9
+ *
10
+ * - **managed** — signed in through this plugin. Its credential lives in the
11
+ * pool's encrypted storage AND in the pre-pool credential document beside it,
12
+ * it is refreshable, and deleting it is this plugin's business.
13
+ * - **adopted** — a SNAPSHOT read out of a local Claude Code sign-in by
14
+ * 'adopt.ts' (marker 'adopted: true', source 'claude-code'). It is used only
15
+ * while its own access token is still valid.
16
+ *
17
+ * An adopted snapshot is NEVER refreshed, and the reason is not an optimisation:
18
+ * Claude Code's refresh token ROTATES. Every refresh consumes the stored token
19
+ * and issues a new one, so two processes refreshing the same grant invalidate
20
+ * each other — whoever loses the race holds a token the server has already
21
+ * retired, and the user ends up signed OUT of Claude Code by this plugin's good
22
+ * intentions. An in-process single-flight cannot fix that, because the race is
23
+ * across PROCESSES: this plugin and Claude Code are separate programs, and the
24
+ * only shared state between them is a file that 'adopt.ts' must never write.
25
+ * Once such a snapshot is expired the pool takes it out of rotation and
26
+ * surfaces ADOPTED_CREDENTIAL_EXPIRED_HINT — sign in again in CLAUDE CODE and
27
+ * re-import, not here.
28
+ *
29
+ * The rule is enforced twice on purpose:
30
+ *
31
+ * 1. {@link claudeNeedsRefresh} returns false for an adopted credential no
32
+ * matter what its expiry says, so neither the routing path nor the auxiliary
33
+ * credential path ever reaches for a refresh.
34
+ * 2. the refresh hook itself refuses one ({@link ClaudeAdoptedCredentialError}),
35
+ * so a future caller that forgets rule 1 fails loudly instead of silently
36
+ * spending a grant that is not ours to spend.
37
+ *
38
+ * ---------------------------------------------------------------------------
39
+ * ONE ACCOUNT, ONE KEY
40
+ * ---------------------------------------------------------------------------
41
+ *
42
+ * 'token-store.ts' keys a stored account by an immutable internalId
43
+ * ('cl_' + 20 hex) and keeps identity aliases ('uuid:..', 'email:..',
44
+ * 'seed:<sha256(refreshToken)>') as an APPEND-ONLY set hanging off it. This pool
45
+ * adopts that same id rather than minting its own, and it resolves every
46
+ * identity question THROUGH it:
47
+ *
48
+ * - {@link ClaudeAccountPool.recordIdFor} turns a credential's strongest alias
49
+ * into the owning record's internalId, never into an alias string and never
50
+ * into a token digest. Both the dedupeKey and the accountId hook return that
51
+ * id, so the pool and the credential document cannot drift into two key
52
+ * spaces — which is how ghost accounts appear: one account, two rows, one of
53
+ * them stale forever.
54
+ * - 'seed:' aliases are LOOKUP keys only. A seed is a digest of a rotating
55
+ * token, so it is never returned, never stored as an account id, and never
56
+ * compared for equality between two credentials.
57
+ *
58
+ * The single accepted limitation is inherited from the store and restated here
59
+ * so nobody has to rediscover it: a credential that states NEITHER a uuid NOR an
60
+ * address has only its token seed as identity, so a re-login produces a
61
+ * different seed and a second record. {@link ClaudeAccountPool.mergeAccounts} is
62
+ * the remedy, and it is a deliberate user action rather than a guess.
63
+ *
64
+ * ---------------------------------------------------------------------------
65
+ * THE DOCUMENT SEAM (why an adapter class exists here)
66
+ * ---------------------------------------------------------------------------
67
+ *
68
+ * 'oauth.ts' signs in and refreshes against a minimal two-method store (read /
69
+ * write of ONE credential), because that module deliberately knows nothing about
70
+ * how accounts are laid out on disk. This line's store is
71
+ * {@link FileCredentialStore} — a MULTI-account document. The bridge is
72
+ * {@link ClaudeCredentialStoreAdapter}: it addresses exactly one record by
73
+ * internalId on read and writes back into that record's own entry (through
74
+ * saveAccount(credentials, { internalId }), which MERGES identity aliases
75
+ * instead of replacing them). Every caller that needs the two-method surface —
76
+ * the sign-in flow, and a later route that acts on one account — gets it from
77
+ * {@link ClaudeAccountPool.credentialStoreFor}.
78
+ *
79
+ * The pool's own refresh deliberately calls 'oauth.ts''s refreshAccessToken
80
+ * rather than its higher-level ensureAccessToken. That is not a shortcut: the
81
+ * single-flight in ensureAccessToken is ONE MODULE-LEVEL PROMISE shared by every
82
+ * caller in the process, so with a pool it would hand the second account the
83
+ * first account's freshly rotated credential — and, through the adapter above,
84
+ * write it into the second account's record. The pool therefore keeps its OWN
85
+ * single-flight, per account (see the inFlight map), which preserves the property
86
+ * the frozen module wanted (one rotation per expiry, no two callers spending the
87
+ * same rotating token) without the cross-account contamination.
88
+ *
89
+ * ---------------------------------------------------------------------------
90
+ * MANAGED WINS A CONFLICT
91
+ * ---------------------------------------------------------------------------
92
+ *
93
+ * The same Claude account can reach the pool twice: as a plugin sign-in and as
94
+ * an adopted snapshot. The two rows merge into one, and the MANAGED one wins the
95
+ * refresh right — it is the only one that is allowed to refresh at all. When
96
+ * they merge the adopted marker must NOT survive, because a surviving
97
+ * 'adopted: true' would silently freeze the merged record out of refreshing
98
+ * forever. That is why this class writes the marker (including its false case)
99
+ * on EVERY account it creates, rather than only on the adopted ones: the core
100
+ * merges a re-authorization as { ...existing, ...created }, so an omitted key
101
+ * would leave the stored marker in place.
102
+ */
103
+ import { AccountPoolCore, type PoolAccountShape, type PoolData } from '../common/account-pool.ts';
104
+ import type { CredentialStore } from '../token-store.ts';
105
+ import type { AccountAuthStatus, PoolAccountSummaryDto } from '../../shared/account-pool-contracts.ts';
106
+ import { FileCredentialStore, type ClaudeCredentials } from './token-store.ts';
107
+ import { ClaudeUnauthorizedError, type ClaudeTokenStore } from './oauth.ts';
108
+ import { CLAUDE_CODE_CREDENTIAL_SOURCE } from './adopt.ts';
109
+ import type { ClaudeFailure } from './client.ts';
110
+ /**
111
+ * How early this pool refreshes, mirroring 'oauth.ts''s own REFRESH_MARGIN_MS.
112
+ *
113
+ * A mirror rather than an import because the frozen module keeps that constant
114
+ * private, and this chunk's rule is to mirror the five-minute margin rather than
115
+ * invent one. The consequence is stated rather than hidden: 'oauth.ts' ALREADY
116
+ * subtracts the same five minutes when it stamps expiresAt, so a credential this
117
+ * pool refreshes is refreshed roughly ten minutes ahead of the real expiry. That
118
+ * direction is the safe one for this line — a refresh that arrives early costs
119
+ * one rotation, while one that arrives late costs a 401 in the middle of a turn.
120
+ */
121
+ export declare const CLAUDE_REFRESH_MARGIN_MS = 300000;
122
+ /**
123
+ * Message raised when the pool holds no account at all.
124
+ *
125
+ * Word for word the string 'oauth.ts' raises when its store is empty, so a user
126
+ * who reads either one is sent to the same place.
127
+ */
128
+ export declare const CLAUDE_EMPTY_POOL_MESSAGE = "Not signed in to Claude. Sign in from Settings > Claude.";
129
+ /**
130
+ * Message for a credential that cannot reach /v1/messages at all.
131
+ *
132
+ * 'user:inference' is the scope that makes a token a SUBSCRIPTION credential;
133
+ * without it the credential authenticates but is not entitled to the routes this
134
+ * provider exists to serve, so the account is listed and refused rather than
135
+ * retried.
136
+ */
137
+ export declare function claudeNonSubscriptionReason(): string;
138
+ /**
139
+ * Message raised if a refresh of a still-valid adopted snapshot were ever
140
+ * attempted.
141
+ *
142
+ * Unreachable through this pool's own paths (see the module comment); it exists
143
+ * because 'unreachable' is a claim the guard has to be able to make good on.
144
+ */
145
+ export declare const ADOPTED_NEVER_REFRESHED_MESSAGE: string;
146
+ /**
147
+ * The adoption markers, as they hang off a credential stored by this pool.
148
+ *
149
+ * FLATTENED onto the credential, unlike the snapshot 'adopt.ts' returns, which
150
+ * nests the token under 'credentials'. Two reasons, both structural:
151
+ *
152
+ * - the pool's hooks (needsRefresh, refresh, authRejectedReason) receive the
153
+ * CREDENTIAL and never the account record, so the marker has to be reachable
154
+ * from there;
155
+ * - every other field this line reads (expiresAt, accessToken) sits at the top
156
+ * level, and a second nesting level would mean two shapes for one account.
157
+ *
158
+ * The predicate is therefore {@link isAdoptedPoolCredential} rather than
159
+ * 'adopt.ts''s own isAdoptedClaudeCredential, which additionally requires that
160
+ * nested 'credentials' record and so answers false for this spelling. The FIELDS
161
+ * it tests — adopted === true and source === 'claude-code' — are the same two,
162
+ * and the source constant is imported from that module rather than re-typed.
163
+ */
164
+ export interface ClaudeAdoptedCredentialMarkers {
165
+ adopted?: true;
166
+ source?: string;
167
+ sourcePath?: string;
168
+ }
169
+ /** One pooled credential: the stored credential, plus the markers above. */
170
+ export type ClaudePoolCredentials = ClaudeCredentials & ClaudeAdoptedCredentialMarkers;
171
+ /** Where a pooled account's credential came from. */
172
+ export type ClaudePoolAccountSource = 'managed' | typeof CLAUDE_CODE_CREDENTIAL_SOURCE;
173
+ /** One pooled Claude account: the credential plus the facts the card renders. */
174
+ export interface ClaudePoolAccount extends PoolAccountShape<ClaudeCredentials> {
175
+ /**
176
+ * Append-only alias set, mirroring the credential document's own.
177
+ *
178
+ * Kept on the row so a merge (or the explicit
179
+ * {@link ClaudeAccountPool.mergeAccounts}) can carry an alias across instead of
180
+ * losing the only handle a seed-only snapshot has.
181
+ */
182
+ identityKeys: string[];
183
+ /** Which store owns the credential. Only managed rows may be refreshed. */
184
+ source: ClaudePoolAccountSource;
185
+ /**
186
+ * Whether this row is a borrowed snapshot.
187
+ *
188
+ * Required rather than optional: every construction site has to state it, which
189
+ * is what keeps a stale true from surviving an in-place re-authorization.
190
+ */
191
+ adopted: boolean;
192
+ /** Claude Code file an adopted snapshot was read from; absent for managed rows. */
193
+ sourcePath?: string;
194
+ /** Address the sign-in reported, when it reported one. */
195
+ email?: string;
196
+ /** Subscription tier the exchange reported, verbatim ('max', 'pro', ...). */
197
+ subscriptionType?: string;
198
+ /** Display form of {@link subscriptionType}; the card's plan badge. */
199
+ planLabel?: string;
200
+ }
201
+ /**
202
+ * One account as the Claude settings card renders it.
203
+ *
204
+ * Declared here rather than under 'src/shared/': this slice is the pool's own
205
+ * vocabulary, and a later chunk imports it from this module instead of growing a
206
+ * second copy that drifts.
207
+ */
208
+ export interface ClaudeAccountSummaryDto extends PoolAccountSummaryDto {
209
+ /** Where the credential came from; a card offers different actions per source. */
210
+ source?: ClaudePoolAccountSource;
211
+ /** Whether this row is a snapshot borrowed from a local Claude Code sign-in. */
212
+ adopted?: boolean;
213
+ /** The Claude Code file an adopted snapshot was read from. */
214
+ sourcePath?: string;
215
+ /** Subscription tier, from the credential itself. */
216
+ subscriptionType?: string;
217
+ }
218
+ /** Encrypted pool file this line owns; the credential document stays the mirror. */
219
+ export declare function claudePoolPath(): string;
220
+ /**
221
+ * Parse one stored credential TOLERANTLY; null when the row cannot serve.
222
+ *
223
+ * Deliberately not 'token-store.ts''s parseClaudeCredentials, and the difference
224
+ * is the point:
225
+ *
226
+ * - that parser is STRICT and refuses a credential with no 'user:inference'
227
+ * scope, which is right for the document it guards but would DELETE the row
228
+ * here. This pool has to be able to LIST a credential it refuses, so the
229
+ * refusal can be reported with a reason (see {@link claudeAuthRejectedReason})
230
+ * instead of the account silently vanishing from the card.
231
+ * - an adopted snapshot states no scopes at all when Claude Code's document did
232
+ * not, and 'adopt.ts' refuses to invent them.
233
+ *
234
+ * What is still refused, because such a row could never serve a request: a
235
+ * missing token, or an expiry that is not a finite number.
236
+ */
237
+ export declare function parseClaudePoolCredentials(value: unknown): ClaudePoolCredentials | null;
238
+ /**
239
+ * Prefix of the id this pool gives a borrowed snapshot.
240
+ *
241
+ * A snapshot has no credential-document record, so there is no internalId to
242
+ * adopt; the pool has to name it. The name is WHERE IT CAME FROM rather than a
243
+ * digest of its tokens, and that choice is load-bearing: Claude Code refreshes
244
+ * its own file, so a re-import of an unchanged sign-in carries a DIFFERENT
245
+ * refresh token and therefore a different 'seed:' alias. Keying on the source
246
+ * file is what makes "import it again after signing in again in Claude Code" an
247
+ * UPDATE of the row the user already has, instead of a new row every time.
248
+ *
249
+ * The residual, stated rather than hidden: two accounts imported one after the
250
+ * other from the SAME path are one row, because the row means 'the sign-in that
251
+ * file currently holds' — which is exactly what a borrowed snapshot is. And a
252
+ * snapshot whose route did not record a path at all shares the 'unstated' name
253
+ * with any other such snapshot, so those collapse into one row too.
254
+ */
255
+ export declare const ADOPTED_ACCOUNT_KEY_PREFIX = "adopted:";
256
+ /** The pool's own name for a borrowed snapshot. Never a token digest. */
257
+ export declare function adoptedPoolAccountKey(credentials: ClaudeCredentials): string;
258
+ /**
259
+ * Whether a pooled credential is an adopted snapshot.
260
+ *
261
+ * The two fields, and only those two: a plain credential has no 'adopted' field
262
+ * and must never be mistaken for a snapshot, because treating a plugin-owned
263
+ * account as disposable would refuse to refresh an account the user added here.
264
+ */
265
+ export declare function isAdoptedPoolCredential(credentials: ClaudeCredentials): boolean;
266
+ /**
267
+ * Whether a credential must be refreshed before the next request.
268
+ *
269
+ * FALSE for every adopted snapshot, valid or expired. That is this chunk's core
270
+ * rule: an adopted credential is borrowed for exactly as long as Claude Code
271
+ * vouched for it, and this pool has no business rotating a token another process
272
+ * is rotating too. See the module comment.
273
+ */
274
+ export declare function claudeNeedsRefresh(credentials: ClaudeCredentials, now: number): boolean;
275
+ /**
276
+ * Why a stored credential is already known to be unusable, or nothing.
277
+ *
278
+ * Reported purely — nothing is written here — and consulted by the core for both
279
+ * routing eligibility and credential usability, which is what makes an expired
280
+ * snapshot non-routable without a write on every read.
281
+ *
282
+ * Order matters: the expired-snapshot hint comes first because it is the
283
+ * actionable one (the remedy is in Claude Code, not here).
284
+ */
285
+ export declare function claudeAuthRejectedReason(credentials: ClaudeCredentials): string | undefined;
286
+ /** Validate and normalize one whole pool document. */
287
+ export declare function parseClaudePoolData(value: unknown): PoolData<ClaudePoolAccount>;
288
+ /**
289
+ * The two-method credential store 'oauth.ts' wants, addressed at ONE account.
290
+ *
291
+ * See the module comment for why this bridge exists at all. The id is the
292
+ * document's own immutable internalId, which is also the pool row's id, so a
293
+ * refresh driven through here lands in the very record the pool lists — no
294
+ * second key space.
295
+ *
296
+ * write goes through saveAccount(credentials, { internalId }) rather than
297
+ * replacing the document, so the record keeps its append-only alias set and
298
+ * whatever a later chunk hung on it. It does NOT special-case an adopted
299
+ * snapshot: nothing in the pool ever asks this adapter to write one (the refresh
300
+ * guard refuses first), and silently discarding a caller's write would be a
301
+ * worse failure than the store's own strict validation.
302
+ */
303
+ export declare class ClaudeCredentialStoreAdapter implements ClaudeTokenStore {
304
+ private readonly store;
305
+ private readonly internalId;
306
+ constructor(store: FileCredentialStore, internalId: string);
307
+ read(): Promise<ClaudeCredentials | null>;
308
+ write(credentials: ClaudeCredentials): Promise<void>;
309
+ }
310
+ /**
311
+ * What the adapter must do with the account that produced a failed request.
312
+ *
313
+ * THE CONTRACT, and it is the adapter's to honour rather than this module's to
314
+ * enforce:
315
+ *
316
+ * - switch accounts, or cool one down, ONLY for a CREDENTIAL failure or an
317
+ * ACCOUNT-SCOPED rate limit. Both are attributable to one account, so another
318
+ * account can plausibly serve the same request.
319
+ * - NEVER rotate on a global rate limit, an overload (529), a server error, a
320
+ * transport failure, or a malformed request. None of them is any account's
321
+ * fault, and rotating would spend the whole pool against a wall every account
322
+ * is behind — while cooling accounts down would take the line offline with a
323
+ * cooldown nobody earned.
324
+ *
325
+ * The rule is NOT re-derived here: 'client.ts' classifyFailure already answers
326
+ * it, and ClaudeFailure.accountScoped is precisely 'this is THIS account's own
327
+ * spent window' (true only for rate_limit_account, and a 429 carrying no unified
328
+ * rate-limit header is deliberately classified GLOBAL). The helper below is that
329
+ * flag applied to a decision, so the adapter has one place to ask.
330
+ */
331
+ export type ClaudeAccountRotationAction = {
332
+ action: 'mark-auth-failed';
333
+ status: AccountAuthStatus;
334
+ } | {
335
+ action: 'cool-down';
336
+ durationMs: number;
337
+ } | {
338
+ action: 'none';
339
+ };
340
+ /**
341
+ * Fallback cooldown for an account-scoped 429 that named no retry-after.
342
+ *
343
+ * Fifteen minutes mirrors the core's own assumption in its all-unavailable
344
+ * message, so a pool that falls back computes the wait a user would have been
345
+ * shown anyway.
346
+ */
347
+ export declare const CLAUDE_DEFAULT_COOLDOWN_MS = 900000;
348
+ /**
349
+ * The rotation decision for one classified failure.
350
+ *
351
+ * @param failure - a 'client.ts' ClaudeFailure, or the three fields of one.
352
+ */
353
+ export declare function claudeAccountRotationAction(failure: Pick<ClaudeFailure, 'kind' | 'accountScoped' | 'retryAfterMs'>, options?: {
354
+ fallbackCooldownMs?: number;
355
+ }): ClaudeAccountRotationAction;
356
+ /** Raised when a refresh of an adopted snapshot is attempted. Never a 401. */
357
+ export declare class ClaudeAdoptedCredentialError extends ClaudeUnauthorizedError {
358
+ constructor(message: string);
359
+ }
360
+ export interface ClaudeAccountPoolOptions {
361
+ /** Pre-pool credential document the pool mirrors its accounts into. */
362
+ store?: FileCredentialStore;
363
+ /** Test seam; production builds the platform backend from the pool path. */
364
+ backend?: CredentialStore<PoolData<ClaudePoolAccount>>;
365
+ /** Accounts this pool accepts; defaults to the core's limit. */
366
+ maxAccounts?: number;
367
+ /** The account the card pinned, or null for automatic selection. */
368
+ preferAccountId?: () => string | null;
369
+ /**
370
+ * Test seam for the token exchange itself.
371
+ *
372
+ * Replacing it does NOT bypass the adopted guard or the per-account
373
+ * single-flight above it, and the document write-back still happens: the seam
374
+ * is the network call, not the policy.
375
+ */
376
+ refreshCredentials?: (credentials: ClaudeCredentials, fetchFn: typeof fetch) => Promise<ClaudeCredentials>;
377
+ }
378
+ /**
379
+ * The Claude subscription account pool.
380
+ *
381
+ * Read the module comment first: it carries the two rules this class exists to
382
+ * enforce — an adopted snapshot is never refreshed, and one account has one key
383
+ * — together with the reasoning behind both.
384
+ */
385
+ export declare class ClaudeAccountPool extends AccountPoolCore<ClaudeCredentials, ClaudePoolAccount, ClaudeAccountSummaryDto> {
386
+ private readonly store;
387
+ private readonly refreshSeam;
388
+ /**
389
+ * Identity alias -> internalId of the record that owns it.
390
+ *
391
+ * Rebuilt from the pool's own rows on every {@link read}, and additionally fed
392
+ * by {@link addAccount} from the credential document, which is what lets an
393
+ * incoming credential be resolved to an existing record BEFORE the core has to
394
+ * decide whether this is a new account.
395
+ *
396
+ * Entries are never pruned, deliberately. Dropping a live row's alias would
397
+ * make the very next credential for that account look new, which is the ghost
398
+ * row this index exists to prevent; a stale entry is overwritten by the next
399
+ * {@link read}, because a live row always re-indexes its own aliases.
400
+ */
401
+ private readonly recordIds;
402
+ /**
403
+ * One in-flight refresh per account.
404
+ *
405
+ * The refresh token rotates, so two concurrent callers refreshing one account
406
+ * would spend it twice: the second exchange carries a token the first already
407
+ * rotated away, the endpoint answers with a final verdict, and the account is
408
+ * marked expired immediately after a refresh that in fact succeeded. Keyed by
409
+ * the record's id — never by a token — so it cannot mix two accounts up.
410
+ */
411
+ private readonly inFlight;
412
+ constructor(options?: ClaudeAccountPoolOptions);
413
+ /** The credential document this pool mirrors into. */
414
+ mirrorStore(): FileCredentialStore;
415
+ /**
416
+ * The two-method store 'oauth.ts' needs, addressed at one account.
417
+ *
418
+ * The seam in one place: the sign-in flow (and any later route that acts on a
419
+ * single account) never has to know the document layout.
420
+ */
421
+ credentialStoreFor(accountId: string): ClaudeTokenStore;
422
+ /**
423
+ * Read the pool, re-indexing the identity aliases on the way.
424
+ *
425
+ * Every routing decision goes through this method, so this is where the index
426
+ * {@link recordIdsFor} reads is kept in step with the rows that actually exist.
427
+ * See the recordIds field for why entries are not pruned.
428
+ */
429
+ read(): Promise<PoolData<ClaudePoolAccount>>;
430
+ /**
431
+ * Add or re-authorize one account.
432
+ *
433
+ * The override exists for ONE reason: the account's id must be the credential
434
+ * document's internalId, and only this method can look that value up before
435
+ * the core has decided whether the account is new. See the module comment's
436
+ * 'one account, one key' section.
437
+ */
438
+ addAccount(credentials: ClaudeCredentials, alias?: string): Promise<ClaudePoolAccount>;
439
+ /**
440
+ * Add, or re-import, one borrowed Claude Code snapshot.
441
+ *
442
+ * A snapshot is never written into this plugin's credential document — it is
443
+ * not this line's sign-in to keep — so its row is named by WHERE IT CAME FROM
444
+ * (see {@link adoptedPoolAccountKey}) and there is nothing to reconcile with
445
+ * the document.
446
+ *
447
+ * MANAGED BEATS ADOPTED, and this is where the two actually meet. A snapshot
448
+ * resolves to an existing row through its token seed, which is how an account
449
+ * imported here and then signed in through THIS plugin is recognized as one
450
+ * account: the row it lands on is already plugin-owned, and overwriting it with
451
+ * a borrowed credential would freeze that account until the snapshot expired —
452
+ * silently, and for no reason the user could see. So the snapshot contributes
453
+ * only what is new evidence (its aliases) and refuses to touch anything else.
454
+ *
455
+ * The id is resolved BEFORE the core is asked whether the account is already
456
+ * pooled, because the core computes the dedupe key at the TOP of its own
457
+ * addAccount: a key that only becomes known afterwards misses its one chance to
458
+ * answer, and every repeated import would add a row.
459
+ */
460
+ private addAdoptedAccount;
461
+ /**
462
+ * Merge two rows that are one account.
463
+ *
464
+ * This is the user's remedy for the identity limitation 'token-store.ts'
465
+ * documents: a credential stating neither a uuid nor an address has only a
466
+ * token digest to go on, so a re-login produces a second record, and nothing
467
+ * can tell that apart from a genuinely different account. The same applies to
468
+ * an imported snapshot, which Claude Code's document gives no identity to.
469
+ *
470
+ * THE MANAGED ROW WINS THE CREDENTIAL — by construction, not by argument
471
+ * order. Only a plugin-owned credential may ever be refreshed, so a merge that
472
+ * kept a snapshot's tokens would leave the account frozen until it expired, for
473
+ * no reason the user could see. When BOTH rows are snapshots there is nothing
474
+ * to promote and the marker survives; otherwise the merged record is
475
+ * plugin-owned and refreshable again.
476
+ *
477
+ * The keeper's id, alias and ordering are kept, since those are what the
478
+ * settings document pins and what the user recognizes.
479
+ */
480
+ mergeAccounts(keepAccountId: string, dropAccountId: string): Promise<ClaudePoolAccount>;
481
+ /**
482
+ * Fold two document records into the one the merged pool row points at.
483
+ *
484
+ * The alias sets are unioned rather than replaced, mirroring the document's own
485
+ * append-only rule: the discarded record's aliases are the only handles a
486
+ * seed-only credential has, and losing them would make the merged account
487
+ * unrecognizable to its own next refresh.
488
+ */
489
+ private reconcileMergedDocument;
490
+ /**
491
+ * Drop an imported snapshot from the pool.
492
+ *
493
+ * A separate action because {@link deleteAccount} refuses one: the row is a
494
+ * borrowed snapshot, and a card must not destroy an account this plugin did
495
+ * not create with the same button that deletes its own. This is what an
496
+ * import card's 'remove import' calls; Claude Code's own file is touched by
497
+ * neither path.
498
+ */
499
+ removeImportedAccount(accountId: string): Promise<void>;
500
+ /**
501
+ * Delete one plugin-owned account.
502
+ *
503
+ * Refused for an imported snapshot, following the WorkBuddy pool's precedent
504
+ * for accounts it did not sign in: the card offers
505
+ * {@link removeImportedAccount} there instead. A managed account is removed
506
+ * from the credential document as well, so the two stores cannot disagree
507
+ * about which accounts exist — the pre-pool projection would otherwise hand
508
+ * back a credential the user just deleted.
509
+ */
510
+ deleteAccount(accountId: string): Promise<void>;
511
+ /**
512
+ * The immutable id of the record that owns a credential, or nothing.
513
+ *
514
+ * Resolution order mirrors 'token-store.ts''s own identity rules, minus the
515
+ * part that must never be a key: a strong alias ('uuid:..', 'email:..') is
516
+ * looked up first, and the 'seed:' digest is consulted ONLY as a lookup, so a
517
+ * credential stating no real identity still finds its record without the pool
518
+ * ever keying an account on a rotating token.
519
+ */
520
+ private recordIdFor;
521
+ /**
522
+ * The key the pool files a credential under, or nothing.
523
+ *
524
+ * A MANAGED credential is named by the credential document's immutable
525
+ * internalId, so the pool and the document agree on one key — and that value is
526
+ * already known here, because a managed credential reaches this point only
527
+ * after the document has stored it and handed its id back.
528
+ *
529
+ * A BORROWED SNAPSHOT has no document record, so it cannot be named by one. It
530
+ * is named by its source file instead ({@link adoptedPoolAccountKey}), which is
531
+ * a fact the credential always carries. Answering `undefined` here for a
532
+ * snapshot would be fatal rather than cautious: the core treats `undefined` as
533
+ * `no dedupe key`, so every import would land as a brand new row — including
534
+ * the re-import this line's own instructions tell the user to perform after
535
+ * signing in again in Claude Code.
536
+ */
537
+ private poolKeyFor;
538
+ /**
539
+ * The id this process already knows for a credential, or nothing.
540
+ *
541
+ * Resolution order mirrors 'token-store.ts''s own identity rules, minus the
542
+ * part that must never be a key: a strong alias ('uuid:..', 'email:..') is
543
+ * looked up first, and the 'seed:' digest is consulted ONLY as a lookup, so a
544
+ * credential stating no real identity still finds its record without the pool
545
+ * ever keying an account on a rotating token.
546
+ */
547
+ private recordIdsFor;
548
+ private indexCredential;
549
+ private indexRecord;
550
+ private indexAccount;
551
+ /**
552
+ * Refresh one account's credential, and nothing else.
553
+ *
554
+ * Two guards live here and both are load-bearing:
555
+ *
556
+ * 1. an adopted snapshot is refused outright (see the module comment);
557
+ * 2. a second caller for the SAME account joins the first one's rotation
558
+ * instead of spending the rotating token twice.
559
+ */
560
+ private refreshCredentials;
561
+ /**
562
+ * The per-account single-flight, resolving the account's id first.
563
+ *
564
+ * Separated from the hook above only because resolving the id consults the
565
+ * credential document, and a hook cannot await: the guard stays synchronous so
566
+ * it can never be skipped by a caller that forgets to wait.
567
+ */
568
+ private singleFlightRefresh;
569
+ /**
570
+ * Rotate one managed credential and keep the credential document in step.
571
+ *
572
+ * The document write is what stops the pre-pool projection from handing out a
573
+ * token the rotation has already spent. Its failure is swallowed on purpose:
574
+ * the credential WAS refreshed, the pool's own record holds it, and a mirror
575
+ * that could not be written must not fail a request that can still be served —
576
+ * the same rule the core applies to its own bookkeeping writes.
577
+ */
578
+ private performRefresh;
579
+ }
580
+ //# sourceMappingURL=account-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-pool.d.ts","sourceRoot":"","sources":["../../../../src/host/claude/account-pool.ts"],"names":[],"mappings":"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"}