@modelprofile.com/authswitch 9.2.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/authority-contract.d.ts +52 -6
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/classes.authoritybroker.d.ts +20 -4
- package/dist_ts/classes.authoritybroker.js +44 -8
- package/dist_ts/classes.authoritycli.d.ts +4 -1
- package/dist_ts/classes.authoritycli.js +42 -7
- package/dist_ts/classes.authorityclient.d.ts +9 -1
- package/dist_ts/classes.authorityclient.js +9 -3
- package/dist_ts/classes.authoritydaemon.js +8 -5
- package/dist_ts/classes.authoritydatabase.d.ts +6 -2
- package/dist_ts/classes.authoritydatabase.js +13 -6
- package/dist_ts/classes.authorityimport.d.ts +4 -4
- package/dist_ts/classes.authorityimport.js +8 -7
- package/dist_ts/classes.authoritymodels.d.ts +11 -3
- package/dist_ts/classes.authoritymodels.js +43 -13
- package/dist_ts/classes.authoritypreuse.d.ts +8 -2
- package/dist_ts/classes.authoritypreuse.js +27 -17
- package/dist_ts/classes.authorityusage.d.ts +4 -3
- package/dist_ts/classes.authorityusage.js +3 -5
- package/dist_ts/classes.cli.js +3 -5
- package/dist_ts/classes.codexpreuse.d.ts +14 -3
- package/dist_ts/classes.codexpreuse.js +127 -44
- package/dist_ts/classes.codexstatus.js +44 -95
- package/dist_ts/classes.login.d.ts +20 -10
- package/dist_ts/classes.login.js +14 -13
- package/dist_ts/classes.openaiprovider.d.ts +53 -0
- package/dist_ts/classes.openaiprovider.js +340 -0
- package/dist_ts/classes.opencodeharness.js +3 -2
- package/dist_ts/classes.service.js +3 -5
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/interfaces.openai.d.ts +118 -0
- package/dist_ts/interfaces.openai.js +3 -0
- package/dist_ts/openaiauth.d.ts +76 -0
- package/dist_ts/openaiauth.js +371 -0
- package/dist_ts/openaiusage.d.ts +59 -0
- package/dist_ts/openaiusage.js +222 -0
- package/dist_ts/plugins.d.ts +1 -3
- package/dist_ts/plugins.js +2 -4
- package/dist_ts/preuse.d.ts +50 -0
- package/dist_ts/preuse.js +63 -4
- package/dist_ts/ts_migration/legacysources/shared.js +4 -3
- package/openai-codex-license.txt +201 -0
- package/package.json +6 -4
- package/readme.md +86 -10
- package/third-party-notices.md +29 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +56 -8
- package/ts/classes.authoritybroker.ts +52 -13
- package/ts/classes.authoritycli.ts +48 -6
- package/ts/classes.authorityclient.ts +11 -3
- package/ts/classes.authoritydaemon.ts +7 -4
- package/ts/classes.authoritydatabase.ts +14 -6
- package/ts/classes.authorityimport.ts +12 -12
- package/ts/classes.authoritymodels.ts +42 -14
- package/ts/classes.authoritypreuse.ts +30 -17
- package/ts/classes.authorityusage.ts +8 -9
- package/ts/classes.cli.ts +3 -3
- package/ts/classes.codexpreuse.ts +130 -40
- package/ts/classes.codexstatus.ts +40 -73
- package/ts/classes.login.ts +34 -21
- package/ts/classes.openaiprovider.ts +365 -0
- package/ts/classes.opencodeharness.ts +3 -2
- package/ts/classes.service.ts +3 -4
- package/ts/index.ts +2 -0
- package/ts/interfaces.openai.ts +142 -0
- package/ts/openaiauth.ts +422 -0
- package/ts/openaiusage.ts +247 -0
- package/ts/plugins.ts +1 -3
- package/ts/preuse.ts +88 -3
- package/ts/ts_migration/legacysources/shared.ts +4 -3
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
3
|
+
import { parseOpenAiTokenClaims } from './openaiauth.js';
|
|
4
|
+
import type { IOpenAiLoginHandle, IOpenAiOAuthCredential } from './interfaces.openai.js';
|
|
2
5
|
import type { IAuthSwitchAccount, IAuthSwitchAccountEvent, IAuthSwitchBinding, IAuthSwitchLogin,
|
|
3
6
|
IAuthSwitchNativeAssignment, IAuthSwitchOperation, IAuthSwitchSnapshot, IReq_AuthSwitchSnapshot,
|
|
4
|
-
TAuthSwitchLoginOwnerTool } from './authority-contract.js';
|
|
7
|
+
TAuthSwitchLoginOwnerTool, TAuthSwitchRevocation } from './authority-contract.js';
|
|
5
8
|
import { AuthSwitchRefusal, isAuthSwitchAccountLabel } from './authority-contract.js';
|
|
6
9
|
import { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
|
|
7
10
|
import type { IStoredAuthorityAccount, IStoredAuthorityBinding, IStoredAuthorityClaudeHome, IStoredAuthorityGrant,
|
|
@@ -9,9 +12,9 @@ import type { IStoredAuthorityAccount, IStoredAuthorityBinding, IStoredAuthority
|
|
|
9
12
|
import type { IAuthSwitchUsageContext } from './classes.authorityusage.js';
|
|
10
13
|
import { AuthSwitchTpmSecretCodec, type IAuthSwitchSecretCodec } from './classes.authoritysecrets.js';
|
|
11
14
|
|
|
12
|
-
type TOpenAiCredential =
|
|
13
|
-
type TOpenAiProvider = Pick<
|
|
14
|
-
'beginLogin' | 'refreshCredential' | 'inspectCredential' | 'getAccountRateLimits' | 'dispose'>;
|
|
15
|
+
type TOpenAiCredential = IOpenAiOAuthCredential;
|
|
16
|
+
type TOpenAiProvider = Pick<AuthSwitchOpenAiProvider,
|
|
17
|
+
'beginLogin' | 'refreshCredential' | 'revokeCredential' | 'inspectCredential' | 'getAccountRateLimits' | 'dispose'>;
|
|
15
18
|
|
|
16
19
|
export interface IAuthSwitchAuthorityBrokerOptions {
|
|
17
20
|
provider?: TOpenAiProvider;
|
|
@@ -46,9 +49,15 @@ const safeScope = (value: unknown, maximum: number): value is string => typeof v
|
|
|
46
49
|
&& value.length > 0 && value.length <= maximum && !/[\u0000-\u001f\u007f]/.test(value);
|
|
47
50
|
const validRevision = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
48
51
|
const tokenExpiry = (credential: TOpenAiCredential): string | null => {
|
|
49
|
-
try { return
|
|
52
|
+
try { return parseOpenAiTokenClaims(credential.accessToken).expiresAt ?? null; }
|
|
50
53
|
catch { return null; }
|
|
51
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* How long a removal's revocations may take in all. The daemon answers a removal once they settle, so the budget
|
|
57
|
+
* keeps a revoking removal inside the 35 seconds `AuthSwitchClient` allows any request, leaving the rest to the
|
|
58
|
+
* drain and the removal itself; a sign-in OpenAI has not confirmed revoking by then reports `failed`.
|
|
59
|
+
*/
|
|
60
|
+
const revocationBudgetMs = 10_000;
|
|
52
61
|
const openAiGrantId = (accountId: string): string => idHash('authswitch-grant-v1', accountId, 'openai_managed', 'chatgpt');
|
|
53
62
|
|
|
54
63
|
/**
|
|
@@ -144,7 +153,7 @@ const publicClaudeAssignment = (home: IStoredAuthorityClaudeHome): IAuthSwitchNa
|
|
|
144
153
|
|
|
145
154
|
interface IManagedOperation {
|
|
146
155
|
id: string;
|
|
147
|
-
handle:
|
|
156
|
+
handle: IOpenAiLoginHandle;
|
|
148
157
|
task: Promise<void>;
|
|
149
158
|
}
|
|
150
159
|
|
|
@@ -172,7 +181,7 @@ export class AuthSwitchAuthorityBroker {
|
|
|
172
181
|
private closing?: Promise<void>;
|
|
173
182
|
|
|
174
183
|
constructor(private readonly database: AuthSwitchAuthorityDatabase, options: IAuthSwitchAuthorityBrokerOptions = {}) {
|
|
175
|
-
this.provider = options.provider ?? new
|
|
184
|
+
this.provider = options.provider ?? new AuthSwitchOpenAiProvider();
|
|
176
185
|
this.secrets = options.secrets ?? new AuthSwitchTpmSecretCodec();
|
|
177
186
|
this.now = options.now ?? Date.now;
|
|
178
187
|
this.proactive = options.proactive ?? true;
|
|
@@ -344,7 +353,7 @@ export class AuthSwitchAuthorityBroker {
|
|
|
344
353
|
}
|
|
345
354
|
|
|
346
355
|
private identity(credential: TOpenAiCredential): { id: string; subject: string; workspaceId: string; email: string | null; plan: string | null } {
|
|
347
|
-
const token =
|
|
356
|
+
const token = parseOpenAiTokenClaims(credential.idToken);
|
|
348
357
|
const subject = token.chatgptUserId;
|
|
349
358
|
const workspaceId = token.chatgptAccountId;
|
|
350
359
|
if (!subject || !workspaceId) throw new Error('OpenAI login omitted a stable user or account identity.');
|
|
@@ -453,7 +462,7 @@ export class AuthSwitchAuthorityBroker {
|
|
|
453
462
|
if (terminal.state === 'cancelled') return publicOperation(terminal);
|
|
454
463
|
throw error;
|
|
455
464
|
}
|
|
456
|
-
let handle:
|
|
465
|
+
let handle: IOpenAiLoginHandle;
|
|
457
466
|
try {
|
|
458
467
|
handle = await this.provider.beginLogin({ flow: 'device' });
|
|
459
468
|
} catch {
|
|
@@ -660,13 +669,43 @@ export class AuthSwitchAuthorityBroker {
|
|
|
660
669
|
return publicAccount(updated.account);
|
|
661
670
|
}
|
|
662
671
|
|
|
663
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Removes an account. Only `revoke: true` reaches the network: it revokes the ChatGPT sign-in the removal
|
|
674
|
+
* discarded, after the removal committed.
|
|
675
|
+
*/
|
|
676
|
+
public async removeAccount(accountId: string, expectedRevision: number,
|
|
677
|
+
options: { revoke: boolean }): Promise<{ account: IAuthSwitchAccount; revoked: TAuthSwitchRevocation }> {
|
|
664
678
|
if (!isId(accountId) || !validRevision(expectedRevision)) throw new Error('Invalid account change.');
|
|
665
679
|
const updateId = plugins.crypto.randomUUID();
|
|
666
|
-
const account = await this.database.removeAccountAndGrants(updateId, accountId,
|
|
680
|
+
const { account, retired } = await this.database.removeAccountAndGrants(updateId, accountId,
|
|
667
681
|
expectedRevision, new Date(this.now()).toISOString());
|
|
668
682
|
this.publish();
|
|
669
|
-
|
|
683
|
+
const revoked = options.revoke ? await this.revokeRetired(account, retired) : 'not_requested';
|
|
684
|
+
return { account: publicAccount(account), revoked };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Revokes the ChatGPT sign-ins a removal discarded, as Codex revokes its own when it logs out, so that sign-in
|
|
689
|
+
* stops working at the service too. Like Codex's, it is best effort: the account is already removed, and the
|
|
690
|
+
* log says when the service did not confirm the revocation. All of them share `revocationBudgetMs`: each is
|
|
691
|
+
* given what remains of it, and one the budget no longer reaches is not sent and counts as not confirmed.
|
|
692
|
+
*/
|
|
693
|
+
private async revokeRetired(account: IStoredAuthorityAccount,
|
|
694
|
+
retired: IStoredAuthorityGrant[]): Promise<Exclude<TAuthSwitchRevocation, 'not_requested'>> {
|
|
695
|
+
if (retired.length === 0) return 'not_applicable';
|
|
696
|
+
const deadline = this.now() + revocationBudgetMs;
|
|
697
|
+
let revoked: 'yes' | 'failed' = 'yes';
|
|
698
|
+
for (const grant of retired) {
|
|
699
|
+
const remainingMs = Math.floor(deadline - this.now());
|
|
700
|
+
const confirmed = remainingMs > 0 && await this.unsealCredential(account, grant)
|
|
701
|
+
.then(credential => this.provider.revokeCredential(credential, { timeoutMs: remainingMs }))
|
|
702
|
+
.then(() => true, () => false);
|
|
703
|
+
if (!confirmed) {
|
|
704
|
+
revoked = 'failed';
|
|
705
|
+
process.stderr.write('Authswitch removed an account, but OpenAI did not confirm revoking its sign-in.\n');
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return revoked;
|
|
670
709
|
}
|
|
671
710
|
|
|
672
711
|
public async bindAccount(input: { accountId: string; loginId: string; purpose: 'openai_managed';
|
|
@@ -835,7 +874,7 @@ export class AuthSwitchAuthorityBroker {
|
|
|
835
874
|
}
|
|
836
875
|
if (!grant.accessExpiresAt) throw new AuthSwitchRefusal('access_not_fresh', accessNotFresh);
|
|
837
876
|
const credential = await this.unsealCredential(account, grant);
|
|
838
|
-
const info =
|
|
877
|
+
const info = parseOpenAiTokenClaims(credential.accessToken);
|
|
839
878
|
if (info.chatgptAccountId !== account.workspaceId || info.chatgptUserId !== account.subject) {
|
|
840
879
|
throw new Error('Account access identity changed.');
|
|
841
880
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { IAuthSwitchDoctorPage, IAuthSwitchOperation, IAuthSwitchPreuseOperation,
|
|
2
|
-
IAuthSwitchSnapshot, IReq_AuthSwitchDoctor,
|
|
2
|
+
IAuthSwitchSnapshot, IReq_AuthSwitchDoctor, IReq_AuthSwitchRemoveAccount,
|
|
3
|
+
IReq_AuthSwitchStartPreuse } from './authority-contract.js';
|
|
3
4
|
import { isAuthSwitchImportRefusal } from './authority-import-contract.js';
|
|
4
5
|
import type { IAuthSwitchImportInventory, IAuthSwitchImportResult, IAuthSwitchImportStatus,
|
|
5
6
|
IReq_AuthSwitchImportStatus, TAuthSwitchImportAction } from './authority-import-contract.js';
|
|
6
7
|
import { AuthSwitchClient } from './classes.authorityclient.js';
|
|
7
8
|
import { resolveAuthSwitchAuthorityPaths } from './classes.authorityservice.js';
|
|
8
|
-
import { defaultPreusePrompt, validatePreuseOptions } from './preuse.js';
|
|
9
|
+
import { defaultPreusePrompt, describePreuseFailure, describePreuseLimit, validatePreuseOptions } from './preuse.js';
|
|
9
10
|
import { plainText } from './formatting.js';
|
|
10
11
|
import * as plugins from './plugins.js';
|
|
11
12
|
|
|
@@ -23,6 +24,8 @@ export interface IAuthSwitchAuthorityCliClient {
|
|
|
23
24
|
startPreuse(input: IReq_AuthSwitchStartPreuse['request']): Promise<IAuthSwitchPreuseOperation>;
|
|
24
25
|
getPreuse(operationId: string): Promise<IAuthSwitchPreuseOperation>;
|
|
25
26
|
cancelPreuse(operationId: string): Promise<IAuthSwitchPreuseOperation>;
|
|
27
|
+
removeAccount(accountId: string, expectedRevision: number,
|
|
28
|
+
options: { revoke: boolean }): Promise<IReq_AuthSwitchRemoveAccount['response']>;
|
|
26
29
|
close(reason?: Error): void;
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -44,6 +47,7 @@ type TAuthorityCliCommand =
|
|
|
44
47
|
| { kind: 'importSubmit'; sourceId: string; sourceDigest: string; json: boolean }
|
|
45
48
|
| { kind: 'add'; json: boolean }
|
|
46
49
|
| { kind: 'reauth'; accountId: string; loginId: string; json: boolean }
|
|
50
|
+
| { kind: 'remove'; accountId: string; revoke: boolean; json: boolean }
|
|
47
51
|
| { kind: 'get'; operationId: string; json: boolean }
|
|
48
52
|
| { kind: 'cancel'; operationId: string; json: boolean }
|
|
49
53
|
| { kind: 'resume'; operationId: string; json: boolean }
|
|
@@ -60,7 +64,7 @@ class AuthorityCliFailure extends Error {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
const usage = 'Usage: authswitch account list [--json] | authswitch account add openai [--json] | authswitch account reauth <account-id> <login-id> [--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]';
|
|
67
|
+
const usage = 'Usage: authswitch account list [--json] | authswitch account add openai [--json] | authswitch account reauth <account-id> <login-id> [--json] | authswitch account remove <account-id> [--revoke] [--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]';
|
|
64
68
|
const safe = (value: string | number | null): string => plainText(value === null ? 'none' : String(value));
|
|
65
69
|
const isId = (value: string | undefined): value is string => Boolean(value && !value.startsWith('-'));
|
|
66
70
|
const isHash = (value: string | undefined): value is string =>
|
|
@@ -143,6 +147,14 @@ const parse = (argv: readonly string[]): TAuthorityCliCommand | null => {
|
|
|
143
147
|
&& (argv.length === 4 || (argv.length === 5 && argv[4] === '--json'))) {
|
|
144
148
|
return { kind: 'reauth', accountId: argv[2], loginId: argv[3], json: argv[4] === '--json' };
|
|
145
149
|
}
|
|
150
|
+
if (argv[0] === 'account' && argv[1] === 'remove' && isId(argv[2])) {
|
|
151
|
+
const options = argv.slice(3);
|
|
152
|
+
const revoke = options.includes('--revoke');
|
|
153
|
+
const json = options.includes('--json');
|
|
154
|
+
if (options.length !== Number(revoke) + Number(json)
|
|
155
|
+
|| options.some(option => option !== '--revoke' && option !== '--json')) return null;
|
|
156
|
+
return { kind: 'remove', accountId: argv[2], revoke, json };
|
|
157
|
+
}
|
|
146
158
|
if (argv[0] === 'account' && argv[1] === 'operation'
|
|
147
159
|
&& (argv[2] === 'get' || argv[2] === 'cancel' || argv[2] === 'resume') && isId(argv[3])
|
|
148
160
|
&& (argv.length === 4 || (argv.length === 5 && argv[4] === '--json'))) {
|
|
@@ -210,11 +222,27 @@ class ScopedAuthorityCliClient implements IAuthSwitchAuthorityCliClient {
|
|
|
210
222
|
return this.client.cancelPreuse(operationId, this.controller.signal);
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
public removeAccount(accountId: string, expectedRevision: number,
|
|
226
|
+
options: { revoke: boolean }): Promise<IReq_AuthSwitchRemoveAccount['response']> {
|
|
227
|
+
return this.client.removeAccount(accountId, expectedRevision, { ...options, signal: this.controller.signal });
|
|
228
|
+
}
|
|
229
|
+
|
|
213
230
|
public close(reason = new Error('Authswitch authority CLI closed.')): void {
|
|
214
231
|
if (!this.controller.signal.aborted) this.controller.abort(reason);
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
const revocationLines: Record<IReq_AuthSwitchRemoveAccount['response']['revoked'], string> = {
|
|
236
|
+
not_requested: 'Its ChatGPT sign-in was not revoked at OpenAI (no --revoke).',
|
|
237
|
+
yes: 'OpenAI confirmed revoking its ChatGPT sign-in.',
|
|
238
|
+
failed: 'OpenAI did not confirm revoking its ChatGPT sign-in; it may still be valid there.',
|
|
239
|
+
not_applicable: 'The authority held no ChatGPT sign-in of its own for it, so nothing was revoked.',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const renderRemoval = (removal: IReq_AuthSwitchRemoveAccount['response']): string =>
|
|
243
|
+
`Removed account ${safe(removal.account.email ?? removal.account.label)} (${safe(removal.account.id)}).\n`
|
|
244
|
+
+ `${revocationLines[removal.revoked]}\n`;
|
|
245
|
+
|
|
218
246
|
const renderList = (snapshot: IAuthSwitchSnapshot): string => {
|
|
219
247
|
const lines = [`Authority accounts — generated ${safe(snapshot.generatedAt)}, revision ${snapshot.revision}`];
|
|
220
248
|
if (snapshot.accounts.length === 0) lines.push('No authority accounts are registered.');
|
|
@@ -332,7 +360,8 @@ const resumeCommand = (operationId: string, json: boolean): string =>
|
|
|
332
360
|
`authswitch account operation resume ${safe(operationId)}${json ? ' --json' : ''}`;
|
|
333
361
|
|
|
334
362
|
const terminalPreuseOperation = (operation: IAuthSwitchPreuseOperation): boolean =>
|
|
335
|
-
operation.state === 'complete' || operation.state === 'not_sent' || operation.state === '
|
|
363
|
+
operation.state === 'complete' || operation.state === 'not_sent' || operation.state === 'refused'
|
|
364
|
+
|| operation.state === 'outcome_unknown';
|
|
336
365
|
|
|
337
366
|
const preuseResumeCommand = (operationId: string, json: boolean): string =>
|
|
338
367
|
`authswitch account preuse operation resume ${safe(operationId)}${json ? ' --json' : ''}`;
|
|
@@ -346,9 +375,9 @@ const renderOperation = (operation: IAuthSwitchOperation, json: boolean): string
|
|
|
346
375
|
if (operation.targetAccountId || operation.targetLoginId) {
|
|
347
376
|
lines.push(` target account=${safe(operation.targetAccountId)}; login=${safe(operation.targetLoginId)}`);
|
|
348
377
|
}
|
|
349
|
-
if (operation.prompt
|
|
378
|
+
if (operation.prompt) {
|
|
350
379
|
lines.push(` Open ${safe(operation.prompt.verificationUrl)}`, ` Code: ${safe(operation.prompt.userCode)}`);
|
|
351
|
-
}
|
|
380
|
+
}
|
|
352
381
|
if (operation.result) {
|
|
353
382
|
lines.push(` completed account=${safe(operation.result.accountId)}; login=${safe(operation.result.loginId)}; account revision=${operation.result.accountRevision}; login generation=${operation.result.loginGeneration}`);
|
|
354
383
|
}
|
|
@@ -368,6 +397,8 @@ const renderPreuseOperation = (operation: IAuthSwitchPreuseOperation, json: bool
|
|
|
368
397
|
if (operation.result) {
|
|
369
398
|
lines.push(` tokens input=${safe(operation.result.inputTokens)}; output=${safe(operation.result.outputTokens)}; total=${safe(operation.result.totalTokens)}`);
|
|
370
399
|
}
|
|
400
|
+
if (operation.limit) lines.push(` ${safe(describePreuseLimit(operation.limit, Date.now()))}`);
|
|
401
|
+
if (operation.failure) lines.push(` Cause: ${safe(describePreuseFailure(operation.failure))}`);
|
|
371
402
|
if (!terminalPreuseOperation(operation)) {
|
|
372
403
|
lines.push(` Resume: ${preuseResumeCommand(operation.id, false)}`);
|
|
373
404
|
}
|
|
@@ -721,6 +752,17 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
|
|
|
721
752
|
await writeOutput(command.json ? JSON.stringify(result) + '\n' : renderImportResult(result));
|
|
722
753
|
return result.status === 'complete' ? 0 : 1;
|
|
723
754
|
}
|
|
755
|
+
if (command.kind === 'remove') {
|
|
756
|
+
const account = (await activeClient.snapshotAll()).accounts.find(item => item.id === command.accountId);
|
|
757
|
+
if (!account) {
|
|
758
|
+
await writeDiagnostic('No authority account has this id. Run `authswitch account list` for the ids.\n');
|
|
759
|
+
return 1;
|
|
760
|
+
}
|
|
761
|
+
const removed = await activeClient.removeAccount(account.id, account.revision, { revoke: command.revoke });
|
|
762
|
+
if (removed.account.id !== account.id || !removed.account.removed) throw new AuthorityCliFailure('response');
|
|
763
|
+
await writeOutput(command.json ? JSON.stringify(removed) + '\n' : renderRemoval(removed));
|
|
764
|
+
return removed.revoked === 'failed' ? 1 : 0;
|
|
765
|
+
}
|
|
724
766
|
if (command.kind === 'preuseGet' || command.kind === 'preuseCancel') {
|
|
725
767
|
const operation = validatePreuseOperation(command.kind === 'preuseGet'
|
|
726
768
|
? await activeClient.getPreuse(command.operationId)
|
|
@@ -387,10 +387,18 @@ export class AuthSwitchClient {
|
|
|
387
387
|
return (await this.request<IReq_AuthSwitchRenameAccount>('authswitch.authority.rename',
|
|
388
388
|
{ accountId, expectedRevision, label }, 35_000, false, signal)).account;
|
|
389
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Removes an account; `revoke: true` also revokes its ChatGPT sign-in at OpenAI (see
|
|
392
|
+
* `IReq_AuthSwitchRemoveAccount`). An older daemon refuses `revoke` as an invalid request before it changes
|
|
393
|
+
* anything, and answers a plain removal without `revoked`, which then is `not_requested`: it never revokes.
|
|
394
|
+
*/
|
|
390
395
|
public async removeAccount(accountId: string, expectedRevision: number,
|
|
391
|
-
signal?: AbortSignal): Promise<IReq_AuthSwitchRemoveAccount['response']
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
options: { revoke?: boolean; signal?: AbortSignal } = {}): Promise<IReq_AuthSwitchRemoveAccount['response']> {
|
|
397
|
+
const response: Partial<IReq_AuthSwitchRemoveAccount['response']> =
|
|
398
|
+
await this.request<IReq_AuthSwitchRemoveAccount>('authswitch.authority.remove',
|
|
399
|
+
{ accountId, expectedRevision, ...(options.revoke ? { revoke: true as const } : {}) }, 35_000, false,
|
|
400
|
+
options.signal);
|
|
401
|
+
return { account: response.account!, revoked: response.revoked ?? 'not_requested' };
|
|
394
402
|
}
|
|
395
403
|
public async switchClaudeNative(accountId: string, loginId: string,
|
|
396
404
|
signal?: AbortSignal): Promise<IReq_AuthSwitchSwitchClaudeNative['response']['handoff']> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
|
|
2
2
|
import * as plugins from './plugins.js';
|
|
3
|
+
import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
3
4
|
import type {
|
|
4
5
|
IAuthSwitchDoctorPage, IReq_AuthSwitchDoctor,
|
|
5
6
|
IReq_AuthSwitchBeginAdd, IReq_AuthSwitchBeginReauth, IReq_AuthSwitchCancelOperation,
|
|
@@ -156,7 +157,7 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
156
157
|
throw new Error('Authswitch authority requestDeliveryMs must be a positive whole number of milliseconds.');
|
|
157
158
|
}
|
|
158
159
|
this.database = new AuthSwitchAuthorityDatabase(options);
|
|
159
|
-
const provider = options.broker?.provider ?? new
|
|
160
|
+
const provider = options.broker?.provider ?? new AuthSwitchOpenAiProvider();
|
|
160
161
|
this.broker = new AuthSwitchAuthorityBroker(this.database, { ...options.broker, provider });
|
|
161
162
|
this.usage = new AuthSwitchAuthorityUsage({
|
|
162
163
|
...options.usage,
|
|
@@ -474,15 +475,17 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
474
475
|
}));
|
|
475
476
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchRemoveAccount>(
|
|
476
477
|
'authswitch.authority.remove', async request => {
|
|
477
|
-
|
|
478
|
-
|
|
478
|
+
const revoke = Object.hasOwn(request, 'revoke');
|
|
479
|
+
if (!hasKeys(request, revoke ? ['accountId', 'expectedRevision', 'revoke'] : ['accountId', 'expectedRevision'])
|
|
480
|
+
|| typeof request.accountId !== 'string' || typeof request.expectedRevision !== 'number'
|
|
481
|
+
|| (revoke && request.revoke !== true)) invalid();
|
|
479
482
|
if (this.drainingAccounts.has(request.accountId) || this.preuseAccounts.has(request.accountId)) {
|
|
480
483
|
throw new AuthSwitchRefusal('account_busy', 'Account runtime drain is already in progress.');
|
|
481
484
|
}
|
|
482
485
|
this.drainingAccounts.add(request.accountId);
|
|
483
486
|
try {
|
|
484
487
|
await this.drainManagedCodexAccount(request.accountId);
|
|
485
|
-
return
|
|
488
|
+
return await this.broker.removeAccount(request.accountId, request.expectedRevision, { revoke });
|
|
486
489
|
} finally { this.drainingAccounts.delete(request.accountId); }
|
|
487
490
|
}));
|
|
488
491
|
this.managementRouter.addTypedHandler(this.handler<IReq_AuthSwitchBindAccount>(
|
|
@@ -567,8 +567,8 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
567
567
|
const draft = change(structuredClone(current));
|
|
568
568
|
const allowed: Record<IStoredAuthorityPreuseOperation['state'], IStoredAuthorityPreuseOperation['state'][]> = {
|
|
569
569
|
preparing: ['request_may_have_started', 'not_sent'],
|
|
570
|
-
request_may_have_started: ['complete', 'outcome_unknown'],
|
|
571
|
-
complete: [], not_sent: [], outcome_unknown: [],
|
|
570
|
+
request_may_have_started: ['complete', 'refused', 'outcome_unknown'],
|
|
571
|
+
complete: [], not_sent: [], refused: [], outcome_unknown: [],
|
|
572
572
|
};
|
|
573
573
|
if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
|
|
574
574
|
|| draft.accountId !== current.accountId || draft.grantId !== current.grantId
|
|
@@ -1209,12 +1209,17 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1209
1209
|
/**
|
|
1210
1210
|
* Remove all managed grants in one transaction. Native owners must be stopped and handed off first, and a
|
|
1211
1211
|
* runtime binding its holder releases must be released first; the daemon's own managed Codex bindings go
|
|
1212
|
-
* with the account.
|
|
1212
|
+
* with the account. `retired` holds the sealed ChatGPT sign-ins the removal discarded, exactly as the attempt
|
|
1213
|
+
* that committed read them, so the caller can revoke them at the provider.
|
|
1213
1214
|
*/
|
|
1214
1215
|
public async removeAccountAndGrants(updateId: string, accountId: string,
|
|
1215
|
-
expectedRevision: number, statusObservedAt: string): Promise<IStoredAuthorityAccount
|
|
1216
|
+
expectedRevision: number, statusObservedAt: string): Promise<{ account: IStoredAuthorityAccount;
|
|
1217
|
+
retired: IStoredAuthorityGrant[] }> {
|
|
1218
|
+
// Collected afresh by every attempt of the transaction, so after it the list is the committed attempt's.
|
|
1219
|
+
let retired: IStoredAuthorityGrant[] = [];
|
|
1216
1220
|
try {
|
|
1217
1221
|
return await this.transaction(async session => {
|
|
1222
|
+
retired = [];
|
|
1218
1223
|
const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
|
|
1219
1224
|
const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
|
|
1220
1225
|
if (!metaStored || !accountStored || accountStored.removed || accountStored.revision !== expectedRevision) {
|
|
@@ -1271,6 +1276,9 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1271
1276
|
}
|
|
1272
1277
|
await scan(async (grant, stored) => {
|
|
1273
1278
|
if (grant.state === 'removed') return;
|
|
1279
|
+
if (grant.purpose === 'openai_managed' && grant.owner === 'daemon' && grant.ciphertext !== null) {
|
|
1280
|
+
retired.push(grant);
|
|
1281
|
+
}
|
|
1274
1282
|
const draft: IStoredAuthorityGrant = {
|
|
1275
1283
|
...grant, state: 'removed', owner: 'none', ciphertext: null, accessExpiresAt: null,
|
|
1276
1284
|
attemptId: null, retryAt: null, retryCount: 0, problem: 'none',
|
|
@@ -1311,13 +1319,13 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1311
1319
|
await this.persistAccount(session, accountStored, removed);
|
|
1312
1320
|
await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
|
|
1313
1321
|
updateId, 'account', accountId);
|
|
1314
|
-
return removed;
|
|
1322
|
+
return { account: removed, retired };
|
|
1315
1323
|
});
|
|
1316
1324
|
} catch (error) {
|
|
1317
1325
|
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1318
1326
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1319
1327
|
const account = await this.readAccount(accountId);
|
|
1320
|
-
if (event && account?.removed && account.updateId === updateId) return account;
|
|
1328
|
+
if (event && account?.removed && account.updateId === updateId) return { account, retired };
|
|
1321
1329
|
throw new Error('Authswitch account removal outcome is unknown; refresh before retrying.');
|
|
1322
1330
|
}
|
|
1323
1331
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
3
|
+
import { parseOpenAiTokenClaims, type IOpenAiTokenClaims } from './openaiauth.js';
|
|
4
|
+
import type { IOpenAiAccess, IOpenAiOAuthCredential, TOpenAiAccountRateLimitsResult, TOpenAiRefreshResult } from './interfaces.openai.js';
|
|
2
5
|
import type { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
|
|
3
6
|
import { authSwitchEnvironmentId } from './classes.operations.js';
|
|
4
7
|
import { AuthSwitchRefusal } from './authority-contract.js';
|
|
@@ -55,14 +58,12 @@ export interface IAuthSwitchImportQuiescence {
|
|
|
55
58
|
/**
|
|
56
59
|
* Exactly the two provider operations an import needs, so nothing here can reach for a third.
|
|
57
60
|
*
|
|
58
|
-
* `
|
|
61
|
+
* `AuthSwitchOpenAiProvider` satisfies this; naming only what is used keeps the rotating call countable and
|
|
59
62
|
* makes it visible that a projection never touches `refreshCredential`.
|
|
60
63
|
*/
|
|
61
64
|
export interface IAuthSwitchImportOpenAiOperations {
|
|
62
|
-
refreshCredential(credential:
|
|
63
|
-
Promise<
|
|
64
|
-
getAccountRateLimits(credential: plugins.flexAccounts.TOpenAiChatGptReadCredential):
|
|
65
|
-
Promise<plugins.flexAccounts.TSmartAiProviderAccountRateLimitsResult>;
|
|
65
|
+
refreshCredential(credential: IOpenAiOAuthCredential): Promise<TOpenAiRefreshResult>;
|
|
66
|
+
getAccountRateLimits(access: IOpenAiAccess): Promise<TOpenAiAccountRateLimitsResult>;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/** The non-rotating Claude proof: one profile read that must name the expected immutable identity. */
|
|
@@ -289,7 +290,7 @@ export class AuthSwitchAuthorityImport {
|
|
|
289
290
|
this.env = options.env;
|
|
290
291
|
this.homeDirectory = options.homeDirectory;
|
|
291
292
|
this.now = options.now ?? Date.now;
|
|
292
|
-
this.openAi = options.openAi ?? new
|
|
293
|
+
this.openAi = options.openAi ?? new AuthSwitchOpenAiProvider();
|
|
293
294
|
this.claudeStatus = options.claudeStatus ?? new ClaudeAccountStatus();
|
|
294
295
|
this.claudeTokens = options.claudeTokens ?? new ClaudeTokenRefresh();
|
|
295
296
|
this.secrets = options.secrets ?? new AuthSwitchTpmSecretCodec();
|
|
@@ -426,8 +427,8 @@ export class AuthSwitchAuthorityImport {
|
|
|
426
427
|
|
|
427
428
|
private externalIdentity(idToken: string): { accountId: string; subject: string; workspaceId: string;
|
|
428
429
|
email: string | null; plan: string | null } {
|
|
429
|
-
let info:
|
|
430
|
-
try { info =
|
|
430
|
+
let info: IOpenAiTokenClaims;
|
|
431
|
+
try { info = parseOpenAiTokenClaims(idToken); }
|
|
431
432
|
catch { return refuse('The submitted login carries no readable ChatGPT identity.'); }
|
|
432
433
|
if (!info.chatgptUserId || !info.chatgptAccountId) {
|
|
433
434
|
refuse('The submitted login omits a stable ChatGPT user or account identity.');
|
|
@@ -618,15 +619,14 @@ export class AuthSwitchAuthorityImport {
|
|
|
618
619
|
identity: { accountId: string; subject: string; workspaceId: string },
|
|
619
620
|
material: TLegacySourceMaterial): Promise<void> {
|
|
620
621
|
if (material.providerId === 'openai') {
|
|
621
|
-
const result = await this.openAi.getAccountRateLimits({
|
|
622
|
-
providerId: 'openai', accessToken: material.credential.accessToken,
|
|
622
|
+
const result = await this.openAi.getAccountRateLimits({ accessToken: material.credential.accessToken,
|
|
623
623
|
accountId: identity.workspaceId, isFedrampAccount: false });
|
|
624
624
|
if (!result.success) {
|
|
625
625
|
refuse(`The stored access token could not be proven live (${result.errorCode}); let the native tool `
|
|
626
626
|
+ 'refresh its own store, then import again.');
|
|
627
627
|
}
|
|
628
628
|
// The claims of the very token that answered must still name this account.
|
|
629
|
-
const claims =
|
|
629
|
+
const claims = parseOpenAiTokenClaims(material.credential.accessToken);
|
|
630
630
|
if (claims.chatgptAccountId !== identity.workspaceId) {
|
|
631
631
|
refuse('The stored access token belongs to another ChatGPT account.');
|
|
632
632
|
}
|
|
@@ -773,7 +773,7 @@ export class AuthSwitchAuthorityImport {
|
|
|
773
773
|
const result = await this.openAi.refreshCredential(material.credential);
|
|
774
774
|
if (!result.success) return null;
|
|
775
775
|
let accessExpiresAt: string | null = null;
|
|
776
|
-
try { accessExpiresAt =
|
|
776
|
+
try { accessExpiresAt = parseOpenAiTokenClaims(result.credential.accessToken).expiresAt ?? null; }
|
|
777
777
|
catch { accessExpiresAt = null; }
|
|
778
778
|
return { material: { providerId: 'openai', credential: { kind: 'chatgptOAuth', providerId: 'openai',
|
|
779
779
|
accessToken: result.credential.accessToken, refreshToken: result.credential.refreshToken,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IAuthSwitchPreuseFailure, IAuthSwitchPreuseLimit, TAuthSwitchClaudeProofFailure,
|
|
3
|
+
TAuthSwitchLoginPrompt } from './authority-contract.js';
|
|
3
4
|
import type { IAuthSwitchStoredUsage } from './classes.authorityusage.js';
|
|
4
5
|
|
|
5
6
|
export interface IStoredAuthorityMeta {
|
|
@@ -225,7 +226,7 @@ export interface IStoredAuthorityDeviceOperation {
|
|
|
225
226
|
export type TStoredAuthorityPreuseProblem =
|
|
226
227
|
| 'none' | 'target_unavailable' | 'access_unavailable' | 'catalog_unavailable'
|
|
227
228
|
| 'account_busy' | 'cancelled' | 'authority_closing' | 'interrupted'
|
|
228
|
-
| 'request_uncertain';
|
|
229
|
+
| 'request_uncertain' | 'usage_limit' | 'rate_limit';
|
|
229
230
|
|
|
230
231
|
/** One quota-consuming request. The prompt and generated prose never enter persistence. */
|
|
231
232
|
export interface IStoredAuthorityPreuseOperation {
|
|
@@ -240,9 +241,13 @@ export interface IStoredAuthorityPreuseOperation {
|
|
|
240
241
|
model: string | null;
|
|
241
242
|
authorizationGeneration: number;
|
|
242
243
|
grantGeneration: number | null;
|
|
243
|
-
state: 'preparing' | 'request_may_have_started' | 'complete' | 'not_sent' | 'outcome_unknown';
|
|
244
|
+
state: 'preparing' | 'request_may_have_started' | 'complete' | 'not_sent' | 'refused' | 'outcome_unknown';
|
|
244
245
|
result: { inputTokens: number | null; outputTokens: number | null; totalTokens: number | null } | null;
|
|
245
246
|
problem: TStoredAuthorityPreuseProblem;
|
|
247
|
+
/** Present exactly when `problem` is `usage_limit` or `rate_limit`. */
|
|
248
|
+
limit?: IAuthSwitchPreuseLimit;
|
|
249
|
+
/** Present only when `problem` is `request_uncertain` and the request failed with a stated cause. */
|
|
250
|
+
failure?: IAuthSwitchPreuseFailure;
|
|
246
251
|
createdAt: string;
|
|
247
252
|
updatedAt: string;
|
|
248
253
|
finishedAt: string | null;
|
|
@@ -271,6 +276,8 @@ export interface IStoredAuthorityOperationDocument {
|
|
|
271
276
|
authorizationGeneration?: number;
|
|
272
277
|
grantGeneration?: number | null;
|
|
273
278
|
problem?: TStoredAuthorityPreuseProblem;
|
|
279
|
+
limit?: IAuthSwitchPreuseLimit;
|
|
280
|
+
failure?: IAuthSwitchPreuseFailure;
|
|
274
281
|
createdAt: string;
|
|
275
282
|
updatedAt: string;
|
|
276
283
|
finishedAt: string | null;
|
|
@@ -603,13 +610,11 @@ const assertStoredAuthorityDeviceOperation: (value: unknown) => asserts value is
|
|
|
603
610
|
: value.accountId === null || value.grantId === null || value.expectedGrantGeneration === null) {
|
|
604
611
|
throw new Error('Invalid authswitch operation target.');
|
|
605
612
|
}
|
|
606
|
-
if (value.prompt !== null && (!object(value.prompt)
|
|
607
|
-
|| (value.prompt
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
? !exactKeys(value.prompt, ['flow', 'authUrl']) || !bounded(value.prompt.authUrl, 2048)
|
|
612
|
-
: true))) throw new Error('Invalid authswitch operation prompt.');
|
|
613
|
+
if (value.prompt !== null && (!object(value.prompt) || value.prompt.flow !== 'device'
|
|
614
|
+
|| !exactKeys(value.prompt, ['flow', 'verificationUrl', 'userCode'])
|
|
615
|
+
|| !bounded(value.prompt.verificationUrl, 2048) || !bounded(value.prompt.userCode, 128))) {
|
|
616
|
+
throw new Error('Invalid authswitch operation prompt.');
|
|
617
|
+
}
|
|
613
618
|
if (value.result !== null && (!object(value.result)
|
|
614
619
|
|| !exactKeys(value.result, ['accountId', 'grantId', 'accountRevision', 'grantGeneration'])
|
|
615
620
|
|| !hash(value.result.accountId) || !hash(value.result.grantId)
|
|
@@ -635,19 +640,33 @@ const modelId = (value: unknown): value is string => typeof value === 'string'
|
|
|
635
640
|
const tokenCount = (value: unknown): value is number | null => value === null
|
|
636
641
|
|| (Number.isSafeInteger(value) && Number(value) >= 0);
|
|
637
642
|
|
|
643
|
+
const preuseLimitProblems: readonly string[] = ['usage_limit', 'rate_limit'];
|
|
644
|
+
const preuseLimit = (value: unknown): boolean => object(value) && exactKeys(value, ['kind', 'window', 'resetsAt'])
|
|
645
|
+
&& ['usage_limit', 'rate_limit'].includes(String(value.kind))
|
|
646
|
+
&& (value.window === null || bounded(value.window, 64)) && (value.resetsAt === null || iso(value.resetsAt));
|
|
647
|
+
const preuseFailure = (value: unknown): boolean => object(value) && exactKeys(value, ['kind', 'status', 'providerCode'])
|
|
648
|
+
&& ['http', 'stream', 'response', 'network', 'aborted', 'unknown'].includes(String(value.kind))
|
|
649
|
+
&& (value.status === null || (Number.isInteger(value.status) && Number(value.status) >= 100 && Number(value.status) <= 599))
|
|
650
|
+
&& (value.providerCode === null || (typeof value.providerCode === 'string' && /^[A-Za-z0-9_.]{1,64}$/.test(value.providerCode)));
|
|
651
|
+
|
|
638
652
|
const assertStoredAuthorityPreuseOperation: (value: unknown) => asserts value is IStoredAuthorityPreuseOperation = value => {
|
|
653
|
+
const limited = object(value) && preuseLimitProblems.includes(String(value.problem));
|
|
654
|
+
const failed = object(value) && Object.hasOwn(value, 'failure');
|
|
639
655
|
if (!object(value) || !exactKeys(value, ['id', 'kind', 'purpose', 'accountId', 'grantId', 'requestHash',
|
|
640
656
|
'requestedModel', 'model', 'authorizationGeneration', 'grantGeneration', 'state', 'result', 'problem',
|
|
657
|
+
...(limited ? ['limit'] : []), ...(failed ? ['failure'] : []),
|
|
641
658
|
'createdAt', 'updatedAt', 'finishedAt', 'revision', 'updateId'])
|
|
659
|
+
|| (limited && !preuseLimit(value.limit))
|
|
660
|
+
|| (failed && (value.problem !== 'request_uncertain' || !preuseFailure(value.failure)))
|
|
642
661
|
|| !uuid(value.id) || value.kind !== 'preuse_openai' || value.purpose !== 'openai_managed'
|
|
643
662
|
|| !hash(value.accountId) || !hash(value.grantId) || !hash(value.requestHash)
|
|
644
663
|
|| (value.requestedModel !== null && !modelId(value.requestedModel))
|
|
645
664
|
|| (value.model !== null && !modelId(value.model))
|
|
646
665
|
|| !revision(value.authorizationGeneration) || value.authorizationGeneration === 0
|
|
647
666
|
|| (value.grantGeneration !== null && (!revision(value.grantGeneration) || value.grantGeneration === 0))
|
|
648
|
-
|| !['preparing', 'request_may_have_started', 'complete', 'not_sent', 'outcome_unknown'].includes(String(value.state))
|
|
667
|
+
|| !['preparing', 'request_may_have_started', 'complete', 'not_sent', 'refused', 'outcome_unknown'].includes(String(value.state))
|
|
649
668
|
|| !['none', 'target_unavailable', 'access_unavailable', 'catalog_unavailable', 'account_busy', 'cancelled',
|
|
650
|
-
'authority_closing', 'interrupted', 'request_uncertain'].includes(String(value.problem))
|
|
669
|
+
'authority_closing', 'interrupted', 'request_uncertain', 'usage_limit', 'rate_limit'].includes(String(value.problem))
|
|
651
670
|
|| !iso(value.createdAt) || !iso(value.updatedAt)
|
|
652
671
|
|| (value.finishedAt !== null && !iso(value.finishedAt))
|
|
653
672
|
|| !revision(value.revision) || value.revision === 0 || !uuid(value.updateId)) {
|
|
@@ -675,13 +694,20 @@ const assertStoredAuthorityPreuseOperation: (value: unknown) => asserts value is
|
|
|
675
694
|
throw new Error('Invalid completed authswitch preuse operation.');
|
|
676
695
|
}
|
|
677
696
|
} else if (value.state === 'not_sent') {
|
|
697
|
+
// Only the account's own usage reading stops a prompt with a limit before anything is sent.
|
|
678
698
|
if (value.model !== null || value.grantGeneration !== null || value.result !== null
|
|
679
|
-
|| value.problem === 'none' || value.problem === 'request_uncertain' || value.
|
|
699
|
+
|| value.problem === 'none' || value.problem === 'request_uncertain' || value.problem === 'rate_limit'
|
|
700
|
+
|| value.finishedAt === null) {
|
|
680
701
|
throw new Error('Invalid unsent authswitch preuse operation.');
|
|
681
702
|
}
|
|
703
|
+
} else if (value.state === 'refused') {
|
|
704
|
+
if (value.model === null || value.grantGeneration === null || value.result !== null
|
|
705
|
+
|| !limited || value.finishedAt === null) {
|
|
706
|
+
throw new Error('Invalid refused authswitch preuse operation.');
|
|
707
|
+
}
|
|
682
708
|
} else if (value.result !== null || value.finishedAt === null || value.model === null
|
|
683
709
|
|| value.grantGeneration === null
|
|
684
|
-
|| !['cancelled', 'authority_closing', 'request_uncertain'].includes(String(value.problem))) {
|
|
710
|
+
|| !['cancelled', 'authority_closing', 'request_uncertain', 'usage_limit', 'rate_limit'].includes(String(value.problem))) {
|
|
685
711
|
throw new Error('Invalid uncertain authswitch preuse operation.');
|
|
686
712
|
}
|
|
687
713
|
};
|
|
@@ -930,6 +956,8 @@ export class AuthSwitchAuthorityOperationModel extends plugins.nosqldb.SmartData
|
|
|
930
956
|
@plugins.nosqldb.svDb() public authorizationGeneration?: number;
|
|
931
957
|
@plugins.nosqldb.svDb() public grantGeneration?: number | null;
|
|
932
958
|
@plugins.nosqldb.svDb() public problem?: TStoredAuthorityPreuseProblem;
|
|
959
|
+
@plugins.nosqldb.svDb() public limit?: IAuthSwitchPreuseLimit;
|
|
960
|
+
@plugins.nosqldb.svDb() public failure?: IAuthSwitchPreuseFailure;
|
|
933
961
|
@plugins.nosqldb.svDb() public createdAt!: string;
|
|
934
962
|
@plugins.nosqldb.svDb() public updatedAt!: string;
|
|
935
963
|
@plugins.nosqldb.svDb() public finishedAt!: string | null;
|