oc-codex-multi-auth 6.15.0 → 6.16.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/README.md +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +145 -55
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +53 -3
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +110 -4
- package/dist/lib/accounts/persistence.js.map +1 -1
- package/dist/lib/accounts/state.d.ts +1 -0
- package/dist/lib/accounts/state.d.ts.map +1 -1
- package/dist/lib/accounts/state.js +11 -0
- package/dist/lib/accounts/state.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +1 -1
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts +2 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +44 -12
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/auth/loopback-flow.d.ts +124 -0
- package/dist/lib/auth/loopback-flow.d.ts.map +1 -0
- package/dist/lib/auth/loopback-flow.js +112 -0
- package/dist/lib/auth/loopback-flow.js.map +1 -0
- package/dist/lib/auth/plan-tier.d.ts +21 -0
- package/dist/lib/auth/plan-tier.d.ts.map +1 -0
- package/dist/lib/auth/plan-tier.js +92 -0
- package/dist/lib/auth/plan-tier.js.map +1 -0
- package/dist/lib/auth/token-utils.d.ts +8 -10
- package/dist/lib/auth/token-utils.d.ts.map +1 -1
- package/dist/lib/auth/token-utils.js +27 -18
- package/dist/lib/auth/token-utils.js.map +1 -1
- package/dist/lib/constants.d.ts +5 -2
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +5 -2
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/quota-notifications.d.ts +15 -0
- package/dist/lib/quota-notifications.d.ts.map +1 -1
- package/dist/lib/quota-notifications.js +42 -12
- package/dist/lib/quota-notifications.js.map +1 -1
- package/dist/lib/schemas.d.ts +3 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +1 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +5 -0
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +2 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +7 -4
- package/dist/lib/tools/codex-limits.js.map +1 -1
- package/dist/lib/tools/codex-list.d.ts.map +1 -1
- package/dist/lib/tools/codex-list.js +13 -1
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-status.d.ts.map +1 -1
- package/dist/lib/tools/codex-status.js +8 -0
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tui-status.d.ts.map +1 -1
- package/dist/lib/tui-status.js +6 -2
- package/dist/lib/tui-status.js.map +1 -1
- package/dist/lib/types.d.ts +1 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,6 +263,8 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t
|
|
|
263
263
|
- unsupported-model handling is strict by default, with opt-in fallback controls
|
|
264
264
|
- TUI quota status follows the account/workspace used by the latest request
|
|
265
265
|
- Business workspace memberships and Personal accounts keep separate usage and quota windows. Business members sharing one workspace are distinguished by their member/seat identity, so their usage is not collapsed into one row.
|
|
266
|
+
- An account identifies itself by its own ChatGPT email and the last 6 characters of its account id, with the email masked when `maskEmail` is on. The OAuth id_token also lists the API-platform organizations the login belongs to; those are not ChatGPT workspaces and are never used to name an account, so logging in clears a label left behind by one. A label you set with `codex-label` is always kept.
|
|
267
|
+
- The ChatGPT plan (`Free`, `Plus`, `Pro`, `Business`, `Business Premium`) is read from the access token, refreshed on every token refresh, and shown by `codex-list` and `codex-status`. `codex-limits` and the TUI read the plan live from the usage endpoint and name it the same way. An unrecognized plan is reported verbatim rather than renamed.
|
|
266
268
|
|
|
267
269
|
---
|
|
268
270
|
|
|
@@ -302,12 +304,13 @@ The feature is disabled by default.
|
|
|
302
304
|
|
|
303
305
|
Each line reports the enabled account with the most headroom in that window,
|
|
304
306
|
together with that same account's reset time, so the pair always describes a
|
|
305
|
-
quota that one account actually has.
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
quota that one account actually has. When a different account recovers sooner,
|
|
308
|
+
that reset is appended under its own label rather than folded into the first
|
|
309
|
+
one. Windows a plan has switched off are skipped rather than counted as full.
|
|
310
|
+
Account identities are omitted for readability and lock-screen privacy:
|
|
308
311
|
|
|
309
312
|
```text
|
|
310
|
-
5h: 10% | resets 22:30
|
|
313
|
+
5h: 10% | resets 02:00 | another account resets 22:30
|
|
311
314
|
Weekly: 72% | resets 22:30 on Aug 30
|
|
312
315
|
```
|
|
313
316
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA2U/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAqjI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -27,11 +27,11 @@ import { createHash } from "node:crypto";
|
|
|
27
27
|
import { homedir } from "node:os";
|
|
28
28
|
import { join } from "node:path";
|
|
29
29
|
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
|
|
30
|
+
import { formatPlanType } from "./lib/auth/plan-tier.js";
|
|
31
|
+
import { startLoopbackFlow, } from "./lib/auth/loopback-flow.js";
|
|
30
32
|
import { buildDeviceCodeInstructions, completeDeviceCodeSession, createDeviceCodeSession, } from "./lib/auth/device-code.js";
|
|
31
33
|
import { applyAccountSelectionFallbacks, persistResolvedAccountSelection, persistAccountPool, resolveAndPersistAccountSelection, resolveAccountSelection, } from "./lib/auth/login-runner.js";
|
|
32
34
|
import { coordinateFlaggedPersistedRefresh, coordinatePersistedRefresh, } from "./lib/storage/coordinated-refresh.js";
|
|
33
|
-
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
34
|
-
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
35
35
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
36
36
|
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getModelAccountPoolMode, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, loadPluginConfig, } from "./lib/config.js";
|
|
37
37
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
@@ -305,39 +305,76 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
};
|
|
308
|
+
const listenerUnavailableMessage = () => `OAuth callback server failed to start on localhost loopback port 1455. ` +
|
|
309
|
+
`Retry with "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}".`;
|
|
310
|
+
const callbackCancelledMessage = () => `OAuth callback timed out or was cancelled. ` +
|
|
311
|
+
`If you are on SSH, WSL, or a headless machine, retry with "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}".`;
|
|
312
|
+
// The login is still live when this fires: the listener is bound and
|
|
313
|
+
// waiting, so opening the URL printed above finishes it. That is the
|
|
314
|
+
// whole recovery path on a host with no opener on PATH.
|
|
315
|
+
const browserOpenFailedMessage = () => `Could not launch your default browser. Open the OAuth URL above in any browser to finish signing in; ` +
|
|
316
|
+
`this login is still waiting on the localhost callback. ` +
|
|
317
|
+
`"${AUTH_LABELS.OAUTH_MANUAL_BROWSER}" does the same thing without trying to launch a browser, ` +
|
|
318
|
+
`and "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}" avoid the callback entirely.`;
|
|
319
|
+
const unavailableMessage = (lifecycle) => {
|
|
320
|
+
switch (lifecycle) {
|
|
321
|
+
case "listener_unavailable":
|
|
322
|
+
return listenerUnavailableMessage();
|
|
323
|
+
default: {
|
|
324
|
+
const unreachable = lifecycle;
|
|
325
|
+
return unreachable;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Every accepted paste must carry this attempt's `state`, a raw code
|
|
331
|
+
* included.
|
|
332
|
+
*
|
|
333
|
+
* That comparison is the manual flow's only in-plugin binding between
|
|
334
|
+
* the pasted value and this login. Accepting a bare code delegates the
|
|
335
|
+
* binding entirely to the authorization server's PKCE enforcement, and
|
|
336
|
+
* hands anything a user is talked into pasting to
|
|
337
|
+
* `exchangeAuthorizationCode` with this attempt's verifier — which is
|
|
338
|
+
* the shape of the attack the state check exists to stop. The raw
|
|
339
|
+
* branch of `parseAuthorizationInput` also returns the whole trimmed
|
|
340
|
+
* input as the code, so a noisy paste would be forwarded verbatim.
|
|
341
|
+
*/
|
|
342
|
+
const manualInputRejection = (parsed, expectedState) => {
|
|
343
|
+
if (!parsed.code) {
|
|
344
|
+
return "No authorization code found. Paste the full callback URL (e.g., http://localhost:1455/auth/callback?code=...&state=...). If browser callback keeps failing, retry with Device Code.";
|
|
345
|
+
}
|
|
346
|
+
switch (parsed.source) {
|
|
347
|
+
case "raw":
|
|
348
|
+
return "That is a bare authorization code. This flow needs the full callback URL, including the state parameter (e.g., http://localhost:1455/auth/callback?code=...&state=...), which is what ties the code to this login attempt. If needed, retry with Device Code.";
|
|
349
|
+
case "url":
|
|
350
|
+
case "query":
|
|
351
|
+
case "fragment":
|
|
352
|
+
if (!parsed.state) {
|
|
353
|
+
return "That callback URL carries no OAuth state, so it cannot be matched to this login attempt. Paste the complete callback URL including its state parameter, or retry with Device Code.";
|
|
354
|
+
}
|
|
355
|
+
if (parsed.state !== expectedState) {
|
|
356
|
+
return "OAuth state mismatch. Restart login and paste the callback URL generated for this login attempt, or retry with Device Code.";
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
default: {
|
|
360
|
+
const unreachable = parsed;
|
|
361
|
+
return unreachable;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
308
365
|
const buildManualOAuthFlow = (pkce, url, expectedState, replaceAll) => ({
|
|
309
366
|
url,
|
|
310
367
|
method: "code",
|
|
311
368
|
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
|
|
312
|
-
validate: (input) =>
|
|
313
|
-
const parsed = parseAuthorizationInput(input);
|
|
314
|
-
if (!parsed.code) {
|
|
315
|
-
return "No authorization code found. Paste the full callback URL (e.g., http://localhost:1455/auth/callback?code=...). If browser callback keeps failing, retry with Device Code.";
|
|
316
|
-
}
|
|
317
|
-
if (!parsed.state) {
|
|
318
|
-
return parsed.source === "raw"
|
|
319
|
-
? "That is a bare authorization code. This flow needs the full callback URL, including the state parameter (e.g., http://localhost:1455/auth/callback?code=...&state=...). If needed, retry with Device Code."
|
|
320
|
-
: "Missing OAuth state. Paste the full callback URL including both code and state parameters. If needed, retry with Device Code.";
|
|
321
|
-
}
|
|
322
|
-
if (parsed.state !== expectedState) {
|
|
323
|
-
return "OAuth state mismatch. Restart login and paste the callback URL generated for this login attempt, or retry with Device Code.";
|
|
324
|
-
}
|
|
325
|
-
return undefined;
|
|
326
|
-
},
|
|
369
|
+
validate: (input) => manualInputRejection(parseAuthorizationInput(input), expectedState),
|
|
327
370
|
callback: async (input) => {
|
|
328
371
|
const parsed = parseAuthorizationInput(input);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
type: "failed",
|
|
332
|
-
reason: "invalid_response",
|
|
333
|
-
message: "Missing authorization code or OAuth state",
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (parsed.state !== expectedState) {
|
|
372
|
+
const rejection = manualInputRejection(parsed, expectedState);
|
|
373
|
+
if (rejection !== undefined || !parsed.code) {
|
|
337
374
|
return {
|
|
338
375
|
type: "failed",
|
|
339
376
|
reason: "invalid_response",
|
|
340
|
-
message:
|
|
377
|
+
message: rejection ?? "Missing authorization code",
|
|
341
378
|
};
|
|
342
379
|
}
|
|
343
380
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -354,21 +391,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
354
391
|
},
|
|
355
392
|
});
|
|
356
393
|
const runOAuthFlow = async (forceNewLogin = false) => {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
catch (err) {
|
|
364
|
-
logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server: ${err?.message ?? String(err)}`);
|
|
365
|
-
serverInfo = null;
|
|
366
|
-
}
|
|
367
|
-
openBrowserUrl(url);
|
|
368
|
-
if (!serverInfo || !serverInfo.ready) {
|
|
369
|
-
serverInfo?.close();
|
|
370
|
-
const message = `OAuth callback server failed to start on localhost loopback port 1455. ` +
|
|
371
|
-
`Retry with "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}".`;
|
|
394
|
+
const session = await startLoopbackFlow({
|
|
395
|
+
openBrowser: true,
|
|
396
|
+
forceNewLogin,
|
|
397
|
+
});
|
|
398
|
+
if (session.type === "unavailable") {
|
|
399
|
+
const message = unavailableMessage(session.lifecycle);
|
|
372
400
|
logWarn(`\n[${PLUGIN_NAME}] ${message}\n`);
|
|
373
401
|
return {
|
|
374
402
|
type: "failed",
|
|
@@ -376,17 +404,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
376
404
|
message,
|
|
377
405
|
};
|
|
378
406
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
407
|
+
logInfo(`OAuth URL: ${session.url}`);
|
|
408
|
+
// Printed first, so the recovery instruction points at a URL the
|
|
409
|
+
// user can already see. The session stays open either way.
|
|
410
|
+
if (!session.browserOpened) {
|
|
411
|
+
logWarn(`\n[${PLUGIN_NAME}] ${browserOpenFailedMessage()}\n`);
|
|
412
|
+
}
|
|
413
|
+
const result = await session.waitAndExchange();
|
|
414
|
+
if (result.type === "cancelled") {
|
|
382
415
|
return {
|
|
383
416
|
type: "failed",
|
|
384
417
|
reason: "unknown",
|
|
385
|
-
message:
|
|
386
|
-
`If you are on SSH, WSL, or a headless machine, retry with "${AUTH_LABELS.OAUTH_DEVICE_CODE}" or "${AUTH_LABELS.OAUTH_MANUAL}".`,
|
|
418
|
+
message: callbackCancelledMessage(),
|
|
387
419
|
};
|
|
388
420
|
}
|
|
389
|
-
return
|
|
421
|
+
return result;
|
|
390
422
|
};
|
|
391
423
|
const showToast = async (message, variant = "success", options) => {
|
|
392
424
|
try {
|
|
@@ -2544,9 +2576,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2544
2576
|
const authPerProjectAccounts = getPerProjectAccounts(authPluginConfig);
|
|
2545
2577
|
setStoragePath(authPerProjectAccounts ? process.cwd() : null);
|
|
2546
2578
|
const accounts = [];
|
|
2579
|
+
// Programmatic input, not a menu choice: a headless caller or
|
|
2580
|
+
// script passes it to say "never launch a browser". Ignoring it
|
|
2581
|
+
// would silently do the opposite — enter the multi-account loop,
|
|
2582
|
+
// try to launch a browser, and bind port 1455 — and would also
|
|
2583
|
+
// drop the replaceAll semantics the manual path carries.
|
|
2547
2584
|
const noBrowser = inputs?.noBrowser === "true" ||
|
|
2548
2585
|
inputs?.["no-browser"] === "true";
|
|
2549
|
-
const useManualMode = noBrowser;
|
|
2550
2586
|
const explicitLoginMode = inputs?.loginMode === "fresh" || inputs?.loginMode === "add"
|
|
2551
2587
|
? inputs.loginMode
|
|
2552
2588
|
: null;
|
|
@@ -2594,8 +2630,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2594
2630
|
parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
|
|
2595
2631
|
}
|
|
2596
2632
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2633
|
+
const planLabel = formatPlanType(usage.planType);
|
|
2634
|
+
if (planLabel)
|
|
2635
|
+
parts.push(`plan:${planLabel}`);
|
|
2599
2636
|
if (usage.credits)
|
|
2600
2637
|
parts.push(`credits:${usage.credits}`);
|
|
2601
2638
|
return parts.length > 0 ? parts.join(", ") : "quota unavailable";
|
|
@@ -3180,10 +3217,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3180
3217
|
if (refreshAccountIndex !== undefined) {
|
|
3181
3218
|
targetCount = 1;
|
|
3182
3219
|
}
|
|
3183
|
-
if (
|
|
3220
|
+
if (noBrowser) {
|
|
3184
3221
|
targetCount = 1;
|
|
3185
|
-
|
|
3186
|
-
|
|
3222
|
+
// Paste-the-code, not the loopback listener: this is the one
|
|
3223
|
+
// path that needs neither a browser on this host nor a
|
|
3224
|
+
// reachable localhost:1455 from wherever the browser runs.
|
|
3187
3225
|
const { pkce, state, url } = await createAuthorizationFlow();
|
|
3188
3226
|
return buildManualOAuthFlow(pkce, url, state, startFresh);
|
|
3189
3227
|
}
|
|
@@ -3274,6 +3312,57 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3274
3312
|
};
|
|
3275
3313
|
},
|
|
3276
3314
|
},
|
|
3315
|
+
{
|
|
3316
|
+
label: AUTH_LABELS.OAUTH_MANUAL_BROWSER,
|
|
3317
|
+
type: "oauth",
|
|
3318
|
+
authorize: async () => {
|
|
3319
|
+
// Must happen BEFORE the callback persists, to ensure correct
|
|
3320
|
+
// storage location: OpenCode invokes callback() separately from
|
|
3321
|
+
// authorize(), so the path has to be pinned here.
|
|
3322
|
+
const manualBrowserPluginConfig = loadPluginConfig();
|
|
3323
|
+
applyUiRuntimeFromConfig(manualBrowserPluginConfig);
|
|
3324
|
+
const manualBrowserPerProjectAccounts = getPerProjectAccounts(manualBrowserPluginConfig);
|
|
3325
|
+
setStoragePath(manualBrowserPerProjectAccounts ? process.cwd() : null);
|
|
3326
|
+
const session = await startLoopbackFlow({ openBrowser: false });
|
|
3327
|
+
if (session.type === "unavailable") {
|
|
3328
|
+
const message = unavailableMessage(session.lifecycle);
|
|
3329
|
+
logWarn(`\n[${PLUGIN_NAME}] ${message}\n`);
|
|
3330
|
+
return {
|
|
3331
|
+
url: "",
|
|
3332
|
+
instructions: message,
|
|
3333
|
+
method: "auto",
|
|
3334
|
+
callback: () => Promise.resolve({
|
|
3335
|
+
type: "failed",
|
|
3336
|
+
reason: "invalid_response",
|
|
3337
|
+
message,
|
|
3338
|
+
}),
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
return {
|
|
3342
|
+
url: session.url,
|
|
3343
|
+
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL_BROWSER,
|
|
3344
|
+
method: "auto",
|
|
3345
|
+
callback: async () => {
|
|
3346
|
+
const result = await session.waitAndExchange();
|
|
3347
|
+
if (result.type === "cancelled") {
|
|
3348
|
+
return {
|
|
3349
|
+
type: "failed",
|
|
3350
|
+
reason: "unknown",
|
|
3351
|
+
message: callbackCancelledMessage(),
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
if (result.type !== "success") {
|
|
3355
|
+
return result;
|
|
3356
|
+
}
|
|
3357
|
+
const resolved = await resolveAndPersistAccountSelection(result, {
|
|
3358
|
+
persistSelections: persistAuthenticatedSelections,
|
|
3359
|
+
replaceAll: false,
|
|
3360
|
+
});
|
|
3361
|
+
return resolved.primary;
|
|
3362
|
+
},
|
|
3363
|
+
};
|
|
3364
|
+
},
|
|
3365
|
+
},
|
|
3277
3366
|
{
|
|
3278
3367
|
label: AUTH_LABELS.OAUTH_DEVICE_CODE,
|
|
3279
3368
|
type: "oauth",
|
|
@@ -3313,8 +3402,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3313
3402
|
label: AUTH_LABELS.OAUTH_MANUAL,
|
|
3314
3403
|
type: "oauth",
|
|
3315
3404
|
authorize: async () => {
|
|
3316
|
-
//
|
|
3317
|
-
//
|
|
3405
|
+
// Must happen BEFORE the callback persists, to ensure correct
|
|
3406
|
+
// storage location: OpenCode invokes callback() separately from
|
|
3407
|
+
// authorize(), so the path has to be pinned here.
|
|
3318
3408
|
const manualPluginConfig = loadPluginConfig();
|
|
3319
3409
|
applyUiRuntimeFromConfig(manualPluginConfig);
|
|
3320
3410
|
const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
|