opencode-antigravity-auth 2.0.0 → 2.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.
Files changed (53) hide show
  1. package/README.md +34 -718
  2. package/dist/index.d.ts +1 -1
  3. package/dist/src/plugin/account-management.d.ts +67 -0
  4. package/dist/src/plugin/account-management.d.ts.map +1 -0
  5. package/dist/src/plugin/account-management.js +174 -0
  6. package/dist/src/plugin/account-management.js.map +1 -0
  7. package/dist/src/plugin/account-rpc.d.ts +70 -0
  8. package/dist/src/plugin/account-rpc.d.ts.map +1 -0
  9. package/dist/src/plugin/account-rpc.js +28 -0
  10. package/dist/src/plugin/account-rpc.js.map +1 -0
  11. package/dist/src/plugin/accounts.d.ts +23 -9
  12. package/dist/src/plugin/accounts.d.ts.map +1 -1
  13. package/dist/src/plugin/accounts.js +190 -22
  14. package/dist/src/plugin/accounts.js.map +1 -1
  15. package/dist/src/plugin/config/models-v2.d.ts +19 -1
  16. package/dist/src/plugin/config/models-v2.d.ts.map +1 -1
  17. package/dist/src/plugin/config/models-v2.js +49 -38
  18. package/dist/src/plugin/config/models-v2.js.map +1 -1
  19. package/dist/src/plugin/quota.d.ts +33 -1
  20. package/dist/src/plugin/quota.d.ts.map +1 -1
  21. package/dist/src/plugin/quota.js +43 -18
  22. package/dist/src/plugin/quota.js.map +1 -1
  23. package/dist/src/plugin/refresh-queue.d.ts +4 -1
  24. package/dist/src/plugin/refresh-queue.d.ts.map +1 -1
  25. package/dist/src/plugin/refresh-queue.js +21 -4
  26. package/dist/src/plugin/refresh-queue.js.map +1 -1
  27. package/dist/src/plugin/storage.d.ts +15 -0
  28. package/dist/src/plugin/storage.d.ts.map +1 -1
  29. package/dist/src/plugin/storage.js +61 -2
  30. package/dist/src/plugin/storage.js.map +1 -1
  31. package/dist/src/plugin/token.d.ts +1 -3
  32. package/dist/src/plugin/token.d.ts.map +1 -1
  33. package/dist/src/plugin/token.js +43 -3
  34. package/dist/src/plugin/token.js.map +1 -1
  35. package/dist/src/plugin/transform/model-resolver.d.ts +2 -5
  36. package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
  37. package/dist/src/plugin/transform/model-resolver.js +9 -1
  38. package/dist/src/plugin/transform/model-resolver.js.map +1 -1
  39. package/dist/src/plugin/types.d.ts +5 -0
  40. package/dist/src/plugin/types.d.ts.map +1 -1
  41. package/dist/src/plugin.d.ts +3 -3
  42. package/dist/src/plugin.d.ts.map +1 -1
  43. package/dist/src/plugin.js +84 -26
  44. package/dist/src/plugin.js.map +1 -1
  45. package/dist/src/tui.d.ts +64 -0
  46. package/dist/src/tui.d.ts.map +1 -0
  47. package/dist/src/tui.js +124 -0
  48. package/dist/src/tui.js.map +1 -0
  49. package/dist/src/v2.d.ts.map +1 -1
  50. package/dist/src/v2.js +386 -105
  51. package/dist/src/v2.js.map +1 -1
  52. package/package.json +9 -3
  53. package/tui.js +1 -0
@@ -12,7 +12,7 @@ import { isEmptyResponseBody, createSyntheticErrorResponse, } from "./plugin/req
12
12
  import { EmptyResponseError } from "./plugin/errors.js";
13
13
  import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token.js";
14
14
  import { startOAuthListener } from "./plugin/server.js";
15
- import { clearAccounts, loadAccounts, saveAccounts, saveAccountsReplace } from "./plugin/storage.js";
15
+ import { clearAccounts, loadAccounts, saveAccountCredentials, saveAccounts, saveAccountsReplace, updateAccountCredentials, updateAccounts } from "./plugin/storage.js";
16
16
  import { AccountManager, parseRateLimitReason, calculateBackoffMs, computeSoftQuotaCacheTtlMs } from "./plugin/accounts.js";
17
17
  import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker/index.js";
18
18
  import { loadConfig, initRuntimeConfig } from "./plugin/config/index.js";
@@ -98,7 +98,19 @@ async function triggerAsyncQuotaRefreshForAccount(accountManager, accountIndex,
98
98
  quotaRefreshInProgressByEmail.delete(accountKey);
99
99
  return;
100
100
  }
101
- const results = await checkAccountsQuota([singleAccount], client, providerId);
101
+ const results = await checkAccountsQuota([singleAccount], client, providerId, { onAccountUpdated: saveAccountCredentials });
102
+ const updatedAccount = results[0]?.updatedAccount;
103
+ if (updatedAccount) {
104
+ const saved = await updateAccounts(storage => {
105
+ const current = storage.accounts.find(item => singleAccount.id ? item.id === singleAccount.id : item.refreshToken === singleAccount.refreshToken);
106
+ if (current?.refreshToken === singleAccount.refreshToken)
107
+ updateAccountCredentials(current, updatedAccount);
108
+ });
109
+ const current = saved?.accounts.find(item => singleAccount.id ? item.id === singleAccount.id : item.refreshToken === updatedAccount.refreshToken);
110
+ if (current && account.parts.refreshToken === singleAccount.refreshToken) {
111
+ accountManager.updateFromAuth(account, { ...accountManager.toAuthDetails(account), refresh: formatRefreshParts(current) }, singleAccount.refreshToken);
112
+ }
113
+ }
102
114
  if (results[0]?.status === "ok" && results[0]?.quota?.groups) {
103
115
  accountManager.updateQuotaCache(accountIndex, results[0].quota.groups);
104
116
  accountManager.requestSaveToDisk();
@@ -993,15 +1005,17 @@ function sleep(ms, signal) {
993
1005
  /**
994
1006
  * Creates an Antigravity OAuth plugin for a specific provider ID.
995
1007
  */
996
- export const createAntigravityPlugin = (providerId) => async ({ client, directory, runtimeVersion = 1, fetch: sendFetch = globalThis.fetch }) => {
1008
+ export const createAntigravityPlugin = (providerId) => async ({ client, directory, runtimeVersion = 1, fetch: sendFetch = globalThis.fetch, canUseAccount }) => {
997
1009
  // Load configuration from files and environment variables
998
1010
  const config = loadConfig(directory);
999
1011
  initRuntimeConfig(config);
1000
1012
  // Cached getAuth function for tool access
1001
1013
  let cachedGetAuth = null;
1002
1014
  const refreshQueues = new Set();
1015
+ const quotaRefreshes = new Set();
1003
1016
  const transportManagers = new Set();
1004
1017
  let disposed = false;
1018
+ let managerGeneration = 0;
1005
1019
  // Initialize debug with config
1006
1020
  initializeDebug(config);
1007
1021
  // Initialize structured logger for TUI integration
@@ -1144,19 +1158,23 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1144
1158
  });
1145
1159
  return {
1146
1160
  async flush() {
1147
- await Promise.allSettled([...transportManagers].map((manager) => manager.flushSaveToDisk()));
1161
+ managerGeneration += 1;
1162
+ await Promise.allSettled([...refreshQueues].map((queue) => queue.stop()));
1163
+ refreshQueues.clear();
1164
+ await Promise.allSettled([...transportManagers].map((manager) => manager.retire()));
1148
1165
  transportManagers.clear();
1149
1166
  },
1150
1167
  async dispose() {
1151
1168
  if (disposed)
1152
1169
  return;
1153
1170
  disposed = true;
1171
+ managerGeneration += 1;
1154
1172
  releaseSignatureCache?.();
1155
1173
  clearInterval(versionRefreshTimer);
1156
- for (const queue of refreshQueues)
1157
- queue.stop();
1174
+ await Promise.allSettled([...refreshQueues].map((queue) => queue.stop()));
1158
1175
  refreshQueues.clear();
1159
- await Promise.allSettled([...transportManagers].map((manager) => manager.flushSaveToDisk()));
1176
+ await Promise.allSettled([...quotaRefreshes]);
1177
+ await Promise.allSettled([...transportManagers].map((manager) => manager.retire()));
1160
1178
  transportManagers.clear();
1161
1179
  cachedGetAuth = null;
1162
1180
  },
@@ -1169,11 +1187,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1169
1187
  loader: async (getAuth, provider) => {
1170
1188
  if (disposed)
1171
1189
  throw new Error("Antigravity plugin has been disposed");
1190
+ const expectedGeneration = managerGeneration;
1172
1191
  if (runtimeVersion === 2) {
1173
- for (const queue of refreshQueues)
1174
- queue.stop();
1192
+ await Promise.allSettled([...refreshQueues].map((queue) => queue.stop()));
1175
1193
  refreshQueues.clear();
1176
- await Promise.allSettled([...transportManagers].map((manager) => manager.flushSaveToDisk()));
1194
+ await Promise.allSettled([...transportManagers].map((manager) => manager.retire()));
1177
1195
  transportManagers.clear();
1178
1196
  }
1179
1197
  // Cache getAuth for tool access
@@ -1196,11 +1214,22 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1196
1214
  const authParts = parseRefreshParts(auth.refresh);
1197
1215
  const storedAccounts = await loadAccounts();
1198
1216
  // Note: AccountManager now ensures the current auth is always included in accounts
1199
- const accountManager = await AccountManager.loadFromDisk(auth);
1200
- if (disposed)
1201
- throw new Error("Antigravity plugin has been disposed");
1217
+ const accountManager = await AccountManager.loadFromDisk(auth, runtimeVersion === 2);
1218
+ if (runtimeVersion === 2)
1219
+ accountManager.useManagedStorage();
1220
+ const abandonManager = async () => {
1221
+ await accountManager.retire();
1222
+ transportManagers.delete(accountManager);
1223
+ throw new Error(disposed ? "Antigravity plugin has been disposed" : "Antigravity accounts changed");
1224
+ };
1225
+ if (disposed || managerGeneration !== expectedGeneration)
1226
+ return abandonManager();
1202
1227
  activeAccountManager = accountManager;
1203
1228
  transportManagers.add(accountManager);
1229
+ if (runtimeVersion === 2)
1230
+ await accountManager.saveToDisk();
1231
+ if (disposed || managerGeneration !== expectedGeneration)
1232
+ return abandonManager();
1204
1233
  if (accountManager.getAccountCount() > 0) {
1205
1234
  accountManager.requestSaveToDisk();
1206
1235
  }
@@ -1242,7 +1271,13 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1242
1271
  if (!isGenerativeLanguageRequest(input)) {
1243
1272
  return sendFetch(input, init);
1244
1273
  }
1274
+ const checkLifetime = () => {
1275
+ if (disposed || (runtimeVersion === 2 && managerGeneration !== expectedGeneration))
1276
+ throw new Error("Antigravity accounts changed; retry the request");
1277
+ };
1278
+ checkLifetime();
1245
1279
  const latestAuth = await getAuth();
1280
+ checkLifetime();
1246
1281
  if (!isOAuthAuth(latestAuth)) {
1247
1282
  return sendFetch(input, init);
1248
1283
  }
@@ -1252,6 +1287,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1252
1287
  const urlString = toUrlString(input);
1253
1288
  const family = getModelFamilyFromUrl(urlString);
1254
1289
  const model = extractModelFromUrl(urlString);
1290
+ const eligible = (account) => !canUseAccount || canUseAccount({ id: account.id, refreshToken: account.parts.refreshToken }, model ?? "");
1291
+ if (!accountManager.getAccounts().some(account => account.enabled !== false && eligible(account))) {
1292
+ throw new Error("No enabled Antigravity account has access to this model. Refresh available models from /antigravity.");
1293
+ }
1255
1294
  const debugLines = [];
1256
1295
  const pushDebug = (line) => {
1257
1296
  if (!isDebugEnabled())
@@ -1303,30 +1342,30 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1303
1342
  if (family !== "gemini")
1304
1343
  return false;
1305
1344
  // Use AccountManager method which properly checks for disabled/cooling-down accounts
1306
- return accountManager.hasOtherAccountWithAntigravityAvailable(currentAccount.index, family, model);
1345
+ return accountManager.hasOtherAccountWithAntigravityAvailable(currentAccount.index, family, model, eligible);
1307
1346
  };
1308
1347
  while (true) {
1309
1348
  // Check for abort at the start of each iteration
1310
1349
  checkAborted();
1311
- const accountCount = accountManager.getAccountCount();
1350
+ const accountCount = accountManager.getEnabledAccounts().filter(eligible).length;
1312
1351
  const routingDecision = resolveHeaderRoutingDecision(urlString, family, config);
1313
1352
  const { cliFirst, preferredHeaderStyle, explicitQuota, allowQuotaFallback, } = routingDecision;
1314
1353
  if (accountCount === 0) {
1315
1354
  throw new Error("No Antigravity accounts available. Run `opencode auth login`.");
1316
1355
  }
1317
1356
  const softQuotaCacheTtlMs = computeSoftQuotaCacheTtlMs(config.soft_quota_cache_ttl_minutes, config.quota_refresh_interval_minutes);
1318
- let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1357
+ let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, eligible);
1319
1358
  if (!account && allowQuotaFallback) {
1320
1359
  const alternateHeaderStyle = preferredHeaderStyle === "antigravity" ? "gemini-cli" : "antigravity";
1321
- account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1360
+ account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, eligible);
1322
1361
  if (account) {
1323
1362
  pushDebug(`selected-by-fallback idx=${account.index} preferred=${preferredHeaderStyle} alternate=${alternateHeaderStyle}`);
1324
1363
  }
1325
1364
  }
1326
1365
  if (!account) {
1327
- if (accountManager.areAllAccountsOverSoftQuota(family, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, model)) {
1366
+ if (accountManager.areAllAccountsOverSoftQuota(family, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, model, eligible)) {
1328
1367
  const threshold = config.soft_quota_threshold_percent;
1329
- const softQuotaWaitMs = accountManager.getMinWaitTimeForSoftQuota(family, threshold, softQuotaCacheTtlMs, model);
1368
+ const softQuotaWaitMs = accountManager.getMinWaitTimeForSoftQuota(family, threshold, softQuotaCacheTtlMs, model, eligible);
1330
1369
  const maxWaitMs = (config.max_rate_limit_wait_seconds ?? 300) * 1000;
1331
1370
  if (softQuotaWaitMs === null || (maxWaitMs > 0 && softQuotaWaitMs > maxWaitMs)) {
1332
1371
  const waitTimeFormatted = softQuotaWaitMs ? formatWaitTime(softQuotaWaitMs) : "unknown";
@@ -1346,7 +1385,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1346
1385
  }
1347
1386
  const strictWait = !allowQuotaFallback;
1348
1387
  // All accounts are rate-limited - wait and retry
1349
- const waitMs = accountManager.getMinWaitTimeForFamily(family, model, preferredHeaderStyle, strictWait) || 60_000;
1388
+ const waitMs = accountManager.getMinWaitTimeForFamily(family, model, preferredHeaderStyle, strictWait, eligible) || 60_000;
1350
1389
  const waitSecValue = Math.max(1, Math.ceil(waitMs / 1000));
1351
1390
  pushDebug(`all-rate-limited family=${family} accounts=${accountCount} waitMs=${waitMs}`);
1352
1391
  if (isDebugEnabled()) {
@@ -1414,7 +1453,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1414
1453
  continue;
1415
1454
  }
1416
1455
  resetAccountFailureState(account.index);
1417
- accountManager.updateFromAuth(account, refreshed);
1456
+ accountManager.updateFromAuth(account, refreshed, parseRefreshParts(authRecord.refresh).refreshToken);
1418
1457
  authRecord = refreshed;
1419
1458
  try {
1420
1459
  await accountManager.saveToDisk();
@@ -1425,9 +1464,24 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1425
1464
  }
1426
1465
  catch (error) {
1427
1466
  if (error instanceof AntigravityTokenRefreshError && error.code === "invalid_grant") {
1428
- const removed = accountManager.removeAccount(account);
1467
+ if (runtimeVersion === 2) {
1468
+ const rejectedToken = parseRefreshParts(authRecord.refresh).refreshToken;
1469
+ const saved = await updateAccounts(storage => {
1470
+ const current = storage.accounts.find(item => account.id ? item.id === account.id : item.refreshToken === rejectedToken);
1471
+ if (current?.refreshToken === rejectedToken)
1472
+ current.enabled = false;
1473
+ });
1474
+ const current = saved?.accounts.find(item => account.id ? item.id === account.id : item.refreshToken === rejectedToken);
1475
+ if (current && current.refreshToken !== rejectedToken) {
1476
+ accountManager.updateFromAuth(account, { type: "oauth", refresh: formatRefreshParts(current) });
1477
+ continue;
1478
+ }
1479
+ }
1480
+ const removed = runtimeVersion === 2
1481
+ ? accountManager.setAccountEnabled(account.index, false)
1482
+ : accountManager.removeAccount(account);
1429
1483
  if (removed) {
1430
- log.warn("Removed revoked account from pool - reauthenticate via `opencode auth login`");
1484
+ log.warn("Revoked account excluded from rotation - reauthenticate via `opencode auth login`");
1431
1485
  try {
1432
1486
  await accountManager.saveToDisk();
1433
1487
  }
@@ -1487,7 +1541,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1487
1541
  }
1488
1542
  if (projectContext.auth.refresh !== authRecord.refresh ||
1489
1543
  projectContext.auth.access !== authRecord.access) {
1490
- accountManager.updateFromAuth(account, projectContext.auth);
1544
+ accountManager.updateFromAuth(account, projectContext.auth, parseRefreshParts(authRecord.refresh).refreshToken);
1491
1545
  authRecord = projectContext.auth;
1492
1546
  try {
1493
1547
  await accountManager.saveToDisk();
@@ -1554,7 +1608,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1554
1608
  // Antigravity-first fallback: exhaust antigravity across ALL accounts before gemini-cli
1555
1609
  if (allowQuotaFallback && family === "gemini" && headerStyle === "antigravity") {
1556
1610
  // Check if ANY other account has antigravity available
1557
- if (accountManager.hasOtherAccountWithAntigravityAvailable(account.index, family, model)) {
1611
+ if (accountManager.hasOtherAccountWithAntigravityAvailable(account.index, family, model, eligible)) {
1558
1612
  // Switch to another account with antigravity (preserve antigravity priority)
1559
1613
  pushDebug(`antigravity rate-limited on account ${account.index}, but available on other accounts. Switching.`);
1560
1614
  shouldSwitchAccount = true;
@@ -1867,7 +1921,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1867
1921
  account.consecutiveFailures = 0;
1868
1922
  getHealthTracker().recordSuccess(account.index);
1869
1923
  accountManager.markAccountUsed(account.index);
1870
- void triggerAsyncQuotaRefreshForAccount(accountManager, account.index, client, providerId, config.quota_refresh_interval_minutes);
1924
+ if (!disposed) {
1925
+ const quotaRefresh = triggerAsyncQuotaRefreshForAccount(accountManager, account.index, client, providerId, config.quota_refresh_interval_minutes);
1926
+ quotaRefreshes.add(quotaRefresh);
1927
+ void quotaRefresh.then(() => quotaRefreshes.delete(quotaRefresh), () => quotaRefreshes.delete(quotaRefresh));
1928
+ }
1871
1929
  }
1872
1930
  logAntigravityDebugResponse(debugContext, response, {
1873
1931
  note: response.ok ? "Success" : `Error ${response.status}`,