oc-codex-multi-auth 6.14.4 → 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 +49 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +159 -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/codex-usage.d.ts +13 -1
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +27 -8
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +28 -0
- package/dist/lib/config.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/desktop-notifications.d.ts +14 -0
- package/dist/lib/desktop-notifications.d.ts.map +1 -0
- package/dist/lib/desktop-notifications.js +59 -0
- package/dist/lib/desktop-notifications.js.map +1 -0
- package/dist/lib/quota-notification-state.d.ts +25 -0
- package/dist/lib/quota-notification-state.d.ts.map +1 -0
- package/dist/lib/quota-notification-state.js +94 -0
- package/dist/lib/quota-notification-state.js.map +1 -0
- package/dist/lib/quota-notifications.d.ts +79 -0
- package/dist/lib/quota-notifications.d.ts.map +1 -0
- package/dist/lib/quota-notifications.js +401 -0
- package/dist/lib/quota-notifications.js.map +1 -0
- package/dist/lib/schemas.d.ts +9 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +7 -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
|
|
|
@@ -293,6 +295,53 @@ Primary config files:
|
|
|
293
295
|
- `~/.config/opencode/tui.json`
|
|
294
296
|
- `~/.opencode/openai-codex-auth-config.json`
|
|
295
297
|
|
|
298
|
+
### Desktop quota notifications
|
|
299
|
+
|
|
300
|
+
Quota notifications are an optional macOS-only feature. While the plugin is
|
|
301
|
+
running, it checks all enabled accounts and alerts through Notification Center
|
|
302
|
+
when the best remaining 5-hour or weekly pool quota crosses 25%, 10%, or 0%.
|
|
303
|
+
The feature is disabled by default.
|
|
304
|
+
|
|
305
|
+
Each line reports the enabled account with the most headroom in that window,
|
|
306
|
+
together with that same account's reset time, so the pair always describes a
|
|
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:
|
|
311
|
+
|
|
312
|
+
```text
|
|
313
|
+
5h: 10% | resets 02:00 | another account resets 22:30
|
|
314
|
+
Weekly: 72% | resets 22:30 on Aug 30
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"quotaNotifications": {
|
|
320
|
+
"enabled": true,
|
|
321
|
+
"intervalMs": 1800000,
|
|
322
|
+
"notifyEveryCheck": false,
|
|
323
|
+
"thresholds": [25, 10, 0]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Add the object above to `~/.opencode/openai-codex-auth-config.json`, or set
|
|
329
|
+
`CODEX_AUTH_QUOTA_NOTIFICATIONS=1`, then quit and restart OpenCode. The minimum
|
|
330
|
+
interval is 30 seconds. If macOS blocks the alert, allow notifications for
|
|
331
|
+
the process shown in **System Settings > Notifications**. The setting is
|
|
332
|
+
ignored on Windows and Linux.
|
|
333
|
+
|
|
334
|
+
Set `"notifyEveryCheck": true` to show the aggregate quota notification after
|
|
335
|
+
every successful poll interval instead of only when a configured threshold is
|
|
336
|
+
crossed. Set `"thresholds": []` to turn threshold alerts off entirely; pair it
|
|
337
|
+
with `"notifyEveryCheck": true` or the monitor has nothing to deliver and stops
|
|
338
|
+
polling.
|
|
339
|
+
|
|
340
|
+
Delivery state lives beside the accounts file the alerts are computed from, so
|
|
341
|
+
OpenCode processes working in the same account scope show only one alert per
|
|
342
|
+
interval. With the default `perProjectAccounts`, that scope is one project:
|
|
343
|
+
two projects have separate account pools and therefore alert independently.
|
|
344
|
+
|
|
296
345
|
### Route models to preferred accounts
|
|
297
346
|
|
|
298
347
|
Use `modelAccountPools` to assign one or more preferred ChatGPT accounts or Business seats to a
|
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,15 +27,16 @@ 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";
|
|
38
38
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
|
|
39
|
+
import { createQuotaMonitor } from "./lib/quota-notifications.js";
|
|
39
40
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
40
41
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
41
42
|
import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
|
|
@@ -304,39 +305,76 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
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
|
+
};
|
|
307
365
|
const buildManualOAuthFlow = (pkce, url, expectedState, replaceAll) => ({
|
|
308
366
|
url,
|
|
309
367
|
method: "code",
|
|
310
368
|
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
|
|
311
|
-
validate: (input) =>
|
|
312
|
-
const parsed = parseAuthorizationInput(input);
|
|
313
|
-
if (!parsed.code) {
|
|
314
|
-
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.";
|
|
315
|
-
}
|
|
316
|
-
if (!parsed.state) {
|
|
317
|
-
return parsed.source === "raw"
|
|
318
|
-
? "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."
|
|
319
|
-
: "Missing OAuth state. Paste the full callback URL including both code and state parameters. If needed, retry with Device Code.";
|
|
320
|
-
}
|
|
321
|
-
if (parsed.state !== expectedState) {
|
|
322
|
-
return "OAuth state mismatch. Restart login and paste the callback URL generated for this login attempt, or retry with Device Code.";
|
|
323
|
-
}
|
|
324
|
-
return undefined;
|
|
325
|
-
},
|
|
369
|
+
validate: (input) => manualInputRejection(parseAuthorizationInput(input), expectedState),
|
|
326
370
|
callback: async (input) => {
|
|
327
371
|
const parsed = parseAuthorizationInput(input);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
type: "failed",
|
|
331
|
-
reason: "invalid_response",
|
|
332
|
-
message: "Missing authorization code or OAuth state",
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
if (parsed.state !== expectedState) {
|
|
372
|
+
const rejection = manualInputRejection(parsed, expectedState);
|
|
373
|
+
if (rejection !== undefined || !parsed.code) {
|
|
336
374
|
return {
|
|
337
375
|
type: "failed",
|
|
338
376
|
reason: "invalid_response",
|
|
339
|
-
message:
|
|
377
|
+
message: rejection ?? "Missing authorization code",
|
|
340
378
|
};
|
|
341
379
|
}
|
|
342
380
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -353,21 +391,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
353
391
|
},
|
|
354
392
|
});
|
|
355
393
|
const runOAuthFlow = async (forceNewLogin = false) => {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
catch (err) {
|
|
363
|
-
logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server: ${err?.message ?? String(err)}`);
|
|
364
|
-
serverInfo = null;
|
|
365
|
-
}
|
|
366
|
-
openBrowserUrl(url);
|
|
367
|
-
if (!serverInfo || !serverInfo.ready) {
|
|
368
|
-
serverInfo?.close();
|
|
369
|
-
const message = `OAuth callback server failed to start on localhost loopback port 1455. ` +
|
|
370
|
-
`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);
|
|
371
400
|
logWarn(`\n[${PLUGIN_NAME}] ${message}\n`);
|
|
372
401
|
return {
|
|
373
402
|
type: "failed",
|
|
@@ -375,17 +404,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
375
404
|
message,
|
|
376
405
|
};
|
|
377
406
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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") {
|
|
381
415
|
return {
|
|
382
416
|
type: "failed",
|
|
383
417
|
reason: "unknown",
|
|
384
|
-
message:
|
|
385
|
-
`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(),
|
|
386
419
|
};
|
|
387
420
|
}
|
|
388
|
-
return
|
|
421
|
+
return result;
|
|
389
422
|
};
|
|
390
423
|
const showToast = async (message, variant = "success", options) => {
|
|
391
424
|
try {
|
|
@@ -1128,10 +1161,22 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1128
1161
|
});
|
|
1129
1162
|
}
|
|
1130
1163
|
};
|
|
1164
|
+
// Created here, after `invalidateAccountManagerCache`, because the
|
|
1165
|
+
// monitor refreshes access tokens unattended. A refresh rotates the
|
|
1166
|
+
// single-use refresh token durably on disk, so the cached
|
|
1167
|
+
// AccountManager holding the old one must be dropped or its debounced
|
|
1168
|
+
// save clobbers the rotation (see `reloadCachedAccountManager` above).
|
|
1169
|
+
const quotaMonitor = createQuotaMonitor({
|
|
1170
|
+
onCredentialsPersisted: invalidateAccountManagerCache,
|
|
1171
|
+
});
|
|
1131
1172
|
// Event handler for session recovery and account selection
|
|
1132
1173
|
const eventHandler = async (input) => {
|
|
1133
1174
|
try {
|
|
1134
1175
|
const { event } = input;
|
|
1176
|
+
if (event.type === "server.instance.disposed") {
|
|
1177
|
+
quotaMonitor.dispose();
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1135
1180
|
// Handle TUI account selection events
|
|
1136
1181
|
// Accepts generic selection events with an index property
|
|
1137
1182
|
if (event.type === "account.select" ||
|
|
@@ -1238,6 +1283,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1238
1283
|
const startupPerProjectAccounts = getPerProjectAccounts(startupPluginConfig);
|
|
1239
1284
|
setStoragePath(startupPerProjectAccounts ? process.cwd() : null);
|
|
1240
1285
|
await backfillHostOpenAIAuthFromPool();
|
|
1286
|
+
quotaMonitor.start();
|
|
1241
1287
|
return {
|
|
1242
1288
|
event: eventHandler,
|
|
1243
1289
|
auth: {
|
|
@@ -2530,9 +2576,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2530
2576
|
const authPerProjectAccounts = getPerProjectAccounts(authPluginConfig);
|
|
2531
2577
|
setStoragePath(authPerProjectAccounts ? process.cwd() : null);
|
|
2532
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.
|
|
2533
2584
|
const noBrowser = inputs?.noBrowser === "true" ||
|
|
2534
2585
|
inputs?.["no-browser"] === "true";
|
|
2535
|
-
const useManualMode = noBrowser;
|
|
2536
2586
|
const explicitLoginMode = inputs?.loginMode === "fresh" || inputs?.loginMode === "add"
|
|
2537
2587
|
? inputs.loginMode
|
|
2538
2588
|
: null;
|
|
@@ -2580,8 +2630,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2580
2630
|
parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
|
|
2581
2631
|
}
|
|
2582
2632
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2633
|
+
const planLabel = formatPlanType(usage.planType);
|
|
2634
|
+
if (planLabel)
|
|
2635
|
+
parts.push(`plan:${planLabel}`);
|
|
2585
2636
|
if (usage.credits)
|
|
2586
2637
|
parts.push(`credits:${usage.credits}`);
|
|
2587
2638
|
return parts.length > 0 ? parts.join(", ") : "quota unavailable";
|
|
@@ -3166,10 +3217,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3166
3217
|
if (refreshAccountIndex !== undefined) {
|
|
3167
3218
|
targetCount = 1;
|
|
3168
3219
|
}
|
|
3169
|
-
if (
|
|
3220
|
+
if (noBrowser) {
|
|
3170
3221
|
targetCount = 1;
|
|
3171
|
-
|
|
3172
|
-
|
|
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.
|
|
3173
3225
|
const { pkce, state, url } = await createAuthorizationFlow();
|
|
3174
3226
|
return buildManualOAuthFlow(pkce, url, state, startFresh);
|
|
3175
3227
|
}
|
|
@@ -3260,6 +3312,57 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3260
3312
|
};
|
|
3261
3313
|
},
|
|
3262
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
|
+
},
|
|
3263
3366
|
{
|
|
3264
3367
|
label: AUTH_LABELS.OAUTH_DEVICE_CODE,
|
|
3265
3368
|
type: "oauth",
|
|
@@ -3299,8 +3402,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3299
3402
|
label: AUTH_LABELS.OAUTH_MANUAL,
|
|
3300
3403
|
type: "oauth",
|
|
3301
3404
|
authorize: async () => {
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
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.
|
|
3304
3408
|
const manualPluginConfig = loadPluginConfig();
|
|
3305
3409
|
applyUiRuntimeFromConfig(manualPluginConfig);
|
|
3306
3410
|
const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
|