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
|
@@ -2,7 +2,9 @@ import type { IAuthSwitchAccount, IAuthSwitchClaudeHostLogin, IAuthSwitchClaudeH
|
|
|
2
2
|
IAuthSwitchDoctorPage, IAuthSwitchUsageRecheck, IAuthSwitchUsageSnapshot, IAuthSwitchOperation, IAuthSwitchPreuseOperation, IAuthSwitchSnapshot, IReq_AuthSwitchDoctor,
|
|
3
3
|
IReq_AuthSwitchAddApiKey, IReq_AuthSwitchRemoveAccount, IReq_AuthSwitchStartPreuse,
|
|
4
4
|
IReq_AuthSwitchSwitchCodexNative, IReq_AuthSwitchReleaseCodexNative } from './authority-contract.js';
|
|
5
|
-
import { asAuthSwitchRefusal, authSwitchClaudeSignInArguments, isAuthSwitchLapsesAt
|
|
5
|
+
import { asAuthSwitchRefusal, authSwitchClaudeSignInArguments, isAuthSwitchLapsesAt,
|
|
6
|
+
type TAuthSwitchApiKeyProvider } from './authority-contract.js';
|
|
7
|
+
import { authSwitchParseUsd, authSwitchUsdText } from './authority-spend.js';
|
|
6
8
|
import type { IAuthSwitchImportInventory, IAuthSwitchImportResult, IAuthSwitchImportStatus,
|
|
7
9
|
IReq_AuthSwitchImportStatus, TAuthSwitchImportAction } from './authority-import-contract.js';
|
|
8
10
|
import { AuthSwitchClient } from './classes.authorityclient.js';
|
|
@@ -13,7 +15,8 @@ import { describePreuseFailure, describePreuseLimit } from './preuse-text.js';
|
|
|
13
15
|
import { plainText } from './formatting.js';
|
|
14
16
|
import { authSwitchLimitList, authSwitchUsageLogin, readAuthSwitchAccountOverview, renderAuthSwitchAccountOverview,
|
|
15
17
|
renderAuthSwitchLimitList } from './authority-overview.js';
|
|
16
|
-
import { authSwitchPaymentProblemText,
|
|
18
|
+
import { authSwitchApiKeyPaymentProblemText, authSwitchPaymentProblemText,
|
|
19
|
+
authSwitchRecheckOutcomeText } from './authority-subscription.js';
|
|
17
20
|
import { localDateTime } from './timetext.js';
|
|
18
21
|
import { authSwitchAccountLifecycle, lapseHorizonMs } from './authority-placement.js';
|
|
19
22
|
import { authSwitchPackageIdentity, authSwitchRelease } from './packageidentity.js';
|
|
@@ -31,7 +34,8 @@ export interface IAuthSwitchAuthorityCliClient {
|
|
|
31
34
|
importStatus(request: IReq_AuthSwitchImportStatus['request']): Promise<IAuthSwitchImportStatus>;
|
|
32
35
|
submitImport(sourceId: string, sourceDigest: string): Promise<IAuthSwitchImportResult>;
|
|
33
36
|
beginAddOpenAi(operationId: string): Promise<IAuthSwitchOperation>;
|
|
34
|
-
|
|
37
|
+
addApiKey(providerId: TAuthSwitchApiKeyProvider, apiKey: string,
|
|
38
|
+
options: { label?: string; spendLimitMicroUsd?: number }): Promise<IReq_AuthSwitchAddApiKey['response']>;
|
|
35
39
|
beginReauthOpenAi(operationId: string, accountId: string, loginId: string,
|
|
36
40
|
purpose: 'openai_managed'): Promise<IAuthSwitchOperation>;
|
|
37
41
|
getOperation(operationId: string): Promise<IAuthSwitchOperation>;
|
|
@@ -55,6 +59,8 @@ export interface IAuthSwitchAuthorityCliClient {
|
|
|
55
59
|
setAccountMaxUsage(accountId: string, expectedRevision: number,
|
|
56
60
|
maxUsagePercent: number | null): Promise<IAuthSwitchAccount>;
|
|
57
61
|
setAccountLapse(accountId: string, expectedRevision: number, lapsesAt: string | null): Promise<IAuthSwitchAccount>;
|
|
62
|
+
setAccountSpendLimit(accountId: string, expectedRevision: number,
|
|
63
|
+
spendLimitMicroUsd: number | null): Promise<IAuthSwitchAccount>;
|
|
58
64
|
close(reason?: Error): void;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -69,10 +75,10 @@ export interface IAuthSwitchAuthorityCliOptions {
|
|
|
69
75
|
/** The Claude Code `account add claude` runs to sign in: `claude` as found on PATH unless stated. */
|
|
70
76
|
claudeExecutable?: string;
|
|
71
77
|
/**
|
|
72
|
-
* Reads the key `account add anthropic-api-key` adds: by default from standard input, a line typed at
|
|
73
|
-
* without echo or everything piped in. The signal aborts when the command is cancelled.
|
|
78
|
+
* Reads the key `account add anthropic-api-key|openai-api-key` adds: by default from standard input, a line typed at
|
|
79
|
+
* a terminal without echo or everything piped in. The signal aborts when the command is cancelled.
|
|
74
80
|
*/
|
|
75
|
-
readApiKey?: (signal: AbortSignal) => Promise<string>;
|
|
81
|
+
readApiKey?: (signal: AbortSignal, providerId: TAuthSwitchApiKeyProvider) => Promise<string>;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
type TAuthorityCliCommand =
|
|
@@ -85,7 +91,7 @@ type TAuthorityCliCommand =
|
|
|
85
91
|
| { kind: 'importSubmit'; sourceId: string; sourceDigest: string; json: boolean }
|
|
86
92
|
| { kind: 'add'; json: boolean }
|
|
87
93
|
| { kind: 'addClaude'; json: boolean }
|
|
88
|
-
| { kind: 'addApiKey'; label?: string; json: boolean }
|
|
94
|
+
| { kind: 'addApiKey'; providerId: TAuthSwitchApiKeyProvider; label?: string; spendLimitMicroUsd?: number; json: boolean }
|
|
89
95
|
| { kind: 'useCodex'; accountId: string; json: boolean }
|
|
90
96
|
| { kind: 'releaseCodex'; accountId: string; endRunningTurns: boolean; json: boolean }
|
|
91
97
|
| { kind: 'releaseClaude'; accountId: string; endNativeSessions: boolean; json: boolean }
|
|
@@ -95,6 +101,7 @@ type TAuthorityCliCommand =
|
|
|
95
101
|
| { kind: 'remove'; accountId: string; revoke: boolean; json: boolean }
|
|
96
102
|
| { kind: 'setMaxUsage'; accountId: string; maxUsagePercent: number | null; json: boolean }
|
|
97
103
|
| { kind: 'setLapse'; accountId: string; lapsesAt: string | null; json: boolean }
|
|
104
|
+
| { kind: 'setSpendLimit'; accountId: string; spendLimitMicroUsd: number | null; json: boolean }
|
|
98
105
|
| { kind: 'get'; operationId: string; json: boolean }
|
|
99
106
|
| { kind: 'cancel'; operationId: string; json: boolean }
|
|
100
107
|
| { kind: 'resume'; operationId: string; json: boolean }
|
|
@@ -111,7 +118,7 @@ class AuthorityCliFailure extends Error {
|
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
const usage = 'Usage: authswitch --help | authswitch --version [--json] | authswitch list [--json] | authswitch limits [--json] | authswitch account list [--json] | authswitch account add openai|claude [--json] | authswitch account add anthropic-api-key [--label <label>] [--json] | authswitch account use codex <account-id> [--json] | authswitch account release codex <account-id> [--end-running-turns] [--json] | authswitch account release claude <account-id> [--end-native-sessions] [--json] | authswitch account repair claude [--json] | authswitch account reauth <account-id> <login-id> [--json] | authswitch account recheck <account-id> [--json] | authswitch account remove <account-id> [--revoke] [--json] | authswitch account set <account-id> --max-usage <percent>|--clear|--lapses-at <date-time>|--clear-lapse [--json] | authswitch account operation get|cancel|resume <operation-id> [--json] | authswitch account preuse <account-id> <login-id>|--all [--prompt <text>] [--model <id>] [--json] | authswitch account preuse operation get|cancel|resume <operation-id> [--json] | authswitch authority doctor [--json] | authswitch authority import inventory [--json] | authswitch authority import status [--json] | authswitch authority import submit <source-id> --digest <source-digest> [--json] | authswitch authority service install|status|enable|start|stop | authswitch authority daemon';
|
|
121
|
+
const usage = 'Usage: authswitch --help | authswitch --version [--json] | authswitch list [--json] | authswitch limits [--json] | authswitch account list [--json] | authswitch account add openai|claude [--json] | authswitch account add anthropic-api-key|openai-api-key [--label <label>] [--spend-limit <dollars>] [--json] | authswitch account use codex <account-id> [--json] | authswitch account release codex <account-id> [--end-running-turns] [--json] | authswitch account release claude <account-id> [--end-native-sessions] [--json] | authswitch account repair claude [--json] | authswitch account reauth <account-id> <login-id> [--json] | authswitch account recheck <account-id> [--json] | authswitch account remove <account-id> [--revoke] [--json] | authswitch account set <account-id> --max-usage <percent>|--clear|--lapses-at <date-time>|--clear-lapse|--spend-limit <dollars>|--clear-spend-limit [--json] | authswitch account operation get|cancel|resume <operation-id> [--json] | authswitch account preuse <account-id> <login-id>|--all [--prompt <text>] [--model <id>] [--json] | authswitch account preuse operation get|cancel|resume <operation-id> [--json] | authswitch authority doctor [--json] | authswitch authority import inventory [--json] | authswitch authority import status [--json] | authswitch authority import submit <source-id> --digest <source-digest> [--json] | authswitch authority service install|status|enable|start|stop | authswitch authority daemon';
|
|
115
122
|
|
|
116
123
|
/** Every command, one per line, with what it does; `authswitch --help` prints it. */
|
|
117
124
|
const help = (): string => `authswitch ${authSwitchRelease.version}
|
|
@@ -126,9 +133,11 @@ Usage
|
|
|
126
133
|
authswitch account add openai|claude [--json]
|
|
127
134
|
sign in a new account: a ChatGPT device sign-in, or Claude
|
|
128
135
|
Code's own sign-in on this terminal
|
|
129
|
-
authswitch account add anthropic-api-key [--label <label>] [--json]
|
|
130
|
-
add an Anthropic API key, read from standard input
|
|
131
|
-
|
|
136
|
+
authswitch account add anthropic-api-key|openai-api-key [--label <label>] [--spend-limit <dollars>] [--json]
|
|
137
|
+
add an Anthropic or OpenAI API key, read from standard input
|
|
138
|
+
and never shown, once its provider accepts it; AGL's proxy
|
|
139
|
+
relays requests on it at API prices, up to its monthly spend
|
|
140
|
+
limit in dollars when one is given
|
|
132
141
|
authswitch account use codex <account-id> [--json]
|
|
133
142
|
switch Codex on this host to that account
|
|
134
143
|
authswitch account release codex <account-id> [--end-running-turns] [--json]
|
|
@@ -149,7 +158,8 @@ Usage
|
|
|
149
158
|
authswitch account recheck <account-id> [--json]
|
|
150
159
|
read a Claude account's profile and usage now; a healthy
|
|
151
160
|
read clears a payment problem (a 402 billing error), so the
|
|
152
|
-
AGL proxy routes to it again
|
|
161
|
+
AGL proxy routes to it again; on an API key it lifts the
|
|
162
|
+
hold, and the provider's next answer shows whether it holds
|
|
153
163
|
authswitch account remove <account-id> [--revoke] [--json]
|
|
154
164
|
remove an account; --revoke also revokes its ChatGPT sign-in
|
|
155
165
|
authswitch account set <account-id> --max-usage <percent>|--clear [--json]
|
|
@@ -161,6 +171,11 @@ Usage
|
|
|
161
171
|
new sessions on it only as a last resort in its last day,
|
|
162
172
|
and from then on routes nothing to it and moves its
|
|
163
173
|
sessions; --clear-lapse makes it active again
|
|
174
|
+
authswitch account set <account-id> --spend-limit <dollars>|--clear-spend-limit [--json]
|
|
175
|
+
an API key's monthly spend limit (calendar month, UTC) on
|
|
176
|
+
what AGL's proxy relayed on it: at the limit the proxy places
|
|
177
|
+
nothing more on the key until the month ends;
|
|
178
|
+
--clear-spend-limit removes it
|
|
164
179
|
authswitch account operation get|cancel|resume <operation-id> [--json]
|
|
165
180
|
follow a sign-in that is still running
|
|
166
181
|
authswitch account preuse <account-id> <login-id>|--all [--prompt <text>] [--model <id>] [--json]
|
|
@@ -286,13 +301,27 @@ const parse = (argv: readonly string[]): TAuthorityCliCommand | null => {
|
|
|
286
301
|
&& (argv.length === 3 || (argv.length === 4 && argv[3] === '--json'))) {
|
|
287
302
|
return { kind: argv[2] === 'openai' ? 'add' : 'addClaude', json: argv[3] === '--json' };
|
|
288
303
|
}
|
|
289
|
-
if (argv[0] === 'account' && argv[1] === 'add' && argv[2] === 'anthropic-api-key') {
|
|
304
|
+
if (argv[0] === 'account' && argv[1] === 'add' && (argv[2] === 'anthropic-api-key' || argv[2] === 'openai-api-key')) {
|
|
290
305
|
// The key itself is never an argument: the shell history and the process list would keep it.
|
|
291
306
|
const json = argv.at(-1) === '--json';
|
|
292
307
|
const options = argv.slice(3, json ? -1 : undefined);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
308
|
+
const command: Extract<TAuthorityCliCommand, { kind: 'addApiKey' }> = { kind: 'addApiKey',
|
|
309
|
+
providerId: argv[2] === 'anthropic-api-key' ? 'anthropic' : 'openai', json };
|
|
310
|
+
for (let index = 0; index < options.length; index += 2) {
|
|
311
|
+
const value = options[index + 1];
|
|
312
|
+
if (value === undefined) return null;
|
|
313
|
+
if (options[index] === '--label' && command.label === undefined) {
|
|
314
|
+
command.label = value;
|
|
315
|
+
} else if (options[index] === '--spend-limit' && command.spendLimitMicroUsd === undefined) {
|
|
316
|
+
// The authority states the range; a value it refuses comes back as its instruction.
|
|
317
|
+
const spendLimitMicroUsd = authSwitchParseUsd(value);
|
|
318
|
+
if (spendLimitMicroUsd === null) return null;
|
|
319
|
+
command.spendLimitMicroUsd = spendLimitMicroUsd;
|
|
320
|
+
} else {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return command;
|
|
296
325
|
}
|
|
297
326
|
if (argv[0] === 'account' && argv[1] === 'use' && argv[2] === 'codex' && isId(argv[3])
|
|
298
327
|
&& (argv.length === 4 || (argv.length === 5 && argv[4] === '--json'))) {
|
|
@@ -350,6 +379,12 @@ const parse = (argv: readonly string[]): TAuthorityCliCommand | null => {
|
|
|
350
379
|
if (options.length === 2 && options[0] === '--lapses-at' && isAuthSwitchLapsesAt(options[1])) {
|
|
351
380
|
return { kind: 'setLapse', accountId: argv[2], lapsesAt: options[1], json };
|
|
352
381
|
}
|
|
382
|
+
if (options.length === 1 && options[0] === '--clear-spend-limit') {
|
|
383
|
+
return { kind: 'setSpendLimit', accountId: argv[2], spendLimitMicroUsd: null, json };
|
|
384
|
+
}
|
|
385
|
+
if (options.length === 2 && options[0] === '--spend-limit' && authSwitchParseUsd(options[1]!) !== null) {
|
|
386
|
+
return { kind: 'setSpendLimit', accountId: argv[2], spendLimitMicroUsd: authSwitchParseUsd(options[1]!), json };
|
|
387
|
+
}
|
|
353
388
|
return null;
|
|
354
389
|
}
|
|
355
390
|
if (argv[0] === 'account' && argv[1] === 'operation'
|
|
@@ -475,8 +510,14 @@ class ScopedAuthorityCliClient implements IAuthSwitchAuthorityCliClient {
|
|
|
475
510
|
return this.client.setAccountLapse(accountId, expectedRevision, lapsesAt, this.controller.signal);
|
|
476
511
|
}
|
|
477
512
|
|
|
478
|
-
public
|
|
479
|
-
|
|
513
|
+
public addApiKey(providerId: TAuthSwitchApiKeyProvider, apiKey: string,
|
|
514
|
+
options: { label?: string; spendLimitMicroUsd?: number }): Promise<IReq_AuthSwitchAddApiKey['response']> {
|
|
515
|
+
return this.client.addApiKey(providerId, apiKey, options, this.controller.signal);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
public setAccountSpendLimit(accountId: string, expectedRevision: number,
|
|
519
|
+
spendLimitMicroUsd: number | null): Promise<IAuthSwitchAccount> {
|
|
520
|
+
return this.client.setAccountSpendLimit(accountId, expectedRevision, spendLimitMicroUsd, this.controller.signal);
|
|
480
521
|
}
|
|
481
522
|
|
|
482
523
|
public close(reason = new Error('Authswitch authority CLI closed.')): void {
|
|
@@ -491,16 +532,26 @@ const revocationLines: Record<IReq_AuthSwitchRemoveAccount['response']['revoked'
|
|
|
491
532
|
not_applicable: 'The authority held no ChatGPT sign-in of its own for it, so nothing was revoked.',
|
|
492
533
|
};
|
|
493
534
|
|
|
535
|
+
/** An API key account's spend limit in words, as `account add` and `account set --spend-limit` state it. */
|
|
536
|
+
const spendLimitText = (account: IAuthSwitchAccount): string => account.spendLimitMicroUsd === null
|
|
537
|
+
? 'no monthly spend limit'
|
|
538
|
+
: `monthly spend limit ${authSwitchUsdText(account.spendLimitMicroUsd)} (calendar month, UTC); at it AGL places `
|
|
539
|
+
+ 'nothing more on the key until the month ends';
|
|
540
|
+
|
|
494
541
|
const renderApiKeyAdded = (account: IAuthSwitchAccount): string =>
|
|
495
|
-
`Added the
|
|
496
|
-
+ '
|
|
542
|
+
`Added the ${account.providerId === 'openai' ? 'OpenAI' : 'Anthropic'} API key`
|
|
543
|
+
+ `${account.keyHint === null ? '' : ` ending ${safe(account.keyHint)}`} as account ${safe(account.label)} `
|
|
544
|
+
+ `(${safe(account.id)}): ${spendLimitText(account)}. AGL's inference proxy relays requests on it at API prices.\n`;
|
|
545
|
+
|
|
546
|
+
const renderSpendLimit = (account: IAuthSwitchAccount): string =>
|
|
547
|
+
`Account ${safe(account.label)} (${safe(account.id)}): ${spendLimitText(account)}.\n`;
|
|
497
548
|
|
|
498
549
|
/**
|
|
499
550
|
* The key `account add anthropic-api-key` adds, from standard input: a line typed at a terminal, which is not echoed
|
|
500
551
|
* (a muted output for the line editor), or everything a pipe or file supplies. A cancelled command stops reading, so
|
|
501
552
|
* neither an open pipe nor the terminal holds it.
|
|
502
553
|
*/
|
|
503
|
-
const readStandardInputApiKey = async (signal: AbortSignal): Promise<string> => {
|
|
554
|
+
const readStandardInputApiKey = async (signal: AbortSignal, providerId: TAuthSwitchApiKeyProvider): Promise<string> => {
|
|
504
555
|
signal.throwIfAborted();
|
|
505
556
|
const input = process.stdin;
|
|
506
557
|
if (!input.isTTY) {
|
|
@@ -516,7 +567,7 @@ const readStandardInputApiKey = async (signal: AbortSignal): Promise<string> =>
|
|
|
516
567
|
for (const chunk of chunks) chunk.fill(0);
|
|
517
568
|
}
|
|
518
569
|
}
|
|
519
|
-
process.stderr.write('Anthropic API key (not shown):
|
|
570
|
+
process.stderr.write(`${providerId === 'openai' ? 'OpenAI' : 'Anthropic'} API key (not shown): `);
|
|
520
571
|
const muted = new plugins.stream.Writable({ write: (_chunk, _encoding, done) => done() });
|
|
521
572
|
const lines = plugins.readline.createInterface({ input, output: muted, terminal: true });
|
|
522
573
|
let cancel = (): void => undefined;
|
|
@@ -558,7 +609,9 @@ const renderLapse = (account: IAuthSwitchAccount, nowArg: number): string =>
|
|
|
558
609
|
const renderRecheck = (account: IAuthSwitchAccount, recheck: IAuthSwitchUsageRecheck): string =>
|
|
559
610
|
`Account ${safe(account.email ?? account.label)} (${safe(account.id)}): ${authSwitchRecheckOutcomeText(recheck.outcome)}\n`
|
|
560
611
|
+ (recheck.usage.paymentProblem
|
|
561
|
-
? `Payment problem since ${localDateTime(recheck.usage.paymentProblem.at)}: ${
|
|
612
|
+
? `Payment problem since ${localDateTime(recheck.usage.paymentProblem.at)}: ${account.organizationId === 'api-key'
|
|
613
|
+
? authSwitchApiKeyPaymentProblemText(recheck.usage.paymentProblem, account.providerId)
|
|
614
|
+
: authSwitchPaymentProblemText}\n`
|
|
562
615
|
: 'No payment problem is held; the AGL proxy routes to it as its usage allows.\n');
|
|
563
616
|
|
|
564
617
|
const renderRemoval = (removal: IReq_AuthSwitchRemoveAccount['response']): string =>
|
|
@@ -880,14 +933,17 @@ const renderCodexRelease = (result: IReq_AuthSwitchReleaseCodexNative['response'
|
|
|
880
933
|
};
|
|
881
934
|
|
|
882
935
|
/**
|
|
883
|
-
* Runs Claude Code's own sign-in in the home the authority made for it, on this terminal
|
|
884
|
-
*
|
|
936
|
+
* Runs Claude Code's own sign-in in the home the authority made for it, on this terminal, with its credential store
|
|
937
|
+
* pinned there too: an inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` would write the new login into another store, over
|
|
938
|
+
* whatever login that one holds, while the sign-in home never gets it. With `--json` its output goes to stderr, so
|
|
939
|
+
* stdout stays JSON Lines. Answers why it could not start, or null once it exited, with its code.
|
|
885
940
|
*/
|
|
886
941
|
const runClaudeSignIn = (executable: string, directory: string, json: boolean,
|
|
887
942
|
signal: AbortSignal): Promise<{ started: false; reason: string } | { started: true; exitCode: number | null }> =>
|
|
888
943
|
new Promise(resolve => {
|
|
889
944
|
const child = plugins.childProcess.spawn(executable, [...authSwitchClaudeSignInArguments], {
|
|
890
|
-
env: { ...process.env, CLAUDE_CONFIG_DIR: directory
|
|
945
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: directory, CLAUDE_SECURESTORAGE_CONFIG_DIR: directory },
|
|
946
|
+
stdio: ['inherit', json ? 2 : 'inherit', 'inherit'],
|
|
891
947
|
});
|
|
892
948
|
const stop = (): void => { child.kill('SIGTERM'); };
|
|
893
949
|
signal.addEventListener('abort', stop, { once: true });
|
|
@@ -1379,8 +1435,9 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
|
|
|
1379
1435
|
await writeDiagnostic('No authority account has this id. Run `authswitch account list` for the ids.\n');
|
|
1380
1436
|
return 1;
|
|
1381
1437
|
}
|
|
1382
|
-
if (
|
|
1383
|
-
|
|
1438
|
+
if (!login || !(login.purpose === 'anthropic_api_key' || login.purpose === 'openai_api_key'
|
|
1439
|
+
|| (account.providerId === 'anthropic' && login.purpose === 'claude_host_native'))) {
|
|
1440
|
+
await writeDiagnostic('Only a Claude account with a Claude sign-in, or an API key, is re-checked.\n');
|
|
1384
1441
|
return 1;
|
|
1385
1442
|
}
|
|
1386
1443
|
const recheck = await activeClient.recheckUsage(account.id, login.id);
|
|
@@ -1389,7 +1446,7 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
|
|
|
1389
1446
|
}
|
|
1390
1447
|
await writeOutput(command.json ? JSON.stringify({ accountId: account.id, loginId: login.id, ...recheck }) + '\n'
|
|
1391
1448
|
: renderRecheck(account, recheck));
|
|
1392
|
-
return recheck.outcome === 'healthy' ? 0 : 1;
|
|
1449
|
+
return recheck.outcome === 'healthy' || recheck.outcome === 'cleared' ? 0 : 1;
|
|
1393
1450
|
}
|
|
1394
1451
|
if (command.kind === 'setMaxUsage') {
|
|
1395
1452
|
const account = (await activeClient.snapshotAll()).accounts.find(item => item.id === command.accountId && !item.removed);
|
|
@@ -1418,10 +1475,29 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
|
|
|
1418
1475
|
await writeOutput(command.json ? JSON.stringify({ account: changed }) + '\n' : renderLapse(changed, now()));
|
|
1419
1476
|
return 0;
|
|
1420
1477
|
}
|
|
1478
|
+
if (command.kind === 'setSpendLimit') {
|
|
1479
|
+
const account = (await activeClient.snapshotAll()).accounts.find(item => item.id === command.accountId && !item.removed);
|
|
1480
|
+
if (!account) {
|
|
1481
|
+
await writeDiagnostic('No authority account has this id. Run `authswitch account list` for the ids.\n');
|
|
1482
|
+
return 1;
|
|
1483
|
+
}
|
|
1484
|
+
const changed = await activeClient.setAccountSpendLimit(account.id, account.revision, command.spendLimitMicroUsd);
|
|
1485
|
+
if (changed.id !== account.id || changed.spendLimitMicroUsd !== command.spendLimitMicroUsd) {
|
|
1486
|
+
throw new AuthorityCliFailure('response');
|
|
1487
|
+
}
|
|
1488
|
+
await writeOutput(command.json ? JSON.stringify({ account: changed }) + '\n' : renderSpendLimit(changed));
|
|
1489
|
+
return 0;
|
|
1490
|
+
}
|
|
1421
1491
|
if (command.kind === 'addApiKey') {
|
|
1422
|
-
const apiKey = (await withinDeadline(() => (options.readApiKey ?? readStandardInputApiKey)(operationController.signal
|
|
1423
|
-
|
|
1424
|
-
|
|
1492
|
+
const apiKey = (await withinDeadline(() => (options.readApiKey ?? readStandardInputApiKey)(operationController.signal,
|
|
1493
|
+
command.providerId))).trim();
|
|
1494
|
+
const added = await activeClient.addApiKey(command.providerId, apiKey, {
|
|
1495
|
+
...(command.label === undefined ? {} : { label: command.label }),
|
|
1496
|
+
...(command.spendLimitMicroUsd === undefined ? {} : { spendLimitMicroUsd: command.spendLimitMicroUsd }) });
|
|
1497
|
+
if (added.account.providerId !== command.providerId || added.account.removed
|
|
1498
|
+
|| (command.spendLimitMicroUsd !== undefined && added.account.spendLimitMicroUsd !== command.spendLimitMicroUsd)) {
|
|
1499
|
+
throw new AuthorityCliFailure('response');
|
|
1500
|
+
}
|
|
1425
1501
|
await writeOutput(command.json ? JSON.stringify(added) + '\n' : renderApiKeyAdded(added.account));
|
|
1426
1502
|
return 0;
|
|
1427
1503
|
}
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
IReq_AuthSwitchRemoveAccount, IReq_AuthSwitchRenameAccount, IReq_AuthSwitchSetAccountLapse, IReq_AuthSwitchSetAccountMaxUsage, IReq_AuthSwitchLiftAccountReserve,
|
|
11
11
|
IReq_AuthSwitchCancelPreuse, IReq_AuthSwitchGetPreuse, IReq_AuthSwitchSnapshot,
|
|
12
12
|
IReq_AuthSwitchStartPreuse, IReq_AuthSwitchUsage, IReq_AuthSwitchObserveUsage, IReq_AuthSwitchReportPaymentProblem,
|
|
13
|
+
IReq_AuthSwitchRecordSpend, IAuthSwitchSpendAnswer, TAuthSwitchPaymentProblemCause, TAuthSwitchApiKeyProvider,
|
|
14
|
+
IReq_AuthSwitchSetAccountSpendLimit,
|
|
13
15
|
IReq_AuthSwitchRecheckUsage, IReq_AuthSwitchMoveClaudeHomeIn, IReq_AuthSwitchMoveClaudeHomeOut,
|
|
14
16
|
IReq_AuthSwitchRegisterClaudeHome, IReq_AuthSwitchResolveClaudeHome, IReq_AuthSwitchUnregisterClaudeHome,
|
|
15
17
|
IReq_AuthSwitchCancelClaudeSignIn, IReq_AuthSwitchGetClaudeSignIn, IReq_AuthSwitchStartClaudeSignIn,
|
|
@@ -118,7 +120,8 @@ export class AuthSwitchDaemonOutdatedError extends Error {
|
|
|
118
120
|
/**
|
|
119
121
|
* A snapshot page carries every member of this release's contract. `schemaVersion` is not bumped for an
|
|
120
122
|
* additive member; version 3 adds the gateway runtime, every account states its `maxUsagePercent` from 38.3.0
|
|
121
|
-
* on, its `lapsesAt` from 38.4.0 on,
|
|
123
|
+
* on, its `lapsesAt` from 38.4.0 on, its `reserveLiftUntil` with reserve lifts, and its `spendLimitMicroUsd` and
|
|
124
|
+
* `keyHint` with API keys of both providers. Consumers still check their supported version.
|
|
122
125
|
*/
|
|
123
126
|
const assertCurrentSnapshot = (snapshot: IAuthSwitchSnapshot): IAuthSwitchSnapshot => {
|
|
124
127
|
const page: Partial<IAuthSwitchSnapshot> = snapshot;
|
|
@@ -126,7 +129,9 @@ const assertCurrentSnapshot = (snapshot: IAuthSwitchSnapshot): IAuthSwitchSnapsh
|
|
|
126
129
|
|| !Array.isArray(page.accounts)
|
|
127
130
|
|| page.accounts.some(account => (account as Partial<IAuthSwitchAccount>).maxUsagePercent === undefined
|
|
128
131
|
|| (account as Partial<IAuthSwitchAccount>).lapsesAt === undefined
|
|
129
|
-
|| (account as Partial<IAuthSwitchAccount>).reserveLiftUntil === undefined
|
|
132
|
+
|| (account as Partial<IAuthSwitchAccount>).reserveLiftUntil === undefined
|
|
133
|
+
|| (account as Partial<IAuthSwitchAccount>).spendLimitMicroUsd === undefined
|
|
134
|
+
|| (account as Partial<IAuthSwitchAccount>).keyHint === undefined)) {
|
|
130
135
|
throw new AuthSwitchDaemonOutdatedError();
|
|
131
136
|
}
|
|
132
137
|
return snapshot;
|
|
@@ -239,14 +244,27 @@ export class AuthSwitchClient {
|
|
|
239
244
|
}
|
|
240
245
|
|
|
241
246
|
/**
|
|
242
|
-
* Reports that
|
|
243
|
-
*
|
|
244
|
-
*
|
|
247
|
+
* Reports that the provider refused an inference request on the login for money at `observedAt` -- Anthropic's
|
|
248
|
+
* billing error (HTTP 402) on a Claude login, or on an API key's login the refusal `options.cause` names -- and
|
|
249
|
+
* returns the held usage, which then carries the payment problem (see `IReq_AuthSwitchReportPaymentProblem`). An
|
|
250
|
+
* authority of 40.2.0 or older refuses the method, one of 40.3.0 a `cause`.
|
|
245
251
|
*/
|
|
246
252
|
public async reportPaymentProblem(accountId: string, loginId: string, observedAt: string,
|
|
247
|
-
signal?: AbortSignal): Promise<IReq_AuthSwitchReportPaymentProblem['response']['usage']> {
|
|
253
|
+
signal?: AbortSignal, options: { cause?: TAuthSwitchPaymentProblemCause } = {}): Promise<IReq_AuthSwitchReportPaymentProblem['response']['usage']> {
|
|
248
254
|
return (await this.request<IReq_AuthSwitchReportPaymentProblem>('authswitch.authority.usage.paymentProblem',
|
|
249
|
-
{ accountId, loginId, observedAt }
|
|
255
|
+
{ accountId, loginId, observedAt, ...(options.cause === undefined ? {} : { cause: options.cause }) },
|
|
256
|
+
10_000, false, signal)).usage;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Records what answers relayed on an API key's login cost (one to `authSwitchSpendAnswerBatchLimit`) and returns the
|
|
261
|
+
* key's usage once the batch committed (see `IReq_AuthSwitchRecordSpend`). An authority of 40.3.0 or older refuses
|
|
262
|
+
* the method.
|
|
263
|
+
*/
|
|
264
|
+
public async recordSpend(accountId: string, loginId: string, answers: IAuthSwitchSpendAnswer[],
|
|
265
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchRecordSpend['response']['usage']> {
|
|
266
|
+
return (await this.request<IReq_AuthSwitchRecordSpend>('authswitch.authority.usage.spend',
|
|
267
|
+
{ accountId, loginId, answers }, 10_000, false, signal)).usage;
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
/**
|
|
@@ -429,13 +447,19 @@ export class AuthSwitchClient {
|
|
|
429
447
|
return (await this.request<IReq_AuthSwitchBeginAdd>('authswitch.authority.add',
|
|
430
448
|
{ operationId, providerId: 'openai', flow: 'device' }, 35_000, false, signal)).operation;
|
|
431
449
|
}
|
|
432
|
-
/**
|
|
433
|
-
|
|
450
|
+
/**
|
|
451
|
+
* Adds an Anthropic or OpenAI API key as an account (see `IReq_AuthSwitchAddApiKey`); the key is not answered back.
|
|
452
|
+
* It waits up to 35 seconds: the authority gives the provider 15 seconds to answer whether it accepts the key.
|
|
453
|
+
*/
|
|
454
|
+
public addApiKey(providerId: TAuthSwitchApiKeyProvider, apiKey: string,
|
|
455
|
+
options: { label?: string; spendLimitMicroUsd?: number } = {},
|
|
434
456
|
signal?: AbortSignal): Promise<IReq_AuthSwitchAddApiKey['response']> {
|
|
435
457
|
return this.request<IReq_AuthSwitchAddApiKey>('authswitch.authority.apikey.add',
|
|
436
|
-
label === undefined ? {
|
|
458
|
+
{ providerId, apiKey, ...(options.label === undefined ? {} : { label: options.label }),
|
|
459
|
+
...(options.spendLimitMicroUsd === undefined ? {} : { spendLimitMicroUsd: options.spendLimitMicroUsd }) },
|
|
437
460
|
35_000, false, signal);
|
|
438
461
|
}
|
|
462
|
+
|
|
439
463
|
public async beginReauthOpenAi(operationId: string, accountId: string, loginId: string,
|
|
440
464
|
purpose: 'openai_managed', signal?: AbortSignal): Promise<IReq_AuthSwitchBeginReauth['response']['operation']> {
|
|
441
465
|
return (await this.request<IReq_AuthSwitchBeginReauth>('authswitch.authority.reauth',
|
|
@@ -484,6 +508,13 @@ export class AuthSwitchClient {
|
|
|
484
508
|
return (await this.request<IReq_AuthSwitchRenameAccount>('authswitch.authority.rename',
|
|
485
509
|
{ accountId, expectedRevision, label }, 35_000, false, signal)).account;
|
|
486
510
|
}
|
|
511
|
+
/** Sets an API key account's monthly spend limit, or clears it with `null` (see `IReq_AuthSwitchSetAccountSpendLimit`). */
|
|
512
|
+
public async setAccountSpendLimit(accountId: string, expectedRevision: number, spendLimitMicroUsd: number | null,
|
|
513
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchSetAccountSpendLimit['response']['account']> {
|
|
514
|
+
return (await this.request<IReq_AuthSwitchSetAccountSpendLimit>('authswitch.authority.spendlimit',
|
|
515
|
+
{ accountId, expectedRevision, spendLimitMicroUsd }, 10_000, false, signal)).account;
|
|
516
|
+
}
|
|
517
|
+
|
|
487
518
|
/** Sets an account's usage threshold, or clears it with `null` (see `IReq_AuthSwitchSetAccountMaxUsage`). */
|
|
488
519
|
public async setAccountMaxUsage(accountId: string, expectedRevision: number, maxUsagePercent: number | null,
|
|
489
520
|
signal?: AbortSignal): Promise<IReq_AuthSwitchSetAccountMaxUsage['response']['account']> {
|
|
@@ -8,9 +8,9 @@ import type {
|
|
|
8
8
|
IReq_AuthSwitchRepairClaudeHost,
|
|
9
9
|
IReq_AuthSwitchEvents, IReq_AuthSwitchGetBinding, IReq_AuthSwitchGetOperation, IReq_AuthSwitchListOperations,
|
|
10
10
|
IReq_AuthSwitchRemoveAccount,
|
|
11
|
-
IReq_AuthSwitchRenameAccount, IReq_AuthSwitchSetAccountLapse, IReq_AuthSwitchSetAccountMaxUsage, IReq_AuthSwitchLiftAccountReserve, IReq_AuthSwitchSnapshot,
|
|
11
|
+
IReq_AuthSwitchRenameAccount, IReq_AuthSwitchSetAccountLapse, IReq_AuthSwitchSetAccountMaxUsage, IReq_AuthSwitchSetAccountSpendLimit, IReq_AuthSwitchLiftAccountReserve, IReq_AuthSwitchSnapshot,
|
|
12
12
|
IReq_AuthSwitchCancelPreuse, IReq_AuthSwitchGetPreuse, IReq_AuthSwitchStartPreuse, IReq_AuthSwitchUsage,
|
|
13
|
-
IReq_AuthSwitchObserveUsage, IReq_AuthSwitchReportPaymentProblem, IReq_AuthSwitchRecheckUsage,
|
|
13
|
+
IReq_AuthSwitchObserveUsage, IReq_AuthSwitchReportPaymentProblem, IReq_AuthSwitchRecheckUsage, IReq_AuthSwitchRecordSpend,
|
|
14
14
|
IReq_AuthSwitchMoveClaudeHomeIn, IReq_AuthSwitchMoveClaudeHomeOut, IReq_AuthSwitchRegisterClaudeHome,
|
|
15
15
|
IReq_AuthSwitchResolveClaudeHome, IReq_AuthSwitchUnregisterClaudeHome,
|
|
16
16
|
IReq_AuthSwitchCancelClaudeSignIn, IReq_AuthSwitchGetClaudeSignIn, IReq_AuthSwitchStartClaudeSignIn,
|
|
@@ -20,14 +20,15 @@ import type {
|
|
|
20
20
|
import type { IAuthSwitchManagedCodexRuntime, IReq_AuthSwitchBindAccount, IReq_AuthSwitchReleaseBinding,
|
|
21
21
|
IReq_AuthSwitchResolveAccess, IReq_AuthSwitchStartManagedCodex, IReq_AuthSwitchStopManagedCodex,
|
|
22
22
|
IReq_AuthSwitchUnbind } from './authority-runtime-contract.js';
|
|
23
|
-
import { AuthSwitchRefusal
|
|
23
|
+
import { AuthSwitchRefusal, authSwitchMalformedRequestText, isAuthSwitchPaymentProblemCause,
|
|
24
|
+
isAuthSwitchSpendAnswerBatch } from './authority-contract.js';
|
|
24
25
|
import type { IReq_AuthSwitchImportInventory, IReq_AuthSwitchImportStatus, IReq_AuthSwitchImportSubmit,
|
|
25
26
|
TAuthSwitchImportSubmission } from './authority-import-contract.js';
|
|
26
27
|
import { AuthSwitchAuthorityImport, AuthSwitchGrantImport,
|
|
27
28
|
type IAuthSwitchAuthorityImportOptions } from './classes.authorityimport.js';
|
|
28
29
|
import { authSwitchBindingCapabilityHash, AuthSwitchAuthorityBroker,
|
|
29
30
|
type IAuthSwitchAuthorityBrokerOptions } from './classes.authoritybroker.js';
|
|
30
|
-
import { AuthSwitchAuthorityDatabase, type IAuthSwitchAuthorityDatabaseOptions } from './classes.authoritydatabase.js';
|
|
31
|
+
import { AuthSwitchAuthorityDatabase, authSwitchBindPurposes, type IAuthSwitchAuthorityDatabaseOptions } from './classes.authoritydatabase.js';
|
|
31
32
|
import { ClaudeNativeAuthority, type IClaudeNativeAuthorityOptions } from './classes.claudeauthority.js';
|
|
32
33
|
import { ClaudeHostCustody, publicClaudeHostRepair, publicClaudeNativeRelease } from './classes.claudehostcustody.js';
|
|
33
34
|
import { ClaudeContainerAuthority, publicClaudeContainerHome,
|
|
@@ -124,9 +125,13 @@ function assertStatedLegacyImport(value: unknown): asserts value is TAuthSwitchA
|
|
|
124
125
|
const hasKeys = (value: unknown, keys: readonly string[]): value is Record<string, unknown> =>
|
|
125
126
|
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
126
127
|
&& Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
128
|
+
/** Every one of `keys`, any of `optional`, and nothing else. */
|
|
129
|
+
const hasOptionalKeys = (value: unknown, keys: readonly string[], optional: readonly string[]): value is Record<string, unknown> =>
|
|
130
|
+
value !== null && typeof value === 'object' && !Array.isArray(value) && keys.every(key => Object.hasOwn(value, key))
|
|
131
|
+
&& Object.keys(value).every(key => keys.includes(key) || optional.includes(key));
|
|
127
132
|
// A malformed request is a caller's own bug, not an instruction for an owner, so it stays what it was:
|
|
128
133
|
// its own text, no refusal marker. A client must not present it as something the owner can act on.
|
|
129
|
-
const invalid = (): never => { throw new plugins.typedrequest.TypedResponseError(
|
|
134
|
+
const invalid = (): never => { throw new plugins.typedrequest.TypedResponseError(authSwitchMalformedRequestText); };
|
|
130
135
|
const validBindingId = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
131
136
|
const validBindingCapability = (value: unknown): value is string => typeof value === 'string'
|
|
132
137
|
&& /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
@@ -241,6 +246,14 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
241
246
|
return this.claudeNative.withNativeUsageSource(context, action);
|
|
242
247
|
},
|
|
243
248
|
claudeStatus: options.claudeStatus ?? new ClaudeAccountStatus(),
|
|
249
|
+
spend: {
|
|
250
|
+
read: (accountId, loginId, nowArg) => this.database.readSpend(accountId, loginId, nowArg),
|
|
251
|
+
record: async (accountId, loginId, answers, nowArg) => {
|
|
252
|
+
const recorded = await this.database.recordSpend(accountId, loginId, answers, nowArg);
|
|
253
|
+
if (recorded.changed) this.broker.notifyChanged();
|
|
254
|
+
return recorded.reading;
|
|
255
|
+
},
|
|
256
|
+
},
|
|
244
257
|
store: {
|
|
245
258
|
read: (accountId, loginId) => this.database.readUsage(accountId, loginId),
|
|
246
259
|
publishIfCurrent: async (context, previousUpdateId, next) => {
|
|
@@ -421,11 +434,19 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
421
434
|
}));
|
|
422
435
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchReportPaymentProblem>(
|
|
423
436
|
'authswitch.authority.usage.paymentProblem', async request => {
|
|
424
|
-
if (!
|
|
437
|
+
if (!hasOptionalKeys(request, ['accountId', 'loginId', 'observedAt'], ['cause']) || !validHash(request.accountId)
|
|
425
438
|
|| !validHash(request.loginId) || typeof request.observedAt !== 'string'
|
|
426
439
|
|| !Number.isFinite(Date.parse(request.observedAt))
|
|
427
|
-
|| new Date(request.observedAt).toISOString() !== request.observedAt
|
|
428
|
-
|
|
440
|
+
|| new Date(request.observedAt).toISOString() !== request.observedAt
|
|
441
|
+
|| (request.cause !== undefined && !isAuthSwitchPaymentProblemCause(request.cause))) invalid();
|
|
442
|
+
return { usage: await this.usage.reportPaymentProblem(request.accountId, request.loginId, request.observedAt,
|
|
443
|
+
request.cause) };
|
|
444
|
+
}));
|
|
445
|
+
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchRecordSpend>(
|
|
446
|
+
'authswitch.authority.usage.spend', async request => {
|
|
447
|
+
if (!hasKeys(request, ['accountId', 'loginId', 'answers']) || !validHash(request.accountId)
|
|
448
|
+
|| !validHash(request.loginId) || !isAuthSwitchSpendAnswerBatch(request.answers)) invalid();
|
|
449
|
+
return { usage: await this.usage.recordSpend(request.accountId, request.loginId, request.answers) };
|
|
429
450
|
}));
|
|
430
451
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchRecheckUsage>(
|
|
431
452
|
'authswitch.authority.usage.recheck', async request => {
|
|
@@ -611,10 +632,11 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
611
632
|
}));
|
|
612
633
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchAddApiKey>(
|
|
613
634
|
'authswitch.authority.apikey.add', async request => {
|
|
614
|
-
if (
|
|
615
|
-
|| request.providerId !== 'anthropic' || typeof request.apiKey !== 'string'
|
|
616
|
-
|| (request.label !== undefined && typeof request.label !== 'string')
|
|
617
|
-
|
|
635
|
+
if (!hasOptionalKeys(request, ['providerId', 'apiKey'], ['label', 'spendLimitMicroUsd'])
|
|
636
|
+
|| (request.providerId !== 'anthropic' && request.providerId !== 'openai') || typeof request.apiKey !== 'string'
|
|
637
|
+
|| (request.label !== undefined && typeof request.label !== 'string')
|
|
638
|
+
|| (request.spendLimitMicroUsd !== undefined && typeof request.spendLimitMicroUsd !== 'number')) invalid();
|
|
639
|
+
return this.broker.addApiKey(request.providerId, request.apiKey, request.label, request.spendLimitMicroUsd);
|
|
618
640
|
}));
|
|
619
641
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchBeginReauth>(
|
|
620
642
|
'authswitch.authority.reauth', async request => {
|
|
@@ -703,6 +725,14 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
703
725
|
return { account: await this.broker.setAccountMaxUsage(request.accountId, request.expectedRevision,
|
|
704
726
|
request.maxUsagePercent) };
|
|
705
727
|
}));
|
|
728
|
+
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchSetAccountSpendLimit>(
|
|
729
|
+
'authswitch.authority.spendlimit', async request => {
|
|
730
|
+
if (!hasKeys(request, ['accountId', 'expectedRevision', 'spendLimitMicroUsd']) || typeof request.accountId !== 'string'
|
|
731
|
+
|| typeof request.expectedRevision !== 'number'
|
|
732
|
+
|| (request.spendLimitMicroUsd !== null && typeof request.spendLimitMicroUsd !== 'number')) invalid();
|
|
733
|
+
return { account: await this.broker.setAccountSpendLimit(request.accountId, request.expectedRevision,
|
|
734
|
+
request.spendLimitMicroUsd) };
|
|
735
|
+
}));
|
|
706
736
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchLiftAccountReserve>(
|
|
707
737
|
'authswitch.authority.reserve.lift', async request => {
|
|
708
738
|
if (!hasKeys(request, ['accountId', 'expectedRevision', 'hours']) || typeof request.accountId !== 'string'
|
|
@@ -743,7 +773,7 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
743
773
|
'authswitch.authority.bind', async request => {
|
|
744
774
|
if (!hasKeys(request, ['accountId', 'loginId', 'purpose', 'runtime', 'scopeId', 'incarnationId'])
|
|
745
775
|
|| typeof request.accountId !== 'string' || typeof request.loginId !== 'string'
|
|
746
|
-
|| !
|
|
776
|
+
|| !authSwitchBindPurposes.includes(request.purpose)
|
|
747
777
|
|| typeof request.scopeId !== 'string'
|
|
748
778
|
|| typeof request.incarnationId !== 'string') invalid();
|
|
749
779
|
return this.broker.bindAccount(request);
|