oc-codex-multi-auth 6.15.0 → 6.17.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.
Files changed (89) hide show
  1. package/README.md +15 -11
  2. package/config/README.md +36 -13
  3. package/config/opencode-legacy.json +150 -0
  4. package/config/opencode-modern.json +48 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +145 -55
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/accounts/persistence.d.ts +53 -3
  9. package/dist/lib/accounts/persistence.d.ts.map +1 -1
  10. package/dist/lib/accounts/persistence.js +110 -4
  11. package/dist/lib/accounts/persistence.js.map +1 -1
  12. package/dist/lib/accounts/state.d.ts +1 -0
  13. package/dist/lib/accounts/state.d.ts.map +1 -1
  14. package/dist/lib/accounts/state.js +11 -0
  15. package/dist/lib/accounts/state.js.map +1 -1
  16. package/dist/lib/accounts.d.ts +1 -1
  17. package/dist/lib/accounts.d.ts.map +1 -1
  18. package/dist/lib/accounts.js +1 -1
  19. package/dist/lib/accounts.js.map +1 -1
  20. package/dist/lib/auth/login-runner.d.ts +2 -0
  21. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  22. package/dist/lib/auth/login-runner.js +44 -12
  23. package/dist/lib/auth/login-runner.js.map +1 -1
  24. package/dist/lib/auth/loopback-flow.d.ts +124 -0
  25. package/dist/lib/auth/loopback-flow.d.ts.map +1 -0
  26. package/dist/lib/auth/loopback-flow.js +112 -0
  27. package/dist/lib/auth/loopback-flow.js.map +1 -0
  28. package/dist/lib/auth/plan-tier.d.ts +21 -0
  29. package/dist/lib/auth/plan-tier.d.ts.map +1 -0
  30. package/dist/lib/auth/plan-tier.js +92 -0
  31. package/dist/lib/auth/plan-tier.js.map +1 -0
  32. package/dist/lib/auth/token-utils.d.ts +8 -10
  33. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  34. package/dist/lib/auth/token-utils.js +27 -18
  35. package/dist/lib/auth/token-utils.js.map +1 -1
  36. package/dist/lib/constants.d.ts +5 -2
  37. package/dist/lib/constants.d.ts.map +1 -1
  38. package/dist/lib/constants.js +5 -2
  39. package/dist/lib/constants.js.map +1 -1
  40. package/dist/lib/prompts/codex.d.ts +1 -1
  41. package/dist/lib/prompts/codex.d.ts.map +1 -1
  42. package/dist/lib/prompts/codex.js +51 -4
  43. package/dist/lib/prompts/codex.js.map +1 -1
  44. package/dist/lib/quota-notifications.d.ts +15 -0
  45. package/dist/lib/quota-notifications.d.ts.map +1 -1
  46. package/dist/lib/quota-notifications.js +42 -12
  47. package/dist/lib/quota-notifications.js.map +1 -1
  48. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  49. package/dist/lib/request/fetch-helpers.js +88 -9
  50. package/dist/lib/request/fetch-helpers.js.map +1 -1
  51. package/dist/lib/request/helpers/client-identity.d.ts.map +1 -1
  52. package/dist/lib/request/helpers/client-identity.js +9 -2
  53. package/dist/lib/request/helpers/client-identity.js.map +1 -1
  54. package/dist/lib/request/helpers/model-map.d.ts +23 -0
  55. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  56. package/dist/lib/request/helpers/model-map.js +92 -2
  57. package/dist/lib/request/helpers/model-map.js.map +1 -1
  58. package/dist/lib/request/helpers/responses-lite.d.ts.map +1 -1
  59. package/dist/lib/request/helpers/responses-lite.js +34 -1
  60. package/dist/lib/request/helpers/responses-lite.js.map +1 -1
  61. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  62. package/dist/lib/request/request-transformer.js +78 -13
  63. package/dist/lib/request/request-transformer.js.map +1 -1
  64. package/dist/lib/schemas.d.ts +15 -0
  65. package/dist/lib/schemas.d.ts.map +1 -1
  66. package/dist/lib/schemas.js +1 -0
  67. package/dist/lib/schemas.js.map +1 -1
  68. package/dist/lib/storage/flagged.d.ts.map +1 -1
  69. package/dist/lib/storage/flagged.js +5 -0
  70. package/dist/lib/storage/flagged.js.map +1 -1
  71. package/dist/lib/storage/migrations.d.ts +2 -0
  72. package/dist/lib/storage/migrations.d.ts.map +1 -1
  73. package/dist/lib/storage/migrations.js.map +1 -1
  74. package/dist/lib/tools/codex-limits.d.ts.map +1 -1
  75. package/dist/lib/tools/codex-limits.js +7 -4
  76. package/dist/lib/tools/codex-limits.js.map +1 -1
  77. package/dist/lib/tools/codex-list.d.ts.map +1 -1
  78. package/dist/lib/tools/codex-list.js +13 -1
  79. package/dist/lib/tools/codex-list.js.map +1 -1
  80. package/dist/lib/tools/codex-status.d.ts.map +1 -1
  81. package/dist/lib/tools/codex-status.js +8 -0
  82. package/dist/lib/tools/codex-status.js.map +1 -1
  83. package/dist/lib/tui-status.d.ts.map +1 -1
  84. package/dist/lib/tui-status.js +6 -2
  85. package/dist/lib/tui-status.js.map +1 -1
  86. package/dist/lib/types.d.ts +1 -0
  87. package/dist/lib/types.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/scripts/install-oc-codex-multi-auth-core.js +5 -5
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
- if (!parsed.code || !parsed.state) {
330
- return {
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: "OAuth state mismatch. Restart login and try again, or retry with Device Code.",
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 { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
358
- logInfo(`OAuth URL: ${url}`);
359
- let serverInfo = null;
360
- try {
361
- serverInfo = await startLocalOAuthServer({ state });
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
- const result = await serverInfo.waitForCode(state);
380
- serverInfo.close();
381
- if (!result) {
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: `OAuth callback timed out or was cancelled. ` +
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 await exchangeAuthorizationCode(result.code, pkce.verifier, REDIRECT_URI);
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
- if (usage.planType)
2598
- parts.push(`plan:${usage.planType}`);
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 (useManualMode) {
3220
+ if (noBrowser) {
3184
3221
  targetCount = 1;
3185
- }
3186
- if (useManualMode) {
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
- // Initialize storage path for manual OAuth flow
3317
- // Must happen BEFORE persistAccountPool to ensure correct storage location
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);