agl 40.4.0 → 40.5.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 (149) hide show
  1. package/changelog.md +27 -0
  2. package/dist_serve/bundle.js +2228 -2177
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +2 -2
  6. package/dist_ts/classes.accountsactions.d.ts +11 -1
  7. package/dist_ts/classes.accountsactions.js +59 -2
  8. package/dist_ts/classes.controller.js +5 -5
  9. package/dist_ts/classes.flexaccountaccess.js +3 -2
  10. package/dist_ts/classes.harnessreleases.d.ts +14 -4
  11. package/dist_ts/classes.harnessreleases.js +28 -4
  12. package/dist_ts/classes.inferencegateway.d.ts +28 -3
  13. package/dist_ts/classes.inferencegateway.js +177 -61
  14. package/dist_ts/classes.inferenceplacement.d.ts +5 -3
  15. package/dist_ts/classes.inferenceplacement.js +2 -2
  16. package/dist_ts/classes.inferencerouter.d.ts +349 -91
  17. package/dist_ts/classes.inferencerouter.js +804 -297
  18. package/dist_ts/classes.inferenceservice.d.ts +14 -2
  19. package/dist_ts/classes.inferenceservice.js +53 -14
  20. package/dist_ts/classes.inferencespend.d.ts +339 -0
  21. package/dist_ts/classes.inferencespend.js +815 -0
  22. package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
  23. package/dist_ts/classes.inferenceusagecache.js +9 -5
  24. package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
  25. package/dist_ts/classes.inferenceusagetap.js +185 -31
  26. package/dist_ts/classes.proxycontrol.d.ts +3 -2
  27. package/dist_ts/classes.proxycontrol.js +1 -2
  28. package/dist_ts/classes.proxydaemon.js +3 -2
  29. package/dist_ts/classes.proxystore.d.ts +6 -0
  30. package/dist_ts/classes.proxystore.js +11 -2
  31. package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
  32. package/dist_ts/fleet/classes.claudeworkers.js +10 -9
  33. package/dist_ts/functions.claudecoderelease.d.ts +12 -6
  34. package/dist_ts/functions.claudecoderelease.js +13 -7
  35. package/dist_ts/functions.inferencecli.js +12 -8
  36. package/dist_ts/interfaces.auth.d.ts +1 -1
  37. package/dist_ts/interfaces.auth.js +3 -1
  38. package/dist_ts/plugins.d.ts +3 -1
  39. package/dist_ts/plugins.js +7 -2
  40. package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
  41. package/dist_ts_authswitch/authority-apikeys.js +65 -0
  42. package/dist_ts_authswitch/authority-contract.d.ts +142 -28
  43. package/dist_ts_authswitch/authority-contract.js +48 -1
  44. package/dist_ts_authswitch/authority-overview.js +8 -1
  45. package/dist_ts_authswitch/authority-placement.d.ts +7 -2
  46. package/dist_ts_authswitch/authority-placement.js +10 -3
  47. package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
  48. package/dist_ts_authswitch/authority-spend.d.ts +103 -0
  49. package/dist_ts_authswitch/authority-spend.js +110 -0
  50. package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
  51. package/dist_ts_authswitch/authority-subscription.js +24 -1
  52. package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
  53. package/dist_ts_authswitch/authority-usagelimit.js +89 -39
  54. package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
  55. package/dist_ts_authswitch/authority-usagewindows.js +41 -16
  56. package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
  57. package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
  58. package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
  59. package/dist_ts_authswitch/classes.authoritycli.js +94 -29
  60. package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
  61. package/dist_ts_authswitch/classes.authorityclient.js +31 -11
  62. package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
  63. package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
  64. package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
  65. package/dist_ts_authswitch/classes.authorityimport.js +2 -2
  66. package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
  67. package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
  68. package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
  69. package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
  70. package/dist_ts_authswitch/classes.authorityusage.js +118 -4
  71. package/dist_ts_authswitch/classes.claudenative.d.ts +16 -7
  72. package/dist_ts_authswitch/classes.claudenative.js +29 -16
  73. package/dist_ts_authswitch/classes.claudesignin.js +5 -2
  74. package/dist_ts_authswitch/testsupport.d.ts +24 -5
  75. package/dist_ts_authswitch/testsupport.js +56 -12
  76. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
  77. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
  78. package/dist_ts_authswitch/ts_migration/index.js +3 -1
  79. package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
  80. package/dist_ts_interfaces/functions.authorityscope.js +35 -8
  81. package/dist_ts_interfaces/interfaces.d.ts +1 -1
  82. package/dist_ts_interfaces/interfaces.js +1 -1
  83. package/dist_ts_interfaces/plugins.d.ts +2 -1
  84. package/dist_ts_interfaces/plugins.js +7 -4
  85. package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
  86. package/dist_ts_interfaces/requests.accounts.js +11 -1
  87. package/dist_ts_interfaces/requests.d.ts +2 -2
  88. package/dist_ts_interfaces/requests.js +1 -1
  89. package/docs/authswitch.md +75 -12
  90. package/package.json +5 -5
  91. package/readme.md +198 -21
  92. package/ts/00_commitinfo_data.ts +1 -1
  93. package/ts/aglapi/aglapisurface.document.ts +2 -2
  94. package/ts/classes.accountsactions.ts +75 -1
  95. package/ts/classes.controller.ts +4 -4
  96. package/ts/classes.flexaccountaccess.ts +2 -1
  97. package/ts/classes.harnessreleases.ts +25 -4
  98. package/ts/classes.inferencegateway.ts +184 -64
  99. package/ts/classes.inferenceplacement.ts +6 -4
  100. package/ts/classes.inferencerouter.ts +1026 -314
  101. package/ts/classes.inferenceservice.ts +56 -13
  102. package/ts/classes.inferencespend.ts +870 -0
  103. package/ts/classes.inferenceusagecache.ts +16 -4
  104. package/ts/classes.inferenceusagetap.ts +159 -34
  105. package/ts/classes.proxycontrol.ts +3 -3
  106. package/ts/classes.proxydaemon.ts +2 -1
  107. package/ts/classes.proxystore.ts +11 -1
  108. package/ts/fleet/classes.claudeworkers.ts +9 -8
  109. package/ts/functions.claudecoderelease.ts +13 -6
  110. package/ts/functions.inferencecli.ts +11 -7
  111. package/ts/interfaces.auth.ts +2 -0
  112. package/ts/plugins.ts +7 -0
  113. package/ts_authswitch/authority-apikeys.ts +85 -0
  114. package/ts_authswitch/authority-contract.ts +167 -29
  115. package/ts_authswitch/authority-overview.ts +7 -0
  116. package/ts_authswitch/authority-placement.ts +12 -4
  117. package/ts_authswitch/authority-runtime-contract.ts +2 -2
  118. package/ts_authswitch/authority-spend.ts +168 -0
  119. package/ts_authswitch/authority-subscription.ts +28 -1
  120. package/ts_authswitch/authority-usagelimit.ts +95 -40
  121. package/ts_authswitch/authority-usagewindows.ts +42 -17
  122. package/ts_authswitch/classes.authoritybroker.ts +161 -68
  123. package/ts_authswitch/classes.authoritycli.ts +108 -32
  124. package/ts_authswitch/classes.authorityclient.ts +41 -10
  125. package/ts_authswitch/classes.authoritydaemon.ts +43 -13
  126. package/ts_authswitch/classes.authoritydatabase.ts +178 -8
  127. package/ts_authswitch/classes.authorityimport.ts +1 -1
  128. package/ts_authswitch/classes.authoritymodels.ts +165 -19
  129. package/ts_authswitch/classes.authorityregistry.ts +3 -0
  130. package/ts_authswitch/classes.authorityusage.ts +171 -10
  131. package/ts_authswitch/classes.claudenative.ts +28 -15
  132. package/ts_authswitch/classes.claudesignin.ts +4 -1
  133. package/ts_authswitch/native-contracts.md +81 -20
  134. package/ts_authswitch/readme.md +1 -1
  135. package/ts_authswitch/testsupport.ts +58 -11
  136. package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
  137. package/ts_authswitch/ts_migration/index.ts +2 -0
  138. package/ts_interfaces/functions.authorityscope.ts +43 -8
  139. package/ts_interfaces/interfaces.ts +1 -1
  140. package/ts_interfaces/plugins.ts +6 -3
  141. package/ts_interfaces/requests.accounts.ts +48 -0
  142. package/ts_interfaces/requests.ts +4 -0
  143. package/ts_web/00_commitinfo_data.ts +1 -1
  144. package/ts_web/accounts.plugins.ts +3 -1
  145. package/ts_web/accounts.templates.ts +20 -11
  146. package/ts_web/classes.accountsreadmodel.ts +322 -70
  147. package/ts_web/elements.accounts.ts +219 -20
  148. package/ts_web/elements.codexconnections.ts +3 -1
  149. package/ts_web/plugins.ts +4 -2
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
- import { AuthSwitchRefusal, authSwitchPreemptibleRuntimes } from './authority-contract.js';
2
+ import { AuthSwitchRefusal, authSwitchPreemptibleRuntimes, type IAuthSwitchSpendAnswer } from './authority-contract.js';
3
+ import { authSwitchSpendExhausted, authSwitchSpendPeriod, type IAuthSwitchSpendReading } from './authority-spend.js';
3
4
  import {
4
5
  AuthSwitchAuthorityAccountModel, AuthSwitchAuthorityBindingModel, AuthSwitchAuthorityClaudeContainerHomeModel,
5
6
  AuthSwitchAuthorityClaudeHandoffModel, AuthSwitchAuthorityClaudeHomeModel, AuthSwitchAuthorityCodexHandoffModel,
@@ -7,6 +8,9 @@ import {
7
8
  AuthSwitchAuthorityEventModel, AuthSwitchAuthorityGrantModel, AuthSwitchAuthorityHandoffModel,
8
9
  AuthSwitchAuthorityMetaModel, AuthSwitchAuthorityMigrationLedgerModel, AuthSwitchAuthorityOperationModel,
9
10
  AuthSwitchAuthorityUsageModel, authorityUsageId, assertStoredAuthorityAccount,
11
+ AuthSwitchAuthoritySpendEntryModel, AuthSwitchAuthoritySpendPeriodModel, authoritySpendEntryId, authoritySpendPeriodId,
12
+ assertStoredAuthoritySpendEntry, assertStoredAuthoritySpendPeriod, type IStoredAuthoritySpendEntry,
13
+ type IStoredAuthoritySpendPeriod,
10
14
  AuthSwitchAuthorityUsageEstimateModel, authorityUsageEstimateId, assertStoredAuthorityUsageEstimate,
11
15
  assertStoredAuthorityBinding, assertStoredAuthorityClaudeContainerHome, assertStoredAuthorityClaudeHandoff,
12
16
  assertStoredAuthorityClaudeHome, assertStoredAuthorityCodexHandoff, assertStoredAuthorityCodexHostHome,
@@ -221,24 +225,50 @@ const storedOperation = (stored: Parameters<typeof AuthSwitchAuthorityOperationM
221
225
  const claudeReceiptSourceKinds: readonly IStoredAuthorityMigrationLedger['sourceKind'][] =
222
226
  ['authswitch_stash', 'authswitch_backup', 'claude_native', 'claude_signin'];
223
227
 
228
+ /** The purposes of a login a runtime binds: a ChatGPT login, a Claude login, or an Anthropic or OpenAI API key. */
229
+ export type TAuthSwitchBindPurpose = 'openai_managed' | 'claude_host_native' | 'anthropic_api_key' | 'openai_api_key';
230
+
231
+ export const authSwitchBindPurposes: readonly TAuthSwitchBindPurpose[] =
232
+ ['openai_managed', 'claude_host_native', 'anthropic_api_key', 'openai_api_key'];
233
+
224
234
  /**
225
235
  * The purpose a runtime bind of this login is accepted under, as far as the stored records decide it, or `null`:
226
236
  * the account is not removed, and the login is its selected, ready, daemon-held login of the account's own provider
227
- * -- its ChatGPT login (`openai_managed`), its Claude login (`claude_host_native`) or its Anthropic API key
228
- * (`anthropic_api_key`). A Claude login further needs a verified import receipt (`hasVerifiedClaudeGrantReceipt`) and
229
- * the daemon's Claude renewer. The authority page's `bindableGrantIds` and the bind itself both decide by this, so a
230
- * login published as bindable is one a bind accepts.
237
+ * -- its ChatGPT login (`openai_managed`), its Claude login (`claude_host_native`) or its Anthropic or OpenAI API key
238
+ * (`anthropic_api_key`, `openai_api_key`). A Claude login further needs a verified import receipt
239
+ * (`hasVerifiedClaudeGrantReceipt`) and the daemon's Claude renewer. The authority page's `bindableGrantIds` and the
240
+ * bind itself both decide by this, so a login published as bindable is one a bind accepts.
231
241
  */
232
242
  export const authorityBindPurpose = (account: IStoredAuthorityAccount, grant: IStoredAuthorityGrant):
233
- 'openai_managed' | 'claude_host_native' | 'anthropic_api_key' | null => {
243
+ TAuthSwitchBindPurpose | null => {
234
244
  if (account.removed || grant.accountId !== account.id || grant.id !== account.primaryGrantId || grant.state !== 'ready'
235
245
  || grant.owner !== 'daemon' || grant.providerId !== account.providerId) return null;
236
246
  if (grant.purpose === 'openai_managed' && grant.providerId === 'openai') return 'openai_managed';
237
247
  if (grant.purpose === 'claude_host_native' && grant.providerId === 'anthropic') return 'claude_host_native';
238
248
  if (grant.purpose === 'anthropic_api_key' && grant.providerId === 'anthropic') return 'anthropic_api_key';
249
+ if (grant.purpose === 'openai_api_key' && grant.providerId === 'openai') return 'openai_api_key';
239
250
  return null;
240
251
  };
241
252
 
253
+ /** How far ahead of the authority's clock a reported answer may have ended: a proxy clock off by more is a fault. */
254
+ const spendClockSkewMs = 86_400_000;
255
+ /** The most entries and months of earlier months one spend report deletes; the rest go with the next reports. */
256
+ const spendPruneBatch = 256;
257
+
258
+ /** An API key's grant of its account: the key of the account's provider, which the account's workspace names. */
259
+ const isApiKeyGrant = (account: IStoredAuthorityAccount, grant: IStoredAuthorityGrant): boolean =>
260
+ grant.accountId === account.id && account.workspaceId === 'api-key' && grant.providerId === account.providerId
261
+ && ((grant.purpose === 'anthropic_api_key' && grant.providerId === 'anthropic')
262
+ || (grant.purpose === 'openai_api_key' && grant.providerId === 'openai'));
263
+
264
+ /** A month's reading from its stored total, or an empty one, with the account's limit as it stands. */
265
+ const spendReading = (period: ReturnType<typeof authSwitchSpendPeriod>, stored: IStoredAuthoritySpendPeriod | null,
266
+ limitMicroUsd: number | null): IAuthSwitchSpendReading => ({
267
+ period: period.key, periodStart: period.start, resetAt: period.end,
268
+ spentMicroUsd: stored?.spentMicroUsd ?? 0, limitMicroUsd, answers: stored?.answers ?? 0,
269
+ unpricedAnswers: stored?.unpricedAnswers ?? 0, lastAnswerAt: stored?.lastAnswerAt ?? null,
270
+ });
271
+
242
272
  /** Separate account, grant, binding and journal records; the only global record is a revision counter. */
243
273
  export class AuthSwitchAuthorityDatabase {
244
274
  private engine?: plugins.nosqldbEngine.LocalNoSqlDb;
@@ -966,6 +996,140 @@ export class AuthSwitchAuthorityDatabase {
966
996
  });
967
997
  }
968
998
 
999
+ /**
1000
+ * An API key's spend in the month `nowArg` falls in, with its account's spend limit as it stands. Refused
1001
+ * (`login_unavailable`) for any login but an API key of its account.
1002
+ */
1003
+ public async readSpend(accountId: string, loginId: string, nowArg: number): Promise<IAuthSwitchSpendReading> {
1004
+ this.db();
1005
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) throw new Error('Invalid authority spend identity.');
1006
+ const period = authSwitchSpendPeriod(nowArg);
1007
+ return this.transaction(async session => {
1008
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1009
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: loginId }, { session });
1010
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
1011
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
1012
+ if (!account || !grant || !isApiKeyGrant(account, grant)) {
1013
+ throw new AuthSwitchRefusal('login_unavailable', 'Only an API key\'s login has a spend.');
1014
+ }
1015
+ const stored = await AuthSwitchAuthoritySpendPeriodModel.exact.findStoredOne(
1016
+ { id: authoritySpendPeriodId(accountId, period.key) }, { session });
1017
+ return spendReading(period, stored ? AuthSwitchAuthoritySpendPeriodModel.exact.toPersisted(stored) : null,
1018
+ account.spendLimitMicroUsd);
1019
+ });
1020
+ }
1021
+
1022
+ /**
1023
+ * Records a spend report of an API key's login in one transaction: each answer's entry, the totals of the months they
1024
+ * count in, and an account event when the report changed whether the key is at its limit or began the month's spend,
1025
+ * so a reader of the snapshot reads the key's usage again. An entry recorded already with the same content is no
1026
+ * change; one with other content refuses the whole report, naming the answer. An answer of a month before the previous
1027
+ * one counts nowhere: only the current and the previous month are kept, and entries and totals of earlier months are
1028
+ * deleted here, a bounded number per report. A removed key's spend is recorded too, so its account keeps the month's
1029
+ * spend if the key comes back. Answers `changed` with the reading of the month `nowArg` falls in.
1030
+ */
1031
+ public async recordSpend(accountId: string, loginId: string, answers: readonly IAuthSwitchSpendAnswer[],
1032
+ nowArg: number): Promise<{ reading: IAuthSwitchSpendReading; changed: boolean }> {
1033
+ this.db();
1034
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) throw new Error('Invalid authority spend identity.');
1035
+ const current = authSwitchSpendPeriod(nowArg);
1036
+ const previous = authSwitchSpendPeriod(Date.parse(current.start) - 1);
1037
+ const updateId = plugins.crypto.randomUUID();
1038
+ try {
1039
+ return await this.transaction(async session => {
1040
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1041
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1042
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: loginId }, { session });
1043
+ if (!metaStored) throw new Error('Authswitch authority metadata is missing.');
1044
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
1045
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
1046
+ if (!account || !grant || !isApiKeyGrant(account, grant)) {
1047
+ throw new AuthSwitchRefusal('login_unavailable', 'Only an API key\'s login records spend.');
1048
+ }
1049
+ const added = new Map<string, { spent: number; answers: number; unpriced: number; last: string }>();
1050
+ for (const answer of answers) {
1051
+ const endedAt = Date.parse(answer.endedAt);
1052
+ if (endedAt > nowArg + spendClockSkewMs) {
1053
+ throw new AuthSwitchRefusal('invalid_input', `The spend report names answer ${answer.answerId} as ending more `
1054
+ + 'than a day after the account service\'s clock; check the clocks, the report is refused.');
1055
+ }
1056
+ const period = authSwitchSpendPeriod(endedAt).key;
1057
+ if (period < previous.key) continue;
1058
+ const entry: IStoredAuthoritySpendEntry = { id: authoritySpendEntryId(accountId, answer.answerId), accountId,
1059
+ answerId: answer.answerId, period, endedAt: answer.endedAt, model: answer.model,
1060
+ serviceTier: answer.serviceTier, microUsd: answer.microUsd, priceSource: answer.priceSource };
1061
+ assertStoredAuthoritySpendEntry(entry);
1062
+ const inserted = await AuthSwitchAuthoritySpendEntryModel.exact.insert(entry, { session });
1063
+ if (inserted.status === 'conflict') {
1064
+ throw new AuthSwitchRefusal('invalid_input', `The spend report names answer ${answer.answerId}, which the `
1065
+ + 'account service recorded with other content; the report is refused.');
1066
+ }
1067
+ if (inserted.status !== 'inserted') continue;
1068
+ const sum = added.get(period) ?? { spent: 0, answers: 0, unpriced: 0, last: answer.endedAt };
1069
+ added.set(period, { spent: sum.spent + (answer.microUsd ?? 0), answers: sum.answers + 1,
1070
+ unpriced: sum.unpriced + (answer.microUsd === null ? 1 : 0),
1071
+ last: Date.parse(answer.endedAt) > Date.parse(sum.last) ? answer.endedAt : sum.last });
1072
+ }
1073
+ const readPeriod = async (key: string): Promise<{
1074
+ stored: Awaited<ReturnType<typeof AuthSwitchAuthoritySpendPeriodModel.exact.findStoredOne>>;
1075
+ persisted: IStoredAuthoritySpendPeriod | null }> => {
1076
+ const stored = await AuthSwitchAuthoritySpendPeriodModel.exact.findStoredOne(
1077
+ { id: authoritySpendPeriodId(accountId, key) }, { session });
1078
+ return { stored, persisted: stored ? AuthSwitchAuthoritySpendPeriodModel.exact.toPersisted(stored) : null };
1079
+ };
1080
+ const before = (await readPeriod(current.key)).persisted;
1081
+ let after = before;
1082
+ for (const [key, sum] of added) {
1083
+ const { stored, persisted } = await readPeriod(key);
1084
+ const next: IStoredAuthoritySpendPeriod = {
1085
+ id: authoritySpendPeriodId(accountId, key), accountId, period: key,
1086
+ spentMicroUsd: (persisted?.spentMicroUsd ?? 0) + sum.spent, answers: (persisted?.answers ?? 0) + sum.answers,
1087
+ unpricedAnswers: (persisted?.unpricedAnswers ?? 0) + sum.unpriced,
1088
+ lastAnswerAt: persisted?.lastAnswerAt && Date.parse(persisted.lastAnswerAt) > Date.parse(sum.last)
1089
+ ? persisted.lastAnswerAt : sum.last,
1090
+ revision: (persisted?.revision ?? 0) + 1, updateId,
1091
+ };
1092
+ assertStoredAuthoritySpendPeriod(next);
1093
+ if (stored) {
1094
+ const changed = await AuthSwitchAuthoritySpendPeriodModel.exact.transition({ current: stored,
1095
+ change: model => Object.assign(model, next) }, { session });
1096
+ if (changed.status !== 'transitioned') throw new Error('Spend total changed while it was recorded.');
1097
+ } else {
1098
+ const inserted = await AuthSwitchAuthoritySpendPeriodModel.exact.insert(next, { session });
1099
+ if (inserted.status !== 'inserted') throw new Error('Spend total changed while it was recorded.');
1100
+ }
1101
+ if (key === current.key) after = next;
1102
+ }
1103
+ const limit = account.spendLimitMicroUsd;
1104
+ const reading = spendReading(current, after, limit);
1105
+ const changed = (before === null && after !== null)
1106
+ || authSwitchSpendExhausted(spendReading(current, before, limit), nowArg) !== authSwitchSpendExhausted(reading, nowArg);
1107
+ if (changed) {
1108
+ await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored), updateId, 'account',
1109
+ accountId);
1110
+ }
1111
+ // Only the current and the previous month are kept.
1112
+ for (const stale of await AuthSwitchAuthoritySpendEntryModel.exact.findStored({ filter: { accountId,
1113
+ period: { $lt: previous.key } }, sort: { accountId: 1, period: 1, id: 1 }, limit: spendPruneBatch, session })) {
1114
+ const deleted = await AuthSwitchAuthoritySpendEntryModel.exact.delete({ current: stale }, { session });
1115
+ if (deleted.status !== 'deleted') throw new Error('Spend entry changed while it was deleted.');
1116
+ }
1117
+ for (const stale of await AuthSwitchAuthoritySpendPeriodModel.exact.findStored({ filter: { accountId,
1118
+ period: { $lt: previous.key } }, sort: { accountId: 1, period: 1 }, limit: spendPruneBatch, session })) {
1119
+ const deleted = await AuthSwitchAuthoritySpendPeriodModel.exact.delete({ current: stale }, { session });
1120
+ if (deleted.status !== 'deleted') throw new Error('Spend total changed while it was deleted.');
1121
+ }
1122
+ return { reading, changed };
1123
+ });
1124
+ } catch (error) {
1125
+ if (error instanceof plugins.nosqldb.SmartdataExactPersistenceError && error.code === 'ambiguous_write') {
1126
+ // A repeat of the report is no change once it committed, so the caller sends it again.
1127
+ throw new Error('The spend report\'s outcome is unknown; send it again.');
1128
+ }
1129
+ throw error;
1130
+ }
1131
+ }
1132
+
969
1133
  /** Delete one login's usage estimate, if one is stored. */
970
1134
  public async deleteUsageEstimate(accountId: string, loginId: string): Promise<void> {
971
1135
  this.db();
@@ -1002,7 +1166,12 @@ export class AuthSwitchAuthorityDatabase {
1002
1166
  AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored),
1003
1167
  AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored));
1004
1168
  if (!sameUsageContext(context, actual) || actual.removed) return false;
1005
- if (actual.providerId === 'openai') {
1169
+ if (actual.purpose === 'anthropic_api_key' || actual.purpose === 'openai_api_key') {
1170
+ // An API key's record holds its payment problem alone, while the key is ready (`assertStoredAuthorityUsage`).
1171
+ if (!isApiKeyGrant(AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored),
1172
+ AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored)) || actual.owner !== 'daemon'
1173
+ || context.nativeSource !== null || actual.grantState !== 'ready') return false;
1174
+ } else if (actual.providerId === 'openai') {
1006
1175
  if (actual.owner !== 'daemon' || actual.purpose !== 'openai_managed'
1007
1176
  || context.nativeSource !== null || !['ready', 'retry_wait'].includes(actual.grantState)) return false;
1008
1177
  } else if (actual.providerId === 'anthropic' && actual.purpose === 'claude_host_native') {
@@ -1111,7 +1280,8 @@ export class AuthSwitchAuthorityDatabase {
1111
1280
  for (const grant of grantPage) {
1112
1281
  const account = grantAccountById.get(grant.accountId);
1113
1282
  const purpose = account ? authorityBindPurpose(account, grant) : null;
1114
- if (purpose === 'openai_managed' || purpose === 'anthropic_api_key' || purpose === 'claude_host_native'
1283
+ if (purpose === 'openai_managed' || purpose === 'anthropic_api_key' || purpose === 'openai_api_key'
1284
+ || purpose === 'claude_host_native'
1115
1285
  && await this.hasVerifiedClaudeGrantReceipt(grant.accountId, grant.id, session)) bindableGrantIds.add(grant.id);
1116
1286
  }
1117
1287
  // Both lists hold the first `limit + 1` ids past the cursor, so their merge holds the page and says whether
@@ -687,7 +687,7 @@ export class AuthSwitchGrantImport {
687
687
  removed: false, revision: existing.revision + 1, statusObservedAt: this.stamp(), updateId }
688
688
  : { id: identity.accountId, providerId: plan.providerId, issuer: plan.providerId,
689
689
  subject: identity.subject, workspaceId: identity.workspaceId, label, email: record.email,
690
- plan: record.plan, primaryGrantId: grantId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1,
690
+ plan: record.plan, primaryGrantId: grantId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1,
691
691
  statusObservedAt: this.stamp(), updateId };
692
692
  }
693
693
 
@@ -1,10 +1,11 @@
1
1
  import * as plugins from './plugins.js';
2
- import { isAuthSwitchLapsesAt, isAuthSwitchMaxUsagePercent, type IAuthSwitchPreuseFailure, type IAuthSwitchPreuseLimit,
2
+ import { isAuthSwitchKeyHint, isAuthSwitchLapsesAt, isAuthSwitchSpendAnswer, isAuthSwitchMaxUsagePercent, type IAuthSwitchPreuseFailure, type IAuthSwitchPreuseLimit,
3
3
  type TAuthSwitchClaudeProofFailure, type TAuthSwitchCodexProofFailure, type TAuthSwitchLoginOwner,
4
4
  type TAuthSwitchLoginPrompt } from './authority-contract.js';
5
5
  import type { IAuthSwitchStoredUsage, IAuthSwitchStoredUsageEstimate } from './classes.authorityusage.js';
6
6
  import { isAuthSwitchUsageEstimateState } from './classes.authorityusageestimate.js';
7
7
  import { isAuthSwitchUsageObservation } from './authority-usageheaders.js';
8
+ import { authSwitchSpendPeriod, isAuthSwitchSpendLimitMicroUsd, isAuthSwitchSpendPeriodKey } from './authority-spend.js';
8
9
 
9
10
  export interface IStoredAuthorityMeta {
10
11
  id: 'authswitch-authority';
@@ -29,6 +30,10 @@ export interface IStoredAuthorityAccount {
29
30
  reserveLiftUntil: string | null;
30
31
  /** When the account's subscription ends, ISO 8601 UTC, or `null` while it renews (`IAuthSwitchAccount.lapsesAt`). */
31
32
  lapsesAt: string | null;
33
+ /** An API key account's monthly spend limit in micro-dollars, or `null` (`IAuthSwitchAccount.spendLimitMicroUsd`). */
34
+ spendLimitMicroUsd: number | null;
35
+ /** How an API key account's key ends, or `null` (`IAuthSwitchAccount.keyHint`). */
36
+ keyHint: string | null;
32
37
  removed: boolean;
33
38
  revision: number;
34
39
  statusObservedAt: string;
@@ -36,7 +41,15 @@ export interface IStoredAuthorityAccount {
36
41
  }
37
42
 
38
43
  export type TStoredGrantPurpose =
39
- | 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native' | 'anthropic_api_key';
44
+ | 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native' | 'anthropic_api_key'
45
+ | 'openai_api_key';
46
+
47
+ /** The purposes of an API key's login: a key the authority alone holds, which nothing renews. */
48
+ export const authorityApiKeyPurposes: readonly TStoredGrantPurpose[] = ['anthropic_api_key', 'openai_api_key'];
49
+
50
+ /** The provider whose key an API key purpose holds. */
51
+ export const authorityApiKeyProvider = (purposeArg: 'anthropic_api_key' | 'openai_api_key'): 'anthropic' | 'openai' =>
52
+ purposeArg === 'anthropic_api_key' ? 'anthropic' : 'openai';
40
53
 
41
54
  /** Each purpose has its own grant, ownership, generation and refresh state. */
42
55
  export interface IStoredAuthorityGrant {
@@ -104,6 +117,43 @@ export interface IStoredAuthorityUsageEstimate extends IAuthSwitchStoredUsageEst
104
117
  export const authorityUsageEstimateId = (accountId: string, loginId: string): string =>
105
118
  plugins.crypto.createHash('sha256').update(JSON.stringify(['authswitch-usage-estimate', accountId, loginId])).digest('hex');
106
119
 
120
+ /**
121
+ * One answer AGL's inference proxy relayed on an API key, as the spend report named it (`IAuthSwitchSpendAnswer`): the
122
+ * ledger's record of what it cost. Its content is the report's alone, so a repeat of the report is the same document and
123
+ * a report of the same answer with other content is a conflict. Kept for the current and the previous month.
124
+ */
125
+ export interface IStoredAuthoritySpendEntry {
126
+ id: string;
127
+ accountId: string;
128
+ answerId: string;
129
+ /** The month the answer counts in, `YYYY-MM` of `endedAt` in UTC. */
130
+ period: string;
131
+ endedAt: string;
132
+ model: string;
133
+ serviceTier: string | null;
134
+ microUsd: number | null;
135
+ priceSource: string | null;
136
+ }
137
+
138
+ export const authoritySpendEntryId = (accountId: string, answerId: string): string =>
139
+ plugins.crypto.createHash('sha256').update(JSON.stringify(['authswitch-spend-entry', accountId, answerId])).digest('hex');
140
+
141
+ /** What an API key spent in one month, the sum of its entries of that month (`IAuthSwitchSpendReading`). */
142
+ export interface IStoredAuthoritySpendPeriod {
143
+ id: string;
144
+ accountId: string;
145
+ period: string;
146
+ spentMicroUsd: number;
147
+ answers: number;
148
+ unpricedAnswers: number;
149
+ lastAnswerAt: string | null;
150
+ revision: number;
151
+ updateId: string;
152
+ }
153
+
154
+ export const authoritySpendPeriodId = (accountId: string, period: string): string =>
155
+ plugins.crypto.createHash('sha256').update(JSON.stringify(['authswitch-spend-period', accountId, period])).digest('hex');
156
+
107
157
  export interface IStoredAuthorityBinding {
108
158
  id: string;
109
159
  accountId: string;
@@ -525,15 +575,26 @@ const usageClaudeStatus = (value: unknown): boolean => object(value)
525
575
  && exactKeys(value.rateLimit, ['retryAt'])
526
576
  && (value.rateLimit.retryAt === null || iso(value.rateLimit.retryAt))));
527
577
  /**
528
- * A payment problem on a Claude login's usage record (`IAuthSwitchPaymentProblem`): when it was seen, and the
529
- * subscription read before it or null. Each subscription value is bounded like a fact's text.
578
+ * A payment problem on a usage record (`IAuthSwitchPaymentProblem`): when it was seen, and the subscription read before
579
+ * it or null. Each subscription value is bounded like a fact's text. A billing problem states no cause, exactly as every
580
+ * Claude login's record always did; an API key's `spend_cap` or `quota` states its cause with no subscription, and a
581
+ * `spend_cap` lifts at the end of the month it was seen in (`until`), never at another time.
530
582
  */
531
- const usagePaymentProblem = (value: unknown): boolean => object(value)
532
- && exactKeys(value, ['at', 'subscription']) && iso(value.at)
533
- && (value.subscription === null || (object(value.subscription)
534
- && exactKeys(value.subscription, ['status', 'createdAt'])
535
- && (value.subscription.status === null || usageStatusText(value.subscription.status))
536
- && (value.subscription.createdAt === null || usageStatusText(value.subscription.createdAt))));
583
+ const usagePaymentProblem = (value: unknown): boolean => object(value) && iso(value.at)
584
+ && ((exactKeys(value, ['at', 'subscription'])
585
+ && (value.subscription === null || (object(value.subscription)
586
+ && exactKeys(value.subscription, ['status', 'createdAt'])
587
+ && (value.subscription.status === null || usageStatusText(value.subscription.status))
588
+ && (value.subscription.createdAt === null || usageStatusText(value.subscription.createdAt)))))
589
+ || (exactKeys(value, ['at', 'subscription', 'cause']) && value.subscription === null && value.cause === 'quota')
590
+ || (exactKeys(value, ['at', 'subscription', 'cause', 'until']) && value.subscription === null
591
+ && value.cause === 'spend_cap' && value.until === authSwitchSpendPeriod(Date.parse(value.at as string)).end));
592
+
593
+ /** An API key's login: a key the authority alone holds, of the account's provider. */
594
+ const usageApiKeyContext = (context: Record<string, unknown>): boolean =>
595
+ ((context.providerId === 'anthropic' && context.purpose === 'anthropic_api_key')
596
+ || (context.providerId === 'openai' && context.purpose === 'openai_api_key'))
597
+ && context.owner === 'daemon' && context.nativeSource === null && context.grantState === 'ready';
537
598
  const usageReading = (value: unknown): boolean => object(value)
538
599
  && (value.kind === 'openai' ? exactKeys(value, ['kind', 'rateLimits']) && usageRateLimits(value.rateLimits)
539
600
  : value.kind === 'anthropic' && exactKeys(value, ['kind', 'status'])
@@ -543,12 +604,31 @@ const storedUsageKeys = ['id', 'accountId', 'loginId', 'context', 'updateId', 'r
543
604
  'staleUntil', 'lastAttemptAt', 'nextAttemptAt', 'failures', 'problem'] as const;
544
605
 
545
606
  /**
546
- * A credential-free usage record. `paymentProblem` is present only while a Claude login is held with one, never as null
547
- * or undefined: an accounts service of 40.2.0 or older refuses every record that has the member.
607
+ * A credential-free usage record. `paymentProblem` is present only while a Claude login or an API key is held with one,
608
+ * never as null or undefined: an accounts service of 40.2.0 or older refuses every record that has the member. An API
609
+ * key's record holds nothing but its payment problem: the authority reads no usage of a key, whose spend is its ledger's.
548
610
  */
549
611
  export const assertStoredAuthorityUsage: (value: unknown) => asserts value is IStoredAuthorityUsage = value => {
612
+ if (object(value) && object(value.context) && usageApiKeyContext(value.context)) {
613
+ if (!(exactKeys(value, storedUsageKeys) || (exactKeys(value, [...storedUsageKeys, 'paymentProblem'])
614
+ && usagePaymentProblem(value.paymentProblem) && object(value.paymentProblem)
615
+ && value.paymentProblem.subscription === null))
616
+ || !hash(value.accountId) || !hash(value.loginId) || value.id !== authorityUsageId(value.accountId, value.loginId)
617
+ || !exactKeys(value.context, ['epoch', 'accountId', 'loginId', 'subjectId', 'workspaceId', 'providerId', 'purpose',
618
+ 'owner', 'grantState', 'removed', 'authorizationGeneration', 'grantGeneration', 'nativeSource'])
619
+ || !uuid(value.context.epoch) || value.context.accountId !== value.accountId || value.context.loginId !== value.loginId
620
+ || !bounded(value.context.subjectId) || value.context.workspaceId !== 'api-key' || value.context.removed !== false
621
+ || !revision(value.context.authorizationGeneration) || !revision(value.context.grantGeneration)
622
+ || !uuid(value.updateId) || value.reading !== null || value.observedAt !== null || value.currentUntil !== null
623
+ || value.staleUntil !== null || !iso(value.lastAttemptAt) || value.nextAttemptAt !== value.lastAttemptAt
624
+ || value.failures !== 0 || value.problem !== 'none') {
625
+ throw new Error('Invalid credential-free authority usage record.');
626
+ }
627
+ return;
628
+ }
550
629
  if (!object(value) || !(exactKeys(value, storedUsageKeys)
551
630
  || (exactKeys(value, [...storedUsageKeys, 'paymentProblem']) && usagePaymentProblem(value.paymentProblem)
631
+ && object(value.paymentProblem) && !Object.hasOwn(value.paymentProblem, 'cause')
552
632
  && object(value.context) && value.context.providerId === 'anthropic'))
553
633
  || !hash(value.accountId) || !hash(value.loginId)
554
634
  || value.id !== authorityUsageId(value.accountId, value.loginId)
@@ -614,6 +694,31 @@ export const assertStoredAuthorityUsageEstimate: (value: unknown) => asserts val
614
694
  }
615
695
  };
616
696
 
697
+ export const assertStoredAuthoritySpendEntry: (value: unknown) => asserts value is IStoredAuthoritySpendEntry = value => {
698
+ if (!object(value) || !exactKeys(value, ['id', 'accountId', 'answerId', 'period', 'endedAt', 'model', 'serviceTier',
699
+ 'microUsd', 'priceSource']) || !hash(value.accountId) || typeof value.answerId !== 'string'
700
+ || value.id !== authoritySpendEntryId(value.accountId, value.answerId) || !isAuthSwitchSpendPeriodKey(value.period)
701
+ || !isAuthSwitchSpendAnswer({ answerId: value.answerId, endedAt: value.endedAt, model: value.model,
702
+ serviceTier: value.serviceTier, microUsd: value.microUsd, priceSource: value.priceSource })
703
+ || authSwitchSpendPeriod(Date.parse(value.endedAt as string)).key !== value.period) {
704
+ throw new Error('Invalid authority spend entry.');
705
+ }
706
+ };
707
+
708
+ export const assertStoredAuthoritySpendPeriod: (value: unknown) => asserts value is IStoredAuthoritySpendPeriod = value => {
709
+ if (!object(value) || !exactKeys(value, ['id', 'accountId', 'period', 'spentMicroUsd', 'answers', 'unpricedAnswers',
710
+ 'lastAnswerAt', 'revision', 'updateId']) || !hash(value.accountId) || !isAuthSwitchSpendPeriodKey(value.period)
711
+ || value.id !== authoritySpendPeriodId(value.accountId, value.period)
712
+ || !revision(value.spentMicroUsd) || !revision(value.answers) || !revision(value.unpricedAnswers)
713
+ || (value.unpricedAnswers as number) > (value.answers as number)
714
+ || (value.answers === 0) !== (value.lastAnswerAt === null)
715
+ || (value.lastAnswerAt !== null && (!iso(value.lastAnswerAt)
716
+ || authSwitchSpendPeriod(Date.parse(value.lastAnswerAt)).key !== value.period))
717
+ || !revision(value.revision) || !uuid(value.updateId)) {
718
+ throw new Error('Invalid authority spend period.');
719
+ }
720
+ };
721
+
617
722
  export const assertStoredAuthorityMeta: (value: unknown) => asserts value is IStoredAuthorityMeta = value => {
618
723
  if (!object(value) || !exactKeys(value, ['id', 'epoch', 'revision', 'updateId'])
619
724
  || value.id !== 'authswitch-authority' || !uuid(value.epoch)
@@ -622,7 +727,8 @@ export const assertStoredAuthorityMeta: (value: unknown) => asserts value is ISt
622
727
 
623
728
  export const assertStoredAuthorityAccount: (value: unknown) => asserts value is IStoredAuthorityAccount = value => {
624
729
  if (!object(value) || !exactKeys(value, ['id', 'providerId', 'issuer', 'subject', 'workspaceId', 'label',
625
- 'email', 'plan', 'primaryGrantId', 'maxUsagePercent', 'reserveLiftUntil', 'lapsesAt', 'removed', 'revision', 'statusObservedAt', 'updateId'])
730
+ 'email', 'plan', 'primaryGrantId', 'maxUsagePercent', 'reserveLiftUntil', 'lapsesAt', 'spendLimitMicroUsd', 'keyHint',
731
+ 'removed', 'revision', 'statusObservedAt', 'updateId'])
626
732
  || !hash(value.id) || typeof value.providerId !== 'string'
627
733
  || !/^[a-z][a-z0-9-]{0,63}$/.test(value.providerId)
628
734
  || !bounded(value.issuer) || !bounded(value.subject) || !bounded(value.workspaceId)
@@ -632,6 +738,8 @@ export const assertStoredAuthorityAccount: (value: unknown) => asserts value is
632
738
  || (value.maxUsagePercent !== null && !isAuthSwitchMaxUsagePercent(value.maxUsagePercent))
633
739
  || (value.reserveLiftUntil !== null && !iso(value.reserveLiftUntil))
634
740
  || (value.lapsesAt !== null && (!iso(value.lapsesAt) || !isAuthSwitchLapsesAt(value.lapsesAt)))
741
+ || (value.spendLimitMicroUsd !== null && !isAuthSwitchSpendLimitMicroUsd(value.spendLimitMicroUsd))
742
+ || (value.keyHint !== null && !isAuthSwitchKeyHint(value.keyHint))
635
743
  || typeof value.removed !== 'boolean' || !revision(value.revision)
636
744
  || !iso(value.statusObservedAt) || !uuid(value.updateId)) throw new Error('Invalid authswitch account record.');
637
745
  };
@@ -642,8 +750,8 @@ export const assertStoredAuthorityGrant: (value: unknown) => asserts value is IS
642
750
  'accessExpiresAt', 'attemptId', 'retryAt', 'retryCount', 'problem', 'statusObservedAt', 'updateId'])
643
751
  || !hash(value.id) || !hash(value.accountId)
644
752
  || typeof value.providerId !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(value.providerId)
645
- || !['openai_managed', 'claude_host_native', 'claude_container_setup', 'opencode_native', 'anthropic_api_key']
646
- .includes(String(value.purpose))
753
+ || !['openai_managed', 'claude_host_native', 'claude_container_setup', 'opencode_native', 'anthropic_api_key',
754
+ 'openai_api_key'].includes(String(value.purpose))
647
755
  || !bounded(value.audience)
648
756
  || !['ready', 'exchange_may_have_been_sent', 'retry_wait', 'needs_reauth', 'native', 'legacy_native_pending', 'handoff_pending', 'handoff_quarantined', 'removed'].includes(String(value.state))
649
757
  || !['daemon', 'claude_native', 'codex_native', 'legacy_native', 'none'].includes(String(value.owner))
@@ -682,10 +790,11 @@ export const assertStoredAuthorityGrant: (value: unknown) => asserts value is IS
682
790
  if (value.purpose === 'claude_container_setup' && value.owner !== 'daemon' && value.owner !== 'none') {
683
791
  throw new Error('Container setup grant has a native owner.');
684
792
  }
685
- // An Anthropic API key is the authority's alone: no native tool holds or renews it.
686
- if (value.purpose === 'anthropic_api_key' && (value.providerId !== 'anthropic'
687
- || (value.owner !== 'daemon' && value.owner !== 'none'))) {
688
- throw new Error('Anthropic API key grant has incompatible custody.');
793
+ // An API key is the authority's alone: no native tool holds or renews it.
794
+ if ((value.purpose === 'anthropic_api_key' || value.purpose === 'openai_api_key')
795
+ && (value.providerId !== authorityApiKeyProvider(value.purpose)
796
+ || (value.owner !== 'daemon' && value.owner !== 'none'))) {
797
+ throw new Error('API key grant has incompatible custody.');
689
798
  }
690
799
  };
691
800
 
@@ -1189,6 +1298,8 @@ export class AuthSwitchAuthorityAccountModel extends plugins.nosqldb.SmartDataDb
1189
1298
  @plugins.nosqldb.svDb() public maxUsagePercent!: number | null;
1190
1299
  @plugins.nosqldb.svDb() public reserveLiftUntil!: string | null;
1191
1300
  @plugins.nosqldb.svDb() public lapsesAt!: string | null;
1301
+ @plugins.nosqldb.svDb() public spendLimitMicroUsd!: number | null;
1302
+ @plugins.nosqldb.svDb() public keyHint!: string | null;
1192
1303
  @plugins.nosqldb.svDb() public removed!: boolean;
1193
1304
  @plugins.nosqldb.svDb() public revision!: number;
1194
1305
  @plugins.nosqldb.svDb() public statusObservedAt!: string;
@@ -1259,6 +1370,41 @@ export class AuthSwitchAuthorityUsageEstimateModel extends plugins.nosqldb.Smart
1259
1370
  @plugins.nosqldb.svDb() public observation!: IAuthSwitchStoredUsageEstimate['observation'];
1260
1371
  }
1261
1372
 
1373
+ @plugins.nosqldb.compoundIndex({ name: 'authority_spend_entry_account_period', key: { accountId: 1, period: 1, id: 1 } })
1374
+ @plugins.nosqldb.managed({ collectionName: 'authswitch_authority_spend_entries' })
1375
+ @plugins.nosqldb.exactPersistence({ assertDocument: assertStoredAuthoritySpendEntry })
1376
+ export class AuthSwitchAuthoritySpendEntryModel extends plugins.nosqldb.SmartDataDbDoc<AuthSwitchAuthoritySpendEntryModel,
1377
+ IStoredAuthoritySpendEntry> {
1378
+ declare static exact: plugins.nosqldb.TExact<AuthSwitchAuthoritySpendEntryModel>;
1379
+ @plugins.nosqldb.unI() public id!: string;
1380
+ @plugins.nosqldb.svDb() public accountId!: string;
1381
+ @plugins.nosqldb.svDb() public answerId!: string;
1382
+ @plugins.nosqldb.svDb() public period!: string;
1383
+ @plugins.nosqldb.svDb() public endedAt!: string;
1384
+ @plugins.nosqldb.svDb() public model!: string;
1385
+ @plugins.nosqldb.svDb() public serviceTier!: string | null;
1386
+ @plugins.nosqldb.svDb() public microUsd!: number | null;
1387
+ @plugins.nosqldb.svDb() public priceSource!: string | null;
1388
+ }
1389
+
1390
+ @plugins.nosqldb.compoundIndex({ name: 'authority_spend_period_account_period', key: { accountId: 1, period: 1 },
1391
+ options: { unique: true } })
1392
+ @plugins.nosqldb.managed({ collectionName: 'authswitch_authority_spend_periods' })
1393
+ @plugins.nosqldb.exactPersistence({ assertDocument: assertStoredAuthoritySpendPeriod })
1394
+ export class AuthSwitchAuthoritySpendPeriodModel extends plugins.nosqldb.SmartDataDbDoc<AuthSwitchAuthoritySpendPeriodModel,
1395
+ IStoredAuthoritySpendPeriod> {
1396
+ declare static exact: plugins.nosqldb.TExact<AuthSwitchAuthoritySpendPeriodModel>;
1397
+ @plugins.nosqldb.unI() public id!: string;
1398
+ @plugins.nosqldb.svDb() public accountId!: string;
1399
+ @plugins.nosqldb.svDb() public period!: string;
1400
+ @plugins.nosqldb.svDb() public spentMicroUsd!: number;
1401
+ @plugins.nosqldb.svDb() public answers!: number;
1402
+ @plugins.nosqldb.svDb() public unpricedAnswers!: number;
1403
+ @plugins.nosqldb.svDb() public lastAnswerAt!: string | null;
1404
+ @plugins.nosqldb.svDb() public revision!: number;
1405
+ @plugins.nosqldb.svDb() public updateId!: string;
1406
+ }
1407
+
1262
1408
  @plugins.nosqldb.compoundIndex({ name: 'authority_binding_account_id', key: { accountId: 1, id: 1 } })
1263
1409
  @plugins.nosqldb.managed({ collectionName: 'authswitch_authority_bindings' })
1264
1410
  @plugins.nosqldb.exactPersistence({ assertDocument: assertStoredAuthorityBinding })
@@ -6,6 +6,7 @@ import {
6
6
  AuthSwitchAuthorityCodexHostHomeModel, AuthSwitchAuthorityEnrollmentModel,
7
7
  AuthSwitchAuthorityEventModel, AuthSwitchAuthorityGrantModel, AuthSwitchAuthorityHandoffModel,
8
8
  AuthSwitchAuthorityMetaModel, AuthSwitchAuthorityMigrationLedgerModel, AuthSwitchAuthorityOperationModel,
9
+ AuthSwitchAuthoritySpendEntryModel, AuthSwitchAuthoritySpendPeriodModel,
9
10
  AuthSwitchAuthorityUsageEstimateModel, AuthSwitchAuthorityUsageModel,
10
11
  } from './classes.authoritymodels.js';
11
12
 
@@ -42,6 +43,8 @@ export const authorityModelRegistry: readonly IAuthorityModelEntry[] = Object.fr
42
43
  define('authswitch_authority_meta', AuthSwitchAuthorityMetaModel),
43
44
  define('authswitch_authority_migration_ledger', AuthSwitchAuthorityMigrationLedgerModel),
44
45
  define('authswitch_authority_operations', AuthSwitchAuthorityOperationModel),
46
+ define('authswitch_authority_spend_entries', AuthSwitchAuthoritySpendEntryModel),
47
+ define('authswitch_authority_spend_periods', AuthSwitchAuthoritySpendPeriodModel),
45
48
  define('authswitch_authority_usage', AuthSwitchAuthorityUsageModel),
46
49
  define('authswitch_authority_usage_estimates', AuthSwitchAuthorityUsageEstimateModel),
47
50
  ]);