@modelprofile.com/authswitch 6.4.0 → 7.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 +302 -6
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/authority-runtime-contract.d.ts +15 -0
- package/dist_ts/authorityrehearsal.child.d.ts +1 -0
- package/dist_ts/authorityrehearsal.child.js +395 -0
- package/dist_ts/classes.accountlist.js +4 -3
- package/dist_ts/classes.authoritybackup.d.ts +134 -0
- package/dist_ts/classes.authoritybackup.js +769 -0
- package/dist_ts/classes.authoritybroker.d.ts +29 -6
- package/dist_ts/classes.authoritybroker.js +413 -122
- package/dist_ts/classes.authoritycli.d.ts +24 -0
- package/dist_ts/classes.authoritycli.js +675 -0
- package/dist_ts/classes.authorityclient.d.ts +34 -9
- package/dist_ts/classes.authorityclient.js +136 -38
- package/dist_ts/classes.authoritydaemon.d.ts +47 -0
- package/dist_ts/classes.authoritydaemon.js +705 -16
- package/dist_ts/classes.authoritydatabase.d.ts +182 -8
- package/dist_ts/classes.authoritydatabase.js +1188 -41
- package/dist_ts/classes.authoritymodels.d.ts +247 -1
- package/dist_ts/classes.authoritymodels.js +870 -6
- package/dist_ts/classes.authoritypreuse.d.ts +47 -0
- package/dist_ts/classes.authoritypreuse.js +278 -0
- package/dist_ts/classes.authorityregistry.d.ts +24 -0
- package/dist_ts/classes.authorityregistry.js +88 -0
- package/dist_ts/classes.authorityrehearsal.d.ts +127 -0
- package/dist_ts/classes.authorityrehearsal.js +632 -0
- 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 +532 -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 +137 -0
- package/dist_ts/classes.claudenative.js +731 -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 +91 -0
- package/dist_ts/classes.codexmanaged.js +404 -0
- 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 +2 -0
- package/dist_ts/index.js +8 -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 +7 -3
- package/dist_ts/plugins.js +9 -4
- package/dist_ts/ts_migration/0001_authority_meta.d.ts +3 -0
- package/dist_ts/ts_migration/0001_authority_meta.js +32 -0
- package/dist_ts/ts_migration/index.d.ts +3 -0
- package/dist_ts/ts_migration/index.js +6 -0
- package/package.json +12 -2
- package/readme.md +205 -13
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +264 -9
- package/ts/authority-runtime-contract.ts +21 -1
- package/ts/authorityrehearsal.child.ts +330 -0
- package/ts/classes.accountlist.ts +3 -2
- package/ts/classes.authoritybackup.ts +780 -0
- package/ts/classes.authoritybroker.ts +410 -120
- package/ts/classes.authoritycli.ts +719 -0
- package/ts/classes.authorityclient.ts +164 -38
- package/ts/classes.authoritydaemon.ts +614 -17
- package/ts/classes.authoritydatabase.ts +1229 -44
- package/ts/classes.authoritymodels.ts +579 -4
- package/ts/classes.authoritypreuse.ts +297 -0
- package/ts/classes.authorityregistry.ts +118 -0
- package/ts/classes.authorityrehearsal.ts +625 -0
- package/ts/classes.authorityusage.ts +527 -0
- package/ts/classes.claudeauthority.ts +541 -0
- package/ts/classes.claudecodeharness.ts +2 -1
- package/ts/classes.claudecodelocks.ts +70 -1
- package/ts/classes.claudenative.ts +816 -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 +402 -0
- 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 +7 -0
- package/ts/interfaces.harness.ts +6 -0
- package/ts/interfaces.list.ts +9 -3
- package/ts/plugins.ts +9 -3
- package/ts/ts_migration/0001_authority_meta.ts +33 -0
- package/ts/ts_migration/index.ts +7 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { commitinfo } from './00_commitinfo_data.js';
|
|
3
|
+
import type { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
4
|
+
|
|
5
|
+
export interface IAuthSwitchManagedCodexOptions {
|
|
6
|
+
/** Private runtime root; only the disposable control socket lives here. */
|
|
7
|
+
runtimeDirectory: string;
|
|
8
|
+
/** Caller-owned, durable native Codex state home, reserved to one account and scope by SmartData. */
|
|
9
|
+
codexHomeDirectory: string;
|
|
10
|
+
workspaceDirectory: string;
|
|
11
|
+
executable?: string;
|
|
12
|
+
/** The daemon is the sole owner of this binding and its capability. */
|
|
13
|
+
broker: AuthSwitchAuthorityBroker;
|
|
14
|
+
bindingId: string;
|
|
15
|
+
capability: string;
|
|
16
|
+
onNotification?: (notification: plugins.crossharness.ICodexAppServerNotification) => void;
|
|
17
|
+
onServerRequest?: (request: plugins.crossharness.ICodexAppServerRequest,
|
|
18
|
+
reply: { respond: (result: unknown) => boolean; reject: (code: number, message: string) => boolean }) => void;
|
|
19
|
+
onUnavailable?: () => void;
|
|
20
|
+
onPreparedRun: (socketPath: string) => Promise<void>;
|
|
21
|
+
onChildSpawned: (pid: number, startedAt: string) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IAuthSwitchCodexInteractiveCommand {
|
|
25
|
+
executable: string;
|
|
26
|
+
args: string[];
|
|
27
|
+
cwd: string;
|
|
28
|
+
env: NodeJS.ProcessEnv;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
32
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
const isAlive = (child: plugins.childProcess.ChildProcess): boolean => child.exitCode === null && child.signalCode === null;
|
|
34
|
+
const wait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
|
35
|
+
|
|
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.');
|
|
85
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
86
|
+
plugins.childProcess.execFile('ps', ['-ww', '-u', String(uid), '-o', 'pid=,lstart='],
|
|
87
|
+
{ timeout: 5_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C' } },
|
|
88
|
+
(error, stdout) => error ? reject(new Error('Managed Codex process inspection failed.')) : resolve(stdout));
|
|
89
|
+
});
|
|
90
|
+
const processes: Array<{ pid: number; startedAt: string; argv: Buffer[] }> = [];
|
|
91
|
+
for (const line of output.split('\n')) {
|
|
92
|
+
const parts = line.trim().split(/\s+/);
|
|
93
|
+
if (parts.length !== 6) continue;
|
|
94
|
+
const pid = Number(parts[0]);
|
|
95
|
+
const date = Date.parse(parts.slice(1, 6).join(' '));
|
|
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 });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
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 };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Daemon-owned Codex app-server. Only access tokens, never refresh grants, enter Codex. */
|
|
124
|
+
export class AuthSwitchManagedCodex {
|
|
125
|
+
private child?: plugins.childProcess.ChildProcess;
|
|
126
|
+
private childExited?: Promise<void>;
|
|
127
|
+
private client?: plugins.crossharness.CodexAppServerClient;
|
|
128
|
+
private runDirectory?: string;
|
|
129
|
+
private homeDirectory?: string;
|
|
130
|
+
private socketPath?: string;
|
|
131
|
+
private workspaceId?: string;
|
|
132
|
+
private grantedGeneration?: number;
|
|
133
|
+
private startPromise?: Promise<void>;
|
|
134
|
+
private closePromise?: Promise<void>;
|
|
135
|
+
private closed = false;
|
|
136
|
+
private ready = false;
|
|
137
|
+
|
|
138
|
+
constructor(private readonly options: IAuthSwitchManagedCodexOptions) {
|
|
139
|
+
if (!plugins.path.isAbsolute(options.runtimeDirectory) || !plugins.path.isAbsolute(options.workspaceDirectory)
|
|
140
|
+
|| !plugins.path.isAbsolute(options.codexHomeDirectory)) {
|
|
141
|
+
throw new Error('Managed Codex requires absolute runtime, home and workspace directories.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public get isReady(): boolean { return this.ready && this.client?.state === 'connected' && !this.closed; }
|
|
146
|
+
|
|
147
|
+
private environment(): NodeJS.ProcessEnv {
|
|
148
|
+
const picked: NodeJS.ProcessEnv = {};
|
|
149
|
+
for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'TERM', 'TMPDIR', 'XDG_RUNTIME_DIR']) {
|
|
150
|
+
if (process.env[key] !== undefined) picked[key] = process.env[key];
|
|
151
|
+
}
|
|
152
|
+
picked.CODEX_HOME = this.homeDirectory;
|
|
153
|
+
return picked;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async requireVersion(): Promise<void> {
|
|
157
|
+
const executable = this.options.executable ?? 'codex';
|
|
158
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
159
|
+
plugins.childProcess.execFile(executable, ['--version'], { timeout: 5_000, maxBuffer: 4096,
|
|
160
|
+
windowsHide: true, env: this.environment() }, (error, stdout) => {
|
|
161
|
+
if (error) reject(new Error('Managed Codex version probe failed.'));
|
|
162
|
+
else resolve(stdout.trim());
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
if (output !== 'codex-cli 0.155.1') throw new Error('Managed Codex requires verified codex-cli 0.155.1.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public start(): Promise<void> {
|
|
169
|
+
if (this.closed || this.startPromise) throw new Error('Managed Codex can start only once.');
|
|
170
|
+
this.startPromise = this.performStart();
|
|
171
|
+
return this.startPromise;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async performStart(): Promise<void> {
|
|
175
|
+
try {
|
|
176
|
+
const root = this.options.runtimeDirectory;
|
|
177
|
+
await plugins.fs.promises.mkdir(root, { recursive: true, mode: 0o700 });
|
|
178
|
+
const rootStat = await plugins.fs.promises.lstat(root);
|
|
179
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || rootStat.uid !== process.getuid?.()) {
|
|
180
|
+
throw new Error('Managed Codex runtime directory is not private.');
|
|
181
|
+
}
|
|
182
|
+
await plugins.fs.promises.chmod(root, 0o700);
|
|
183
|
+
this.runDirectory = await plugins.fs.promises.mkdtemp(plugins.path.join(root, 'codex-'));
|
|
184
|
+
await plugins.fs.promises.chmod(this.runDirectory, 0o700);
|
|
185
|
+
this.homeDirectory = this.options.codexHomeDirectory;
|
|
186
|
+
this.socketPath = plugins.path.join(this.runDirectory, 'app.sock');
|
|
187
|
+
if (Buffer.byteLength(this.socketPath, 'utf8') > 100) throw new Error('Managed Codex socket path is too long.');
|
|
188
|
+
const homeStat = await plugins.fs.promises.lstat(this.homeDirectory);
|
|
189
|
+
if (!homeStat.isDirectory() || homeStat.isSymbolicLink() || homeStat.uid !== process.getuid?.()
|
|
190
|
+
|| (homeStat.mode & 0o077) !== 0 || await plugins.fs.promises.realpath(this.homeDirectory) !== this.homeDirectory) {
|
|
191
|
+
throw new Error('Managed Codex home is not a private owned directory.');
|
|
192
|
+
}
|
|
193
|
+
try { await plugins.fs.promises.lstat(plugins.path.join(this.homeDirectory, 'auth.json'));
|
|
194
|
+
throw new Error('Managed Codex home contains a native credential file.');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (!isRecord(error) || error.code !== 'ENOENT') throw error;
|
|
197
|
+
}
|
|
198
|
+
await this.requireVersion();
|
|
199
|
+
if (this.closed) throw new Error('Managed Codex startup was cancelled.');
|
|
200
|
+
const initial = await this.options.broker.resolveAccess(this.options.bindingId,
|
|
201
|
+
this.options.capability, 60_000);
|
|
202
|
+
if (this.closed) throw new Error('Managed Codex startup was cancelled.');
|
|
203
|
+
this.workspaceId = initial.accountId;
|
|
204
|
+
this.grantedGeneration = initial.grantGeneration;
|
|
205
|
+
await this.options.onPreparedRun(this.socketPath);
|
|
206
|
+
if (this.closed) throw new Error('Managed Codex startup was cancelled.');
|
|
207
|
+
const child = plugins.childProcess.spawn(this.options.executable ?? 'codex', [
|
|
208
|
+
'app-server', '--listen', `unix://${this.socketPath}`, '--strict-config',
|
|
209
|
+
'-c', 'cli_auth_credentials_store="ephemeral"',
|
|
210
|
+
], { cwd: this.options.workspaceDirectory, env: this.environment(), shell: false,
|
|
211
|
+
detached: process.platform !== 'win32', stdio: 'ignore', windowsHide: true });
|
|
212
|
+
this.child = child;
|
|
213
|
+
this.childExited = new Promise(resolve => {
|
|
214
|
+
child.once('error', () => resolve());
|
|
215
|
+
child.once('exit', () => resolve());
|
|
216
|
+
});
|
|
217
|
+
if (!child.pid) throw new Error('Managed Codex app-server process has no PID.');
|
|
218
|
+
let observed: Awaited<ReturnType<typeof findManagedCodexProcess>> = null;
|
|
219
|
+
for (let attempt = 0; attempt < 20 && !observed; attempt++) {
|
|
220
|
+
observed = await findManagedCodexProcess(this.socketPath);
|
|
221
|
+
if (!observed) await wait(25);
|
|
222
|
+
}
|
|
223
|
+
if (!observed || observed.pid !== child.pid) throw new Error('Managed Codex app-server process identity could not be verified.');
|
|
224
|
+
await this.options.onChildSpawned(observed.pid, observed.startedAt);
|
|
225
|
+
const deadline = Date.now() + 10_000;
|
|
226
|
+
while (true) {
|
|
227
|
+
if (this.closed) throw new Error('Managed Codex startup was cancelled.');
|
|
228
|
+
if (!isAlive(child)) throw new Error('Managed Codex app-server exited before accepting connections.');
|
|
229
|
+
try {
|
|
230
|
+
if ((await plugins.fs.promises.lstat(this.socketPath)).isSocket()) break;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (!isRecord(error) || error.code !== 'ENOENT') throw error;
|
|
233
|
+
}
|
|
234
|
+
if (Date.now() >= deadline) throw new Error('Managed Codex app-server socket startup timed out.');
|
|
235
|
+
await wait(25);
|
|
236
|
+
}
|
|
237
|
+
const client = new plugins.crossharness.CodexAppServerClient({
|
|
238
|
+
transport: { type: 'unix', socketPath: this.socketPath },
|
|
239
|
+
clientInfo: { name: 'authswitch', title: 'Authswitch', version: commitinfo.version },
|
|
240
|
+
experimentalApi: true, handshakeTimeoutMs: 5_000, initializeTimeoutMs: 5_000,
|
|
241
|
+
onNotification: notification => this.options.onNotification?.(notification),
|
|
242
|
+
onServerRequest: request => { void this.handleServerRequest(request); },
|
|
243
|
+
onClose: () => {
|
|
244
|
+
const wasReady = this.ready;
|
|
245
|
+
this.ready = false;
|
|
246
|
+
if (wasReady && !this.closed) this.options.onUnavailable?.();
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
this.client = client;
|
|
250
|
+
await client.connect();
|
|
251
|
+
if (client.serverVersion !== '0.155.1') throw new Error('Managed Codex app-server version changed.');
|
|
252
|
+
const login: unknown = await client.request('account/login/start', {
|
|
253
|
+
type: 'chatgptAuthTokens', accessToken: initial.accessToken, chatgptAccountId: initial.accountId,
|
|
254
|
+
}, 5_000);
|
|
255
|
+
if (!isRecord(login) || login.type !== 'chatgptAuthTokens') {
|
|
256
|
+
throw new Error('Managed Codex external-token login was not acknowledged.');
|
|
257
|
+
}
|
|
258
|
+
if (this.closed) throw new Error('Managed Codex startup was cancelled.');
|
|
259
|
+
this.ready = true;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
try { await this.close(); } catch (cleanupError) {
|
|
262
|
+
throw new AggregateError([error, cleanupError], 'Managed Codex startup and cleanup failed.');
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async handleServerRequest(request: plugins.crossharness.ICodexAppServerRequest): Promise<void> {
|
|
269
|
+
const client = this.client;
|
|
270
|
+
if (!client || this.closed || !this.ready) return;
|
|
271
|
+
if (request.method !== 'account/chatgptAuthTokens/refresh') {
|
|
272
|
+
if (!this.options.onServerRequest) {
|
|
273
|
+
client.rejectRequest(request.id, -32601, 'Managed Codex request requires an owner handler.');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
this.options.onServerRequest(request, {
|
|
278
|
+
respond: result => client.respond(request.id, result),
|
|
279
|
+
reject: (code, message) => client.rejectRequest(request.id, code, message),
|
|
280
|
+
});
|
|
281
|
+
} catch {
|
|
282
|
+
client.rejectRequest(request.id, -32603, 'Managed Codex owner request failed.');
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const prior = this.grantedGeneration;
|
|
287
|
+
const workspaceId = this.workspaceId;
|
|
288
|
+
const params = request.params;
|
|
289
|
+
if (params.reason !== 'unauthorized' || !workspaceId || prior === undefined
|
|
290
|
+
|| (params.previousAccountId !== undefined && params.previousAccountId !== null
|
|
291
|
+
&& params.previousAccountId !== workspaceId)) {
|
|
292
|
+
client.rejectRequest(request.id, -32602, 'Managed Codex account identity is invalid.');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const refreshed = await this.options.broker.resolveAccess(this.options.bindingId,
|
|
297
|
+
this.options.capability, 60_000, prior);
|
|
298
|
+
if (this.closed || request.signal.aborted) return;
|
|
299
|
+
if (refreshed.accountId !== workspaceId || refreshed.grantGeneration <= prior) {
|
|
300
|
+
client.rejectRequest(request.id, -32603, 'Managed Codex account refresh did not advance.');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.grantedGeneration = refreshed.grantGeneration;
|
|
304
|
+
client.respond(request.id, { accessToken: refreshed.accessToken,
|
|
305
|
+
chatgptAccountId: refreshed.accountId, chatgptPlanType: null });
|
|
306
|
+
} catch {
|
|
307
|
+
if (!request.signal.aborted) client.rejectRequest(request.id, -32603, 'Managed Codex account needs reauthentication.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The interactive CLI attaches to the owned app-server; the daemon remains its auth control peer. */
|
|
312
|
+
public interactiveCommand(): IAuthSwitchCodexInteractiveCommand {
|
|
313
|
+
if (!this.isReady || !this.socketPath) throw new Error('Managed Codex is unavailable.');
|
|
314
|
+
return { executable: this.options.executable ?? 'codex', args: ['--remote', `unix://${this.socketPath}`],
|
|
315
|
+
cwd: this.options.workspaceDirectory, env: this.environment() };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Native app-server thread/turn API for noninteractive consumers; notifications go to onNotification. */
|
|
319
|
+
public async startThread(options: { approvalPolicy: 'never' | 'on-request'; model?: string;
|
|
320
|
+
signal?: AbortSignal }): Promise<string> {
|
|
321
|
+
if (!this.isReady || !this.client || !options || !['never', 'on-request'].includes(options.approvalPolicy)
|
|
322
|
+
|| (options.model !== undefined && (typeof options.model !== 'string' || options.model.length > 128))) {
|
|
323
|
+
throw new Error('Managed Codex thread request is unavailable or invalid.');
|
|
324
|
+
}
|
|
325
|
+
const started: unknown = await this.client.request('thread/start', {
|
|
326
|
+
cwd: this.options.workspaceDirectory, approvalPolicy: options.approvalPolicy,
|
|
327
|
+
...(options.model ? { model: options.model } : {}),
|
|
328
|
+
}, 30_000, options.signal);
|
|
329
|
+
if (!isRecord(started) || !isRecord(started.thread) || typeof started.thread.id !== 'string') {
|
|
330
|
+
throw new Error('Managed Codex returned an invalid thread.');
|
|
331
|
+
}
|
|
332
|
+
return started.thread.id;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public async resumeThread(threadId: string, options: { approvalPolicy: 'never' | 'on-request';
|
|
336
|
+
model?: string; signal?: AbortSignal }): Promise<void> {
|
|
337
|
+
if (!this.isReady || !this.client || typeof threadId !== 'string' || !threadId || threadId.length > 256
|
|
338
|
+
|| !options || !['never', 'on-request'].includes(options.approvalPolicy)
|
|
339
|
+
|| (options.model !== undefined && (typeof options.model !== 'string' || options.model.length > 128))) {
|
|
340
|
+
throw new Error('Managed Codex resume request is unavailable or invalid.');
|
|
341
|
+
}
|
|
342
|
+
const resumed: unknown = await this.client.request('thread/resume', { threadId,
|
|
343
|
+
cwd: this.options.workspaceDirectory, approvalPolicy: options.approvalPolicy,
|
|
344
|
+
...(options.model ? { model: options.model } : {}),
|
|
345
|
+
}, 30_000, options.signal);
|
|
346
|
+
if (!isRecord(resumed) || !isRecord(resumed.thread) || resumed.thread.id !== threadId) {
|
|
347
|
+
throw new Error('Managed Codex could not resume the requested thread from its assigned home.');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public async startTextTurn(text: string, options: { approvalPolicy: 'never' | 'on-request';
|
|
352
|
+
model?: string; threadId?: string; signal?: AbortSignal }): Promise<{ threadId: string; turnId: string }> {
|
|
353
|
+
if (!this.isReady || !this.client || typeof text !== 'string' || !text.trim() || text.length > 1_000_000
|
|
354
|
+
|| !options || !['never', 'on-request'].includes(options.approvalPolicy)
|
|
355
|
+
|| (options.model !== undefined && (typeof options.model !== 'string' || options.model.length > 128))
|
|
356
|
+
|| (options.threadId !== undefined && (typeof options.threadId !== 'string' || options.threadId.length > 256))) {
|
|
357
|
+
throw new Error('Managed Codex turn request is unavailable or invalid.');
|
|
358
|
+
}
|
|
359
|
+
let threadId = options.threadId;
|
|
360
|
+
if (threadId) {
|
|
361
|
+
await this.resumeThread(threadId, options);
|
|
362
|
+
} else {
|
|
363
|
+
threadId = await this.startThread(options);
|
|
364
|
+
}
|
|
365
|
+
const turn: unknown = await this.client.request('turn/start', { threadId,
|
|
366
|
+
input: [{ type: 'text', text, text_elements: [] }],
|
|
367
|
+
approvalPolicy: options.approvalPolicy,
|
|
368
|
+
...(options.model ? { model: options.model } : {}),
|
|
369
|
+
}, 30_000, options.signal);
|
|
370
|
+
if (!isRecord(turn) || !isRecord(turn.turn) || typeof turn.turn.id !== 'string') {
|
|
371
|
+
throw new Error('Managed Codex returned an invalid turn.');
|
|
372
|
+
}
|
|
373
|
+
return { threadId, turnId: turn.turn.id };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public close(): Promise<void> {
|
|
377
|
+
if (this.closePromise) return this.closePromise;
|
|
378
|
+
const wasReady = this.ready;
|
|
379
|
+
this.closed = true;
|
|
380
|
+
this.ready = false;
|
|
381
|
+
this.closePromise = this.performClose();
|
|
382
|
+
if (wasReady) this.options.onUnavailable?.();
|
|
383
|
+
return this.closePromise;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private async performClose(): Promise<void> {
|
|
387
|
+
this.client?.close('Managed Codex stopped.');
|
|
388
|
+
const child = this.child;
|
|
389
|
+
if (child && isAlive(child) && child.pid) {
|
|
390
|
+
try { process.kill(process.platform === 'win32' ? child.pid : -child.pid, 'SIGTERM'); }
|
|
391
|
+
catch (error) { if (!isRecord(error) || error.code !== 'ESRCH') throw error; }
|
|
392
|
+
await Promise.race([this.childExited, wait(3_000)]);
|
|
393
|
+
if (isAlive(child)) {
|
|
394
|
+
try { process.kill(process.platform === 'win32' ? child.pid : -child.pid, 'SIGKILL'); }
|
|
395
|
+
catch (error) { if (!isRecord(error) || error.code !== 'ESRCH') throw error; }
|
|
396
|
+
await Promise.race([this.childExited, wait(3_000)]);
|
|
397
|
+
}
|
|
398
|
+
if (isAlive(child)) throw new Error('Managed Codex process did not stop.');
|
|
399
|
+
}
|
|
400
|
+
if (this.runDirectory) await plugins.fs.promises.rm(this.runDirectory, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -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,12 +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';
|
|
28
|
+
export * from './classes.authoritydaemon.js';
|
|
27
29
|
export * from './classes.authorityservice.js';
|
|
28
30
|
|
|
29
31
|
import { AuthSwitchCli } from './classes.cli.js';
|
|
32
|
+
import { runAuthSwitchAuthorityCli } from './classes.authoritycli.js';
|
|
30
33
|
import { AuthSwitchAuthorityService, resolveAuthSwitchAuthorityPaths, runAuthSwitchAuthorityDaemon } from './classes.authorityservice.js';
|
|
31
34
|
|
|
32
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
|
+
}
|
|
33
40
|
if (argvArg[0] === 'authority') {
|
|
34
41
|
const paths = resolveAuthSwitchAuthorityPaths();
|
|
35
42
|
if (argvArg[1] === 'daemon' && argvArg.length === 2) {
|
package/ts/interfaces.harness.ts
CHANGED
|
@@ -73,7 +73,13 @@ export interface IHarnessCredentialDrift {
|
|
|
73
73
|
|
|
74
74
|
/** Account IDs are opaque and scoped to one harness. Credentials never cross this interface. */
|
|
75
75
|
export interface IHarnessAccount {
|
|
76
|
+
/** Opaque authswitch identifier used for harness operations. */
|
|
76
77
|
id: string;
|
|
78
|
+
/**
|
|
79
|
+
* Stable account identifier reported by the provider, scoped by this harness and `slotId`.
|
|
80
|
+
* Null means the credential carries no exact provider identity. Never infer it from `id` or `label`.
|
|
81
|
+
*/
|
|
82
|
+
providerAccountId: string | null;
|
|
77
83
|
label: string;
|
|
78
84
|
/** Accounts in different slots can be active together (for example OpenCode providers). */
|
|
79
85
|
slotId?: string;
|