@modelprofile.com/authswitch 9.1.0 → 10.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 (75) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/authority-contract.d.ts +56 -6
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-runtime-contract.d.ts +29 -0
  5. package/dist_ts/classes.authoritybroker.d.ts +36 -4
  6. package/dist_ts/classes.authoritybroker.js +71 -12
  7. package/dist_ts/classes.authoritycli.d.ts +4 -1
  8. package/dist_ts/classes.authoritycli.js +42 -7
  9. package/dist_ts/classes.authorityclient.d.ts +15 -2
  10. package/dist_ts/classes.authorityclient.js +31 -8
  11. package/dist_ts/classes.authoritydaemon.d.ts +17 -0
  12. package/dist_ts/classes.authoritydaemon.js +50 -14
  13. package/dist_ts/classes.authoritydatabase.d.ts +14 -3
  14. package/dist_ts/classes.authoritydatabase.js +25 -10
  15. package/dist_ts/classes.authorityimport.d.ts +4 -4
  16. package/dist_ts/classes.authorityimport.js +8 -7
  17. package/dist_ts/classes.authoritymodels.d.ts +11 -3
  18. package/dist_ts/classes.authoritymodels.js +43 -13
  19. package/dist_ts/classes.authoritypreuse.d.ts +8 -2
  20. package/dist_ts/classes.authoritypreuse.js +27 -17
  21. package/dist_ts/classes.authorityusage.d.ts +4 -3
  22. package/dist_ts/classes.authorityusage.js +3 -5
  23. package/dist_ts/classes.cli.js +3 -5
  24. package/dist_ts/classes.codexpreuse.d.ts +14 -3
  25. package/dist_ts/classes.codexpreuse.js +127 -44
  26. package/dist_ts/classes.codexstatus.js +44 -95
  27. package/dist_ts/classes.login.d.ts +20 -10
  28. package/dist_ts/classes.login.js +14 -13
  29. package/dist_ts/classes.openaiprovider.d.ts +53 -0
  30. package/dist_ts/classes.openaiprovider.js +340 -0
  31. package/dist_ts/classes.opencodeharness.js +3 -2
  32. package/dist_ts/classes.service.js +3 -5
  33. package/dist_ts/index.d.ts +2 -0
  34. package/dist_ts/index.js +3 -1
  35. package/dist_ts/interfaces.openai.d.ts +118 -0
  36. package/dist_ts/interfaces.openai.js +3 -0
  37. package/dist_ts/openaiauth.d.ts +76 -0
  38. package/dist_ts/openaiauth.js +371 -0
  39. package/dist_ts/openaiusage.d.ts +59 -0
  40. package/dist_ts/openaiusage.js +222 -0
  41. package/dist_ts/plugins.d.ts +1 -3
  42. package/dist_ts/plugins.js +2 -4
  43. package/dist_ts/preuse.d.ts +50 -0
  44. package/dist_ts/preuse.js +63 -4
  45. package/dist_ts/ts_migration/legacysources/shared.js +4 -3
  46. package/openai-codex-license.txt +201 -0
  47. package/package.json +6 -4
  48. package/readme.md +121 -26
  49. package/third-party-notices.md +29 -0
  50. package/ts/00_commitinfo_data.ts +1 -1
  51. package/ts/authority-contract.ts +60 -8
  52. package/ts/authority-runtime-contract.ts +30 -0
  53. package/ts/classes.authoritybroker.ts +80 -17
  54. package/ts/classes.authoritycli.ts +48 -6
  55. package/ts/classes.authorityclient.ts +32 -9
  56. package/ts/classes.authoritydaemon.ts +54 -14
  57. package/ts/classes.authoritydatabase.ts +28 -11
  58. package/ts/classes.authorityimport.ts +12 -12
  59. package/ts/classes.authoritymodels.ts +42 -14
  60. package/ts/classes.authoritypreuse.ts +30 -17
  61. package/ts/classes.authorityusage.ts +8 -9
  62. package/ts/classes.cli.ts +3 -3
  63. package/ts/classes.codexpreuse.ts +130 -40
  64. package/ts/classes.codexstatus.ts +40 -73
  65. package/ts/classes.login.ts +34 -21
  66. package/ts/classes.openaiprovider.ts +365 -0
  67. package/ts/classes.opencodeharness.ts +3 -2
  68. package/ts/classes.service.ts +3 -4
  69. package/ts/index.ts +2 -0
  70. package/ts/interfaces.openai.ts +142 -0
  71. package/ts/openaiauth.ts +422 -0
  72. package/ts/openaiusage.ts +247 -0
  73. package/ts/plugins.ts +1 -3
  74. package/ts/preuse.ts +88 -3
  75. package/ts/ts_migration/legacysources/shared.ts +4 -3
@@ -567,8 +567,8 @@ export class AuthSwitchAuthorityDatabase {
567
567
  const draft = change(structuredClone(current));
568
568
  const allowed: Record<IStoredAuthorityPreuseOperation['state'], IStoredAuthorityPreuseOperation['state'][]> = {
569
569
  preparing: ['request_may_have_started', 'not_sent'],
570
- request_may_have_started: ['complete', 'outcome_unknown'],
571
- complete: [], not_sent: [], outcome_unknown: [],
570
+ request_may_have_started: ['complete', 'refused', 'outcome_unknown'],
571
+ complete: [], not_sent: [], refused: [], outcome_unknown: [],
572
572
  };
573
573
  if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
574
574
  || draft.accountId !== current.accountId || draft.grantId !== current.grantId
@@ -1209,12 +1209,17 @@ export class AuthSwitchAuthorityDatabase {
1209
1209
  /**
1210
1210
  * Remove all managed grants in one transaction. Native owners must be stopped and handed off first, and a
1211
1211
  * runtime binding its holder releases must be released first; the daemon's own managed Codex bindings go
1212
- * with the account.
1212
+ * with the account. `retired` holds the sealed ChatGPT sign-ins the removal discarded, exactly as the attempt
1213
+ * that committed read them, so the caller can revoke them at the provider.
1213
1214
  */
1214
1215
  public async removeAccountAndGrants(updateId: string, accountId: string,
1215
- expectedRevision: number, statusObservedAt: string): Promise<IStoredAuthorityAccount> {
1216
+ expectedRevision: number, statusObservedAt: string): Promise<{ account: IStoredAuthorityAccount;
1217
+ retired: IStoredAuthorityGrant[] }> {
1218
+ // Collected afresh by every attempt of the transaction, so after it the list is the committed attempt's.
1219
+ let retired: IStoredAuthorityGrant[] = [];
1216
1220
  try {
1217
1221
  return await this.transaction(async session => {
1222
+ retired = [];
1218
1223
  const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1219
1224
  const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1220
1225
  if (!metaStored || !accountStored || accountStored.removed || accountStored.revision !== expectedRevision) {
@@ -1267,11 +1272,13 @@ export class AuthSwitchAuthorityDatabase {
1267
1272
  const listed = names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
1268
1273
  throw new AuthSwitchRefusal('account_busy', `This account still backs ${listed} runtime bindings. `
1269
1274
  + 'Stop those runtimes and release their bindings before removing the account. A holder that lost '
1270
- + 'its capability binds the same runtime and scope again and releases the capability that returns; '
1271
- + 'if binding again is refused, reauthenticate the account first.');
1275
+ + 'its capability unbinds its own runtime and scope instead, which needs no sign-in.');
1272
1276
  }
1273
1277
  await scan(async (grant, stored) => {
1274
1278
  if (grant.state === 'removed') return;
1279
+ if (grant.purpose === 'openai_managed' && grant.owner === 'daemon' && grant.ciphertext !== null) {
1280
+ retired.push(grant);
1281
+ }
1275
1282
  const draft: IStoredAuthorityGrant = {
1276
1283
  ...grant, state: 'removed', owner: 'none', ciphertext: null, accessExpiresAt: null,
1277
1284
  attemptId: null, retryAt: null, retryCount: 0, problem: 'none',
@@ -1312,13 +1319,13 @@ export class AuthSwitchAuthorityDatabase {
1312
1319
  await this.persistAccount(session, accountStored, removed);
1313
1320
  await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
1314
1321
  updateId, 'account', accountId);
1315
- return removed;
1322
+ return { account: removed, retired };
1316
1323
  });
1317
1324
  } catch (error) {
1318
1325
  if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1319
1326
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
1320
1327
  const account = await this.readAccount(accountId);
1321
- if (event && account?.removed && account.updateId === updateId) return account;
1328
+ if (event && account?.removed && account.updateId === updateId) return { account, retired };
1322
1329
  throw new Error('Authswitch account removal outcome is unknown; refresh before retrying.');
1323
1330
  }
1324
1331
  }
@@ -1349,9 +1356,16 @@ export class AuthSwitchAuthorityDatabase {
1349
1356
  }
1350
1357
  }
1351
1358
 
1359
+ /**
1360
+ * Binds a runtime and scope, or rebinds them. The binding's revision is the authority revision of the event
1361
+ * that writes it, handed to `change` as `revision`: the authority revision never repeats, so no two binds of
1362
+ * a runtime and scope ever share one -- not even across a release, which deletes the record, followed by a
1363
+ * bind that inserts it again -- and a holder that names a binding by the revision it read never names its
1364
+ * successor. Records written before carry smaller revisions, which a later bind therefore still exceeds.
1365
+ */
1352
1366
  public async changeBinding(updateId: string, bindingId: string, accountId: string, grantId: string,
1353
1367
  change: (draft: IStoredAuthorityBinding | null, account: IStoredAuthorityAccount,
1354
- grant: IStoredAuthorityGrant | null) => IStoredAuthorityBinding): Promise<{
1368
+ grant: IStoredAuthorityGrant | null, revision: number) => IStoredAuthorityBinding): Promise<{
1355
1369
  meta: IStoredAuthorityMeta; binding: IStoredAuthorityBinding;
1356
1370
  }> {
1357
1371
  try {
@@ -1363,8 +1377,11 @@ export class AuthSwitchAuthorityDatabase {
1363
1377
  const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
1364
1378
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
1365
1379
  const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
1366
- const draft = change(current ? structuredClone(AuthSwitchAuthorityBindingModel.exact.toPersisted(current)) : null, account, grant);
1367
- if (draft.id !== bindingId || draft.accountId !== accountId || draft.updateId !== updateId) throw new Error('Binding change has an invalid identity.');
1380
+ const revision = metaStored.revision + 1;
1381
+ const draft = change(current ? structuredClone(AuthSwitchAuthorityBindingModel.exact.toPersisted(current)) : null,
1382
+ account, grant, revision);
1383
+ if (draft.id !== bindingId || draft.accountId !== accountId || draft.updateId !== updateId
1384
+ || draft.revision !== revision) throw new Error('Binding change has an invalid identity.');
1368
1385
  if (draft.grantId !== grantId || grant?.accountId !== accountId) {
1369
1386
  throw new Error('Binding login does not belong to the account.');
1370
1387
  }
@@ -1,4 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
3
+ import { parseOpenAiTokenClaims, type IOpenAiTokenClaims } from './openaiauth.js';
4
+ import type { IOpenAiAccess, IOpenAiOAuthCredential, TOpenAiAccountRateLimitsResult, TOpenAiRefreshResult } from './interfaces.openai.js';
2
5
  import type { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
3
6
  import { authSwitchEnvironmentId } from './classes.operations.js';
4
7
  import { AuthSwitchRefusal } from './authority-contract.js';
@@ -55,14 +58,12 @@ export interface IAuthSwitchImportQuiescence {
55
58
  /**
56
59
  * Exactly the two provider operations an import needs, so nothing here can reach for a third.
57
60
  *
58
- * `OpenAiProviderAdapter` satisfies this; naming only what is used keeps the rotating call countable and
61
+ * `AuthSwitchOpenAiProvider` satisfies this; naming only what is used keeps the rotating call countable and
59
62
  * makes it visible that a projection never touches `refreshCredential`.
60
63
  */
61
64
  export interface IAuthSwitchImportOpenAiOperations {
62
- refreshCredential(credential: plugins.flexAccounts.IOpenAiChatGptOAuthCredential):
63
- Promise<plugins.flexAccounts.TSmartAiProviderRefreshResult>;
64
- getAccountRateLimits(credential: plugins.flexAccounts.TOpenAiChatGptReadCredential):
65
- Promise<plugins.flexAccounts.TSmartAiProviderAccountRateLimitsResult>;
65
+ refreshCredential(credential: IOpenAiOAuthCredential): Promise<TOpenAiRefreshResult>;
66
+ getAccountRateLimits(access: IOpenAiAccess): Promise<TOpenAiAccountRateLimitsResult>;
66
67
  }
67
68
 
68
69
  /** The non-rotating Claude proof: one profile read that must name the expected immutable identity. */
@@ -289,7 +290,7 @@ export class AuthSwitchAuthorityImport {
289
290
  this.env = options.env;
290
291
  this.homeDirectory = options.homeDirectory;
291
292
  this.now = options.now ?? Date.now;
292
- this.openAi = options.openAi ?? new plugins.flexAccounts.OpenAiProviderAdapter();
293
+ this.openAi = options.openAi ?? new AuthSwitchOpenAiProvider();
293
294
  this.claudeStatus = options.claudeStatus ?? new ClaudeAccountStatus();
294
295
  this.claudeTokens = options.claudeTokens ?? new ClaudeTokenRefresh();
295
296
  this.secrets = options.secrets ?? new AuthSwitchTpmSecretCodec();
@@ -426,8 +427,8 @@ export class AuthSwitchAuthorityImport {
426
427
 
427
428
  private externalIdentity(idToken: string): { accountId: string; subject: string; workspaceId: string;
428
429
  email: string | null; plan: string | null } {
429
- let info: ReturnType<typeof plugins.flexAuth.parseOpenAiChatGptTokenInfo>;
430
- try { info = plugins.flexAuth.parseOpenAiChatGptTokenInfo(idToken); }
430
+ let info: IOpenAiTokenClaims;
431
+ try { info = parseOpenAiTokenClaims(idToken); }
431
432
  catch { return refuse('The submitted login carries no readable ChatGPT identity.'); }
432
433
  if (!info.chatgptUserId || !info.chatgptAccountId) {
433
434
  refuse('The submitted login omits a stable ChatGPT user or account identity.');
@@ -618,15 +619,14 @@ export class AuthSwitchAuthorityImport {
618
619
  identity: { accountId: string; subject: string; workspaceId: string },
619
620
  material: TLegacySourceMaterial): Promise<void> {
620
621
  if (material.providerId === 'openai') {
621
- const result = await this.openAi.getAccountRateLimits({ kind: 'chatgptAccess',
622
- providerId: 'openai', accessToken: material.credential.accessToken,
622
+ const result = await this.openAi.getAccountRateLimits({ accessToken: material.credential.accessToken,
623
623
  accountId: identity.workspaceId, isFedrampAccount: false });
624
624
  if (!result.success) {
625
625
  refuse(`The stored access token could not be proven live (${result.errorCode}); let the native tool `
626
626
  + 'refresh its own store, then import again.');
627
627
  }
628
628
  // The claims of the very token that answered must still name this account.
629
- const claims = plugins.flexAuth.parseOpenAiChatGptTokenInfo(material.credential.accessToken);
629
+ const claims = parseOpenAiTokenClaims(material.credential.accessToken);
630
630
  if (claims.chatgptAccountId !== identity.workspaceId) {
631
631
  refuse('The stored access token belongs to another ChatGPT account.');
632
632
  }
@@ -773,7 +773,7 @@ export class AuthSwitchAuthorityImport {
773
773
  const result = await this.openAi.refreshCredential(material.credential);
774
774
  if (!result.success) return null;
775
775
  let accessExpiresAt: string | null = null;
776
- try { accessExpiresAt = plugins.flexAuth.parseOpenAiChatGptTokenInfo(result.credential.accessToken).expiresAt ?? null; }
776
+ try { accessExpiresAt = parseOpenAiTokenClaims(result.credential.accessToken).expiresAt ?? null; }
777
777
  catch { accessExpiresAt = null; }
778
778
  return { material: { providerId: 'openai', credential: { kind: 'chatgptOAuth', providerId: 'openai',
779
779
  accessToken: result.credential.accessToken, refreshToken: result.credential.refreshToken,
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
- import type { TAuthSwitchClaudeProofFailure, TAuthSwitchLoginPrompt } from './authority-contract.js';
2
+ import type { IAuthSwitchPreuseFailure, IAuthSwitchPreuseLimit, TAuthSwitchClaudeProofFailure,
3
+ TAuthSwitchLoginPrompt } from './authority-contract.js';
3
4
  import type { IAuthSwitchStoredUsage } from './classes.authorityusage.js';
4
5
 
5
6
  export interface IStoredAuthorityMeta {
@@ -225,7 +226,7 @@ export interface IStoredAuthorityDeviceOperation {
225
226
  export type TStoredAuthorityPreuseProblem =
226
227
  | 'none' | 'target_unavailable' | 'access_unavailable' | 'catalog_unavailable'
227
228
  | 'account_busy' | 'cancelled' | 'authority_closing' | 'interrupted'
228
- | 'request_uncertain';
229
+ | 'request_uncertain' | 'usage_limit' | 'rate_limit';
229
230
 
230
231
  /** One quota-consuming request. The prompt and generated prose never enter persistence. */
231
232
  export interface IStoredAuthorityPreuseOperation {
@@ -240,9 +241,13 @@ export interface IStoredAuthorityPreuseOperation {
240
241
  model: string | null;
241
242
  authorizationGeneration: number;
242
243
  grantGeneration: number | null;
243
- state: 'preparing' | 'request_may_have_started' | 'complete' | 'not_sent' | 'outcome_unknown';
244
+ state: 'preparing' | 'request_may_have_started' | 'complete' | 'not_sent' | 'refused' | 'outcome_unknown';
244
245
  result: { inputTokens: number | null; outputTokens: number | null; totalTokens: number | null } | null;
245
246
  problem: TStoredAuthorityPreuseProblem;
247
+ /** Present exactly when `problem` is `usage_limit` or `rate_limit`. */
248
+ limit?: IAuthSwitchPreuseLimit;
249
+ /** Present only when `problem` is `request_uncertain` and the request failed with a stated cause. */
250
+ failure?: IAuthSwitchPreuseFailure;
246
251
  createdAt: string;
247
252
  updatedAt: string;
248
253
  finishedAt: string | null;
@@ -271,6 +276,8 @@ export interface IStoredAuthorityOperationDocument {
271
276
  authorizationGeneration?: number;
272
277
  grantGeneration?: number | null;
273
278
  problem?: TStoredAuthorityPreuseProblem;
279
+ limit?: IAuthSwitchPreuseLimit;
280
+ failure?: IAuthSwitchPreuseFailure;
274
281
  createdAt: string;
275
282
  updatedAt: string;
276
283
  finishedAt: string | null;
@@ -603,13 +610,11 @@ const assertStoredAuthorityDeviceOperation: (value: unknown) => asserts value is
603
610
  : value.accountId === null || value.grantId === null || value.expectedGrantGeneration === null) {
604
611
  throw new Error('Invalid authswitch operation target.');
605
612
  }
606
- if (value.prompt !== null && (!object(value.prompt)
607
- || (value.prompt.flow === 'device'
608
- ? !exactKeys(value.prompt, ['flow', 'verificationUrl', 'userCode'])
609
- || !bounded(value.prompt.verificationUrl, 2048) || !bounded(value.prompt.userCode, 128)
610
- : value.prompt.flow === 'browser'
611
- ? !exactKeys(value.prompt, ['flow', 'authUrl']) || !bounded(value.prompt.authUrl, 2048)
612
- : true))) throw new Error('Invalid authswitch operation prompt.');
613
+ if (value.prompt !== null && (!object(value.prompt) || value.prompt.flow !== 'device'
614
+ || !exactKeys(value.prompt, ['flow', 'verificationUrl', 'userCode'])
615
+ || !bounded(value.prompt.verificationUrl, 2048) || !bounded(value.prompt.userCode, 128))) {
616
+ throw new Error('Invalid authswitch operation prompt.');
617
+ }
613
618
  if (value.result !== null && (!object(value.result)
614
619
  || !exactKeys(value.result, ['accountId', 'grantId', 'accountRevision', 'grantGeneration'])
615
620
  || !hash(value.result.accountId) || !hash(value.result.grantId)
@@ -635,19 +640,33 @@ const modelId = (value: unknown): value is string => typeof value === 'string'
635
640
  const tokenCount = (value: unknown): value is number | null => value === null
636
641
  || (Number.isSafeInteger(value) && Number(value) >= 0);
637
642
 
643
+ const preuseLimitProblems: readonly string[] = ['usage_limit', 'rate_limit'];
644
+ const preuseLimit = (value: unknown): boolean => object(value) && exactKeys(value, ['kind', 'window', 'resetsAt'])
645
+ && ['usage_limit', 'rate_limit'].includes(String(value.kind))
646
+ && (value.window === null || bounded(value.window, 64)) && (value.resetsAt === null || iso(value.resetsAt));
647
+ const preuseFailure = (value: unknown): boolean => object(value) && exactKeys(value, ['kind', 'status', 'providerCode'])
648
+ && ['http', 'stream', 'response', 'network', 'aborted', 'unknown'].includes(String(value.kind))
649
+ && (value.status === null || (Number.isInteger(value.status) && Number(value.status) >= 100 && Number(value.status) <= 599))
650
+ && (value.providerCode === null || (typeof value.providerCode === 'string' && /^[A-Za-z0-9_.]{1,64}$/.test(value.providerCode)));
651
+
638
652
  const assertStoredAuthorityPreuseOperation: (value: unknown) => asserts value is IStoredAuthorityPreuseOperation = value => {
653
+ const limited = object(value) && preuseLimitProblems.includes(String(value.problem));
654
+ const failed = object(value) && Object.hasOwn(value, 'failure');
639
655
  if (!object(value) || !exactKeys(value, ['id', 'kind', 'purpose', 'accountId', 'grantId', 'requestHash',
640
656
  'requestedModel', 'model', 'authorizationGeneration', 'grantGeneration', 'state', 'result', 'problem',
657
+ ...(limited ? ['limit'] : []), ...(failed ? ['failure'] : []),
641
658
  'createdAt', 'updatedAt', 'finishedAt', 'revision', 'updateId'])
659
+ || (limited && !preuseLimit(value.limit))
660
+ || (failed && (value.problem !== 'request_uncertain' || !preuseFailure(value.failure)))
642
661
  || !uuid(value.id) || value.kind !== 'preuse_openai' || value.purpose !== 'openai_managed'
643
662
  || !hash(value.accountId) || !hash(value.grantId) || !hash(value.requestHash)
644
663
  || (value.requestedModel !== null && !modelId(value.requestedModel))
645
664
  || (value.model !== null && !modelId(value.model))
646
665
  || !revision(value.authorizationGeneration) || value.authorizationGeneration === 0
647
666
  || (value.grantGeneration !== null && (!revision(value.grantGeneration) || value.grantGeneration === 0))
648
- || !['preparing', 'request_may_have_started', 'complete', 'not_sent', 'outcome_unknown'].includes(String(value.state))
667
+ || !['preparing', 'request_may_have_started', 'complete', 'not_sent', 'refused', 'outcome_unknown'].includes(String(value.state))
649
668
  || !['none', 'target_unavailable', 'access_unavailable', 'catalog_unavailable', 'account_busy', 'cancelled',
650
- 'authority_closing', 'interrupted', 'request_uncertain'].includes(String(value.problem))
669
+ 'authority_closing', 'interrupted', 'request_uncertain', 'usage_limit', 'rate_limit'].includes(String(value.problem))
651
670
  || !iso(value.createdAt) || !iso(value.updatedAt)
652
671
  || (value.finishedAt !== null && !iso(value.finishedAt))
653
672
  || !revision(value.revision) || value.revision === 0 || !uuid(value.updateId)) {
@@ -675,13 +694,20 @@ const assertStoredAuthorityPreuseOperation: (value: unknown) => asserts value is
675
694
  throw new Error('Invalid completed authswitch preuse operation.');
676
695
  }
677
696
  } else if (value.state === 'not_sent') {
697
+ // Only the account's own usage reading stops a prompt with a limit before anything is sent.
678
698
  if (value.model !== null || value.grantGeneration !== null || value.result !== null
679
- || value.problem === 'none' || value.problem === 'request_uncertain' || value.finishedAt === null) {
699
+ || value.problem === 'none' || value.problem === 'request_uncertain' || value.problem === 'rate_limit'
700
+ || value.finishedAt === null) {
680
701
  throw new Error('Invalid unsent authswitch preuse operation.');
681
702
  }
703
+ } else if (value.state === 'refused') {
704
+ if (value.model === null || value.grantGeneration === null || value.result !== null
705
+ || !limited || value.finishedAt === null) {
706
+ throw new Error('Invalid refused authswitch preuse operation.');
707
+ }
682
708
  } else if (value.result !== null || value.finishedAt === null || value.model === null
683
709
  || value.grantGeneration === null
684
- || !['cancelled', 'authority_closing', 'request_uncertain'].includes(String(value.problem))) {
710
+ || !['cancelled', 'authority_closing', 'request_uncertain', 'usage_limit', 'rate_limit'].includes(String(value.problem))) {
685
711
  throw new Error('Invalid uncertain authswitch preuse operation.');
686
712
  }
687
713
  };
@@ -930,6 +956,8 @@ export class AuthSwitchAuthorityOperationModel extends plugins.nosqldb.SmartData
930
956
  @plugins.nosqldb.svDb() public authorizationGeneration?: number;
931
957
  @plugins.nosqldb.svDb() public grantGeneration?: number | null;
932
958
  @plugins.nosqldb.svDb() public problem?: TStoredAuthorityPreuseProblem;
959
+ @plugins.nosqldb.svDb() public limit?: IAuthSwitchPreuseLimit;
960
+ @plugins.nosqldb.svDb() public failure?: IAuthSwitchPreuseFailure;
933
961
  @plugins.nosqldb.svDb() public createdAt!: string;
934
962
  @plugins.nosqldb.svDb() public updatedAt!: string;
935
963
  @plugins.nosqldb.svDb() public finishedAt!: string | null;
@@ -1,4 +1,5 @@
1
1
  import * as plugins from './plugins.js';
2
+ import type { IOpenAiAccess } from './interfaces.openai.js';
2
3
  import type { IAuthSwitchPreuseOperation } from './authority-contract.js';
3
4
  import { AuthSwitchRefusal } from './authority-contract.js';
4
5
  import { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
@@ -6,10 +7,10 @@ import { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
6
7
  import type { IStoredAuthorityPreuseOperation, TStoredAuthorityPreuseProblem } from './classes.authoritymodels.js';
7
8
  import { CodexPreuse, type ICodexPreuseLifecycle } from './classes.codexpreuse.js';
8
9
  import type { IHarnessPreuseOptions, IHarnessPreuseResult } from './interfaces.harness.js';
9
- import { validatePreuseOptions } from './preuse.js';
10
+ import { PreuseFailureError, PreuseLimitError, validatePreuseOptions } from './preuse.js';
10
11
 
11
12
  export interface IAuthSwitchAuthorityPreuseExecutor {
12
- runAccess(credential: plugins.flexAccounts.IOpenAiChatGptAccessCredential,
13
+ runAccess(access: IOpenAiAccess,
13
14
  options: IHarnessPreuseOptions, lifecycle: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult>;
14
15
  }
15
16
 
@@ -41,7 +42,8 @@ const hash = (value: unknown): value is string => typeof value === 'string' && /
41
42
  const publicOperation = (operation: IStoredAuthorityPreuseOperation): IAuthSwitchPreuseOperation => ({
42
43
  id: operation.id, accountId: operation.accountId, loginId: operation.grantId,
43
44
  purpose: operation.purpose, requestedModel: operation.requestedModel, model: operation.model,
44
- state: operation.state, result: operation.result, problem: operation.problem,
45
+ state: operation.state, result: operation.result, problem: operation.problem, limit: operation.limit ?? null,
46
+ failure: operation.failure ?? null,
45
47
  createdAt: operation.createdAt, updatedAt: operation.updatedAt,
46
48
  finishedAt: operation.finishedAt, revision: operation.revision,
47
49
  });
@@ -204,11 +206,8 @@ export class AuthSwitchAuthorityPreuse {
204
206
  const access = await this.broker.resolveUsageAccess(context, undefined, this.minAccessValidityMs);
205
207
  signal.throwIfAborted();
206
208
  phase = operation.requestedModel === null ? 'catalog' : 'access';
207
- const credential: plugins.flexAccounts.IOpenAiChatGptAccessCredential = {
208
- kind: 'chatgptAccess', providerId: 'openai', accessToken: access.accessToken,
209
- accountId: access.accountId, isFedrampAccount: access.isFedrampAccount,
210
- };
211
- const result = await this.executor.runAccess(credential,
209
+ const result = await this.executor.runAccess({ accessToken: access.accessToken,
210
+ accountId: access.accountId, isFedrampAccount: access.isFedrampAccount },
212
211
  { prompt, ...(operation.requestedModel === null ? {} : { model: operation.requestedModel }), signal },
213
212
  { beforeRequest: async model => {
214
213
  signal.throwIfAborted();
@@ -224,26 +223,40 @@ export class AuthSwitchAuthorityPreuse {
224
223
  state: 'complete', result: { inputTokens: count(result.inputTokens), outputTokens: count(result.outputTokens),
225
224
  totalTokens: count(result.totalTokens) }, problem: 'none', updatedAt: now, finishedAt: now,
226
225
  revision: prior.revision + 1, updateId }));
227
- } catch {
228
- await this.fail(operation.id, phase, signal);
226
+ } catch (error) {
227
+ await this.fail(operation.id, phase, signal,
228
+ error instanceof PreuseLimitError || error instanceof PreuseFailureError ? error : null);
229
229
  }
230
230
  }
231
231
 
232
- private async fail(operationId: string, phase: 'target' | 'access' | 'catalog', signal: AbortSignal): Promise<void> {
232
+ /**
233
+ * Settles a preuse that did not complete. A limit is named: the account's usage reading stopped it before
234
+ * anything was sent, or the provider refused the prompt -- before its answer began, which consumed nothing and
235
+ * settles `refused`, or during it, which stays `outcome_unknown`. Any other stated cause of a sent request is
236
+ * named as its `failure`.
237
+ */
238
+ private async fail(operationId: string, phase: 'target' | 'access' | 'catalog', signal: AbortSignal,
239
+ stated: PreuseLimitError | PreuseFailureError | null): Promise<void> {
233
240
  let current: IStoredAuthorityPreuseOperation;
234
241
  try { current = await this.readStored(operationId); }
235
242
  catch { return; }
236
243
  if (!['preparing', 'request_may_have_started'].includes(current.state)) return;
237
244
  const marked = current.state === 'request_may_have_started';
238
- const problem: TStoredAuthorityPreuseProblem = marked
239
- ? this.closed ? 'authority_closing' : signal.aborted ? 'cancelled' : 'request_uncertain'
240
- : this.closed ? 'authority_closing' : signal.aborted ? 'cancelled'
241
- : phase === 'target' ? 'target_unavailable' : phase === 'catalog' ? 'catalog_unavailable' : 'access_unavailable';
245
+ const stopped = this.closed ? 'authority_closing' : signal.aborted ? 'cancelled' : null;
246
+ const limit = stopped === null && stated instanceof PreuseLimitError && (stated.source === 'provider') === marked
247
+ ? stated : null;
248
+ const failure = stopped === null && marked && stated instanceof PreuseFailureError ? stated.failure : null;
249
+ const problem: TStoredAuthorityPreuseProblem = stopped ?? (limit ? limit.limit.kind
250
+ : marked ? 'request_uncertain'
251
+ : phase === 'target' ? 'target_unavailable' : phase === 'catalog' ? 'catalog_unavailable' : 'access_unavailable');
252
+ const state: IStoredAuthorityPreuseOperation['state'] = !marked ? 'not_sent'
253
+ : limit && !limit.generationStarted ? 'refused' : 'outcome_unknown';
242
254
  const updateId = plugins.crypto.randomUUID();
243
255
  const now = new Date(this.now()).toISOString();
244
256
  try {
245
- await this.database.changePreuseOperation(updateId, operationId, prior => ({ ...prior,
246
- state: marked ? 'outcome_unknown' : 'not_sent', problem,
257
+ await this.database.changePreuseOperation(updateId, operationId, prior => ({ ...prior, state, problem,
258
+ ...(limit ? { limit: { ...limit.limit } } : {}),
259
+ ...(failure ? { failure: { ...failure } } : {}),
247
260
  updatedAt: now, finishedAt: now, revision: prior.revision + 1, updateId }));
248
261
  } catch { /* A durable request marker remains recoverable as outcome_unknown on restart. */ }
249
262
  }
@@ -1,9 +1,10 @@
1
1
  import * as plugins from './plugins.js';
2
+ import type { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
3
+ import type { IOpenAiAccountRateLimits, IOpenAiRateLimitDetails, IOpenAiRateLimitWindow } from './interfaces.openai.js';
2
4
  import type { IClaudeLogin, IClaudeStatusRead } from './classes.claudestatus.js';
3
5
  import type { IHarnessAccountStatus } from './interfaces.harness.js';
4
6
 
5
- type TRateLimits = plugins.flexAccounts.ISmartAiProviderAccountRateLimits;
6
- type TAccessCredential = plugins.flexAccounts.IOpenAiChatGptAccessCredential;
7
+ type TRateLimits = IOpenAiAccountRateLimits;
7
8
 
8
9
  export type TAuthSwitchUsageProblem =
9
10
  | 'none' | 'unsupported' | 'access_unavailable' | 'provider_unavailable'
@@ -115,7 +116,7 @@ export interface IAuthSwitchAuthorityUsageOptions {
115
116
  action: (source: { context: IAuthSwitchUsageContext; login: IClaudeLogin;
116
117
  readContext(): Promise<IAuthSwitchUsageContext | null> }) => Promise<IAuthSwitchUsageSnapshot>): Promise<IAuthSwitchUsageSnapshot>;
117
118
  store: IAuthSwitchUsageStore;
118
- provider: Pick<plugins.flexAccounts.ISmartAiProviderAdapter, 'getAccountRateLimits'>;
119
+ provider: Pick<AuthSwitchOpenAiProvider, 'getAccountRateLimits'>;
119
120
  now?: () => number;
120
121
  currentMaxAgeMs?: number;
121
122
  maxStaleAgeMs?: number;
@@ -199,11 +200,11 @@ const publicClaudeStatus = (source: IHarnessAccountStatus): IAuthSwitchClaudeSta
199
200
 
200
201
  /** Allowlist the provider projection before it enters a management DTO or stored usage record. */
201
202
  const publicRateLimits = (source: TRateLimits): TRateLimits => {
202
- const window = (value: plugins.flexAccounts.ISmartAiProviderRateLimitWindow) => ({
203
+ const window = (value: IOpenAiRateLimitWindow) => ({
203
204
  usedPercent: value.usedPercent, limitWindowSeconds: value.limitWindowSeconds,
204
205
  resetAfterSeconds: value.resetAfterSeconds, resetsAt: value.resetsAt,
205
206
  });
206
- const detail = (value: plugins.flexAccounts.ISmartAiProviderRateLimitDetails) => ({
207
+ const detail = (value: IOpenAiRateLimitDetails) => ({
207
208
  allowed: value.allowed, limitReached: value.limitReached,
208
209
  ...(value.primaryWindow ? { primaryWindow: window(value.primaryWindow) } : {}),
209
210
  ...(value.secondaryWindow ? { secondaryWindow: window(value.secondaryWindow) } : {}),
@@ -511,10 +512,8 @@ export class AuthSwitchAuthorityUsage {
511
512
  }
512
513
 
513
514
  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 });
515
+ return this.options.provider.getAccountRateLimits({ accessToken: access.accessToken, accountId: access.accountId,
516
+ isFedrampAccount: access.isFedrampAccount }, { timeoutMs: this.policy.providerTimeoutMs });
518
517
  }
519
518
 
520
519
  /** Wait for admitted reads before the daemon disposes the shared provider or database. */
package/ts/classes.cli.ts CHANGED
@@ -372,11 +372,11 @@ ${bold('Environment')}
372
372
  const cancel = () => { cancelled = true; void handle?.cancel().catch(() => undefined); };
373
373
  process.on('SIGINT', cancel); process.on('SIGTERM', cancel);
374
374
  try {
375
- handle = await harness.beginLogin({ providerId: provider.providerId, flow: provider.flows.includes('device') ? 'device' : provider.flows[0] });
375
+ handle = await harness.beginLogin({ providerId: provider.providerId, flow: 'device' });
376
376
  if (cancelled) await handle.cancel();
377
- else if (handle.prompt.flow === 'device') {
377
+ else {
378
378
  process.stdout.write(`Open ${plainText(handle.prompt.verificationUrl)}\nCode: ${plainText(handle.prompt.userCode)}\nWaiting for login. Press Ctrl+C to cancel.\n`);
379
- } else process.stdout.write(`Open ${plainText(handle.prompt.authUrl)}\nWaiting for login. Press Ctrl+C to cancel.\n`);
379
+ }
380
380
  return this.reportOutcome((await handle.completion).outcome);
381
381
  } finally {
382
382
  process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel);