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.
- package/changelog.md +27 -0
- package/dist_serve/bundle.js +2228 -2177
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +2 -2
- package/dist_ts/classes.accountsactions.d.ts +11 -1
- package/dist_ts/classes.accountsactions.js +59 -2
- package/dist_ts/classes.controller.js +5 -5
- package/dist_ts/classes.flexaccountaccess.js +3 -2
- package/dist_ts/classes.harnessreleases.d.ts +14 -4
- package/dist_ts/classes.harnessreleases.js +28 -4
- package/dist_ts/classes.inferencegateway.d.ts +28 -3
- package/dist_ts/classes.inferencegateway.js +177 -61
- package/dist_ts/classes.inferenceplacement.d.ts +5 -3
- package/dist_ts/classes.inferenceplacement.js +2 -2
- package/dist_ts/classes.inferencerouter.d.ts +349 -91
- package/dist_ts/classes.inferencerouter.js +804 -297
- package/dist_ts/classes.inferenceservice.d.ts +14 -2
- package/dist_ts/classes.inferenceservice.js +53 -14
- package/dist_ts/classes.inferencespend.d.ts +339 -0
- package/dist_ts/classes.inferencespend.js +815 -0
- package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
- package/dist_ts/classes.inferenceusagecache.js +9 -5
- package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
- package/dist_ts/classes.inferenceusagetap.js +185 -31
- package/dist_ts/classes.proxycontrol.d.ts +3 -2
- package/dist_ts/classes.proxycontrol.js +1 -2
- package/dist_ts/classes.proxydaemon.js +3 -2
- package/dist_ts/classes.proxystore.d.ts +6 -0
- package/dist_ts/classes.proxystore.js +11 -2
- package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
- package/dist_ts/fleet/classes.claudeworkers.js +10 -9
- package/dist_ts/functions.claudecoderelease.d.ts +12 -6
- package/dist_ts/functions.claudecoderelease.js +13 -7
- package/dist_ts/functions.inferencecli.js +12 -8
- package/dist_ts/interfaces.auth.d.ts +1 -1
- package/dist_ts/interfaces.auth.js +3 -1
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
- package/dist_ts_authswitch/authority-apikeys.js +65 -0
- package/dist_ts_authswitch/authority-contract.d.ts +142 -28
- package/dist_ts_authswitch/authority-contract.js +48 -1
- package/dist_ts_authswitch/authority-overview.js +8 -1
- package/dist_ts_authswitch/authority-placement.d.ts +7 -2
- package/dist_ts_authswitch/authority-placement.js +10 -3
- package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
- package/dist_ts_authswitch/authority-spend.d.ts +103 -0
- package/dist_ts_authswitch/authority-spend.js +110 -0
- package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
- package/dist_ts_authswitch/authority-subscription.js +24 -1
- package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
- package/dist_ts_authswitch/authority-usagelimit.js +89 -39
- package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
- package/dist_ts_authswitch/authority-usagewindows.js +41 -16
- package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
- package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
- package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
- package/dist_ts_authswitch/classes.authoritycli.js +94 -29
- package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
- package/dist_ts_authswitch/classes.authorityclient.js +31 -11
- package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
- package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
- package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
- package/dist_ts_authswitch/classes.authorityimport.js +2 -2
- package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
- package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
- package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
- package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
- package/dist_ts_authswitch/classes.authorityusage.js +118 -4
- package/dist_ts_authswitch/classes.claudenative.d.ts +16 -7
- package/dist_ts_authswitch/classes.claudenative.js +29 -16
- package/dist_ts_authswitch/classes.claudesignin.js +5 -2
- package/dist_ts_authswitch/testsupport.d.ts +24 -5
- package/dist_ts_authswitch/testsupport.js +56 -12
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
- package/dist_ts_authswitch/ts_migration/index.js +3 -1
- package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
- package/dist_ts_interfaces/functions.authorityscope.js +35 -8
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/plugins.d.ts +2 -1
- package/dist_ts_interfaces/plugins.js +7 -4
- package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
- package/dist_ts_interfaces/requests.accounts.js +11 -1
- package/dist_ts_interfaces/requests.d.ts +2 -2
- package/dist_ts_interfaces/requests.js +1 -1
- package/docs/authswitch.md +75 -12
- package/package.json +5 -5
- package/readme.md +198 -21
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +2 -2
- package/ts/classes.accountsactions.ts +75 -1
- package/ts/classes.controller.ts +4 -4
- package/ts/classes.flexaccountaccess.ts +2 -1
- package/ts/classes.harnessreleases.ts +25 -4
- package/ts/classes.inferencegateway.ts +184 -64
- package/ts/classes.inferenceplacement.ts +6 -4
- package/ts/classes.inferencerouter.ts +1026 -314
- package/ts/classes.inferenceservice.ts +56 -13
- package/ts/classes.inferencespend.ts +870 -0
- package/ts/classes.inferenceusagecache.ts +16 -4
- package/ts/classes.inferenceusagetap.ts +159 -34
- package/ts/classes.proxycontrol.ts +3 -3
- package/ts/classes.proxydaemon.ts +2 -1
- package/ts/classes.proxystore.ts +11 -1
- package/ts/fleet/classes.claudeworkers.ts +9 -8
- package/ts/functions.claudecoderelease.ts +13 -6
- package/ts/functions.inferencecli.ts +11 -7
- package/ts/interfaces.auth.ts +2 -0
- package/ts/plugins.ts +7 -0
- package/ts_authswitch/authority-apikeys.ts +85 -0
- package/ts_authswitch/authority-contract.ts +167 -29
- package/ts_authswitch/authority-overview.ts +7 -0
- package/ts_authswitch/authority-placement.ts +12 -4
- package/ts_authswitch/authority-runtime-contract.ts +2 -2
- package/ts_authswitch/authority-spend.ts +168 -0
- package/ts_authswitch/authority-subscription.ts +28 -1
- package/ts_authswitch/authority-usagelimit.ts +95 -40
- package/ts_authswitch/authority-usagewindows.ts +42 -17
- package/ts_authswitch/classes.authoritybroker.ts +161 -68
- package/ts_authswitch/classes.authoritycli.ts +108 -32
- package/ts_authswitch/classes.authorityclient.ts +41 -10
- package/ts_authswitch/classes.authoritydaemon.ts +43 -13
- package/ts_authswitch/classes.authoritydatabase.ts +178 -8
- package/ts_authswitch/classes.authorityimport.ts +1 -1
- package/ts_authswitch/classes.authoritymodels.ts +165 -19
- package/ts_authswitch/classes.authorityregistry.ts +3 -0
- package/ts_authswitch/classes.authorityusage.ts +171 -10
- package/ts_authswitch/classes.claudenative.ts +28 -15
- package/ts_authswitch/classes.claudesignin.ts +4 -1
- package/ts_authswitch/native-contracts.md +81 -20
- package/ts_authswitch/readme.md +1 -1
- package/ts_authswitch/testsupport.ts +58 -11
- package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
- package/ts_authswitch/ts_migration/index.ts +2 -0
- package/ts_interfaces/functions.authorityscope.ts +43 -8
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/plugins.ts +6 -3
- package/ts_interfaces/requests.accounts.ts +48 -0
- package/ts_interfaces/requests.ts +4 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/accounts.plugins.ts +3 -1
- package/ts_web/accounts.templates.ts +20 -11
- package/ts_web/classes.accountsreadmodel.ts +322 -70
- package/ts_web/elements.accounts.ts +219 -20
- package/ts_web/elements.codexconnections.ts +3 -1
- 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
|
|
229
|
-
* the daemon's Claude renewer. The authority page's `bindableGrantIds` and the
|
|
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
|
-
|
|
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.
|
|
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 === '
|
|
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
|
|
529
|
-
*
|
|
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'])
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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,
|
|
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', '
|
|
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
|
|
686
|
-
if (value.purpose === 'anthropic_api_key'
|
|
687
|
-
|
|
688
|
-
|
|
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
|
]);
|