@modelprofile.com/authswitch 6.5.0 → 8.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 +1 -1
- package/dist_ts/authority-contract.d.ts +300 -6
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/authority-runtime-contract.d.ts +13 -0
- package/dist_ts/classes.accountlist.js +4 -3
- package/dist_ts/classes.authoritybroker.d.ts +25 -5
- package/dist_ts/classes.authoritybroker.js +388 -123
- package/dist_ts/classes.authoritycli.d.ts +24 -0
- package/dist_ts/classes.authoritycli.js +675 -0
- package/dist_ts/classes.authorityclient.d.ts +19 -7
- package/dist_ts/classes.authorityclient.js +51 -14
- package/dist_ts/classes.authoritydaemon.d.ts +28 -1
- package/dist_ts/classes.authoritydaemon.js +477 -36
- package/dist_ts/classes.authoritydatabase.d.ts +155 -11
- package/dist_ts/classes.authoritydatabase.js +1037 -69
- package/dist_ts/classes.authoritymodels.d.ts +229 -23
- package/dist_ts/classes.authoritymodels.js +867 -128
- package/dist_ts/classes.authoritypreuse.d.ts +47 -0
- package/dist_ts/classes.authoritypreuse.js +277 -0
- package/dist_ts/classes.authorityregistry.d.ts +9 -0
- package/dist_ts/classes.authorityregistry.js +32 -0
- package/dist_ts/classes.authoritysecrets.d.ts +1 -1
- package/dist_ts/classes.authoritysecrets.js +2 -2
- package/dist_ts/classes.authorityusage.d.ts +160 -0
- package/dist_ts/classes.authorityusage.js +429 -0
- package/dist_ts/classes.claudeauthority.d.ts +66 -0
- package/dist_ts/classes.claudeauthority.js +528 -0
- package/dist_ts/classes.claudecodeharness.js +3 -2
- package/dist_ts/classes.claudecodelocks.d.ts +13 -0
- package/dist_ts/classes.claudecodelocks.js +75 -3
- package/dist_ts/classes.claudenative.d.ts +130 -0
- package/dist_ts/classes.claudenative.js +708 -0
- package/dist_ts/classes.claudestatus.d.ts +9 -0
- package/dist_ts/classes.claudestatus.js +26 -9
- package/dist_ts/classes.claudetokenrefresh.d.ts +15 -1
- package/dist_ts/classes.claudetokenrefresh.js +74 -21
- package/dist_ts/classes.codexharness.js +4 -2
- package/dist_ts/classes.codexmanaged.d.ts +12 -1
- package/dist_ts/classes.codexmanaged.js +75 -14
- package/dist_ts/classes.codexpreuse.d.ts +8 -0
- package/dist_ts/classes.codexpreuse.js +24 -6
- package/dist_ts/classes.fileharness.d.ts +2 -0
- package/dist_ts/classes.fileharness.js +6 -4
- package/dist_ts/classes.limits.js +12 -7
- package/dist_ts/classes.opencodeharness.js +2 -2
- package/dist_ts/claudehttp.d.ts +10 -0
- package/dist_ts/claudehttp.js +29 -3
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/interfaces.harness.d.ts +6 -0
- package/dist_ts/interfaces.list.d.ts +9 -3
- package/dist_ts/plugins.d.ts +6 -3
- package/dist_ts/plugins.js +8 -4
- package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -0
- package/dist_ts/ts_migration/0001_authority_meta.js +44 -0
- package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
- package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
- package/dist_ts/ts_migration/index.d.ts +6 -0
- package/dist_ts/ts_migration/index.js +12 -0
- package/package.json +3 -3
- package/readme.md +177 -14
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +262 -9
- package/ts/authority-runtime-contract.ts +14 -0
- package/ts/classes.accountlist.ts +3 -2
- package/ts/classes.authoritybroker.ts +385 -121
- package/ts/classes.authoritycli.ts +719 -0
- package/ts/classes.authorityclient.ts +75 -18
- package/ts/classes.authoritydaemon.ts +444 -36
- package/ts/classes.authoritydatabase.ts +1076 -75
- package/ts/classes.authoritymodels.ts +642 -144
- package/ts/classes.authoritypreuse.ts +296 -0
- package/ts/classes.authorityregistry.ts +47 -0
- package/ts/classes.authoritysecrets.ts +1 -1
- package/ts/classes.authorityusage.ts +527 -0
- package/ts/classes.claudeauthority.ts +537 -0
- package/ts/classes.claudecodeharness.ts +2 -1
- package/ts/classes.claudecodelocks.ts +70 -1
- package/ts/classes.claudenative.ts +785 -0
- package/ts/classes.claudestatus.ts +38 -9
- package/ts/classes.claudetokenrefresh.ts +82 -19
- package/ts/classes.codexharness.ts +3 -1
- package/ts/classes.codexmanaged.ts +76 -13
- package/ts/classes.codexpreuse.ts +33 -5
- package/ts/classes.fileharness.ts +7 -3
- package/ts/classes.limits.ts +11 -6
- package/ts/classes.opencodeharness.ts +1 -1
- package/ts/claudehttp.ts +31 -2
- package/ts/index.ts +6 -0
- package/ts/interfaces.harness.ts +6 -0
- package/ts/interfaces.list.ts +9 -3
- package/ts/plugins.ts +8 -3
- package/ts/ts_migration/0001_authority_meta.ts +45 -0
- package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
- package/ts/ts_migration/index.ts +12 -0
|
@@ -108,7 +108,7 @@ const claudeUsageWindows = (usage: Record<string, unknown>): IClaudeUsageWindows
|
|
|
108
108
|
type TClaudeProfile =
|
|
109
109
|
| { kind: 'found'; plan: string | undefined; facts: IHarnessStatusFact[] }
|
|
110
110
|
| { kind: 'mismatch' }
|
|
111
|
-
| { kind: 'unavailable'; problem: string };
|
|
111
|
+
| { kind: 'unavailable'; problem: string; credentialRejected: boolean };
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* One Claude OAuth login as a status lookup needs it: the token to ask with, and what the store that keeps it knows
|
|
@@ -125,6 +125,14 @@ export interface IClaudeLogin {
|
|
|
125
125
|
plan?: string;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** Structured proof from the same profile and usage requests used for harness status. */
|
|
129
|
+
export interface IClaudeStatusRead {
|
|
130
|
+
status: IHarnessAccountStatus;
|
|
131
|
+
profile: 'verified' | 'unverified' | 'mismatch' | 'unavailable' | 'unsupported_scope';
|
|
132
|
+
usage: 'available' | 'unavailable' | 'unsupported_scope';
|
|
133
|
+
credentialRejected: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
128
136
|
/**
|
|
129
137
|
* How long a session reuses an account's profile. The watch polls every few minutes, and the profile (plan,
|
|
130
138
|
* organization, billing type) changes far more rarely, so it is read at most hourly there.
|
|
@@ -167,7 +175,8 @@ export class ClaudeAccountStatus {
|
|
|
167
175
|
let body: Record<string, unknown>;
|
|
168
176
|
try { body = await this.get('profile', loginArg.accessToken, signal); }
|
|
169
177
|
catch (error) {
|
|
170
|
-
if (error instanceof ClaudeLoginRejectedError) return { kind: 'unavailable',
|
|
178
|
+
if (error instanceof ClaudeLoginRejectedError) return { kind: 'unavailable',
|
|
179
|
+
problem: `Profile: ${error.message}`, credentialRejected: true };
|
|
171
180
|
throw error;
|
|
172
181
|
}
|
|
173
182
|
try {
|
|
@@ -181,7 +190,8 @@ export class ClaudeAccountStatus {
|
|
|
181
190
|
if (credentialText(value)) facts.push({ section: 'Account', label: String(label), value: String(value) });
|
|
182
191
|
}
|
|
183
192
|
return { kind: 'found', plan: credentialText(organization.organization_type)?.replace(/^claude_/, ''), facts };
|
|
184
|
-
} catch { return { kind: 'unavailable',
|
|
193
|
+
} catch { return { kind: 'unavailable',
|
|
194
|
+
problem: 'Claude profile response is unsupported; no profile values were inferred.', credentialRejected: false }; }
|
|
185
195
|
}
|
|
186
196
|
|
|
187
197
|
/** The plan the login was saved with, the only one known while the service cannot be asked. */
|
|
@@ -195,25 +205,43 @@ export class ClaudeAccountStatus {
|
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
public async read(loginArg: IClaudeLogin, optionsArg: IHarnessStatusOptions = {}): Promise<IHarnessAccountStatus> {
|
|
208
|
+
return (await this.readDetailed(loginArg, optionsArg)).status;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** One request pair; callers requiring immutable identity use the proof rather than display text. */
|
|
212
|
+
public async readDetailed(loginArg: IClaudeLogin,
|
|
213
|
+
optionsArg: IHarnessStatusOptions = {}): Promise<IClaudeStatusRead> {
|
|
198
214
|
const result: IHarnessAccountStatus = { facts: [], problems: [], ...this.storedSummary(loginArg) };
|
|
199
215
|
// Only a store that keeps the login's scopes can tell beforehand that the service will not answer about the account.
|
|
200
216
|
if (loginArg.scopes.length && !loginArg.scopes.includes('user:profile')) {
|
|
201
217
|
result.facts.push({ section: 'Availability', label: 'Live account status', value: 'This login lacks the user:profile scope. Log in through Claude Code to obtain subscriber status.' });
|
|
202
|
-
return result;
|
|
218
|
+
return { status: result, profile: 'unsupported_scope', usage: 'unsupported_scope', credentialRejected: false };
|
|
203
219
|
}
|
|
204
220
|
const token = credentialText(loginArg.accessToken);
|
|
205
|
-
if (!token) return { facts: [], problems: ['No usable Claude Code access token is available.'] }
|
|
221
|
+
if (!token) return { status: { facts: [], problems: ['No usable Claude Code access token is available.'] },
|
|
222
|
+
profile: 'unavailable', usage: 'unavailable', credentialRejected: false };
|
|
206
223
|
const [profile, usage] = await Promise.allSettled([this.profile(loginArg, optionsArg), this.get('usage', token, optionsArg.signal)]);
|
|
207
224
|
const failure = (reason: unknown): string => reason instanceof ClaudeRequestError ? reason.message : 'Lookup failed.';
|
|
225
|
+
let profileOutcome: IClaudeStatusRead['profile'] = 'unavailable';
|
|
226
|
+
let usageOutcome: IClaudeStatusRead['usage'] = 'unavailable';
|
|
227
|
+
let credentialRejected = usage.status === 'rejected' && usage.reason instanceof ClaudeLoginRejectedError;
|
|
208
228
|
if (profile.status === 'fulfilled') {
|
|
209
229
|
const found = profile.value;
|
|
210
|
-
if (found.kind === 'mismatch') return { facts: [], problems: [MISMATCH] }
|
|
211
|
-
|
|
230
|
+
if (found.kind === 'mismatch') return { status: { facts: [], problems: [MISMATCH] },
|
|
231
|
+
profile: 'mismatch', usage: 'unavailable', credentialRejected: false };
|
|
232
|
+
if (found.kind === 'unavailable') {
|
|
233
|
+
result.problems.push(found.problem);
|
|
234
|
+
credentialRejected ||= found.credentialRejected;
|
|
235
|
+
}
|
|
212
236
|
else {
|
|
237
|
+
profileOutcome = loginArg.identity ? 'verified' : 'unverified';
|
|
213
238
|
if (found.plan) result.summary = { ...result.summary, subscription: { plan: found.plan, source: 'live' } };
|
|
214
239
|
result.facts.push(...found.facts.map(fact => ({ ...fact })));
|
|
215
240
|
}
|
|
216
|
-
} else
|
|
241
|
+
} else {
|
|
242
|
+
result.problems.push(`Profile: ${failure(profile.reason)}`);
|
|
243
|
+
credentialRejected ||= profile.reason instanceof ClaudeLoginRejectedError;
|
|
244
|
+
}
|
|
217
245
|
if (usage.status === 'fulfilled') {
|
|
218
246
|
try {
|
|
219
247
|
const { windows, unsupported } = claudeUsageWindows(usage.value);
|
|
@@ -232,12 +260,13 @@ export class ClaudeAccountStatus {
|
|
|
232
260
|
if (limit !== undefined && limit > 0 && used !== undefined) result.facts.push({ section: 'Extra usage', label: 'Monthly budget used', value: `${Math.round(used / limit * 10000) / 100}%` });
|
|
233
261
|
}
|
|
234
262
|
if (unsupported.length) result.facts.push({ section: 'Availability', label: 'Unsupported limits', value: `The service returned limits without a supported reset window, which are not shown: ${unsupported.join(', ')}.` });
|
|
263
|
+
usageOutcome = 'available';
|
|
235
264
|
} catch { result.problems.push('Claude usage response is unsupported; missing values were not inferred.'); }
|
|
236
265
|
} else result.problems.push(`Usage: ${failure(usage.reason)}`);
|
|
237
266
|
const limited = [profile, usage].flatMap(item => item.status === 'rejected' && item.reason instanceof ClaudeRateLimitError ? [item.reason] : []);
|
|
238
267
|
if (limited.length) result.rateLimit = { retryAt: latestRetryAt(limited.map(error => error.retryAt)) };
|
|
239
268
|
// The missing renewal dates are the harness's `renewalUnavailableReason`, stated once by the views that show them.
|
|
240
269
|
result.facts.push({ section: 'Availability', label: 'Earned resets', value: 'authswitch does not read earned reset credits for Claude Code logins.' });
|
|
241
|
-
return result;
|
|
270
|
+
return { status: result, profile: profileOutcome, usage: usageOutcome, credentialRejected };
|
|
242
271
|
}
|
|
243
272
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { claudeRequest, claudeRotatingRequest, ClaudeAccountOnHoldError, ClaudeLoginRejectedError,
|
|
2
|
+
ClaudeRequestError, ClaudeRotatingRequestError, oauthErrorCode } from './claudehttp.js';
|
|
3
|
+
|
|
4
|
+
const credentialText = (value: unknown): string | undefined =>
|
|
5
|
+
typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
3
6
|
|
|
4
7
|
/** Claude Code 2.1.273's token endpoint (`TOKEN_URL`, bundle offset @188533776). */
|
|
5
8
|
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
@@ -36,6 +39,13 @@ export interface IClaudeTokens {
|
|
|
36
39
|
scopes?: string[];
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/** Authority callers persist a pre-send marker, then classify every failed rotating request. */
|
|
43
|
+
export type TClaudeAuthorityRefreshResult =
|
|
44
|
+
| { success: true; tokens: IClaudeTokens }
|
|
45
|
+
| { success: false; requestOutcome: 'notSent' | 'outcomeUnknown';
|
|
46
|
+
reason: 'not_refreshable' | 'provider_rejected' | 'account_on_hold' | 'provider_unavailable' | 'unsupported_response';
|
|
47
|
+
responseStatus?: number };
|
|
48
|
+
|
|
39
49
|
/** How Claude Code 2.1.273 builds one refresh request for a stored login. */
|
|
40
50
|
export interface IClaudeGrant {
|
|
41
51
|
/** The login's own OAuth client, or Claude Code's public one. */
|
|
@@ -104,10 +114,10 @@ export class ClaudeTokenRefresh {
|
|
|
104
114
|
* Whether Claude Code would refresh this login before using it: its access token expires within five minutes. A
|
|
105
115
|
* login without a numeric expiry or without a refresh token is used as it is, exactly as Claude Code does.
|
|
106
116
|
*/
|
|
107
|
-
public due(oauthArg: Record<string, unknown
|
|
117
|
+
public due(oauthArg: Record<string, unknown>, minValidityMs = CLAUDE_REFRESH_MARGIN_MS): boolean {
|
|
108
118
|
const expiresAt = oauthArg.expiresAt;
|
|
109
119
|
return typeof expiresAt === 'number' && Number.isFinite(expiresAt) && credentialText(oauthArg.refreshToken) !== undefined
|
|
110
|
-
&& this.now() + CLAUDE_REFRESH_MARGIN_MS >= expiresAt;
|
|
120
|
+
&& this.now() + Math.max(CLAUDE_REFRESH_MARGIN_MS, minValidityMs) >= expiresAt;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
/** One refresh request, as Claude Code sends it: a JSON body, the login's client, and the scopes of this attempt. */
|
|
@@ -121,26 +131,56 @@ export class ClaudeTokenRefresh {
|
|
|
121
131
|
});
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
private attemptForAuthority(refreshTokenArg: string, grantArg: IClaudeGrant, scopesArg: readonly string[]) {
|
|
135
|
+
return claudeRotatingRequest(this.fetcher, {
|
|
136
|
+
url: TOKEN_URL, method: 'POST', headers: { 'Content-Type': 'application/json' }, subject: 'Claude sign-in',
|
|
137
|
+
body: JSON.stringify({ grant_type: 'refresh_token', refresh_token: refreshTokenArg,
|
|
138
|
+
client_id: grantArg.clientId, scope: grantScope(scopesArg) }),
|
|
139
|
+
timeoutMs: REFRESH_TIMEOUT_MS, readErrorBody: true, now: this.now,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The authority must already have durably marked this grant as possibly sent. */
|
|
144
|
+
public async refreshForAuthority(oauthArg: Record<string, unknown>): Promise<TClaudeAuthorityRefreshResult> {
|
|
131
145
|
const refreshToken = credentialText(oauthArg.refreshToken);
|
|
132
146
|
const grant = claudeGrant(oauthArg);
|
|
133
147
|
if (!refreshToken || !grant) {
|
|
134
|
-
|
|
148
|
+
return { success: false, requestOutcome: 'notSent', reason: 'not_refreshable' };
|
|
135
149
|
}
|
|
136
|
-
let
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
let response: Awaited<ReturnType<typeof this.attemptForAuthority>>;
|
|
151
|
+
try {
|
|
152
|
+
response = await this.attemptForAuthority(refreshToken, grant, grant.scopes);
|
|
153
|
+
// A definitive invalid_scope response is the one safe condition in which Claude retries the same grant.
|
|
154
|
+
if (response.status === 400 && oauthErrorCode(response.body) === 'invalid_scope'
|
|
155
|
+
&& grant.fallbackScopes !== null) {
|
|
156
|
+
response = await this.attemptForAuthority(refreshToken, grant, grant.fallbackScopes);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { success: false,
|
|
160
|
+
requestOutcome: error instanceof ClaudeRotatingRequestError ? error.requestOutcome : 'outcomeUnknown',
|
|
161
|
+
reason: 'provider_unavailable' };
|
|
140
162
|
}
|
|
141
|
-
if (accountOnHold(status, body))
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
if (accountOnHold(response.status, response.body)) {
|
|
164
|
+
return { success: false, requestOutcome: 'outcomeUnknown', reason: 'account_on_hold',
|
|
165
|
+
responseStatus: response.status };
|
|
166
|
+
}
|
|
167
|
+
if ((response.status === 400 || response.status === 401)
|
|
168
|
+
&& oauthErrorCode(response.body) === 'invalid_grant') {
|
|
169
|
+
return { success: false, requestOutcome: 'outcomeUnknown', reason: 'provider_rejected',
|
|
170
|
+
responseStatus: response.status };
|
|
171
|
+
}
|
|
172
|
+
if (response.status < 200 || response.status > 299) {
|
|
173
|
+
return { success: false, requestOutcome: 'outcomeUnknown', reason: 'provider_unavailable',
|
|
174
|
+
responseStatus: response.status };
|
|
175
|
+
}
|
|
176
|
+
try { return { success: true, tokens: this.tokensFromResponse(response.body, refreshToken) }; }
|
|
177
|
+
catch {
|
|
178
|
+
return { success: false, requestOutcome: 'outcomeUnknown', reason: 'unsupported_response',
|
|
179
|
+
responseStatus: response.status };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private tokensFromResponse(body: Record<string, unknown> | null, refreshToken: string): IClaudeTokens {
|
|
144
184
|
const receivedAt = this.now();
|
|
145
185
|
const accessToken = credentialText(body?.access_token);
|
|
146
186
|
const tokenType = body?.token_type;
|
|
@@ -158,4 +198,27 @@ export class ClaudeTokenRefresh {
|
|
|
158
198
|
...(scope === null || !scope.length ? {} : { scopes: scope }),
|
|
159
199
|
};
|
|
160
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* New tokens for the login. A response without a refresh token keeps the one sent, and one without a scope keeps the
|
|
204
|
+
* stored scopes, as Claude Code merges them. Throws `ClaudeRequestError` with a fixed diagnostic; a grant the service
|
|
205
|
+
* rejects is `ClaudeLoginRejectedError`, an account the service put on hold is `ClaudeAccountOnHoldError`, and a rate
|
|
206
|
+
* limit is `ClaudeRateLimitError`.
|
|
207
|
+
*/
|
|
208
|
+
public async refresh(oauthArg: Record<string, unknown>): Promise<IClaudeTokens> {
|
|
209
|
+
const refreshToken = credentialText(oauthArg.refreshToken);
|
|
210
|
+
const grant = claudeGrant(oauthArg);
|
|
211
|
+
if (!refreshToken || !grant) {
|
|
212
|
+
throw new ClaudeRequestError('This saved login has no refresh token, and no subscriber scope or plan to refresh with. Log in again with Claude Code and save it.');
|
|
213
|
+
}
|
|
214
|
+
let { status, body } = await this.attempt(refreshToken, grant, grant.scopes);
|
|
215
|
+
// The service refusing Claude Code's current scope set is the one failure it sends a second request for.
|
|
216
|
+
if (status === 400 && oauthErrorCode(body) === 'invalid_scope' && grant.fallbackScopes !== null) {
|
|
217
|
+
({ status, body } = await this.attempt(refreshToken, grant, grant.fallbackScopes));
|
|
218
|
+
}
|
|
219
|
+
if (accountOnHold(status, body)) throw new ClaudeAccountOnHoldError();
|
|
220
|
+
if ((status === 400 || status === 401) && oauthErrorCode(body) === 'invalid_grant') throw new ClaudeLoginRejectedError();
|
|
221
|
+
if (status < 200 || status > 299) throw new ClaudeRequestError(`The Claude sign-in service returned HTTP ${status}.`);
|
|
222
|
+
return this.tokensFromResponse(body, refreshToken);
|
|
223
|
+
}
|
|
161
224
|
}
|
|
@@ -69,6 +69,7 @@ export class CodexHarness implements IAuthHarness {
|
|
|
69
69
|
const isActive = this.recordOf(entryArg, active.identity);
|
|
70
70
|
return {
|
|
71
71
|
id: entryArg.email,
|
|
72
|
+
providerAccountId: entryArg.accountId,
|
|
72
73
|
label: entryArg.email,
|
|
73
74
|
isActive,
|
|
74
75
|
isStashed,
|
|
@@ -77,7 +78,8 @@ export class CodexHarness implements IAuthHarness {
|
|
|
77
78
|
};
|
|
78
79
|
});
|
|
79
80
|
if (active.identity && !accounts.some((accountArg) => accountArg.isActive)) {
|
|
80
|
-
accounts.unshift({ id: this.activeAccountId(active.identity.email, active.identity.accountId),
|
|
81
|
+
accounts.unshift({ id: this.activeAccountId(active.identity.email, active.identity.accountId),
|
|
82
|
+
providerAccountId: active.identity.accountId, label: active.identity.email, isActive: true,
|
|
81
83
|
isStashed: false, savedAt: null, details: [] });
|
|
82
84
|
}
|
|
83
85
|
return {
|
|
@@ -5,7 +5,7 @@ import type { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
|
5
5
|
export interface IAuthSwitchManagedCodexOptions {
|
|
6
6
|
/** Private runtime root; only the disposable control socket lives here. */
|
|
7
7
|
runtimeDirectory: string;
|
|
8
|
-
/** Caller-owned, durable native Codex state home, reserved to one account and scope by
|
|
8
|
+
/** Caller-owned, durable native Codex state home, reserved to one account and scope by NoSQLDB. */
|
|
9
9
|
codexHomeDirectory: string;
|
|
10
10
|
workspaceDirectory: string;
|
|
11
11
|
executable?: string;
|
|
@@ -33,28 +33,91 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
33
33
|
const isAlive = (child: plugins.childProcess.ChildProcess): boolean => child.exitCode === null && child.signalCode === null;
|
|
34
34
|
const wait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
export type TManagedCodexProcessObservation =
|
|
37
|
+
| { state: 'absent' }
|
|
38
|
+
| { state: 'observed'; pid: number; startedAt: string }
|
|
39
|
+
| { state: 'multiple_claimants' };
|
|
40
|
+
|
|
41
|
+
const linuxArgv = async (pid: number): Promise<Buffer[] | null> => {
|
|
42
|
+
let raw: Buffer;
|
|
43
|
+
try {
|
|
44
|
+
raw = await plugins.fs.promises.readFile(`/proc/${pid}/cmdline`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (isRecord(error) && error.code === 'ENOENT') return null;
|
|
47
|
+
throw new Error('Managed Codex process argument inspection failed.');
|
|
48
|
+
}
|
|
49
|
+
if (raw.length === 0) return [];
|
|
50
|
+
const argv: Buffer[] = [];
|
|
51
|
+
let start = 0;
|
|
52
|
+
for (let index = 0; index < raw.length; index++) {
|
|
53
|
+
if (raw[index] !== 0) continue;
|
|
54
|
+
argv.push(raw.subarray(start, index));
|
|
55
|
+
start = index + 1;
|
|
56
|
+
}
|
|
57
|
+
if (start < raw.length) argv.push(raw.subarray(start));
|
|
58
|
+
return argv;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const claimsManagedCodexSocket = (argv: readonly Buffer[], socketPath: string): boolean => {
|
|
62
|
+
const command = Buffer.from('app-server');
|
|
63
|
+
const option = Buffer.from('--listen');
|
|
64
|
+
const value = Buffer.from(`unix://${socketPath}`);
|
|
65
|
+
for (let index = 0; index + 2 < argv.length; index++) {
|
|
66
|
+
if (argv[index].equals(command) && argv[index + 1].equals(option) && argv[index + 2].equals(value)) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Same-user Linux process inspection for a bounded socket set, using exact /proc argv boundaries. */
|
|
72
|
+
export const observeManagedCodexProcesses = async (
|
|
73
|
+
socketPaths: readonly string[],
|
|
74
|
+
): Promise<Map<string, TManagedCodexProcessObservation>> => {
|
|
75
|
+
const unique = [...new Set(socketPaths)];
|
|
76
|
+
if (unique.some(value => typeof value !== 'string' || value.length === 0)) {
|
|
77
|
+
throw new Error('Managed Codex process selector is invalid.');
|
|
78
|
+
}
|
|
79
|
+
if (unique.length === 0) return new Map();
|
|
80
|
+
if (process.platform !== 'linux') {
|
|
81
|
+
throw new Error('Exact managed Codex process inspection is available only on Linux.');
|
|
82
|
+
}
|
|
83
|
+
const uid = process.getuid?.();
|
|
84
|
+
if (uid === undefined) throw new Error('Managed Codex process owner inspection is unavailable.');
|
|
39
85
|
const output = await new Promise<string>((resolve, reject) => {
|
|
40
|
-
plugins.childProcess.execFile('ps', ['-ww', '-u', String(
|
|
86
|
+
plugins.childProcess.execFile('ps', ['-ww', '-u', String(uid), '-o', 'pid=,lstart='],
|
|
41
87
|
{ timeout: 5_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C' } },
|
|
42
88
|
(error, stdout) => error ? reject(new Error('Managed Codex process inspection failed.')) : resolve(stdout));
|
|
43
89
|
});
|
|
44
|
-
const
|
|
45
|
-
const matches: Array<{ pid: number; startedAt: string }> = [];
|
|
90
|
+
const processes: Array<{ pid: number; startedAt: string; argv: Buffer[] }> = [];
|
|
46
91
|
for (const line of output.split('\n')) {
|
|
47
92
|
const parts = line.trim().split(/\s+/);
|
|
48
|
-
if (parts.length
|
|
93
|
+
if (parts.length !== 6) continue;
|
|
49
94
|
const pid = Number(parts[0]);
|
|
50
95
|
const date = Date.parse(parts.slice(1, 6).join(' '));
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
if (Number.isSafeInteger(pid) && pid > 0 && Number.isFinite(date)) {
|
|
97
|
+
const argv = await linuxArgv(pid);
|
|
98
|
+
if (argv) processes.push({ pid, startedAt: new Date(date).toISOString(), argv });
|
|
54
99
|
}
|
|
55
100
|
}
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
const result = new Map<string, TManagedCodexProcessObservation>();
|
|
102
|
+
for (const socketPath of unique) {
|
|
103
|
+
const matches = processes.filter(process => claimsManagedCodexSocket(process.argv, socketPath));
|
|
104
|
+
result.set(socketPath, matches.length === 0
|
|
105
|
+
? { state: 'absent' }
|
|
106
|
+
: matches.length === 1
|
|
107
|
+
? { state: 'observed', pid: matches[0].pid, startedAt: matches[0].startedAt }
|
|
108
|
+
: { state: 'multiple_claimants' });
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Same-user process inspection: a unique private --listen socket is the pre-PID crash identity. */
|
|
114
|
+
export const findManagedCodexProcess = async (socketPath: string): Promise<{ pid: number; startedAt: string } | null> => {
|
|
115
|
+
const observed = (await observeManagedCodexProcesses([socketPath])).get(socketPath);
|
|
116
|
+
if (!observed || observed.state === 'absent') return null;
|
|
117
|
+
if (observed.state === 'multiple_claimants') {
|
|
118
|
+
throw new Error('Multiple managed Codex processes claim one private socket.');
|
|
119
|
+
}
|
|
120
|
+
return { pid: observed.pid, startedAt: observed.startedAt };
|
|
58
121
|
};
|
|
59
122
|
|
|
60
123
|
/** Daemon-owned Codex app-server. Only access tokens, never refresh grants, enter Codex. */
|
|
@@ -3,6 +3,11 @@ import type { ICodexIdentity } from './interfaces.js';
|
|
|
3
3
|
import type { IHarnessPreuseOptions, IHarnessPreuseResult } from './interfaces.harness.js';
|
|
4
4
|
import { PreuseError, validatePreuseOptions } from './preuse.js';
|
|
5
5
|
|
|
6
|
+
export interface ICodexPreuseLifecycle {
|
|
7
|
+
/** Persist the may-have-started marker before the inference API is invoked. */
|
|
8
|
+
beforeRequest(model: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/** One tool-free request through the FlexHarness ChatGPT provider, with no credential writes. */
|
|
7
12
|
export class CodexPreuse {
|
|
8
13
|
constructor(private readonly fetcher: typeof fetch = globalThis.fetch) {}
|
|
@@ -11,9 +16,6 @@ export class CodexPreuse {
|
|
|
11
16
|
validatePreuseOptions(optionsArg);
|
|
12
17
|
if (identityArg.kind !== 'chatgpt') throw new PreuseError('Codex preuse requires a ChatGPT subscription login; API-key logins have no subscription reset window.');
|
|
13
18
|
if (!identityArg.accountId) throw new PreuseError('This login has no account ID for preuse.');
|
|
14
|
-
const signal = optionsArg.signal
|
|
15
|
-
? AbortSignal.any([optionsArg.signal, AbortSignal.timeout(180_000)])
|
|
16
|
-
: AbortSignal.timeout(180_000);
|
|
17
19
|
let credentials: plugins.flexAuth.IOpenAiChatGptAuthCredentials;
|
|
18
20
|
let oauth: plugins.flexAccounts.IOpenAiChatGptOAuthCredential;
|
|
19
21
|
try {
|
|
@@ -24,12 +26,37 @@ export class CodexPreuse {
|
|
|
24
26
|
} catch {
|
|
25
27
|
throw new PreuseError('No complete, matching Codex login is available for preuse. Log in again with Codex and save the account.');
|
|
26
28
|
}
|
|
29
|
+
return this.runCredential(oauth, credentials, optionsArg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Authority-owned access grants contain no refresh token and are never serialized by this helper. */
|
|
33
|
+
public async runAccess(credentialArg: plugins.flexAccounts.IOpenAiChatGptAccessCredential,
|
|
34
|
+
optionsArg: IHarnessPreuseOptions, lifecycleArg: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult> {
|
|
35
|
+
validatePreuseOptions(optionsArg);
|
|
36
|
+
if (!credentialArg || credentialArg.kind !== 'chatgptAccess' || credentialArg.providerId !== 'openai'
|
|
37
|
+
|| typeof credentialArg.accessToken !== 'string' || !credentialArg.accessToken
|
|
38
|
+
|| typeof credentialArg.accountId !== 'string' || !credentialArg.accountId
|
|
39
|
+
|| typeof credentialArg.isFedrampAccount !== 'boolean') {
|
|
40
|
+
throw new PreuseError('No complete, matching Codex login is available for preuse. Log in again with Codex and save the account.');
|
|
41
|
+
}
|
|
42
|
+
return this.runCredential(credentialArg, {
|
|
43
|
+
accessToken: credentialArg.accessToken, accountId: credentialArg.accountId,
|
|
44
|
+
isFedrampAccount: credentialArg.isFedrampAccount,
|
|
45
|
+
}, optionsArg, lifecycleArg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async runCredential(readCredentialArg: plugins.flexAccounts.TOpenAiChatGptReadCredential,
|
|
49
|
+
credentialsArg: plugins.flexAuth.IOpenAiChatGptAuthCredentials, optionsArg: IHarnessPreuseOptions,
|
|
50
|
+
lifecycleArg?: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult> {
|
|
51
|
+
const signal = optionsArg.signal
|
|
52
|
+
? AbortSignal.any([optionsArg.signal, AbortSignal.timeout(180_000)])
|
|
53
|
+
: AbortSignal.timeout(180_000);
|
|
27
54
|
let model = optionsArg.model;
|
|
28
55
|
let reasoningEffort: string | undefined;
|
|
29
56
|
if (!model) {
|
|
30
57
|
const adapter = new plugins.flexAccounts.OpenAiProviderAdapter({ fetch: this.fetcher });
|
|
31
58
|
try {
|
|
32
|
-
const catalog = await adapter.listModels(
|
|
59
|
+
const catalog = await adapter.listModels(readCredentialArg, { signal, timeoutMs: 10_000 });
|
|
33
60
|
if (!catalog.success) throw new PreuseError(catalog.errorCode === 'CREDENTIAL_REFRESH_REQUIRED'
|
|
34
61
|
? 'The saved login has expired. Log in again with Codex and save it before preuse.'
|
|
35
62
|
: 'Could not read the account model catalog. No preuse prompt was sent.');
|
|
@@ -45,9 +72,10 @@ export class CodexPreuse {
|
|
|
45
72
|
let requestStarted = false;
|
|
46
73
|
try {
|
|
47
74
|
signal.throwIfAborted();
|
|
48
|
-
const connection = plugins.flexAuth.createOpenAiChatGptModelConnection(
|
|
75
|
+
const connection = plugins.flexAuth.createOpenAiChatGptModelConnection(credentialsArg);
|
|
49
76
|
connection.settings.fetch = this.fetcher;
|
|
50
77
|
const languageModel = plugins.flexOpenAi.createOpenAiModelProvider().getModel({ provider: 'openai', model, connection });
|
|
78
|
+
await lifecycleArg?.beforeRequest(model);
|
|
51
79
|
requestStarted = true;
|
|
52
80
|
const stream = plugins.flexModels.streamText({
|
|
53
81
|
model: languageModel,
|
|
@@ -6,6 +6,8 @@ import { plainText } from './formatting.js';
|
|
|
6
6
|
export interface IFileAccount {
|
|
7
7
|
slotId: string;
|
|
8
8
|
identity: string;
|
|
9
|
+
/** Exact provider-native account identifier when the credential carries one. */
|
|
10
|
+
providerAccountId: string | null;
|
|
9
11
|
label: string;
|
|
10
12
|
credential: Record<string, unknown>;
|
|
11
13
|
}
|
|
@@ -80,14 +82,16 @@ export abstract class FileHarness implements IAuthHarness {
|
|
|
80
82
|
const snapshot = this.snapshot();
|
|
81
83
|
const saved = this.store.list();
|
|
82
84
|
const accounts: IHarnessState['accounts'] = saved.map(entry => {
|
|
83
|
-
this.saved(entry);
|
|
84
|
-
return { id: entry.id,
|
|
85
|
+
const account = this.saved(entry);
|
|
86
|
+
return { id: entry.id, providerAccountId: account.providerAccountId, slotId: entry.slotId, label: entry.label,
|
|
87
|
+
isActive: false, isStashed: true, savedAt: entry.savedAt, details: [`Provider: ${entry.slotId}`] };
|
|
85
88
|
});
|
|
86
89
|
for (const active of snapshot.accounts) {
|
|
87
90
|
const id = this.store.id(active.slotId, active.identity);
|
|
88
91
|
const existing = accounts.find(account => account.id === id);
|
|
89
92
|
if (existing) { existing.isActive = true; existing.label = active.label; }
|
|
90
|
-
else accounts.unshift({ id,
|
|
93
|
+
else accounts.unshift({ id, providerAccountId: active.providerAccountId, slotId: active.slotId, label: active.label,
|
|
94
|
+
isActive: true, isStashed: false, savedAt: null, details: [`Provider: ${active.slotId}`] });
|
|
91
95
|
}
|
|
92
96
|
for (const account of accounts) {
|
|
93
97
|
if (accounts.some(other => other.id !== account.id && other.label === account.label)) account.details.push(`Account ID: ${account.id}`);
|
package/ts/classes.limits.ts
CHANGED
|
@@ -52,6 +52,7 @@ const accountKeys = (harnessArg: IHarnessAccountList, accountArg: TListedAccount
|
|
|
52
52
|
harnessId: harnessArg.id,
|
|
53
53
|
provider: providerName(harnessArg, accountArg),
|
|
54
54
|
account: plainText(accountArg.label),
|
|
55
|
+
providerAccountId: accountArg.providerAccountId,
|
|
55
56
|
accountId: accountArg.id,
|
|
56
57
|
...(accountArg.slotId === undefined ? {} : { slotId: plainText(accountArg.slotId) }),
|
|
57
58
|
});
|
|
@@ -164,11 +165,13 @@ export const accountLimits = (listArg: IAccountList, harnessesArg: readonly THar
|
|
|
164
165
|
limitType: null, scope: null, windowSeconds: null, usedPercent: null, resetAt: null, resetsIn: UNAVAILABLE, severity: null, headline: false,
|
|
165
166
|
} as const;
|
|
166
167
|
if (problem !== null) {
|
|
167
|
-
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null,
|
|
168
|
+
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, providerAccountId: null,
|
|
169
|
+
accountId: null, isActive: false, accountType: null, billing: null, ...empty, unavailableReason: problem });
|
|
168
170
|
continue;
|
|
169
171
|
}
|
|
170
172
|
if (!harness.accounts.length) {
|
|
171
|
-
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null,
|
|
173
|
+
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, providerAccountId: null,
|
|
174
|
+
accountId: null, isActive: false, accountType: null, billing: null, ...empty,
|
|
172
175
|
unavailableReason: harness.saveUnavailableReason === null ? 'No accounts are known for this harness.' : plainText(harness.saveUnavailableReason) });
|
|
173
176
|
continue;
|
|
174
177
|
}
|
|
@@ -192,7 +195,7 @@ export const accountLimits = (listArg: IAccountList, harnessesArg: readonly THar
|
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
const limits = byProviderThenAccount(rows);
|
|
195
|
-
return { schemaVersion:
|
|
198
|
+
return { schemaVersion: 2, generatedAt: listArg.generatedAt, complete: listArg.complete, limits, notes: [...collectNotes(limits), ...renewalNotes] };
|
|
196
199
|
};
|
|
197
200
|
|
|
198
201
|
/**
|
|
@@ -214,14 +217,16 @@ export const activeAccounts = (listArg: IAccountList, harnessesArg: readonly THa
|
|
|
214
217
|
return index < 0 ? null : activeDrift(traits, drifts.splice(index, 1)[0]);
|
|
215
218
|
};
|
|
216
219
|
if (problem !== null) {
|
|
217
|
-
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null,
|
|
220
|
+
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, providerAccountId: null,
|
|
221
|
+
accountId: null, accountType: null, savedAt: null, savedAgo: UNAVAILABLE, source: 'none', unavailableReason: problem, drift: null });
|
|
218
222
|
continue;
|
|
219
223
|
}
|
|
220
224
|
const active = harness.accounts.filter(account => account.isActive);
|
|
221
225
|
if (!active.length) {
|
|
222
226
|
const stashed = harness.accounts.filter(account => account.isStashed);
|
|
223
227
|
rows.push({
|
|
224
|
-
harnessId: harness.id, provider: providerName(harness), account: null,
|
|
228
|
+
harnessId: harness.id, provider: providerName(harness), account: null, providerAccountId: null,
|
|
229
|
+
accountId: null, accountType: null,
|
|
225
230
|
savedAt: null, savedAgo: UNAVAILABLE, source: stashed.length ? 'stash only' : 'none',
|
|
226
231
|
unavailableReason: harness.saveUnavailableReason !== null ? plainText(harness.saveUnavailableReason)
|
|
227
232
|
: stashed.length ? `No login is active; ${stashed.length} saved account(s) can be activated with authswitch ${plainText(harness.id)} use.`
|
|
@@ -245,7 +250,7 @@ export const activeAccounts = (listArg: IAccountList, harnessesArg: readonly THa
|
|
|
245
250
|
const active = byProviderThenAccount(rows);
|
|
246
251
|
const driftNotes = active.filter(row => row.drift !== null)
|
|
247
252
|
.map(row => `${row.provider}${row.account === null ? '' : ` · ${row.account}`}: ${row.drift!.reason}`);
|
|
248
|
-
return { schemaVersion:
|
|
253
|
+
return { schemaVersion: 2, generatedAt: listArg.generatedAt, complete: listArg.complete, active, notes: [...collectNotes(active), ...driftNotes, ...unattached] };
|
|
249
254
|
};
|
|
250
255
|
|
|
251
256
|
/**
|
|
@@ -100,7 +100,7 @@ export class OpenCodeHarness extends FileHarness {
|
|
|
100
100
|
const fingerprint = credentialHash(secret);
|
|
101
101
|
const identity = info.accountId ? `oauth:${info.accountId}` : `${type}:${fingerprint}`;
|
|
102
102
|
const label = info.email ?? (info.accountId ? `openai:${info.accountId}` : `${slotId} ${type === 'api' ? 'API key' : type} ${fingerprint.slice(0, 12)}`);
|
|
103
|
-
return { slotId, identity, label, credential };
|
|
103
|
+
return { slotId, identity, providerAccountId: info.accountId ?? null, label, credential };
|
|
104
104
|
}
|
|
105
105
|
protected snapshot(): IFileHarnessSnapshot {
|
|
106
106
|
if (this.env.OPENCODE_AUTH_CONTENT !== undefined) return { accounts: [], unavailable: 'OPENCODE_AUTH_CONTENT overrides the credential file. Unset it before managing file logins.' };
|
package/ts/claudehttp.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
2
|
-
import { credentialRecord } from './classes.credentialstore.js';
|
|
3
2
|
import { retryAtFrom } from './ratelimit.js';
|
|
4
3
|
|
|
5
4
|
/** A fixed, local diagnostic. Nothing a service sent (a body, a header) and no request header ever becomes one. */
|
|
6
5
|
export class ClaudeRequestError extends Error {}
|
|
7
6
|
|
|
7
|
+
/** A rotating refresh POST is never assumed unsent after fetch has been invoked. */
|
|
8
|
+
export type TClaudeRotatingRequestOutcome = 'notSent' | 'outcomeUnknown';
|
|
9
|
+
export class ClaudeRotatingRequestError extends ClaudeRequestError {
|
|
10
|
+
constructor(message: string, public readonly requestOutcome: TClaudeRotatingRequestOutcome) {
|
|
11
|
+
super(message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
/** How every Claude lookup reports a login the service does not accept (HTTP 401, or a refresh grant it rejected). */
|
|
9
16
|
export const CLAUDE_LOGIN_REJECTED = 'Login expired or was rejected. Log in again with Claude Code and save it.';
|
|
10
17
|
|
|
@@ -80,7 +87,11 @@ const jsonBody = async (responseArg: Response, subjectArg: string): Promise<Reco
|
|
|
80
87
|
chunks.push(part.value);
|
|
81
88
|
}
|
|
82
89
|
} finally { reader.releaseLock(); }
|
|
83
|
-
try {
|
|
90
|
+
try {
|
|
91
|
+
const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
92
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
93
|
+
? parsed as Record<string, unknown> : null;
|
|
94
|
+
}
|
|
84
95
|
catch { return null; }
|
|
85
96
|
};
|
|
86
97
|
|
|
@@ -112,3 +123,21 @@ export const claudeRequest = async (fetcherArg: typeof fetch, requestArg: IClaud
|
|
|
112
123
|
throw new ClaudeRequestError(`The ${requestArg.subject} service could not be reached.`);
|
|
113
124
|
}
|
|
114
125
|
};
|
|
126
|
+
|
|
127
|
+
/** Use only after the authority has durably committed its pre-send attempt marker. */
|
|
128
|
+
export const claudeRotatingRequest = async (fetcherArg: typeof fetch,
|
|
129
|
+
requestArg: IClaudeRequest & { method: 'POST' }): Promise<IClaudeResponse> => {
|
|
130
|
+
if (requestArg.signal?.aborted) {
|
|
131
|
+
throw new ClaudeRotatingRequestError('The Claude sign-in request was cancelled before sending.', 'notSent');
|
|
132
|
+
}
|
|
133
|
+
let invoked = false;
|
|
134
|
+
const observedFetch: typeof fetch = (input, init) => {
|
|
135
|
+
invoked = true;
|
|
136
|
+
return fetcherArg(input, init);
|
|
137
|
+
};
|
|
138
|
+
try { return await claudeRequest(observedFetch, requestArg); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
const message = error instanceof ClaudeRequestError ? error.message : 'The Claude sign-in request failed.';
|
|
141
|
+
throw new ClaudeRotatingRequestError(message, invoked ? 'outcomeUnknown' : 'notSent');
|
|
142
|
+
}
|
|
143
|
+
};
|
package/ts/index.ts
CHANGED
|
@@ -24,13 +24,19 @@ export * from './classes.watch.js';
|
|
|
24
24
|
export * from './classes.watchlock.js';
|
|
25
25
|
export * from './watchpolicy.js';
|
|
26
26
|
export * from './classes.authorityclient.js';
|
|
27
|
+
export * from './classes.authoritycli.js';
|
|
27
28
|
export * from './classes.authoritydaemon.js';
|
|
28
29
|
export * from './classes.authorityservice.js';
|
|
29
30
|
|
|
30
31
|
import { AuthSwitchCli } from './classes.cli.js';
|
|
32
|
+
import { runAuthSwitchAuthorityCli } from './classes.authoritycli.js';
|
|
31
33
|
import { AuthSwitchAuthorityService, resolveAuthSwitchAuthorityPaths, runAuthSwitchAuthorityDaemon } from './classes.authorityservice.js';
|
|
32
34
|
|
|
33
35
|
export const runCli = async (argvArg: string[] = process.argv.slice(2)): Promise<void> => {
|
|
36
|
+
if (argvArg[0] === 'account' || (argvArg[0] === 'authority' && argvArg[1] === 'doctor')) {
|
|
37
|
+
process.exitCode = await runAuthSwitchAuthorityCli(argvArg);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
34
40
|
if (argvArg[0] === 'authority') {
|
|
35
41
|
const paths = resolveAuthSwitchAuthorityPaths();
|
|
36
42
|
if (argvArg[1] === 'daemon' && argvArg.length === 2) {
|