@modelprofile.com/authswitch 9.1.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/authority-contract.d.ts +56 -6
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/authority-runtime-contract.d.ts +29 -0
- package/dist_ts/classes.authoritybroker.d.ts +36 -4
- package/dist_ts/classes.authoritybroker.js +71 -12
- package/dist_ts/classes.authoritycli.d.ts +4 -1
- package/dist_ts/classes.authoritycli.js +42 -7
- package/dist_ts/classes.authorityclient.d.ts +15 -2
- package/dist_ts/classes.authorityclient.js +31 -8
- package/dist_ts/classes.authoritydaemon.d.ts +17 -0
- package/dist_ts/classes.authoritydaemon.js +50 -14
- package/dist_ts/classes.authoritydatabase.d.ts +14 -3
- package/dist_ts/classes.authoritydatabase.js +25 -10
- 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 +121 -26
- package/third-party-notices.md +29 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +60 -8
- package/ts/authority-runtime-contract.ts +30 -0
- package/ts/classes.authoritybroker.ts +80 -17
- package/ts/classes.authoritycli.ts +48 -6
- package/ts/classes.authorityclient.ts +32 -9
- package/ts/classes.authoritydaemon.ts +54 -14
- package/ts/classes.authoritydatabase.ts +28 -11
- 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
|
@@ -567,8 +567,8 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
567
567
|
const draft = change(structuredClone(current));
|
|
568
568
|
const allowed: Record<IStoredAuthorityPreuseOperation['state'], IStoredAuthorityPreuseOperation['state'][]> = {
|
|
569
569
|
preparing: ['request_may_have_started', 'not_sent'],
|
|
570
|
-
request_may_have_started: ['complete', 'outcome_unknown'],
|
|
571
|
-
complete: [], not_sent: [], outcome_unknown: [],
|
|
570
|
+
request_may_have_started: ['complete', 'refused', 'outcome_unknown'],
|
|
571
|
+
complete: [], not_sent: [], refused: [], outcome_unknown: [],
|
|
572
572
|
};
|
|
573
573
|
if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
|
|
574
574
|
|| draft.accountId !== current.accountId || draft.grantId !== current.grantId
|
|
@@ -1209,12 +1209,17 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1209
1209
|
/**
|
|
1210
1210
|
* Remove all managed grants in one transaction. Native owners must be stopped and handed off first, and a
|
|
1211
1211
|
* runtime binding its holder releases must be released first; the daemon's own managed Codex bindings go
|
|
1212
|
-
* with the account.
|
|
1212
|
+
* with the account. `retired` holds the sealed ChatGPT sign-ins the removal discarded, exactly as the attempt
|
|
1213
|
+
* that committed read them, so the caller can revoke them at the provider.
|
|
1213
1214
|
*/
|
|
1214
1215
|
public async removeAccountAndGrants(updateId: string, accountId: string,
|
|
1215
|
-
expectedRevision: number, statusObservedAt: string): Promise<IStoredAuthorityAccount
|
|
1216
|
+
expectedRevision: number, statusObservedAt: string): Promise<{ account: IStoredAuthorityAccount;
|
|
1217
|
+
retired: IStoredAuthorityGrant[] }> {
|
|
1218
|
+
// Collected afresh by every attempt of the transaction, so after it the list is the committed attempt's.
|
|
1219
|
+
let retired: IStoredAuthorityGrant[] = [];
|
|
1216
1220
|
try {
|
|
1217
1221
|
return await this.transaction(async session => {
|
|
1222
|
+
retired = [];
|
|
1218
1223
|
const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
|
|
1219
1224
|
const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
|
|
1220
1225
|
if (!metaStored || !accountStored || accountStored.removed || accountStored.revision !== expectedRevision) {
|
|
@@ -1267,11 +1272,13 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1267
1272
|
const listed = names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
1268
1273
|
throw new AuthSwitchRefusal('account_busy', `This account still backs ${listed} runtime bindings. `
|
|
1269
1274
|
+ 'Stop those runtimes and release their bindings before removing the account. A holder that lost '
|
|
1270
|
-
+ 'its capability
|
|
1271
|
-
+ 'if binding again is refused, reauthenticate the account first.');
|
|
1275
|
+
+ 'its capability unbinds its own runtime and scope instead, which needs no sign-in.');
|
|
1272
1276
|
}
|
|
1273
1277
|
await scan(async (grant, stored) => {
|
|
1274
1278
|
if (grant.state === 'removed') return;
|
|
1279
|
+
if (grant.purpose === 'openai_managed' && grant.owner === 'daemon' && grant.ciphertext !== null) {
|
|
1280
|
+
retired.push(grant);
|
|
1281
|
+
}
|
|
1275
1282
|
const draft: IStoredAuthorityGrant = {
|
|
1276
1283
|
...grant, state: 'removed', owner: 'none', ciphertext: null, accessExpiresAt: null,
|
|
1277
1284
|
attemptId: null, retryAt: null, retryCount: 0, problem: 'none',
|
|
@@ -1312,13 +1319,13 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1312
1319
|
await this.persistAccount(session, accountStored, removed);
|
|
1313
1320
|
await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
|
|
1314
1321
|
updateId, 'account', accountId);
|
|
1315
|
-
return removed;
|
|
1322
|
+
return { account: removed, retired };
|
|
1316
1323
|
});
|
|
1317
1324
|
} catch (error) {
|
|
1318
1325
|
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1319
1326
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1320
1327
|
const account = await this.readAccount(accountId);
|
|
1321
|
-
if (event && account?.removed && account.updateId === updateId) return account;
|
|
1328
|
+
if (event && account?.removed && account.updateId === updateId) return { account, retired };
|
|
1322
1329
|
throw new Error('Authswitch account removal outcome is unknown; refresh before retrying.');
|
|
1323
1330
|
}
|
|
1324
1331
|
}
|
|
@@ -1349,9 +1356,16 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1349
1356
|
}
|
|
1350
1357
|
}
|
|
1351
1358
|
|
|
1359
|
+
/**
|
|
1360
|
+
* Binds a runtime and scope, or rebinds them. The binding's revision is the authority revision of the event
|
|
1361
|
+
* that writes it, handed to `change` as `revision`: the authority revision never repeats, so no two binds of
|
|
1362
|
+
* a runtime and scope ever share one -- not even across a release, which deletes the record, followed by a
|
|
1363
|
+
* bind that inserts it again -- and a holder that names a binding by the revision it read never names its
|
|
1364
|
+
* successor. Records written before carry smaller revisions, which a later bind therefore still exceeds.
|
|
1365
|
+
*/
|
|
1352
1366
|
public async changeBinding(updateId: string, bindingId: string, accountId: string, grantId: string,
|
|
1353
1367
|
change: (draft: IStoredAuthorityBinding | null, account: IStoredAuthorityAccount,
|
|
1354
|
-
grant: IStoredAuthorityGrant | null) => IStoredAuthorityBinding): Promise<{
|
|
1368
|
+
grant: IStoredAuthorityGrant | null, revision: number) => IStoredAuthorityBinding): Promise<{
|
|
1355
1369
|
meta: IStoredAuthorityMeta; binding: IStoredAuthorityBinding;
|
|
1356
1370
|
}> {
|
|
1357
1371
|
try {
|
|
@@ -1363,8 +1377,11 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1363
1377
|
const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
|
|
1364
1378
|
const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
|
|
1365
1379
|
const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1380
|
+
const revision = metaStored.revision + 1;
|
|
1381
|
+
const draft = change(current ? structuredClone(AuthSwitchAuthorityBindingModel.exact.toPersisted(current)) : null,
|
|
1382
|
+
account, grant, revision);
|
|
1383
|
+
if (draft.id !== bindingId || draft.accountId !== accountId || draft.updateId !== updateId
|
|
1384
|
+
|| draft.revision !== revision) throw new Error('Binding change has an invalid identity.');
|
|
1368
1385
|
if (draft.grantId !== grantId || grant?.accountId !== accountId) {
|
|
1369
1386
|
throw new Error('Binding login does not belong to the account.');
|
|
1370
1387
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
3
|
+
import { parseOpenAiTokenClaims, type IOpenAiTokenClaims } from './openaiauth.js';
|
|
4
|
+
import type { IOpenAiAccess, IOpenAiOAuthCredential, TOpenAiAccountRateLimitsResult, TOpenAiRefreshResult } from './interfaces.openai.js';
|
|
2
5
|
import type { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
|
|
3
6
|
import { authSwitchEnvironmentId } from './classes.operations.js';
|
|
4
7
|
import { AuthSwitchRefusal } from './authority-contract.js';
|
|
@@ -55,14 +58,12 @@ export interface IAuthSwitchImportQuiescence {
|
|
|
55
58
|
/**
|
|
56
59
|
* Exactly the two provider operations an import needs, so nothing here can reach for a third.
|
|
57
60
|
*
|
|
58
|
-
* `
|
|
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;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IOpenAiAccess } from './interfaces.openai.js';
|
|
2
3
|
import type { IAuthSwitchPreuseOperation } from './authority-contract.js';
|
|
3
4
|
import { AuthSwitchRefusal } from './authority-contract.js';
|
|
4
5
|
import { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
@@ -6,10 +7,10 @@ import { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
|
|
|
6
7
|
import type { IStoredAuthorityPreuseOperation, TStoredAuthorityPreuseProblem } from './classes.authoritymodels.js';
|
|
7
8
|
import { CodexPreuse, type ICodexPreuseLifecycle } from './classes.codexpreuse.js';
|
|
8
9
|
import type { IHarnessPreuseOptions, IHarnessPreuseResult } from './interfaces.harness.js';
|
|
9
|
-
import { validatePreuseOptions } from './preuse.js';
|
|
10
|
+
import { PreuseFailureError, PreuseLimitError, validatePreuseOptions } from './preuse.js';
|
|
10
11
|
|
|
11
12
|
export interface IAuthSwitchAuthorityPreuseExecutor {
|
|
12
|
-
runAccess(
|
|
13
|
+
runAccess(access: IOpenAiAccess,
|
|
13
14
|
options: IHarnessPreuseOptions, lifecycle: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult>;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -41,7 +42,8 @@ const hash = (value: unknown): value is string => typeof value === 'string' && /
|
|
|
41
42
|
const publicOperation = (operation: IStoredAuthorityPreuseOperation): IAuthSwitchPreuseOperation => ({
|
|
42
43
|
id: operation.id, accountId: operation.accountId, loginId: operation.grantId,
|
|
43
44
|
purpose: operation.purpose, requestedModel: operation.requestedModel, model: operation.model,
|
|
44
|
-
state: operation.state, result: operation.result, problem: operation.problem,
|
|
45
|
+
state: operation.state, result: operation.result, problem: operation.problem, limit: operation.limit ?? null,
|
|
46
|
+
failure: operation.failure ?? null,
|
|
45
47
|
createdAt: operation.createdAt, updatedAt: operation.updatedAt,
|
|
46
48
|
finishedAt: operation.finishedAt, revision: operation.revision,
|
|
47
49
|
});
|
|
@@ -204,11 +206,8 @@ export class AuthSwitchAuthorityPreuse {
|
|
|
204
206
|
const access = await this.broker.resolveUsageAccess(context, undefined, this.minAccessValidityMs);
|
|
205
207
|
signal.throwIfAborted();
|
|
206
208
|
phase = operation.requestedModel === null ? 'catalog' : 'access';
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
accountId: access.accountId, isFedrampAccount: access.isFedrampAccount,
|
|
210
|
-
};
|
|
211
|
-
const result = await this.executor.runAccess(credential,
|
|
209
|
+
const result = await this.executor.runAccess({ accessToken: access.accessToken,
|
|
210
|
+
accountId: access.accountId, isFedrampAccount: access.isFedrampAccount },
|
|
212
211
|
{ prompt, ...(operation.requestedModel === null ? {} : { model: operation.requestedModel }), signal },
|
|
213
212
|
{ beforeRequest: async model => {
|
|
214
213
|
signal.throwIfAborted();
|
|
@@ -224,26 +223,40 @@ export class AuthSwitchAuthorityPreuse {
|
|
|
224
223
|
state: 'complete', result: { inputTokens: count(result.inputTokens), outputTokens: count(result.outputTokens),
|
|
225
224
|
totalTokens: count(result.totalTokens) }, problem: 'none', updatedAt: now, finishedAt: now,
|
|
226
225
|
revision: prior.revision + 1, updateId }));
|
|
227
|
-
} catch {
|
|
228
|
-
await this.fail(operation.id, phase, signal
|
|
226
|
+
} catch (error) {
|
|
227
|
+
await this.fail(operation.id, phase, signal,
|
|
228
|
+
error instanceof PreuseLimitError || error instanceof PreuseFailureError ? error : null);
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Settles a preuse that did not complete. A limit is named: the account's usage reading stopped it before
|
|
234
|
+
* anything was sent, or the provider refused the prompt -- before its answer began, which consumed nothing and
|
|
235
|
+
* settles `refused`, or during it, which stays `outcome_unknown`. Any other stated cause of a sent request is
|
|
236
|
+
* named as its `failure`.
|
|
237
|
+
*/
|
|
238
|
+
private async fail(operationId: string, phase: 'target' | 'access' | 'catalog', signal: AbortSignal,
|
|
239
|
+
stated: PreuseLimitError | PreuseFailureError | null): Promise<void> {
|
|
233
240
|
let current: IStoredAuthorityPreuseOperation;
|
|
234
241
|
try { current = await this.readStored(operationId); }
|
|
235
242
|
catch { return; }
|
|
236
243
|
if (!['preparing', 'request_may_have_started'].includes(current.state)) return;
|
|
237
244
|
const marked = current.state === 'request_may_have_started';
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
245
|
+
const stopped = this.closed ? 'authority_closing' : signal.aborted ? 'cancelled' : null;
|
|
246
|
+
const limit = stopped === null && stated instanceof PreuseLimitError && (stated.source === 'provider') === marked
|
|
247
|
+
? stated : null;
|
|
248
|
+
const failure = stopped === null && marked && stated instanceof PreuseFailureError ? stated.failure : null;
|
|
249
|
+
const problem: TStoredAuthorityPreuseProblem = stopped ?? (limit ? limit.limit.kind
|
|
250
|
+
: marked ? 'request_uncertain'
|
|
251
|
+
: phase === 'target' ? 'target_unavailable' : phase === 'catalog' ? 'catalog_unavailable' : 'access_unavailable');
|
|
252
|
+
const state: IStoredAuthorityPreuseOperation['state'] = !marked ? 'not_sent'
|
|
253
|
+
: limit && !limit.generationStarted ? 'refused' : 'outcome_unknown';
|
|
242
254
|
const updateId = plugins.crypto.randomUUID();
|
|
243
255
|
const now = new Date(this.now()).toISOString();
|
|
244
256
|
try {
|
|
245
|
-
await this.database.changePreuseOperation(updateId, operationId, prior => ({ ...prior,
|
|
246
|
-
|
|
257
|
+
await this.database.changePreuseOperation(updateId, operationId, prior => ({ ...prior, state, problem,
|
|
258
|
+
...(limit ? { limit: { ...limit.limit } } : {}),
|
|
259
|
+
...(failure ? { failure: { ...failure } } : {}),
|
|
247
260
|
updatedAt: now, finishedAt: now, revision: prior.revision + 1, updateId }));
|
|
248
261
|
} catch { /* A durable request marker remains recoverable as outcome_unknown on restart. */ }
|
|
249
262
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import type { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
3
|
+
import type { IOpenAiAccountRateLimits, IOpenAiRateLimitDetails, IOpenAiRateLimitWindow } from './interfaces.openai.js';
|
|
2
4
|
import type { IClaudeLogin, IClaudeStatusRead } from './classes.claudestatus.js';
|
|
3
5
|
import type { IHarnessAccountStatus } from './interfaces.harness.js';
|
|
4
6
|
|
|
5
|
-
type TRateLimits =
|
|
6
|
-
type TAccessCredential = plugins.flexAccounts.IOpenAiChatGptAccessCredential;
|
|
7
|
+
type TRateLimits = IOpenAiAccountRateLimits;
|
|
7
8
|
|
|
8
9
|
export type TAuthSwitchUsageProblem =
|
|
9
10
|
| 'none' | 'unsupported' | 'access_unavailable' | 'provider_unavailable'
|
|
@@ -115,7 +116,7 @@ export interface IAuthSwitchAuthorityUsageOptions {
|
|
|
115
116
|
action: (source: { context: IAuthSwitchUsageContext; login: IClaudeLogin;
|
|
116
117
|
readContext(): Promise<IAuthSwitchUsageContext | null> }) => Promise<IAuthSwitchUsageSnapshot>): Promise<IAuthSwitchUsageSnapshot>;
|
|
117
118
|
store: IAuthSwitchUsageStore;
|
|
118
|
-
provider: Pick<
|
|
119
|
+
provider: Pick<AuthSwitchOpenAiProvider, 'getAccountRateLimits'>;
|
|
119
120
|
now?: () => number;
|
|
120
121
|
currentMaxAgeMs?: number;
|
|
121
122
|
maxStaleAgeMs?: number;
|
|
@@ -199,11 +200,11 @@ const publicClaudeStatus = (source: IHarnessAccountStatus): IAuthSwitchClaudeSta
|
|
|
199
200
|
|
|
200
201
|
/** Allowlist the provider projection before it enters a management DTO or stored usage record. */
|
|
201
202
|
const publicRateLimits = (source: TRateLimits): TRateLimits => {
|
|
202
|
-
const window = (value:
|
|
203
|
+
const window = (value: IOpenAiRateLimitWindow) => ({
|
|
203
204
|
usedPercent: value.usedPercent, limitWindowSeconds: value.limitWindowSeconds,
|
|
204
205
|
resetAfterSeconds: value.resetAfterSeconds, resetsAt: value.resetsAt,
|
|
205
206
|
});
|
|
206
|
-
const detail = (value:
|
|
207
|
+
const detail = (value: IOpenAiRateLimitDetails) => ({
|
|
207
208
|
allowed: value.allowed, limitReached: value.limitReached,
|
|
208
209
|
...(value.primaryWindow ? { primaryWindow: window(value.primaryWindow) } : {}),
|
|
209
210
|
...(value.secondaryWindow ? { secondaryWindow: window(value.secondaryWindow) } : {}),
|
|
@@ -511,10 +512,8 @@ export class AuthSwitchAuthorityUsage {
|
|
|
511
512
|
}
|
|
512
513
|
|
|
513
514
|
private query(access: IAuthSwitchUsageAccess): ReturnType<IAuthSwitchAuthorityUsageOptions['provider']['getAccountRateLimits']> {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
isFedrampAccount: access.isFedrampAccount };
|
|
517
|
-
return this.options.provider.getAccountRateLimits(credential, { timeoutMs: this.policy.providerTimeoutMs });
|
|
515
|
+
return this.options.provider.getAccountRateLimits({ accessToken: access.accessToken, accountId: access.accountId,
|
|
516
|
+
isFedrampAccount: access.isFedrampAccount }, { timeoutMs: this.policy.providerTimeoutMs });
|
|
518
517
|
}
|
|
519
518
|
|
|
520
519
|
/** Wait for admitted reads before the daemon disposes the shared provider or database. */
|
package/ts/classes.cli.ts
CHANGED
|
@@ -372,11 +372,11 @@ ${bold('Environment')}
|
|
|
372
372
|
const cancel = () => { cancelled = true; void handle?.cancel().catch(() => undefined); };
|
|
373
373
|
process.on('SIGINT', cancel); process.on('SIGTERM', cancel);
|
|
374
374
|
try {
|
|
375
|
-
handle = await harness.beginLogin({ providerId: provider.providerId, flow:
|
|
375
|
+
handle = await harness.beginLogin({ providerId: provider.providerId, flow: 'device' });
|
|
376
376
|
if (cancelled) await handle.cancel();
|
|
377
|
-
else
|
|
377
|
+
else {
|
|
378
378
|
process.stdout.write(`Open ${plainText(handle.prompt.verificationUrl)}\nCode: ${plainText(handle.prompt.userCode)}\nWaiting for login. Press Ctrl+C to cancel.\n`);
|
|
379
|
-
}
|
|
379
|
+
}
|
|
380
380
|
return this.reportOutcome((await handle.completion).outcome);
|
|
381
381
|
} finally {
|
|
382
382
|
process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel);
|