@modelprofile.com/authswitch 6.5.0 → 8.0.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 (95) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +300 -6
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-runtime-contract.d.ts +13 -0
  5. package/dist_ts/classes.accountlist.js +4 -3
  6. package/dist_ts/classes.authoritybroker.d.ts +25 -5
  7. package/dist_ts/classes.authoritybroker.js +388 -123
  8. package/dist_ts/classes.authoritycli.d.ts +24 -0
  9. package/dist_ts/classes.authoritycli.js +675 -0
  10. package/dist_ts/classes.authorityclient.d.ts +19 -7
  11. package/dist_ts/classes.authorityclient.js +51 -14
  12. package/dist_ts/classes.authoritydaemon.d.ts +28 -1
  13. package/dist_ts/classes.authoritydaemon.js +477 -36
  14. package/dist_ts/classes.authoritydatabase.d.ts +155 -11
  15. package/dist_ts/classes.authoritydatabase.js +1037 -69
  16. package/dist_ts/classes.authoritymodels.d.ts +229 -23
  17. package/dist_ts/classes.authoritymodels.js +867 -128
  18. package/dist_ts/classes.authoritypreuse.d.ts +47 -0
  19. package/dist_ts/classes.authoritypreuse.js +277 -0
  20. package/dist_ts/classes.authorityregistry.d.ts +9 -0
  21. package/dist_ts/classes.authorityregistry.js +32 -0
  22. package/dist_ts/classes.authoritysecrets.d.ts +1 -1
  23. package/dist_ts/classes.authoritysecrets.js +2 -2
  24. package/dist_ts/classes.authorityusage.d.ts +160 -0
  25. package/dist_ts/classes.authorityusage.js +429 -0
  26. package/dist_ts/classes.claudeauthority.d.ts +66 -0
  27. package/dist_ts/classes.claudeauthority.js +528 -0
  28. package/dist_ts/classes.claudecodeharness.js +3 -2
  29. package/dist_ts/classes.claudecodelocks.d.ts +13 -0
  30. package/dist_ts/classes.claudecodelocks.js +75 -3
  31. package/dist_ts/classes.claudenative.d.ts +130 -0
  32. package/dist_ts/classes.claudenative.js +708 -0
  33. package/dist_ts/classes.claudestatus.d.ts +9 -0
  34. package/dist_ts/classes.claudestatus.js +26 -9
  35. package/dist_ts/classes.claudetokenrefresh.d.ts +15 -1
  36. package/dist_ts/classes.claudetokenrefresh.js +74 -21
  37. package/dist_ts/classes.codexharness.js +4 -2
  38. package/dist_ts/classes.codexmanaged.d.ts +12 -1
  39. package/dist_ts/classes.codexmanaged.js +75 -14
  40. package/dist_ts/classes.codexpreuse.d.ts +8 -0
  41. package/dist_ts/classes.codexpreuse.js +24 -6
  42. package/dist_ts/classes.fileharness.d.ts +2 -0
  43. package/dist_ts/classes.fileharness.js +6 -4
  44. package/dist_ts/classes.limits.js +12 -7
  45. package/dist_ts/classes.opencodeharness.js +2 -2
  46. package/dist_ts/claudehttp.d.ts +10 -0
  47. package/dist_ts/claudehttp.js +29 -3
  48. package/dist_ts/index.d.ts +1 -0
  49. package/dist_ts/index.js +7 -1
  50. package/dist_ts/interfaces.harness.d.ts +6 -0
  51. package/dist_ts/interfaces.list.d.ts +9 -3
  52. package/dist_ts/plugins.d.ts +6 -3
  53. package/dist_ts/plugins.js +8 -4
  54. package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -0
  55. package/dist_ts/ts_migration/0001_authority_meta.js +44 -0
  56. package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
  57. package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
  58. package/dist_ts/ts_migration/index.d.ts +6 -0
  59. package/dist_ts/ts_migration/index.js +12 -0
  60. package/package.json +3 -3
  61. package/readme.md +177 -14
  62. package/ts/00_commitinfo_data.ts +1 -1
  63. package/ts/authority-contract.ts +262 -9
  64. package/ts/authority-runtime-contract.ts +14 -0
  65. package/ts/classes.accountlist.ts +3 -2
  66. package/ts/classes.authoritybroker.ts +385 -121
  67. package/ts/classes.authoritycli.ts +719 -0
  68. package/ts/classes.authorityclient.ts +75 -18
  69. package/ts/classes.authoritydaemon.ts +444 -36
  70. package/ts/classes.authoritydatabase.ts +1076 -75
  71. package/ts/classes.authoritymodels.ts +642 -144
  72. package/ts/classes.authoritypreuse.ts +296 -0
  73. package/ts/classes.authorityregistry.ts +47 -0
  74. package/ts/classes.authoritysecrets.ts +1 -1
  75. package/ts/classes.authorityusage.ts +527 -0
  76. package/ts/classes.claudeauthority.ts +537 -0
  77. package/ts/classes.claudecodeharness.ts +2 -1
  78. package/ts/classes.claudecodelocks.ts +70 -1
  79. package/ts/classes.claudenative.ts +785 -0
  80. package/ts/classes.claudestatus.ts +38 -9
  81. package/ts/classes.claudetokenrefresh.ts +82 -19
  82. package/ts/classes.codexharness.ts +3 -1
  83. package/ts/classes.codexmanaged.ts +76 -13
  84. package/ts/classes.codexpreuse.ts +33 -5
  85. package/ts/classes.fileharness.ts +7 -3
  86. package/ts/classes.limits.ts +11 -6
  87. package/ts/classes.opencodeharness.ts +1 -1
  88. package/ts/claudehttp.ts +31 -2
  89. package/ts/index.ts +6 -0
  90. package/ts/interfaces.harness.ts +6 -0
  91. package/ts/interfaces.list.ts +9 -3
  92. package/ts/plugins.ts +8 -3
  93. package/ts/ts_migration/0001_authority_meta.ts +45 -0
  94. package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
  95. package/ts/ts_migration/index.ts +12 -0
@@ -0,0 +1,527 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { IClaudeLogin, IClaudeStatusRead } from './classes.claudestatus.js';
3
+ import type { IHarnessAccountStatus } from './interfaces.harness.js';
4
+
5
+ type TRateLimits = plugins.flexAccounts.ISmartAiProviderAccountRateLimits;
6
+ type TAccessCredential = plugins.flexAccounts.IOpenAiChatGptAccessCredential;
7
+
8
+ export type TAuthSwitchUsageProblem =
9
+ | 'none' | 'unsupported' | 'access_unavailable' | 'provider_unavailable'
10
+ | 'provider_rejected' | 'profile_unavailable' | 'usage_unavailable'
11
+ | 'context_changed' | 'expired';
12
+
13
+ export interface IAuthSwitchNativeUsageSource {
14
+ homeId: string;
15
+ credentialDigest: string;
16
+ configDigest: string;
17
+ accessTokenDigest: string;
18
+ }
19
+
20
+ /** A credential-free, exact view of the account and login used for one usage read. */
21
+ export interface IAuthSwitchUsageContext {
22
+ epoch: string;
23
+ accountId: string;
24
+ loginId: string;
25
+ subjectId: string;
26
+ workspaceId: string;
27
+ providerId: string;
28
+ purpose: string;
29
+ owner: string;
30
+ grantState: string;
31
+ removed: boolean;
32
+ authorizationGeneration: number;
33
+ grantGeneration: number;
34
+ nativeSource: IAuthSwitchNativeUsageSource | null;
35
+ }
36
+
37
+ /** A bounded, credential-free projection; it is independent of the legacy harness API. */
38
+ export interface IAuthSwitchClaudeStatus {
39
+ plan: { name: string; source: 'live' | 'stored' } | null;
40
+ windows: { label: string; scope: 'account' | 'feature'; durationSeconds: number;
41
+ usedPercent: number; resetAt: string | null; severity?: 'normal' | 'warning' | 'critical';
42
+ headline?: true }[] | null;
43
+ facts: { section: string | null; label: string; value: string;
44
+ summaryKey?: 'subscription' | 'billing' | 'usageWindows' | 'resets' | 'resetDetails' }[];
45
+ problems: string[];
46
+ rateLimit: { retryAt: string | null } | null;
47
+ }
48
+
49
+ export type TAuthSwitchUsageReading =
50
+ | { kind: 'openai'; rateLimits: TRateLimits }
51
+ | { kind: 'anthropic'; status: IAuthSwitchClaudeStatus };
52
+
53
+ export interface IAuthSwitchUsageAccess {
54
+ accessToken: string;
55
+ accountId: string;
56
+ isFedrampAccount: boolean;
57
+ expiresAt: string;
58
+ grantGeneration: number;
59
+ }
60
+
61
+ /** Persist through NoSQLDB in the authority integration. No credential belongs in this record. */
62
+ export interface IAuthSwitchStoredUsage {
63
+ context: IAuthSwitchUsageContext;
64
+ updateId: string;
65
+ reading: TAuthSwitchUsageReading | null;
66
+ observedAt: string | null;
67
+ currentUntil: string | null;
68
+ staleUntil: string | null;
69
+ lastAttemptAt: string;
70
+ nextAttemptAt: string;
71
+ failures: number;
72
+ problem: Exclude<TAuthSwitchUsageProblem, 'context_changed' | 'expired'>;
73
+ }
74
+
75
+ export interface IAuthSwitchUsageSnapshot {
76
+ accountId: string;
77
+ loginId: string;
78
+ state: 'current' | 'partial' | 'stale' | 'unavailable';
79
+ reading: TAuthSwitchUsageReading | null;
80
+ observedAt: string | null;
81
+ currentUntil: string | null;
82
+ staleUntil: string | null;
83
+ lastAttemptAt: string | null;
84
+ nextAttemptAt: string | null;
85
+ problem: TAuthSwitchUsageProblem;
86
+ }
87
+
88
+ export interface IAuthSwitchUsageStore {
89
+ /** Read only the exact login's record. The helper discards a mismatched context. */
90
+ read(accountId: string, loginId: string): Promise<IAuthSwitchStoredUsage | null>;
91
+ /**
92
+ * In one NoSQLDB transaction, re-read epoch/account/login and compare every semantic
93
+ * context field, compare the previous usage update ID, write next, and append a revisioned
94
+ * account event. The account row revision may increase in that transaction; it is not
95
+ * part of the usage validity key and must not invalidate the reading just published.
96
+ * A null previousUpdateId also permits replacing a record for a different context;
97
+ * it must reject a concurrently written record for this same context.
98
+ * Return false on either conflict; do not publish an old provider callback.
99
+ */
100
+ publishIfCurrent(context: IAuthSwitchUsageContext, previousUpdateId: string | null,
101
+ next: IAuthSwitchStoredUsage): Promise<boolean>;
102
+ }
103
+
104
+ export interface IAuthSwitchAuthorityUsageOptions {
105
+ /** Re-read from the owning NoSQLDB authority. Return null for an absent login. */
106
+ readContext(accountId: string, loginId: string): Promise<IAuthSwitchUsageContext | null>;
107
+ /** Broker-owned access resolution; rejected generation may perform at most one durable refresh. */
108
+ resolveAccess(context: IAuthSwitchUsageContext, rejectedGrantGeneration: number | undefined,
109
+ minValidityMs: number): Promise<IAuthSwitchUsageAccess>;
110
+ resolveClaudeLogin?(context: IAuthSwitchUsageContext, rejectedGrantGeneration: number | undefined,
111
+ minValidityMs: number): Promise<{ login: IClaudeLogin; grantGeneration: number; expiresAt: string | null }>;
112
+ claudeStatus?: Pick<{ readDetailed(login: IClaudeLogin): Promise<IClaudeStatusRead> }, 'readDetailed'>;
113
+ /** The callback holds the qualified vendor lease through the whole read and publication. */
114
+ withNativeSource?(context: IAuthSwitchUsageContext,
115
+ action: (source: { context: IAuthSwitchUsageContext; login: IClaudeLogin;
116
+ readContext(): Promise<IAuthSwitchUsageContext | null> }) => Promise<IAuthSwitchUsageSnapshot>): Promise<IAuthSwitchUsageSnapshot>;
117
+ store: IAuthSwitchUsageStore;
118
+ provider: Pick<plugins.flexAccounts.ISmartAiProviderAdapter, 'getAccountRateLimits'>;
119
+ now?: () => number;
120
+ currentMaxAgeMs?: number;
121
+ maxStaleAgeMs?: number;
122
+ minAttemptIntervalMs?: number;
123
+ retryBaseMs?: number;
124
+ retryMaxMs?: number;
125
+ providerTimeoutMs?: number;
126
+ minAccessValidityMs?: number;
127
+ }
128
+
129
+ const defaults = {
130
+ currentMaxAgeMs: 60_000,
131
+ maxStaleAgeMs: 300_000,
132
+ minAttemptIntervalMs: 15_000,
133
+ retryBaseMs: 30_000,
134
+ retryMaxMs: 300_000,
135
+ providerTimeoutMs: 15_000,
136
+ minAccessValidityMs: 60_000,
137
+ };
138
+
139
+ const sameContext = (left: IAuthSwitchUsageContext, right: IAuthSwitchUsageContext): boolean =>
140
+ left.epoch === right.epoch && left.accountId === right.accountId && left.loginId === right.loginId
141
+ && left.subjectId === right.subjectId && left.workspaceId === right.workspaceId
142
+ && left.providerId === right.providerId
143
+ && left.purpose === right.purpose && left.owner === right.owner && left.grantState === right.grantState
144
+ && left.removed === right.removed && left.authorizationGeneration === right.authorizationGeneration
145
+ && left.grantGeneration === right.grantGeneration
146
+ && (left.nativeSource === null ? right.nativeSource === null
147
+ : right.nativeSource !== null && left.nativeSource.homeId === right.nativeSource.homeId
148
+ && left.nativeSource.credentialDigest === right.nativeSource.credentialDigest
149
+ && left.nativeSource.configDigest === right.nativeSource.configDigest
150
+ && left.nativeSource.accessTokenDigest === right.nativeSource.accessTokenDigest);
151
+
152
+ const sameGrantAuthority = (left: IAuthSwitchUsageContext, right: IAuthSwitchUsageContext): boolean =>
153
+ left.epoch === right.epoch && left.accountId === right.accountId && left.loginId === right.loginId
154
+ && left.subjectId === right.subjectId && left.workspaceId === right.workspaceId
155
+ && left.providerId === right.providerId
156
+ && left.purpose === right.purpose && left.owner === right.owner
157
+ && left.authorizationGeneration === right.authorizationGeneration && !right.removed
158
+ && (left.nativeSource === null ? right.nativeSource === null : sameContext(left, right));
159
+
160
+ const usable = (context: IAuthSwitchUsageContext): boolean =>
161
+ !context.removed && ((context.providerId === 'openai' && context.purpose === 'openai_managed'
162
+ && context.owner === 'daemon' && context.nativeSource === null
163
+ && (context.grantState === 'ready' || context.grantState === 'retry_wait'))
164
+ || (context.providerId === 'anthropic' && context.purpose === 'claude_host_native'
165
+ && ((context.owner === 'daemon' && context.nativeSource === null
166
+ && (context.grantState === 'ready' || context.grantState === 'retry_wait'))
167
+ || (context.owner === 'claude_native' && context.nativeSource !== null
168
+ && context.grantState === 'native'))));
169
+
170
+ const timestamp = (millis: number): string => new Date(millis).toISOString();
171
+ const future = (value: string | null, now: number): boolean => value !== null && Date.parse(value) > now;
172
+
173
+ const safeText = (value: string): string => {
174
+ const text = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, '');
175
+ if (Array.from(text).length > 500) throw new Error('Claude status text exceeds the authority projection bound.');
176
+ return text;
177
+ };
178
+ const publicClaudeStatus = (source: IHarnessAccountStatus): IAuthSwitchClaudeStatus => {
179
+ if ((source.summary?.usageWindows?.length ?? 0) > 128 || source.facts.length > 128
180
+ || source.problems.length > 32) throw new Error('Claude status exceeds the authority projection bound.');
181
+ return {
182
+ plan: source.summary?.subscription
183
+ ? { name: safeText(source.summary.subscription.plan), source: source.summary.subscription.source } : null,
184
+ windows: source.summary?.usageWindows?.map(window => ({
185
+ label: safeText(window.label), scope: window.scope ?? 'account',
186
+ durationSeconds: window.durationSeconds, usedPercent: window.usedPercent, resetAt: window.resetAt,
187
+ ...(window.severity ? { severity: window.severity } : {}),
188
+ ...(window.headline ? { headline: true as const } : {}),
189
+ })) ?? null,
190
+ facts: source.facts.map(fact => ({
191
+ section: fact.section ? safeText(fact.section) : null,
192
+ label: safeText(fact.label), value: safeText(fact.value),
193
+ ...(fact.summaryKey ? { summaryKey: fact.summaryKey } : {}),
194
+ })),
195
+ problems: source.problems.map(safeText),
196
+ rateLimit: source.rateLimit ? { retryAt: source.rateLimit.retryAt } : null,
197
+ };
198
+ };
199
+
200
+ /** Allowlist the provider projection before it enters a management DTO or stored usage record. */
201
+ const publicRateLimits = (source: TRateLimits): TRateLimits => {
202
+ const window = (value: plugins.flexAccounts.ISmartAiProviderRateLimitWindow) => ({
203
+ usedPercent: value.usedPercent, limitWindowSeconds: value.limitWindowSeconds,
204
+ resetAfterSeconds: value.resetAfterSeconds, resetsAt: value.resetsAt,
205
+ });
206
+ const detail = (value: plugins.flexAccounts.ISmartAiProviderRateLimitDetails) => ({
207
+ allowed: value.allowed, limitReached: value.limitReached,
208
+ ...(value.primaryWindow ? { primaryWindow: window(value.primaryWindow) } : {}),
209
+ ...(value.secondaryWindow ? { secondaryWindow: window(value.secondaryWindow) } : {}),
210
+ });
211
+ return {
212
+ providerId: 'openai', plan: source.plan, observedAt: source.observedAt,
213
+ ...(source.rateLimit ? { rateLimit: detail(source.rateLimit) } : {}),
214
+ additionalRateLimits: source.additionalRateLimits.map(item => ({
215
+ limitName: item.limitName, meteredFeature: item.meteredFeature,
216
+ ...(item.rateLimit ? { rateLimit: detail(item.rateLimit) } : {}),
217
+ })),
218
+ ...(source.rateLimitReachedType === undefined ? {} : { rateLimitReachedType: source.rateLimitReachedType }),
219
+ ...(source.rateLimitResetCreditsAvailableCount === undefined ? {}
220
+ : { rateLimitResetCreditsAvailableCount: source.rateLimitResetCreditsAvailableCount }),
221
+ };
222
+ };
223
+
224
+ /** On-demand daemon cache coordinator. The injected store is the only publication authority. */
225
+ export class AuthSwitchAuthorityUsage {
226
+ private readonly now: () => number;
227
+ private readonly policy: typeof defaults;
228
+ private readonly pending = new Map<string, Promise<IAuthSwitchUsageSnapshot>>();
229
+ private readonly admitted = new Set<Promise<IAuthSwitchUsageSnapshot>>();
230
+ private closed = false;
231
+ private closing?: Promise<void>;
232
+
233
+ constructor(private readonly options: IAuthSwitchAuthorityUsageOptions) {
234
+ this.now = options.now ?? Date.now;
235
+ this.policy = {
236
+ currentMaxAgeMs: options.currentMaxAgeMs ?? defaults.currentMaxAgeMs,
237
+ maxStaleAgeMs: options.maxStaleAgeMs ?? defaults.maxStaleAgeMs,
238
+ minAttemptIntervalMs: options.minAttemptIntervalMs ?? defaults.minAttemptIntervalMs,
239
+ retryBaseMs: options.retryBaseMs ?? defaults.retryBaseMs,
240
+ retryMaxMs: options.retryMaxMs ?? defaults.retryMaxMs,
241
+ providerTimeoutMs: options.providerTimeoutMs ?? defaults.providerTimeoutMs,
242
+ minAccessValidityMs: options.minAccessValidityMs ?? defaults.minAccessValidityMs,
243
+ };
244
+ if (Object.values(this.policy).some(value => !Number.isSafeInteger(value) || value < 1 || value > 3_600_000)
245
+ || this.policy.maxStaleAgeMs <= this.policy.currentMaxAgeMs
246
+ || this.policy.retryMaxMs < this.policy.retryBaseMs) {
247
+ throw new Error('Invalid authority usage freshness policy.');
248
+ }
249
+ }
250
+
251
+ private unavailable(accountId: string, loginId: string, problem: TAuthSwitchUsageProblem): IAuthSwitchUsageSnapshot {
252
+ return { accountId, loginId, state: 'unavailable', reading: null, observedAt: null,
253
+ currentUntil: null, staleUntil: null, lastAttemptAt: null, nextAttemptAt: null, problem };
254
+ }
255
+
256
+ private snapshot(context: IAuthSwitchUsageContext, record: IAuthSwitchStoredUsage | null,
257
+ now = this.now()): IAuthSwitchUsageSnapshot {
258
+ if (!record || !sameContext(context, record.context)) {
259
+ return this.unavailable(context.accountId, context.loginId, 'expired');
260
+ }
261
+ const state = record.reading && future(record.currentUntil, now)
262
+ ? record.problem === 'none' ? 'current'
263
+ : record.reading.kind === 'anthropic' && ['profile_unavailable', 'usage_unavailable'].includes(record.problem)
264
+ ? 'partial' : 'stale'
265
+ : record.reading && future(record.staleUntil, now) ? 'stale' : 'unavailable';
266
+ return {
267
+ accountId: context.accountId, loginId: context.loginId, state,
268
+ reading: state === 'unavailable' ? null : record.reading,
269
+ observedAt: state === 'unavailable' ? null : record.observedAt,
270
+ currentUntil: record.currentUntil, staleUntil: record.staleUntil,
271
+ lastAttemptAt: record.lastAttemptAt, nextAttemptAt: record.nextAttemptAt,
272
+ problem: state === 'unavailable' && record.reading && !future(record.staleUntil, now)
273
+ ? 'expired' : record.problem,
274
+ };
275
+ }
276
+
277
+ private async currentContext(accountId: string, loginId: string): Promise<IAuthSwitchUsageContext | null> {
278
+ try { return await this.options.readContext(accountId, loginId); }
279
+ catch { return null; }
280
+ }
281
+
282
+ public get(accountId: string, loginId: string, force = false): Promise<IAuthSwitchUsageSnapshot> {
283
+ if (this.closed) return Promise.reject(new Error('Authority usage helper is closed.'));
284
+ const request = this.read(accountId, loginId, force);
285
+ this.admitted.add(request);
286
+ void request.finally(() => this.admitted.delete(request)).catch(() => undefined);
287
+ return request;
288
+ }
289
+
290
+ private async read(accountId: string, loginId: string, force: boolean): Promise<IAuthSwitchUsageSnapshot> {
291
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
292
+ throw new Error('Invalid authority usage identity.');
293
+ }
294
+ const context = await this.currentContext(accountId, loginId);
295
+ if (!context) return this.unavailable(accountId, loginId, 'access_unavailable');
296
+ if (context.accountId !== accountId || context.loginId !== loginId) {
297
+ return this.unavailable(accountId, loginId, 'context_changed');
298
+ }
299
+ if (context.providerId === 'anthropic' && context.purpose === 'claude_host_native'
300
+ && context.owner === 'claude_native' && context.grantState === 'native') {
301
+ if (!this.options.withNativeSource) return this.unavailable(accountId, loginId, 'access_unavailable');
302
+ try {
303
+ return await this.options.withNativeSource(context, async source => {
304
+ const readCurrent = async () => {
305
+ try { return await source.readContext(); } catch { return null; }
306
+ };
307
+ return this.readWithContext(source.context, force, readCurrent, source.login);
308
+ });
309
+ } catch { return this.unavailable(accountId, loginId, 'access_unavailable'); }
310
+ }
311
+ return this.readWithContext(context, force, () => this.currentContext(accountId, loginId), null);
312
+ }
313
+
314
+ private readWithContext(context: IAuthSwitchUsageContext, force: boolean,
315
+ readCurrent: () => Promise<IAuthSwitchUsageContext | null>, nativeLogin: IClaudeLogin | null): Promise<IAuthSwitchUsageSnapshot> {
316
+ if (!usable(context)) return Promise.resolve(this.unavailable(context.accountId, context.loginId,
317
+ context.removed || ['handoff_pending', 'handoff_quarantined', 'legacy_native_pending'].includes(context.grantState)
318
+ ? 'access_unavailable' : 'unsupported'));
319
+ const key = JSON.stringify([context.epoch, context.accountId, context.loginId,
320
+ context.subjectId, context.workspaceId,
321
+ context.authorizationGeneration, context.grantGeneration, context.owner, context.grantState,
322
+ context.nativeSource]);
323
+ const existing = this.pending.get(key);
324
+ if (existing) return existing;
325
+ const task = this.readForContext(context, force, readCurrent, nativeLogin).finally(() => {
326
+ if (this.pending.get(key) === task) this.pending.delete(key);
327
+ });
328
+ this.pending.set(key, task);
329
+ return task;
330
+ }
331
+
332
+ /** Coalesce the entire read/decision/fetch lifecycle, including the first stored read. */
333
+ private async readForContext(context: IAuthSwitchUsageContext, force: boolean,
334
+ readCurrent: () => Promise<IAuthSwitchUsageContext | null>, nativeLogin: IClaudeLogin | null): Promise<IAuthSwitchUsageSnapshot> {
335
+ let stored: IAuthSwitchStoredUsage | null;
336
+ try { stored = await this.options.store.read(context.accountId, context.loginId); }
337
+ catch { return this.unavailable(context.accountId, context.loginId, 'access_unavailable'); }
338
+ const record = stored && sameContext(context, stored.context) ? stored : null;
339
+ const verified = await readCurrent();
340
+ if (!verified || !sameContext(context, verified)) {
341
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
342
+ }
343
+ const now = this.now();
344
+ const cached = this.snapshot(context, record, now);
345
+ if (record && future(record.nextAttemptAt, now)) return cached;
346
+ if (cached.state === 'current' && !force) return cached;
347
+ return this.refresh(context, record, readCurrent, nativeLogin);
348
+ }
349
+
350
+ private async refresh(initial: IAuthSwitchUsageContext,
351
+ initialRecord: IAuthSwitchStoredUsage | null,
352
+ readCurrent: () => Promise<IAuthSwitchUsageContext | null>,
353
+ nativeLogin: IClaudeLogin | null): Promise<IAuthSwitchUsageSnapshot> {
354
+ let context = initial;
355
+ let previous = initialRecord;
356
+ let reading: TAuthSwitchUsageReading | null = null;
357
+ let problem: IAuthSwitchStoredUsage['problem'] = 'none';
358
+ try {
359
+ if (context.providerId === 'openai') {
360
+ let access = await this.options.resolveAccess(context, undefined, this.policy.minAccessValidityMs);
361
+ const resolved = await readCurrent();
362
+ if (!resolved || !sameGrantAuthority(context, resolved) || !usable(resolved)
363
+ || resolved.grantGeneration !== access.grantGeneration) {
364
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
365
+ }
366
+ context = resolved;
367
+ if (!this.validAccess(context, access)) throw new Error('Access grant is unavailable.');
368
+ if (!sameContext(initial, context)) {
369
+ const stored = await this.options.store.read(context.accountId, context.loginId);
370
+ previous = stored && sameContext(context, stored.context) ? stored : null;
371
+ }
372
+ let response: Awaited<ReturnType<IAuthSwitchAuthorityUsageOptions['provider']['getAccountRateLimits']>>;
373
+ try { response = await this.query(access); }
374
+ catch { problem = 'provider_unavailable'; response = { success: false, errorCode: 'ACCOUNT_RATE_LIMITS_FAILED' }; }
375
+ if (!response.success && response.errorCode === 'CREDENTIAL_REFRESH_REQUIRED') {
376
+ const beforeRotation = await readCurrent();
377
+ if (!beforeRotation || !sameContext(context, beforeRotation)) {
378
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
379
+ }
380
+ access = await this.options.resolveAccess(context, access.grantGeneration, this.policy.minAccessValidityMs);
381
+ const rotated = await readCurrent();
382
+ if (!rotated || !sameGrantAuthority(context, rotated) || !usable(rotated)
383
+ || rotated.grantGeneration !== access.grantGeneration
384
+ || rotated.grantGeneration <= context.grantGeneration) {
385
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
386
+ }
387
+ context = rotated;
388
+ if (!this.validAccess(context, access)) throw new Error('Access grant is unavailable.');
389
+ const stored = await this.options.store.read(context.accountId, context.loginId);
390
+ previous = stored && sameContext(context, stored.context) ? stored : null;
391
+ try { response = await this.query(access); }
392
+ catch { problem = 'provider_unavailable'; response = { success: false, errorCode: 'ACCOUNT_RATE_LIMITS_FAILED' }; }
393
+ }
394
+ if (response.success) reading = { kind: 'openai', rateLimits: publicRateLimits(response.rateLimits) };
395
+ else if (problem === 'none') problem = response.errorCode === 'CREDENTIAL_REFRESH_REQUIRED'
396
+ ? 'provider_rejected' : 'provider_unavailable';
397
+ } else if (context.providerId === 'anthropic' && this.options.claudeStatus) {
398
+ let login = nativeLogin;
399
+ let grantGeneration = context.grantGeneration;
400
+ if (context.owner === 'daemon') {
401
+ if (!this.options.resolveClaudeLogin) throw new Error('Claude status access is unavailable.');
402
+ const access = await this.options.resolveClaudeLogin(context, undefined, this.policy.minAccessValidityMs);
403
+ login = access.login;
404
+ grantGeneration = access.grantGeneration;
405
+ if (login.scopes.includes('user:profile')
406
+ && !future(access.expiresAt, this.now() + this.policy.minAccessValidityMs)) {
407
+ throw new Error('Claude status access is not fresh enough.');
408
+ }
409
+ }
410
+ const resolved = await readCurrent();
411
+ if (!resolved || !sameGrantAuthority(context, resolved) || !usable(resolved)
412
+ || resolved.grantGeneration !== grantGeneration || !login) {
413
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
414
+ }
415
+ context = resolved;
416
+ if (!sameContext(initial, context)) {
417
+ const stored = await this.options.store.read(context.accountId, context.loginId);
418
+ previous = stored && sameContext(context, stored.context) ? stored : null;
419
+ }
420
+ if (!login.scopes.includes('user:profile')) {
421
+ problem = 'unsupported';
422
+ } else {
423
+ let result = await this.options.claudeStatus.readDetailed(login);
424
+ if (result.credentialRejected && context.owner === 'daemon') {
425
+ const beforeRotation = await readCurrent();
426
+ if (!beforeRotation || !sameContext(context, beforeRotation)) {
427
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
428
+ }
429
+ const rotated = await this.options.resolveClaudeLogin!(context, grantGeneration,
430
+ this.policy.minAccessValidityMs);
431
+ const updated = await readCurrent();
432
+ if (!updated || !sameGrantAuthority(context, updated) || !usable(updated)
433
+ || updated.grantGeneration !== rotated.grantGeneration
434
+ || updated.grantGeneration <= grantGeneration
435
+ || (rotated.login.scopes.includes('user:profile')
436
+ && !future(rotated.expiresAt, this.now() + this.policy.minAccessValidityMs))) {
437
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
438
+ }
439
+ context = updated;
440
+ login = rotated.login;
441
+ const stored = await this.options.store.read(context.accountId, context.loginId);
442
+ previous = stored && sameContext(context, stored.context) ? stored : null;
443
+ if (login.scopes.includes('user:profile')) {
444
+ result = await this.options.claudeStatus.readDetailed(login);
445
+ } else {
446
+ problem = 'unsupported';
447
+ }
448
+ }
449
+ if (problem !== 'unsupported' && result.profile === 'mismatch') {
450
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
451
+ } else if (problem !== 'unsupported' && result.credentialRejected) problem = 'provider_rejected';
452
+ else if (problem !== 'unsupported' && context.owner === 'claude_native' && result.profile !== 'verified') {
453
+ problem = 'profile_unavailable';
454
+ } else if (problem !== 'unsupported') {
455
+ try {
456
+ reading = { kind: 'anthropic', status: publicClaudeStatus(result.status) };
457
+ problem = result.profile !== 'verified' ? 'profile_unavailable'
458
+ : result.usage !== 'available' ? 'usage_unavailable' : 'none';
459
+ } catch { problem = 'unsupported'; }
460
+ }
461
+ }
462
+ } else {
463
+ problem = 'unsupported';
464
+ }
465
+ } catch {
466
+ problem = 'access_unavailable';
467
+ }
468
+ const latest = await readCurrent();
469
+ if (!latest || !sameContext(context, latest) || !usable(latest)) {
470
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
471
+ }
472
+ const now = this.now();
473
+ const failures = reading && problem === 'none' ? 0 : Math.min((previous?.failures ?? 0) + 1, 16);
474
+ const retryMs = Math.min(this.policy.retryBaseMs * 2 ** Math.min(failures - 1, 10), this.policy.retryMaxMs);
475
+ const retained = !reading && previous?.reading && future(previous.staleUntil, now) ? previous : null;
476
+ const next: IAuthSwitchStoredUsage = {
477
+ context, updateId: plugins.crypto.randomUUID(),
478
+ reading: reading ?? retained?.reading ?? null,
479
+ observedAt: reading ? timestamp(now) : retained?.observedAt ?? null,
480
+ currentUntil: reading ? timestamp(now + this.policy.currentMaxAgeMs) : retained?.currentUntil ?? null,
481
+ staleUntil: reading ? timestamp(now + this.policy.maxStaleAgeMs) : retained?.staleUntil ?? null,
482
+ lastAttemptAt: timestamp(now),
483
+ nextAttemptAt: timestamp(now + (reading && problem === 'none' ? this.policy.minAttemptIntervalMs : retryMs)),
484
+ failures, problem,
485
+ };
486
+ try {
487
+ if (!await this.options.store.publishIfCurrent(context, previous?.updateId ?? null, next)) {
488
+ const current = await readCurrent();
489
+ if (!current || !sameContext(context, current)) {
490
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
491
+ }
492
+ const stored = await this.options.store.read(context.accountId, context.loginId);
493
+ const confirmed = await readCurrent();
494
+ if (!confirmed || !sameContext(context, confirmed)) {
495
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
496
+ }
497
+ return this.snapshot(context, stored);
498
+ }
499
+ } catch { return this.unavailable(context.accountId, context.loginId, 'access_unavailable'); }
500
+ const confirmed = await readCurrent();
501
+ if (!confirmed || !sameContext(context, confirmed)) {
502
+ return this.unavailable(context.accountId, context.loginId, 'context_changed');
503
+ }
504
+ return this.snapshot(context, next);
505
+ }
506
+
507
+ private validAccess(context: IAuthSwitchUsageContext, access: IAuthSwitchUsageAccess): boolean {
508
+ return Boolean(access.accessToken) && access.accountId === context.workspaceId
509
+ && access.grantGeneration === context.grantGeneration
510
+ && future(access.expiresAt, this.now() + this.policy.minAccessValidityMs);
511
+ }
512
+
513
+ private query(access: IAuthSwitchUsageAccess): ReturnType<IAuthSwitchAuthorityUsageOptions['provider']['getAccountRateLimits']> {
514
+ const credential: TAccessCredential = { kind: 'chatgptAccess', providerId: 'openai',
515
+ accessToken: access.accessToken, accountId: access.accountId,
516
+ isFedrampAccount: access.isFedrampAccount };
517
+ return this.options.provider.getAccountRateLimits(credential, { timeoutMs: this.policy.providerTimeoutMs });
518
+ }
519
+
520
+ /** Wait for admitted reads before the daemon disposes the shared provider or database. */
521
+ public close(): Promise<void> {
522
+ if (this.closing) return this.closing;
523
+ this.closed = true;
524
+ this.closing = Promise.allSettled([...this.admitted]).then(() => undefined);
525
+ return this.closing;
526
+ }
527
+ }