@open-vibe-lab/open-sub-auth 0.1.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/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/cli/claude-3WKImhqM.mjs +56 -0
- package/dist/cli/claude-3WKImhqM.mjs.map +1 -0
- package/dist/cli/index.d.mts +2 -0
- package/dist/cli/index.mjs +90 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/cli/login-_HdTW5J_.mjs +33 -0
- package/dist/cli/login-_HdTW5J_.mjs.map +1 -0
- package/dist/cli/logout-CZm-tSVH.mjs +23 -0
- package/dist/cli/logout-CZm-tSVH.mjs.map +1 -0
- package/dist/cli/manager-CKGbp7Yz.mjs +331 -0
- package/dist/cli/manager-CKGbp7Yz.mjs.map +1 -0
- package/dist/cli/oauth-pkce-Bi02-h23.mjs +282 -0
- package/dist/cli/oauth-pkce-Bi02-h23.mjs.map +1 -0
- package/dist/cli/openai-codex-DJi_Q6Zm.mjs +102 -0
- package/dist/cli/openai-codex-DJi_Q6Zm.mjs.map +1 -0
- package/dist/cli/registry-Cp-_Ipc6.mjs +96 -0
- package/dist/cli/registry-Cp-_Ipc6.mjs.map +1 -0
- package/dist/cli/status-C-vkcjVM.mjs +47 -0
- package/dist/cli/status-C-vkcjVM.mjs.map +1 -0
- package/dist/cli/token-DF_-h4Rb.mjs +23 -0
- package/dist/cli/token-DF_-h4Rb.mjs.map +1 -0
- package/dist/cli/ui-CdGEuLwh.mjs +34 -0
- package/dist/cli/ui-CdGEuLwh.mjs.map +1 -0
- package/dist/index.cjs +859 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +328 -0
- package/dist/index.d.mts +328 -0
- package/dist/index.mjs +827 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, pbkdf2Sync, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
//#region src/errors.ts
|
|
9
|
+
var OpenSubAuthError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "OpenSubAuthError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var AuthenticationError = class extends OpenSubAuthError {
|
|
16
|
+
constructor(message, provider) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.provider = provider;
|
|
19
|
+
this.name = "AuthenticationError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var TokenExpiredError = class extends OpenSubAuthError {
|
|
23
|
+
constructor(provider) {
|
|
24
|
+
super(`Access token for "${provider}" has expired and no refresh token is available. Please login again.`);
|
|
25
|
+
this.provider = provider;
|
|
26
|
+
this.name = "TokenExpiredError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var TokenRefreshError = class extends OpenSubAuthError {
|
|
30
|
+
constructor(provider, cause_) {
|
|
31
|
+
super(`Failed to refresh token for "${provider}": ${cause_ instanceof Error ? cause_.message : String(cause_)}`);
|
|
32
|
+
this.provider = provider;
|
|
33
|
+
this.cause_ = cause_;
|
|
34
|
+
this.name = "TokenRefreshError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ProviderNotFoundError = class extends OpenSubAuthError {
|
|
38
|
+
constructor(providerName) {
|
|
39
|
+
super(`Provider "${providerName}" is not registered. Available providers can be listed with listProviders().`);
|
|
40
|
+
this.providerName = providerName;
|
|
41
|
+
this.name = "ProviderNotFoundError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var NoCredentialError = class extends OpenSubAuthError {
|
|
45
|
+
constructor(provider, accountId) {
|
|
46
|
+
const msg = accountId ? `No stored credential for "${provider}" account "${accountId}". Please login first.` : `No stored credential for "${provider}". Please login first.`;
|
|
47
|
+
super(msg);
|
|
48
|
+
this.provider = provider;
|
|
49
|
+
this.accountId = accountId;
|
|
50
|
+
this.name = "NoCredentialError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var OAuthCallbackError = class extends OpenSubAuthError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "OAuthCallbackError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var OAuthTimeoutError = class extends OpenSubAuthError {
|
|
60
|
+
constructor(timeoutMs) {
|
|
61
|
+
super(`OAuth login timed out after ${Math.round(timeoutMs / 1e3)} seconds. Please try again.`);
|
|
62
|
+
this.timeoutMs = timeoutMs;
|
|
63
|
+
this.name = "OAuthTimeoutError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var StateMismatchError = class extends OpenSubAuthError {
|
|
67
|
+
constructor() {
|
|
68
|
+
super("OAuth state parameter mismatch — possible CSRF attack. Please try again.");
|
|
69
|
+
this.name = "StateMismatchError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/providers/registry.ts
|
|
74
|
+
const providers = /* @__PURE__ */ new Map();
|
|
75
|
+
/** Register a provider factory */
|
|
76
|
+
function registerProvider(name, factory) {
|
|
77
|
+
providers.set(name, factory);
|
|
78
|
+
}
|
|
79
|
+
/** Get a provider instance by name */
|
|
80
|
+
function getProvider(name) {
|
|
81
|
+
const factory = providers.get(name);
|
|
82
|
+
if (!factory) throw new ProviderNotFoundError(name);
|
|
83
|
+
return factory();
|
|
84
|
+
}
|
|
85
|
+
/** List all registered provider names */
|
|
86
|
+
function listProviders() {
|
|
87
|
+
return Array.from(providers.keys());
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/token/manager.ts
|
|
91
|
+
/** Buffer time before expiry to trigger refresh (5 minutes) */
|
|
92
|
+
const REFRESH_BUFFER_MS = 300 * 1e3;
|
|
93
|
+
/** Central token lifecycle manager */
|
|
94
|
+
var TokenManager = class {
|
|
95
|
+
store;
|
|
96
|
+
/** Per-provider+account mutex to prevent concurrent refreshes */
|
|
97
|
+
refreshLocks = /* @__PURE__ */ new Map();
|
|
98
|
+
constructor(store) {
|
|
99
|
+
this.store = store;
|
|
100
|
+
}
|
|
101
|
+
/** Run the interactive login flow for a provider, store the tokens */
|
|
102
|
+
async login(providerName, options) {
|
|
103
|
+
const provider = getProvider(providerName);
|
|
104
|
+
const tokenSet = await provider.login(options);
|
|
105
|
+
const accountId = provider.getAccountId(tokenSet);
|
|
106
|
+
const accountLabel = provider.getAccountLabel?.(tokenSet);
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const credential = {
|
|
109
|
+
tokenSet,
|
|
110
|
+
metadata: {
|
|
111
|
+
provider: providerName,
|
|
112
|
+
accountId,
|
|
113
|
+
accountLabel,
|
|
114
|
+
createdAt: now,
|
|
115
|
+
lastRefreshedAt: now
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
await this.store.set(providerName, accountId, credential);
|
|
119
|
+
return credential;
|
|
120
|
+
}
|
|
121
|
+
/** Get a valid access token, refreshing if needed */
|
|
122
|
+
async getToken(providerName, accountId) {
|
|
123
|
+
const credential = await this.resolveCredential(providerName, accountId);
|
|
124
|
+
const { tokenSet } = credential;
|
|
125
|
+
if (this.isTokenValid(tokenSet.expiresAt)) return tokenSet.accessToken;
|
|
126
|
+
if (!tokenSet.refreshToken) throw new TokenExpiredError(providerName);
|
|
127
|
+
await this.refreshWithLock(providerName, credential);
|
|
128
|
+
const refreshed = await this.store.get(providerName, credential.metadata.accountId);
|
|
129
|
+
if (!refreshed) throw new TokenExpiredError(providerName);
|
|
130
|
+
return refreshed.tokenSet.accessToken;
|
|
131
|
+
}
|
|
132
|
+
/** Get authenticated headers for API calls */
|
|
133
|
+
async getAuthHeaders(providerName, accountId) {
|
|
134
|
+
const token = await this.getToken(providerName, accountId);
|
|
135
|
+
return getProvider(providerName).getAuthHeaders(token);
|
|
136
|
+
}
|
|
137
|
+
/** Remove stored tokens for a provider */
|
|
138
|
+
async logout(providerName, accountId) {
|
|
139
|
+
if (accountId) await this.store.delete(providerName, accountId);
|
|
140
|
+
else {
|
|
141
|
+
const credentials = await this.store.list(providerName);
|
|
142
|
+
for (const cred of credentials) await this.store.delete(providerName, cred.metadata.accountId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Get status of all stored credentials */
|
|
146
|
+
async status() {
|
|
147
|
+
return (await this.store.list()).map((cred) => {
|
|
148
|
+
let displayName = cred.metadata.provider;
|
|
149
|
+
try {
|
|
150
|
+
displayName = getProvider(cred.metadata.provider).config.displayName;
|
|
151
|
+
} catch {}
|
|
152
|
+
return {
|
|
153
|
+
provider: cred.metadata.provider,
|
|
154
|
+
displayName,
|
|
155
|
+
accountId: cred.metadata.accountId,
|
|
156
|
+
accountLabel: cred.metadata.accountLabel,
|
|
157
|
+
isExpired: !this.isTokenValid(cred.tokenSet.expiresAt),
|
|
158
|
+
expiresAt: cred.tokenSet.expiresAt,
|
|
159
|
+
hasRefreshToken: cred.tokenSet.refreshToken !== null
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
isTokenValid(expiresAt) {
|
|
164
|
+
return Date.now() + REFRESH_BUFFER_MS < expiresAt;
|
|
165
|
+
}
|
|
166
|
+
async resolveCredential(providerName, accountId) {
|
|
167
|
+
if (accountId) {
|
|
168
|
+
const credential = await this.store.get(providerName, accountId);
|
|
169
|
+
if (!credential) throw new NoCredentialError(providerName, accountId);
|
|
170
|
+
return credential;
|
|
171
|
+
}
|
|
172
|
+
const credentials = await this.store.list(providerName);
|
|
173
|
+
if (credentials.length === 0) throw new NoCredentialError(providerName);
|
|
174
|
+
return credentials.sort((a, b) => b.metadata.lastRefreshedAt - a.metadata.lastRefreshedAt)[0];
|
|
175
|
+
}
|
|
176
|
+
async refreshWithLock(providerName, credential) {
|
|
177
|
+
const lockKey = `${providerName}::${credential.metadata.accountId}`;
|
|
178
|
+
const existingLock = this.refreshLocks.get(lockKey);
|
|
179
|
+
if (existingLock) {
|
|
180
|
+
await existingLock;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const refreshPromise = this.doRefresh(providerName, credential);
|
|
184
|
+
this.refreshLocks.set(lockKey, refreshPromise);
|
|
185
|
+
try {
|
|
186
|
+
await refreshPromise;
|
|
187
|
+
} finally {
|
|
188
|
+
this.refreshLocks.delete(lockKey);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async doRefresh(providerName, credential) {
|
|
192
|
+
const provider = getProvider(providerName);
|
|
193
|
+
const { refreshToken } = credential.tokenSet;
|
|
194
|
+
if (!refreshToken) throw new TokenExpiredError(providerName);
|
|
195
|
+
let newTokenSet;
|
|
196
|
+
try {
|
|
197
|
+
newTokenSet = await provider.refresh(refreshToken);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new TokenRefreshError(providerName, err);
|
|
200
|
+
}
|
|
201
|
+
if (!newTokenSet.refreshToken && refreshToken) newTokenSet.refreshToken = refreshToken;
|
|
202
|
+
const updatedCredential = {
|
|
203
|
+
tokenSet: newTokenSet,
|
|
204
|
+
metadata: {
|
|
205
|
+
...credential.metadata,
|
|
206
|
+
lastRefreshedAt: Date.now()
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
await this.store.set(providerName, credential.metadata.accountId, updatedCredential);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/storage/file-store.ts
|
|
214
|
+
const ALGORITHM = "aes-256-gcm";
|
|
215
|
+
const KEY_LENGTH = 32;
|
|
216
|
+
const IV_LENGTH = 12;
|
|
217
|
+
const SALT_LENGTH = 16;
|
|
218
|
+
const PBKDF2_ITERATIONS = 1e5;
|
|
219
|
+
const AUTH_TAG_LENGTH = 16;
|
|
220
|
+
/** Token store backed by an encrypted JSON file */
|
|
221
|
+
var FileStore = class {
|
|
222
|
+
filePath;
|
|
223
|
+
constructor(filePath) {
|
|
224
|
+
this.filePath = filePath ?? join(process.env.OPEN_SUB_AUTH_HOME ?? join(homedir$1(), ".open-sub-auth"), "credentials.json");
|
|
225
|
+
}
|
|
226
|
+
async get(provider, accountId) {
|
|
227
|
+
const data = this.readFile();
|
|
228
|
+
const key = `${provider}::${accountId}`;
|
|
229
|
+
const entry = data.credentials[key];
|
|
230
|
+
if (!entry) return null;
|
|
231
|
+
try {
|
|
232
|
+
const decrypted = this.decrypt(entry);
|
|
233
|
+
return JSON.parse(decrypted);
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async set(provider, accountId, credential) {
|
|
239
|
+
const data = this.readFile();
|
|
240
|
+
const key = `${provider}::${accountId}`;
|
|
241
|
+
data.credentials[key] = this.encrypt(JSON.stringify(credential));
|
|
242
|
+
this.writeFile(data);
|
|
243
|
+
}
|
|
244
|
+
async delete(provider, accountId) {
|
|
245
|
+
const data = this.readFile();
|
|
246
|
+
const key = `${provider}::${accountId}`;
|
|
247
|
+
delete data.credentials[key];
|
|
248
|
+
this.writeFile(data);
|
|
249
|
+
}
|
|
250
|
+
async list(provider) {
|
|
251
|
+
const data = this.readFile();
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const [key, entry] of Object.entries(data.credentials)) {
|
|
254
|
+
const [keyProvider] = key.split("::");
|
|
255
|
+
if (provider && keyProvider !== provider) continue;
|
|
256
|
+
try {
|
|
257
|
+
const decrypted = this.decrypt(entry);
|
|
258
|
+
results.push(JSON.parse(decrypted));
|
|
259
|
+
} catch {}
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
deriveKey(salt) {
|
|
264
|
+
return pbkdf2Sync(`${hostname()}:${userInfo().username}`, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
265
|
+
}
|
|
266
|
+
encrypt(plaintext) {
|
|
267
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
268
|
+
const key = this.deriveKey(salt);
|
|
269
|
+
const iv = randomBytes(IV_LENGTH);
|
|
270
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
271
|
+
return {
|
|
272
|
+
data: Buffer.concat([
|
|
273
|
+
cipher.update(plaintext, "utf8"),
|
|
274
|
+
cipher.final(),
|
|
275
|
+
cipher.getAuthTag()
|
|
276
|
+
]).toString("base64"),
|
|
277
|
+
iv: iv.toString("base64"),
|
|
278
|
+
salt: salt.toString("base64")
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
decrypt(entry) {
|
|
282
|
+
const salt = Buffer.from(entry.salt, "base64");
|
|
283
|
+
const key = this.deriveKey(salt);
|
|
284
|
+
const iv = Buffer.from(entry.iv, "base64");
|
|
285
|
+
const raw = Buffer.from(entry.data, "base64");
|
|
286
|
+
const authTag = raw.subarray(raw.length - AUTH_TAG_LENGTH);
|
|
287
|
+
const ciphertext = raw.subarray(0, raw.length - AUTH_TAG_LENGTH);
|
|
288
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
289
|
+
decipher.setAuthTag(authTag);
|
|
290
|
+
return decipher.update(ciphertext) + decipher.final("utf8");
|
|
291
|
+
}
|
|
292
|
+
readFile() {
|
|
293
|
+
try {
|
|
294
|
+
const content = readFileSync(this.filePath, "utf8");
|
|
295
|
+
return JSON.parse(content);
|
|
296
|
+
} catch {
|
|
297
|
+
return {
|
|
298
|
+
version: 1,
|
|
299
|
+
credentials: {}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
writeFile(data) {
|
|
304
|
+
mkdirSync(dirname(this.filePath), {
|
|
305
|
+
recursive: true,
|
|
306
|
+
mode: 448
|
|
307
|
+
});
|
|
308
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
function homedir$1() {
|
|
312
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/storage/keychain-store.ts
|
|
316
|
+
const SERVICE_NAME = "open-sub-auth";
|
|
317
|
+
const INDEX_ACCOUNT = "__index__";
|
|
318
|
+
/** Token store backed by the OS keychain via cross-keychain */
|
|
319
|
+
var KeychainStore = class {
|
|
320
|
+
keychain = null;
|
|
321
|
+
async getKeychain() {
|
|
322
|
+
if (!this.keychain) this.keychain = await import("cross-keychain");
|
|
323
|
+
return this.keychain;
|
|
324
|
+
}
|
|
325
|
+
makeKey(provider, accountId) {
|
|
326
|
+
return `${provider}__${accountId}`;
|
|
327
|
+
}
|
|
328
|
+
async get(provider, accountId) {
|
|
329
|
+
const kc = await this.getKeychain();
|
|
330
|
+
try {
|
|
331
|
+
const value = await kc.getPassword(SERVICE_NAME, this.makeKey(provider, accountId));
|
|
332
|
+
if (!value) return null;
|
|
333
|
+
return JSON.parse(value);
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async set(provider, accountId, credential) {
|
|
339
|
+
const kc = await this.getKeychain();
|
|
340
|
+
const key = this.makeKey(provider, accountId);
|
|
341
|
+
const value = JSON.stringify(credential);
|
|
342
|
+
try {
|
|
343
|
+
await kc.deletePassword(SERVICE_NAME, key);
|
|
344
|
+
} catch {}
|
|
345
|
+
await kc.setPassword(SERVICE_NAME, key, value);
|
|
346
|
+
await this.addToIndex(key);
|
|
347
|
+
}
|
|
348
|
+
async delete(provider, accountId) {
|
|
349
|
+
const kc = await this.getKeychain();
|
|
350
|
+
const key = this.makeKey(provider, accountId);
|
|
351
|
+
try {
|
|
352
|
+
await kc.deletePassword(SERVICE_NAME, key);
|
|
353
|
+
} catch {}
|
|
354
|
+
await this.removeFromIndex(key);
|
|
355
|
+
}
|
|
356
|
+
async list(provider) {
|
|
357
|
+
const index = await this.getIndex();
|
|
358
|
+
const results = [];
|
|
359
|
+
for (const key of index) {
|
|
360
|
+
const [keyProvider, keyAccountId] = key.split("__");
|
|
361
|
+
if (provider && keyProvider !== provider) continue;
|
|
362
|
+
if (!keyProvider || !keyAccountId) continue;
|
|
363
|
+
const credential = await this.get(keyProvider, keyAccountId);
|
|
364
|
+
if (credential) results.push(credential);
|
|
365
|
+
}
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
async getIndex() {
|
|
369
|
+
const kc = await this.getKeychain();
|
|
370
|
+
try {
|
|
371
|
+
const value = await kc.getPassword(SERVICE_NAME, INDEX_ACCOUNT);
|
|
372
|
+
if (!value) return [];
|
|
373
|
+
return JSON.parse(value);
|
|
374
|
+
} catch {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async setIndex(index) {
|
|
379
|
+
const kc = await this.getKeychain();
|
|
380
|
+
try {
|
|
381
|
+
await kc.deletePassword(SERVICE_NAME, INDEX_ACCOUNT);
|
|
382
|
+
} catch {}
|
|
383
|
+
await kc.setPassword(SERVICE_NAME, INDEX_ACCOUNT, JSON.stringify(index));
|
|
384
|
+
}
|
|
385
|
+
async addToIndex(key) {
|
|
386
|
+
const index = await this.getIndex();
|
|
387
|
+
if (!index.includes(key)) {
|
|
388
|
+
index.push(key);
|
|
389
|
+
await this.setIndex(index);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async removeFromIndex(key) {
|
|
393
|
+
const filtered = (await this.getIndex()).filter((k) => k !== key);
|
|
394
|
+
await this.setIndex(filtered);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/storage/store.ts
|
|
399
|
+
/**
|
|
400
|
+
* Create a token store, preferring the OS keychain with encrypted file fallback.
|
|
401
|
+
* @param preferKeychain - If true (default), try keychain first
|
|
402
|
+
*/
|
|
403
|
+
async function createTokenStore(preferKeychain = true) {
|
|
404
|
+
if (preferKeychain) try {
|
|
405
|
+
const store = new KeychainStore();
|
|
406
|
+
await store.get("__probe__", "__probe__");
|
|
407
|
+
return store;
|
|
408
|
+
} catch {}
|
|
409
|
+
return new FileStore();
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/core/browser.ts
|
|
413
|
+
/** Open a URL in the user's default browser. Does not throw on failure. */
|
|
414
|
+
function openBrowser(url) {
|
|
415
|
+
const os = platform();
|
|
416
|
+
try {
|
|
417
|
+
if (os === "darwin") execFile("open", [url]);
|
|
418
|
+
else if (os === "win32") execFile("cmd", [
|
|
419
|
+
"/c",
|
|
420
|
+
"start",
|
|
421
|
+
"",
|
|
422
|
+
url
|
|
423
|
+
]);
|
|
424
|
+
else execFile("xdg-open", [url]);
|
|
425
|
+
} catch {}
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/core/callback-server.ts
|
|
429
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
430
|
+
<html><head><meta charset="utf-8"><title>Authorization Successful</title>
|
|
431
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f8f9fa}
|
|
432
|
+
.card{text-align:center;padding:2rem;border-radius:12px;background:white;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
|
|
433
|
+
h1{color:#22c55e;font-size:1.5rem}p{color:#666}</style></head>
|
|
434
|
+
<body><div class="card"><h1>Authorization Successful</h1><p>You can close this window and return to the terminal.</p></div></body></html>`;
|
|
435
|
+
const ERROR_HTML = `<!DOCTYPE html>
|
|
436
|
+
<html><head><meta charset="utf-8"><title>Authorization Failed</title>
|
|
437
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f8f9fa}
|
|
438
|
+
.card{text-align:center;padding:2rem;border-radius:12px;background:white;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
|
|
439
|
+
h1{color:#ef4444;font-size:1.5rem}p{color:#666}</style></head>
|
|
440
|
+
<body><div class="card"><h1>Authorization Failed</h1><p>Something went wrong. Please try again.</p></div></body></html>`;
|
|
441
|
+
/** Start a local HTTP server to receive the OAuth callback redirect */
|
|
442
|
+
function startCallbackServer(options) {
|
|
443
|
+
const { expectedState, timeout = 12e4, path = "/callback" } = options;
|
|
444
|
+
const port = options.port ?? 0;
|
|
445
|
+
return new Promise((resolveSetup, _rejectSetup) => {
|
|
446
|
+
let server;
|
|
447
|
+
let timeoutId;
|
|
448
|
+
const resultPromise = new Promise((resolveResult, rejectResult) => {
|
|
449
|
+
server = createServer((req, res) => {
|
|
450
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
451
|
+
if (url.pathname !== path) {
|
|
452
|
+
res.writeHead(404);
|
|
453
|
+
res.end("Not found");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const code = url.searchParams.get("code");
|
|
457
|
+
const state = url.searchParams.get("state");
|
|
458
|
+
const error = url.searchParams.get("error");
|
|
459
|
+
if (error) {
|
|
460
|
+
const errorDesc = url.searchParams.get("error_description") ?? error;
|
|
461
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
462
|
+
res.end(ERROR_HTML);
|
|
463
|
+
cleanup();
|
|
464
|
+
rejectResult(new OAuthCallbackError(`OAuth error: ${errorDesc}`));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!code) {
|
|
468
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
469
|
+
res.end(ERROR_HTML);
|
|
470
|
+
cleanup();
|
|
471
|
+
rejectResult(new OAuthCallbackError("No authorization code in callback"));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (state && state !== expectedState) {
|
|
475
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
476
|
+
res.end(ERROR_HTML);
|
|
477
|
+
cleanup();
|
|
478
|
+
rejectResult(new OAuthCallbackError("State mismatch — possible CSRF attack. Please try again."));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
482
|
+
res.end(SUCCESS_HTML);
|
|
483
|
+
cleanup();
|
|
484
|
+
resolveResult({
|
|
485
|
+
code,
|
|
486
|
+
state: state ?? expectedState
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
const cleanup = () => {
|
|
490
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
491
|
+
server.close();
|
|
492
|
+
};
|
|
493
|
+
timeoutId = setTimeout(() => {
|
|
494
|
+
cleanup();
|
|
495
|
+
rejectResult(new OAuthTimeoutError(timeout));
|
|
496
|
+
}, timeout);
|
|
497
|
+
server.on("error", (err) => {
|
|
498
|
+
cleanup();
|
|
499
|
+
rejectResult(new OAuthCallbackError(`Callback server error: ${err.message}`));
|
|
500
|
+
});
|
|
501
|
+
server.listen(port, "127.0.0.1", () => {
|
|
502
|
+
const addr = server.address();
|
|
503
|
+
resolveSetup({
|
|
504
|
+
result: resultPromise,
|
|
505
|
+
port: typeof addr === "object" && addr ? addr.port : port,
|
|
506
|
+
close: cleanup
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/core/crypto.ts
|
|
514
|
+
/** Generate a cryptographically random PKCE code verifier (43-128 chars, base64url) */
|
|
515
|
+
function generateCodeVerifier() {
|
|
516
|
+
return randomBytes(32).toString("base64url");
|
|
517
|
+
}
|
|
518
|
+
/** Generate a PKCE code challenge from a verifier using S256 method */
|
|
519
|
+
function generateCodeChallenge(verifier) {
|
|
520
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
521
|
+
}
|
|
522
|
+
/** Generate both PKCE code verifier and challenge */
|
|
523
|
+
function generatePKCE() {
|
|
524
|
+
const codeVerifier = generateCodeVerifier();
|
|
525
|
+
return {
|
|
526
|
+
codeVerifier,
|
|
527
|
+
codeChallenge: generateCodeChallenge(codeVerifier)
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/** Generate a random state parameter for CSRF protection */
|
|
531
|
+
function generateState() {
|
|
532
|
+
return randomBytes(32).toString("base64url");
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/core/manual-code-input.ts
|
|
536
|
+
/**
|
|
537
|
+
* Parse a "code#state" string (Anthropic's manual paste format).
|
|
538
|
+
* Also supports a plain authorization code (state validated separately).
|
|
539
|
+
*/
|
|
540
|
+
function parseCodeAndState(input, expectedState) {
|
|
541
|
+
const trimmed = input.trim();
|
|
542
|
+
if (!trimmed) throw new OAuthCallbackError("Empty authorization code input");
|
|
543
|
+
const hashIndex = trimmed.indexOf("#");
|
|
544
|
+
if (hashIndex !== -1) {
|
|
545
|
+
const code = trimmed.slice(0, hashIndex);
|
|
546
|
+
const state = trimmed.slice(hashIndex + 1);
|
|
547
|
+
if (!code) throw new OAuthCallbackError("Empty authorization code in code#state input");
|
|
548
|
+
if (state !== expectedState) throw new StateMismatchError();
|
|
549
|
+
return {
|
|
550
|
+
code,
|
|
551
|
+
state
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
code: trimmed,
|
|
556
|
+
state: expectedState
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/** Prompt the user to paste the authorization code from their browser */
|
|
560
|
+
function promptForCode(expectedState) {
|
|
561
|
+
return new Promise((resolve, reject) => {
|
|
562
|
+
const rl = createInterface({
|
|
563
|
+
input: process.stdin,
|
|
564
|
+
output: process.stderr
|
|
565
|
+
});
|
|
566
|
+
rl.question("Paste the authorization code (or code#state) from your browser: ", (answer) => {
|
|
567
|
+
rl.close();
|
|
568
|
+
try {
|
|
569
|
+
resolve(parseCodeAndState(answer, expectedState));
|
|
570
|
+
} catch (err) {
|
|
571
|
+
reject(err);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/core/oauth-pkce.ts
|
|
578
|
+
/** Build the full OAuth authorization URL with PKCE and state params */
|
|
579
|
+
function buildAuthorizationUrl(config, codeChallenge, state, redirectUri) {
|
|
580
|
+
if (!config.authorizationEndpoint) throw new OAuthCallbackError(`Provider "${config.name}" has no authorization endpoint`);
|
|
581
|
+
const url = new URL(config.authorizationEndpoint);
|
|
582
|
+
url.searchParams.set("response_type", "code");
|
|
583
|
+
url.searchParams.set("client_id", config.clientId);
|
|
584
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
585
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
586
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
587
|
+
url.searchParams.set("state", state);
|
|
588
|
+
if (config.scopes?.length) url.searchParams.set("scope", config.scopes.join(" "));
|
|
589
|
+
return url.toString();
|
|
590
|
+
}
|
|
591
|
+
/** Exchange an authorization code for tokens */
|
|
592
|
+
async function exchangeCode(config, code, codeVerifier, redirectUri, state) {
|
|
593
|
+
const useJson = config.tokenBodyFormat === "json";
|
|
594
|
+
const params = {
|
|
595
|
+
grant_type: "authorization_code",
|
|
596
|
+
code,
|
|
597
|
+
code_verifier: codeVerifier,
|
|
598
|
+
client_id: config.clientId,
|
|
599
|
+
redirect_uri: redirectUri
|
|
600
|
+
};
|
|
601
|
+
if (state !== void 0) params.state = state;
|
|
602
|
+
const response = await fetch(config.tokenEndpoint, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "Content-Type": useJson ? "application/json" : "application/x-www-form-urlencoded" },
|
|
605
|
+
body: useJson ? JSON.stringify(params) : new URLSearchParams(params).toString()
|
|
606
|
+
});
|
|
607
|
+
if (!response.ok) {
|
|
608
|
+
const errorText = await response.text();
|
|
609
|
+
throw new OAuthCallbackError(`Token exchange failed (${response.status}): ${errorText}`);
|
|
610
|
+
}
|
|
611
|
+
return parseTokenResponse(await response.json());
|
|
612
|
+
}
|
|
613
|
+
/** Refresh an access token using a refresh token */
|
|
614
|
+
async function refreshAccessToken(config, refreshToken) {
|
|
615
|
+
const useJson = config.tokenBodyFormat === "json";
|
|
616
|
+
const params = {
|
|
617
|
+
grant_type: "refresh_token",
|
|
618
|
+
refresh_token: refreshToken,
|
|
619
|
+
client_id: config.clientId
|
|
620
|
+
};
|
|
621
|
+
const response = await fetch(config.tokenEndpoint, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: { "Content-Type": useJson ? "application/json" : "application/x-www-form-urlencoded" },
|
|
624
|
+
body: useJson ? JSON.stringify(params) : new URLSearchParams(params).toString()
|
|
625
|
+
});
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
const errorText = await response.text();
|
|
628
|
+
throw new OAuthCallbackError(`Token refresh failed (${response.status}): ${errorText}`);
|
|
629
|
+
}
|
|
630
|
+
return parseTokenResponse(await response.json());
|
|
631
|
+
}
|
|
632
|
+
/** Execute the full PKCE flow: generate params, open browser, wait for callback, exchange code */
|
|
633
|
+
async function executePKCEFlow(options) {
|
|
634
|
+
const { config, loginOptions } = options;
|
|
635
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
636
|
+
const state = config.stateIsVerifier ? codeVerifier : generateState();
|
|
637
|
+
const manual = loginOptions?.manual ?? false;
|
|
638
|
+
let authResult;
|
|
639
|
+
let redirectUri;
|
|
640
|
+
if (manual && options.manualRedirectUri) {
|
|
641
|
+
redirectUri = options.manualRedirectUri;
|
|
642
|
+
const authUrl = buildAuthorizationUrl(config, codeChallenge, state, redirectUri);
|
|
643
|
+
if (loginOptions?.onOpenBrowser) loginOptions.onOpenBrowser(authUrl);
|
|
644
|
+
else openBrowser(authUrl);
|
|
645
|
+
process.stderr.write(`\nOpen this URL in your browser if it didn't open automatically:\n${authUrl}\n\n`);
|
|
646
|
+
authResult = await promptForCode(state);
|
|
647
|
+
} else {
|
|
648
|
+
const server = await startCallbackServer({
|
|
649
|
+
port: loginOptions?.port ?? 0,
|
|
650
|
+
expectedState: state,
|
|
651
|
+
timeout: loginOptions?.timeout ?? 12e4
|
|
652
|
+
});
|
|
653
|
+
redirectUri = `http://127.0.0.1:${server.port}/callback`;
|
|
654
|
+
const authUrl = buildAuthorizationUrl(config, codeChallenge, state, redirectUri);
|
|
655
|
+
if (loginOptions?.onOpenBrowser) loginOptions.onOpenBrowser(authUrl);
|
|
656
|
+
else openBrowser(authUrl);
|
|
657
|
+
process.stderr.write(`\nOpen this URL in your browser if it didn't open automatically:\n${authUrl}\n\nWaiting for authorization...\n`);
|
|
658
|
+
try {
|
|
659
|
+
authResult = await server.result;
|
|
660
|
+
} catch (err) {
|
|
661
|
+
server.close();
|
|
662
|
+
throw err;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const exchangeState = config.tokenBodyFormat === "json" ? authResult.state : void 0;
|
|
666
|
+
return exchangeCode(config, authResult.code, codeVerifier, redirectUri, exchangeState);
|
|
667
|
+
}
|
|
668
|
+
function parseTokenResponse(data) {
|
|
669
|
+
const accessToken = data.access_token;
|
|
670
|
+
if (!accessToken) throw new OAuthCallbackError("No access_token in token response");
|
|
671
|
+
const expiresIn = data.expires_in ?? 3600;
|
|
672
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
673
|
+
return {
|
|
674
|
+
accessToken,
|
|
675
|
+
refreshToken: data.refresh_token ?? null,
|
|
676
|
+
expiresAt,
|
|
677
|
+
idToken: data.id_token,
|
|
678
|
+
tokenType: (data.token_type ?? "bearer").toLowerCase(),
|
|
679
|
+
scopes: data.scope ? data.scope.split(" ") : void 0,
|
|
680
|
+
raw: data
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/providers/claude.ts
|
|
685
|
+
const CLAUDE_CONFIG = {
|
|
686
|
+
name: "claude",
|
|
687
|
+
displayName: "Claude Pro/Max",
|
|
688
|
+
authorizationEndpoint: "https://claude.ai/oauth/authorize",
|
|
689
|
+
tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
|
|
690
|
+
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
691
|
+
scopes: [
|
|
692
|
+
"org:create_api_key",
|
|
693
|
+
"user:profile",
|
|
694
|
+
"user:inference"
|
|
695
|
+
],
|
|
696
|
+
grantType: "authorization_code",
|
|
697
|
+
tokenBodyFormat: "json",
|
|
698
|
+
stateIsVerifier: true
|
|
699
|
+
};
|
|
700
|
+
/** Redirect URI used in manual/headless mode (user copies code from browser) */
|
|
701
|
+
const MANUAL_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
|
|
702
|
+
var ClaudeProvider = class {
|
|
703
|
+
config = CLAUDE_CONFIG;
|
|
704
|
+
async login(options) {
|
|
705
|
+
return executePKCEFlow({
|
|
706
|
+
config: this.config,
|
|
707
|
+
loginOptions: {
|
|
708
|
+
...options,
|
|
709
|
+
manual: true
|
|
710
|
+
},
|
|
711
|
+
manualRedirectUri: MANUAL_REDIRECT_URI
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
async refresh(refreshToken) {
|
|
715
|
+
return refreshAccessToken(this.config, refreshToken);
|
|
716
|
+
}
|
|
717
|
+
getAuthHeaders(accessToken) {
|
|
718
|
+
return {
|
|
719
|
+
authorization: `Bearer ${accessToken}`,
|
|
720
|
+
"anthropic-version": "2023-06-01",
|
|
721
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
722
|
+
"content-type": "application/json"
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
getAccountId(tokenSet) {
|
|
726
|
+
const source = tokenSet.refreshToken ?? tokenSet.accessToken;
|
|
727
|
+
return createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
728
|
+
}
|
|
729
|
+
getAccountLabel(_tokenSet) {}
|
|
730
|
+
};
|
|
731
|
+
registerProvider("claude", () => new ClaudeProvider());
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/token/jwt.ts
|
|
734
|
+
/**
|
|
735
|
+
* Decode a JWT token's payload without verifying the signature.
|
|
736
|
+
* Used to extract user info from id_tokens (e.g., OpenAI).
|
|
737
|
+
*/
|
|
738
|
+
function decodeJWT(token) {
|
|
739
|
+
const parts = token.split(".");
|
|
740
|
+
if (parts.length !== 3) throw new Error("Invalid JWT format: expected 3 parts");
|
|
741
|
+
const payload = parts[1];
|
|
742
|
+
if (!payload) throw new Error("Invalid JWT: empty payload");
|
|
743
|
+
const padded = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
744
|
+
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
745
|
+
return JSON.parse(decoded);
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/providers/openai-codex.ts
|
|
749
|
+
const OPENAI_CODEX_CONFIG = {
|
|
750
|
+
name: "openai-codex",
|
|
751
|
+
displayName: "OpenAI ChatGPT Plus/Pro",
|
|
752
|
+
authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
|
|
753
|
+
tokenEndpoint: "https://auth.openai.com/oauth/token",
|
|
754
|
+
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
755
|
+
redirectUri: "http://localhost:1455/auth/callback",
|
|
756
|
+
scopes: [
|
|
757
|
+
"openid",
|
|
758
|
+
"profile",
|
|
759
|
+
"email",
|
|
760
|
+
"offline_access"
|
|
761
|
+
],
|
|
762
|
+
grantType: "authorization_code"
|
|
763
|
+
};
|
|
764
|
+
var OpenAICodexProvider = class {
|
|
765
|
+
config = OPENAI_CODEX_CONFIG;
|
|
766
|
+
async login(options) {
|
|
767
|
+
return executePKCEFlow({
|
|
768
|
+
config: this.config,
|
|
769
|
+
loginOptions: {
|
|
770
|
+
...options,
|
|
771
|
+
port: options?.port ?? 1455
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async refresh(refreshToken) {
|
|
776
|
+
return refreshAccessToken(this.config, refreshToken);
|
|
777
|
+
}
|
|
778
|
+
getAuthHeaders(accessToken) {
|
|
779
|
+
return {
|
|
780
|
+
authorization: `Bearer ${accessToken}`,
|
|
781
|
+
"content-type": "application/json"
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
getAccountId(tokenSet) {
|
|
785
|
+
if (tokenSet.idToken) try {
|
|
786
|
+
const claims = decodeJWT(tokenSet.idToken);
|
|
787
|
+
if (claims.sub) return claims.sub;
|
|
788
|
+
} catch {}
|
|
789
|
+
return createHash("sha256").update(tokenSet.accessToken).digest("hex").slice(0, 16);
|
|
790
|
+
}
|
|
791
|
+
getAccountLabel(tokenSet) {
|
|
792
|
+
if (tokenSet.idToken) try {
|
|
793
|
+
const claims = decodeJWT(tokenSet.idToken);
|
|
794
|
+
if (claims.email) return claims.email;
|
|
795
|
+
} catch {}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
/**
|
|
799
|
+
* Import tokens from an existing Codex CLI installation.
|
|
800
|
+
* Reads from ~/.codex/auth.json or $CODEX_HOME/auth.json.
|
|
801
|
+
*/
|
|
802
|
+
function importFromCodexCli() {
|
|
803
|
+
const paths = [process.env.CODEX_HOME ? join(process.env.CODEX_HOME, "auth.json") : null, join(homedir(), ".codex", "auth.json")].filter(Boolean);
|
|
804
|
+
for (const filePath of paths) {
|
|
805
|
+
if (!existsSync(filePath)) continue;
|
|
806
|
+
try {
|
|
807
|
+
const content = readFileSync(filePath, "utf8");
|
|
808
|
+
const data = JSON.parse(content);
|
|
809
|
+
if (!data.tokens?.access_token) continue;
|
|
810
|
+
return {
|
|
811
|
+
accessToken: data.tokens.access_token,
|
|
812
|
+
refreshToken: data.tokens.refresh_token ?? null,
|
|
813
|
+
expiresAt: Date.now() + 36e5,
|
|
814
|
+
idToken: data.tokens.id_token,
|
|
815
|
+
tokenType: "bearer"
|
|
816
|
+
};
|
|
817
|
+
} catch {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
registerProvider("openai-codex", () => new OpenAICodexProvider());
|
|
824
|
+
//#endregion
|
|
825
|
+
export { AuthenticationError, ClaudeProvider, FileStore, KeychainStore, NoCredentialError, OAuthCallbackError, OAuthTimeoutError, OpenAICodexProvider, OpenSubAuthError, ProviderNotFoundError, StateMismatchError, TokenExpiredError, TokenManager, TokenRefreshError, buildAuthorizationUrl, createTokenStore, decodeJWT, exchangeCode, executePKCEFlow, generateCodeChallenge, generateCodeVerifier, generatePKCE, generateState, getProvider, importFromCodexCli, listProviders, openBrowser, parseCodeAndState, promptForCode, refreshAccessToken, registerProvider, startCallbackServer };
|
|
826
|
+
|
|
827
|
+
//# sourceMappingURL=index.mjs.map
|