@bivy/bivy 0.16.18-staging.2 → 0.16.18-staging.3
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.
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// not guesses. Owning these lets Bivy run the OAuth login + token refresh itself,
|
|
8
8
|
// so no credential operation depends on Pi.
|
|
9
9
|
//
|
|
10
|
-
// Covered (fully Bivy-owned): Anthropic (Claude Pro/Max), OpenAI Codex (ChatGPT
|
|
11
|
-
// xAI (Grok). GitHub Copilot (two-stage token + dynamic base URL entangled with
|
|
10
|
+
// Covered (fully Bivy-owned): Anthropic (Claude Pro/Max), OpenAI Codex (ChatGPT;
|
|
11
|
+
// device-code), xAI (Grok). GitHub Copilot (two-stage token + dynamic base URL entangled with
|
|
12
12
|
// Pi's request layer) and Radius (self-describing gateway) are intentionally not
|
|
13
13
|
// reimplemented here.
|
|
14
14
|
/** Anthropic uses a JSON token body; OpenAI/xAI use form-encoded. */
|
|
@@ -17,12 +17,16 @@ export const MODEL_OAUTH_PROVIDERS = {
|
|
|
17
17
|
id: "anthropic",
|
|
18
18
|
displayName: "Anthropic (Claude Pro/Max)",
|
|
19
19
|
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
20
|
-
|
|
20
|
+
// Claude Code's `setup-token` flow avoids localhost entirely: Anthropic hosts
|
|
21
|
+
// the callback page and shows a code the user can paste back into Bivy. The
|
|
22
|
+
// resulting long-lived token is inference-only, which is exactly what agent
|
|
23
|
+
// model requests need and works well from a remote PWA/headless node.
|
|
24
|
+
scopes: "user:inference",
|
|
21
25
|
flow: "auth_code",
|
|
22
26
|
tokenEncoding: "json",
|
|
23
|
-
authorizeUrl: "https://claude.
|
|
27
|
+
authorizeUrl: "https://claude.com/cai/oauth/authorize",
|
|
24
28
|
tokenUrl: "https://platform.claude.com/v1/oauth/token",
|
|
25
|
-
|
|
29
|
+
redirectUri: "https://platform.claude.com/oauth/code/callback",
|
|
26
30
|
authorizeParams: { code: "true" },
|
|
27
31
|
stateIsVerifier: true,
|
|
28
32
|
refreshSkewMs: 5 * 60 * 1000,
|
|
@@ -33,11 +37,13 @@ export const MODEL_OAUTH_PROVIDERS = {
|
|
|
33
37
|
displayName: "OpenAI (ChatGPT Plus/Pro)",
|
|
34
38
|
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
35
39
|
scopes: "openid profile email offline_access",
|
|
36
|
-
flow: "
|
|
40
|
+
flow: "openai_codex_device_code",
|
|
37
41
|
tokenEncoding: "form",
|
|
38
42
|
authorizeUrl: "https://auth.openai.com/oauth/authorize",
|
|
39
43
|
tokenUrl: "https://auth.openai.com/oauth/token",
|
|
40
44
|
callback: { port: 1455, path: "/auth/callback", redirectHost: "localhost" },
|
|
45
|
+
deviceAuthUrl: "https://auth.openai.com/api/accounts/deviceauth/usercode",
|
|
46
|
+
deviceTokenUrl: "https://auth.openai.com/api/accounts/deviceauth/token",
|
|
41
47
|
authorizeParams: {
|
|
42
48
|
id_token_add_organizations: "true",
|
|
43
49
|
codex_cli_simplified_flow: "true",
|
|
@@ -199,7 +199,7 @@ function startCallbackServer(host, port, pathName, expectedState, signal) {
|
|
|
199
199
|
async function loginAuthCode(provider, interaction) {
|
|
200
200
|
const { verifier, challenge } = createPkce();
|
|
201
201
|
const state = provider.stateIsVerifier ? verifier : randomBytes(16).toString("hex");
|
|
202
|
-
const redirectUri = `http://${provider.callback.redirectHost}:${provider.callback.port}${provider.callback.path}`;
|
|
202
|
+
const redirectUri = provider.redirectUri ?? `http://${provider.callback.redirectHost}:${provider.callback.port}${provider.callback.path}`;
|
|
203
203
|
const authorizeUrl = buildAuthorizeUrl(provider, { challenge, state, redirectUri });
|
|
204
204
|
interaction.notify({
|
|
205
205
|
type: "auth_url",
|
|
@@ -300,6 +300,88 @@ async function loginDeviceCode(provider, interaction) {
|
|
|
300
300
|
return tokensFrom(provider, payload);
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
+
async function postJsonObject(url, body, signal) {
|
|
304
|
+
const res = await fetch(url, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
307
|
+
body: JSON.stringify(body),
|
|
308
|
+
signal,
|
|
309
|
+
});
|
|
310
|
+
const text = await res.text();
|
|
311
|
+
let payload = {};
|
|
312
|
+
try {
|
|
313
|
+
payload = JSON.parse(text);
|
|
314
|
+
}
|
|
315
|
+
catch { /* non-JSON error body */ }
|
|
316
|
+
return { status: res.status, ok: res.ok, payload, text };
|
|
317
|
+
}
|
|
318
|
+
function nestedOAuthErrorCode(payload) {
|
|
319
|
+
const error = payload.error;
|
|
320
|
+
if (typeof error === "string")
|
|
321
|
+
return error;
|
|
322
|
+
if (error && typeof error === "object") {
|
|
323
|
+
const code = error.code;
|
|
324
|
+
return typeof code === "string" ? code : "";
|
|
325
|
+
}
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
async function loginOpenAICodexDeviceCode(provider, interaction) {
|
|
329
|
+
const started = await postJsonObject(provider.deviceAuthUrl, { client_id: provider.clientId }, interaction.signal);
|
|
330
|
+
if (!started.ok)
|
|
331
|
+
throw new Error(`OpenAI Codex device authorization failed (${started.status}): ${started.text.slice(0, 200)}`);
|
|
332
|
+
const deviceAuthId = typeof started.payload.device_auth_id === "string" ? started.payload.device_auth_id : "";
|
|
333
|
+
const userCode = typeof started.payload.user_code === "string" ? started.payload.user_code : "";
|
|
334
|
+
const rawInterval = Number(started.payload.interval);
|
|
335
|
+
const interval = Number.isFinite(rawInterval) && rawInterval >= 0 ? rawInterval : 5;
|
|
336
|
+
if (!deviceAuthId || !userCode)
|
|
337
|
+
throw new Error(`Invalid OpenAI Codex device authorization response: ${JSON.stringify(started.payload)}`);
|
|
338
|
+
const expiresInSeconds = 15 * 60;
|
|
339
|
+
interaction.notify({
|
|
340
|
+
type: "device_code",
|
|
341
|
+
userCode,
|
|
342
|
+
verificationUri: "https://auth.openai.com/codex/device",
|
|
343
|
+
intervalSeconds: interval,
|
|
344
|
+
expiresInSeconds,
|
|
345
|
+
});
|
|
346
|
+
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
347
|
+
let authorizationCode = "";
|
|
348
|
+
let codeVerifier = "";
|
|
349
|
+
let waitMs = interval * 1000;
|
|
350
|
+
await sleep(waitMs);
|
|
351
|
+
while (Date.now() <= deadline) {
|
|
352
|
+
if (interaction.signal?.aborted)
|
|
353
|
+
throw new Error("Login aborted");
|
|
354
|
+
const polled = await postJsonObject(provider.deviceTokenUrl, { device_auth_id: deviceAuthId, user_code: userCode }, interaction.signal);
|
|
355
|
+
if (polled.ok) {
|
|
356
|
+
authorizationCode = typeof polled.payload.authorization_code === "string" ? polled.payload.authorization_code : "";
|
|
357
|
+
codeVerifier = typeof polled.payload.code_verifier === "string" ? polled.payload.code_verifier : "";
|
|
358
|
+
if (!authorizationCode || !codeVerifier)
|
|
359
|
+
throw new Error(`Invalid OpenAI Codex device token response: ${JSON.stringify(polled.payload)}`);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
const errorCode = nestedOAuthErrorCode(polled.payload);
|
|
363
|
+
if (polled.status === 403 || polled.status === 404 || errorCode === "deviceauth_authorization_pending") {
|
|
364
|
+
await sleep(waitMs);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (errorCode === "slow_down") {
|
|
368
|
+
waitMs += 5000;
|
|
369
|
+
await sleep(waitMs);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
throw new Error(`OpenAI Codex device authorization failed (${polled.status}): ${polled.text.slice(0, 200)}`);
|
|
373
|
+
}
|
|
374
|
+
if (!authorizationCode || !codeVerifier)
|
|
375
|
+
throw new Error("OpenAI Codex device login timed out. Please try again.");
|
|
376
|
+
const payload = await postToken(provider.tokenUrl, provider.tokenEncoding, {
|
|
377
|
+
grant_type: "authorization_code",
|
|
378
|
+
client_id: provider.clientId,
|
|
379
|
+
code: authorizationCode,
|
|
380
|
+
code_verifier: codeVerifier,
|
|
381
|
+
redirect_uri: "https://auth.openai.com/deviceauth/callback",
|
|
382
|
+
});
|
|
383
|
+
return tokensFrom(provider, payload);
|
|
384
|
+
}
|
|
303
385
|
// --- Public API --------------------------------------------------------------
|
|
304
386
|
/** Provider ids Bivy can natively drive a subscription login for. */
|
|
305
387
|
export { isNativeOAuthProvider, nativeOAuthProviderIds } from "./model-oauth-providers.js";
|
|
@@ -312,7 +394,9 @@ export async function loginModelOAuth(credsDir, providerId, interaction, label =
|
|
|
312
394
|
const provider = getModelOAuthProvider(providerId);
|
|
313
395
|
if (!provider)
|
|
314
396
|
throw new Error(`Provider "${providerId}" does not support subscription login`);
|
|
315
|
-
const tokens = provider.flow === "device_code" ? await loginDeviceCode(provider, interaction)
|
|
397
|
+
const tokens = provider.flow === "device_code" ? await loginDeviceCode(provider, interaction)
|
|
398
|
+
: provider.flow === "openai_codex_device_code" ? await loginOpenAICodexDeviceCode(provider, interaction)
|
|
399
|
+
: await loginAuthCode(provider, interaction);
|
|
316
400
|
const credential = { type: "oauth", access: tokens.access, refresh: tokens.refresh, expires: tokens.expires, refreshedAt: tokens.refreshedAt, ...(tokens.accountId ? { accountId: tokens.accountId } : {}) };
|
|
317
401
|
await createCredentialVault(credsDir).modifyRecord(providerId, label, async () => credential);
|
|
318
402
|
}
|
package/package.json
CHANGED