@eddyskywalker/dsh-chatgpt-subscription 0.10.11 → 0.10.12
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/CHANGELOG.md +8 -0
- package/lib/index.js +435 -281
- package/lib/types/host/claude/account-pool.d.ts +4 -21
- package/lib/types/host/claude/account-pool.d.ts.map +1 -1
- package/lib/types/host/codex-account-pool.d.ts.map +1 -1
- package/lib/types/host/common/account-pool.d.ts +93 -6
- package/lib/types/host/common/account-pool.d.ts.map +1 -1
- package/lib/types/host/minimax-code/account-pool.d.ts +2 -2
- package/lib/types/host/minimax-code/account-pool.d.ts.map +1 -1
- package/lib/types/host/oauth-service.d.ts.map +1 -1
- package/lib/types/host/reasoning-collapse-guard/index.d.ts +26 -11
- package/lib/types/host/reasoning-collapse-guard/index.d.ts.map +1 -1
- package/lib/types/host/workbuddy/account-pool.d.ts +8 -8
- package/lib/types/host/workbuddy/account-pool.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -905,6 +905,16 @@ function poolCredentialAccount$1(filePath) {
|
|
|
905
905
|
return createHash("sha256").update(path.resolve(filePath)).digest("hex");
|
|
906
906
|
}
|
|
907
907
|
const poolOperations = /* @__PURE__ */ new Map();
|
|
908
|
+
const poolIdentityRevisions = /* @__PURE__ */ new Map();
|
|
909
|
+
const poolRefreshes = /* @__PURE__ */ new Map();
|
|
910
|
+
/**
|
|
911
|
+
* A selected row was deleted, or became unusable, while a caller awaited it.
|
|
912
|
+
*
|
|
913
|
+
* Distinct from every other credential error because a MODEL REQUEST can still
|
|
914
|
+
* be served by another account: routing excludes the row and tries the next
|
|
915
|
+
* one, while a caller that named the account still gets the plain refusal.
|
|
916
|
+
*/
|
|
917
|
+
var PoolAccountUnavailableError = class extends LlmError {};
|
|
908
918
|
/**
|
|
909
919
|
* The shared multi-account pool: encrypted storage, eligibility filtering,
|
|
910
920
|
* rotation strategy, 429 cooldowns and per-account auth failures.
|
|
@@ -926,11 +936,14 @@ var AccountPoolCore = class {
|
|
|
926
936
|
* can therefore invalidate on identity change without paying a credential read
|
|
927
937
|
* to discover it.
|
|
928
938
|
*/
|
|
929
|
-
|
|
939
|
+
/** Pool-file identity of this instance: locks, revisions and flights key on it. */
|
|
940
|
+
operationKey;
|
|
930
941
|
constructor(hooks) {
|
|
931
942
|
this.hooks = hooks;
|
|
932
943
|
this.filePath = hooks.poolFile;
|
|
933
944
|
this.backend = hooks.backend ?? createPoolBackend(hooks);
|
|
945
|
+
const resolved = path.resolve(this.filePath);
|
|
946
|
+
this.operationKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
934
947
|
}
|
|
935
948
|
/**
|
|
936
949
|
* A cheap, in-memory revision of the pool's serving identity.
|
|
@@ -946,15 +959,21 @@ var AccountPoolCore = class {
|
|
|
946
959
|
* would report one account's usage under another account's name.
|
|
947
960
|
*/
|
|
948
961
|
currentIdentityRevision() {
|
|
949
|
-
return this.
|
|
962
|
+
return poolIdentityRevisions.get(this.operationKey) ?? 0;
|
|
950
963
|
}
|
|
951
964
|
/** Human description of where this pool's credentials live. */
|
|
952
965
|
path() {
|
|
953
966
|
if (process.platform === "win32") return `${this.filePath}.dpapi`;
|
|
954
967
|
return `${process.platform === "darwin" ? "Keychain" : "Secret Service"}: ${this.hooks.keychainService}/${poolCredentialAccount$1(this.filePath)}`;
|
|
955
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* Queue one storage transaction for this pool file.
|
|
971
|
+
*
|
|
972
|
+
* NOT reentrant: a transaction must use {@link loadPoolData} and
|
|
973
|
+
* {@link updatePool} rather than calling the public read/write methods.
|
|
974
|
+
*/
|
|
956
975
|
serialize(operation) {
|
|
957
|
-
const key =
|
|
976
|
+
const key = this.operationKey;
|
|
958
977
|
const result = (poolOperations.get(key) || Promise.resolve()).then(operation);
|
|
959
978
|
const settled = result.then(() => void 0, () => void 0);
|
|
960
979
|
poolOperations.set(key, settled);
|
|
@@ -978,31 +997,74 @@ var AccountPoolCore = class {
|
|
|
978
997
|
* getter write to disk.
|
|
979
998
|
*/
|
|
980
999
|
read() {
|
|
981
|
-
return this.serialize(
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1000
|
+
return this.serialize(() => this.loadPoolData());
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Load the pool without holding the file lock.
|
|
1004
|
+
*
|
|
1005
|
+
* Providers override this to maintain their own in-memory indexes, so every
|
|
1006
|
+
* load — including one inside {@link updatePool} — refreshes them.
|
|
1007
|
+
*/
|
|
1008
|
+
async loadPoolData() {
|
|
1009
|
+
let current = null;
|
|
1010
|
+
try {
|
|
1011
|
+
current = await this.backend.load();
|
|
1012
|
+
} catch {}
|
|
1013
|
+
if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
|
|
1014
|
+
try {
|
|
1015
|
+
const legacy = await this.hooks.legacyAccount?.();
|
|
1016
|
+
if (legacy) return {
|
|
997
1017
|
version: 1,
|
|
998
|
-
|
|
999
|
-
|
|
1018
|
+
activeAccountId: legacy.id,
|
|
1019
|
+
rotationStrategy: current?.rotationStrategy || "sequential",
|
|
1020
|
+
accounts: [legacy]
|
|
1000
1021
|
};
|
|
1001
|
-
}
|
|
1022
|
+
} catch {}
|
|
1023
|
+
return current || {
|
|
1024
|
+
version: 1,
|
|
1025
|
+
rotationStrategy: "sequential",
|
|
1026
|
+
accounts: []
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
async persistPoolData(data) {
|
|
1030
|
+
poolIdentityRevisions.set(this.operationKey, this.currentIdentityRevision() + 1);
|
|
1031
|
+
await this.saveVerified(data);
|
|
1002
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Replace the whole document (initialization, import), NOT a read-modify-write.
|
|
1035
|
+
* Prefer {@link updatePool} for mutations so the draft is read under the lock.
|
|
1036
|
+
*/
|
|
1003
1037
|
write(data) {
|
|
1004
|
-
this.
|
|
1005
|
-
|
|
1038
|
+
return this.serialize(() => this.persistPoolData(data));
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Read the LATEST document, mutate it and persist it as one file transaction.
|
|
1042
|
+
*
|
|
1043
|
+
* This is the whole fix for the read-modify-write race: the old shape read a
|
|
1044
|
+
* snapshot, awaited (a token refresh, a settings write, an encrypted round
|
|
1045
|
+
* trip) and then wrote that whole stale snapshot back, erasing whatever landed
|
|
1046
|
+
* in between — including another account's freshly rotated token.
|
|
1047
|
+
*
|
|
1048
|
+
* The callback runs under the file lock, so it must not call read, write or
|
|
1049
|
+
* updatePool (the lock is not reentrant) and must not do network I/O.
|
|
1050
|
+
* Provider-local stores belong in `afterCommit`, which runs only once the
|
|
1051
|
+
* pool write is verified, and must not re-enter the pool either.
|
|
1052
|
+
*/
|
|
1053
|
+
updatePool(mutate, afterCommit) {
|
|
1054
|
+
return this.serialize(async () => {
|
|
1055
|
+
const data = await this.loadPoolData();
|
|
1056
|
+
const before = structuredClone(data);
|
|
1057
|
+
const result = await mutate(data);
|
|
1058
|
+
if (!isDeepStrictEqual(before, data)) {
|
|
1059
|
+
await this.persistPoolData(data);
|
|
1060
|
+
await afterCommit?.(data, result).catch(() => void 0);
|
|
1061
|
+
for (const account of data.accounts) if (!isDeepStrictEqual(before.accounts.find((entry) => entry.id === account.id)?.credentials, account.credentials)) await this.hooks.credentialsCommitted?.(account).catch(() => void 0);
|
|
1062
|
+
const previousPrimary = before.accounts.find((account) => account.isPrimary);
|
|
1063
|
+
const primary = data.accounts.find((account) => account.isPrimary);
|
|
1064
|
+
if (previousPrimary?.id !== primary?.id || !isDeepStrictEqual(previousPrimary?.credentials, primary?.credentials)) await this.hooks.mirrorPrimary?.(primary?.credentials ?? null).catch(() => void 0);
|
|
1065
|
+
}
|
|
1066
|
+
return result;
|
|
1067
|
+
});
|
|
1006
1068
|
}
|
|
1007
1069
|
/** Public summaries, with cooldowns that already expired reported as absent. */
|
|
1008
1070
|
async listAccounts() {
|
|
@@ -1031,8 +1093,17 @@ var AccountPoolCore = class {
|
|
|
1031
1093
|
return this.hooks.extendSummary ? this.hooks.extendSummary(account, summary, now) : summary;
|
|
1032
1094
|
}
|
|
1033
1095
|
/** Add or re-authorize one account; an existing dedupe key updates in place. */
|
|
1034
|
-
|
|
1035
|
-
|
|
1096
|
+
addAccount(credentials, alias) {
|
|
1097
|
+
return this.updatePool((data) => this.addAccountToPool(data, credentials, alias));
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Add or re-authorize inside an existing transaction.
|
|
1101
|
+
*
|
|
1102
|
+
* Capacity, dedupe and the primary decision are all taken against the live
|
|
1103
|
+
* document, so two simultaneous sign-ins cannot each pass a check the other
|
|
1104
|
+
* has already invalidated.
|
|
1105
|
+
*/
|
|
1106
|
+
addAccountToPool(data, credentials, alias) {
|
|
1036
1107
|
const key = this.hooks.dedupeKey?.(credentials);
|
|
1037
1108
|
const existingIndex = key === void 0 ? -1 : data.accounts.findIndex((account) => this.hooks.dedupeKey?.(account.credentials) === key);
|
|
1038
1109
|
const maxAccounts = this.hooks.maxAccounts ?? 20;
|
|
@@ -1058,77 +1129,79 @@ var AccountPoolCore = class {
|
|
|
1058
1129
|
if (existingIndex >= 0) data.accounts[existingIndex] = account;
|
|
1059
1130
|
else data.accounts.push(account);
|
|
1060
1131
|
if (!data.activeAccountId || account.isPrimary) data.activeAccountId = account.id;
|
|
1061
|
-
await this.write(data);
|
|
1062
|
-
if (account.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
|
|
1063
1132
|
return account;
|
|
1064
1133
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
this.hooks.mirrorPrimary?.(primary.credentials).catch(() => void 0);
|
|
1072
|
-
}
|
|
1073
|
-
await this.write(data);
|
|
1134
|
+
setPrimary(accountId) {
|
|
1135
|
+
return this.updatePool((data) => {
|
|
1136
|
+
for (const account of data.accounts) account.isPrimary = account.id === accountId;
|
|
1137
|
+
const primary = data.accounts.find((account) => account.isPrimary);
|
|
1138
|
+
if (primary) data.activeAccountId = primary.id;
|
|
1139
|
+
});
|
|
1074
1140
|
}
|
|
1075
1141
|
/**
|
|
1076
1142
|
* Persist a refreshed credential for one account.
|
|
1077
1143
|
*
|
|
1144
|
+
* `expected` makes this a conditional write: when the stored credential is
|
|
1145
|
+
* no longer the one the caller started from, something newer won the race (a
|
|
1146
|
+
* re-login, a rotation committed while this call was in flight) and the
|
|
1147
|
+
* latest row is returned WITHOUT being overwritten. A deleted account is
|
|
1148
|
+
* never brought back.
|
|
1149
|
+
*
|
|
1078
1150
|
* Used by a forced refresh (a 401 mid-request) rather than by the rotating
|
|
1079
|
-
* getEffectiveAccount path, which
|
|
1151
|
+
* getEffectiveAccount path, which refreshes through its own single-flight.
|
|
1080
1152
|
*/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (target.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
|
|
1088
|
-
return target;
|
|
1153
|
+
updateAccountCredentials(accountId, credentials, expected) {
|
|
1154
|
+
return this.updatePool((data) => {
|
|
1155
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1156
|
+
if (target && (expected === void 0 || isDeepStrictEqual(target.credentials, expected))) target.credentials = credentials;
|
|
1157
|
+
return target;
|
|
1158
|
+
});
|
|
1089
1159
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
await this.write(data);
|
|
1096
|
-
}
|
|
1160
|
+
setAlias(accountId, alias) {
|
|
1161
|
+
return this.updatePool((data) => {
|
|
1162
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1163
|
+
if (target && alias.trim()) target.alias = alias.trim();
|
|
1164
|
+
});
|
|
1097
1165
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1166
|
+
deleteAccount(accountId) {
|
|
1167
|
+
return this.updatePool((data) => this.deleteAccountFromPool(data, accountId));
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Remove one account inside a transaction.
|
|
1171
|
+
*
|
|
1172
|
+
* Provider refusal rules (a snapshot this plugin does not own, a desktop
|
|
1173
|
+
* account it may only hide) belong in an override of this, so the check and
|
|
1174
|
+
* the removal cannot be separated by a concurrent change.
|
|
1175
|
+
*/
|
|
1176
|
+
deleteAccountFromPool(data, accountId) {
|
|
1100
1177
|
const wasPrimary = data.accounts.find((account) => account.id === accountId)?.isPrimary;
|
|
1101
1178
|
data.accounts = data.accounts.filter((account) => account.id !== accountId);
|
|
1102
|
-
if (wasPrimary && data.accounts.length > 0)
|
|
1103
|
-
data.accounts[0].isPrimary = true;
|
|
1104
|
-
this.hooks.mirrorPrimary?.(data.accounts[0].credentials).catch(() => void 0);
|
|
1105
|
-
} else if (data.accounts.length === 0) this.hooks.mirrorPrimary?.(null).catch(() => void 0);
|
|
1179
|
+
if (wasPrimary && data.accounts.length > 0) data.accounts[0].isPrimary = true;
|
|
1106
1180
|
if (data.activeAccountId === accountId) data.activeAccountId = data.accounts.find((account) => account.isPrimary)?.id ?? data.accounts[0]?.id;
|
|
1107
|
-
await this.write(data);
|
|
1108
1181
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1182
|
+
setStrategy(strategy) {
|
|
1183
|
+
return this.updatePool((data) => {
|
|
1184
|
+
data.rotationStrategy = strategy;
|
|
1185
|
+
});
|
|
1113
1186
|
}
|
|
1114
1187
|
/** Cool one account down after an upstream 429. */
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1188
|
+
markCooldown(accountId, durationMs, reason) {
|
|
1189
|
+
return this.updatePool((data) => {
|
|
1190
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1191
|
+
if (target) {
|
|
1192
|
+
target.cooldownUntil = Date.now() + Math.max(MIN_COOLDOWN_MS, durationMs);
|
|
1193
|
+
target.cooldownReason = reason;
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1123
1196
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1197
|
+
clearCooldown(accountId) {
|
|
1198
|
+
return this.updatePool((data) => {
|
|
1199
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1200
|
+
if (target) {
|
|
1201
|
+
target.cooldownUntil = void 0;
|
|
1202
|
+
target.cooldownReason = void 0;
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1132
1205
|
}
|
|
1133
1206
|
/**
|
|
1134
1207
|
* Record that an account can no longer authenticate.
|
|
@@ -1136,23 +1209,23 @@ var AccountPoolCore = class {
|
|
|
1136
1209
|
* The account stays in the pool: signing in again is what restores it, and a
|
|
1137
1210
|
* deleted account would take the user's alias and ordering with it.
|
|
1138
1211
|
*/
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1212
|
+
markAuthFailed(accountId, reason, status = "expired") {
|
|
1213
|
+
return this.updatePool((data) => {
|
|
1214
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1215
|
+
if (target) {
|
|
1216
|
+
target.authStatus = status;
|
|
1217
|
+
target.authFailedReason = reason;
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1147
1220
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1221
|
+
clearAuthFailed(accountId) {
|
|
1222
|
+
return this.updatePool((data) => {
|
|
1223
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1224
|
+
if (target) {
|
|
1225
|
+
target.authStatus = void 0;
|
|
1226
|
+
target.authFailedReason = void 0;
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1156
1229
|
}
|
|
1157
1230
|
/** Whether an account may serve a request right now. */
|
|
1158
1231
|
isEligible(account, now, triedAccountIds) {
|
|
@@ -1192,24 +1265,16 @@ var AccountPoolCore = class {
|
|
|
1192
1265
|
*
|
|
1193
1266
|
* @param fetchFn - fetch used if the credential is close to expiry.
|
|
1194
1267
|
*/
|
|
1195
|
-
async getCredentialAccount(fetchFn = fetch) {
|
|
1268
|
+
async getCredentialAccount(fetchFn = fetch, forceRefresh = false) {
|
|
1196
1269
|
const data = await this.read();
|
|
1197
1270
|
if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
|
|
1198
|
-
const now = Date.now();
|
|
1199
1271
|
const usable = data.accounts.filter((account) => this.isCredentialUsable(account));
|
|
1200
1272
|
if (usable.length === 0) throw new LlmError(`全部 ${data.accounts.length} 个 ${this.hooks.displayName} 账号均需要重新登录。`, "AUTH", { status: 401 });
|
|
1201
1273
|
const selected = this.selectAccount(usable, data);
|
|
1202
|
-
|
|
1203
|
-
const refreshed = await this.hooks.refresh(selected.credentials, fetchFn);
|
|
1204
|
-
await this.updateAccountCredentials(selected.id, refreshed);
|
|
1205
|
-
return {
|
|
1206
|
-
account: selected,
|
|
1207
|
-
credentials: refreshed
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1274
|
+
const account = await this.freshAccount(selected, fetchFn, forceRefresh);
|
|
1210
1275
|
return {
|
|
1211
|
-
account
|
|
1212
|
-
credentials:
|
|
1276
|
+
account,
|
|
1277
|
+
credentials: account.credentials
|
|
1213
1278
|
};
|
|
1214
1279
|
}
|
|
1215
1280
|
/**
|
|
@@ -1244,8 +1309,8 @@ var AccountPoolCore = class {
|
|
|
1244
1309
|
* looks unexpired, and only the service's own refusal says so. Renewing it is
|
|
1245
1310
|
* what separates that from a credential that genuinely needs a new sign-in.
|
|
1246
1311
|
*/
|
|
1247
|
-
async renewCredential(accountId, fetchFn = fetch) {
|
|
1248
|
-
return this.credentialFor(accountId, fetchFn, true);
|
|
1312
|
+
async renewCredential(accountId, fetchFn = fetch, expected) {
|
|
1313
|
+
return this.credentialFor(accountId, fetchFn, true, expected);
|
|
1249
1314
|
}
|
|
1250
1315
|
/**
|
|
1251
1316
|
* Resolve, and if needed renew, one account's credential.
|
|
@@ -1255,29 +1320,106 @@ var AccountPoolCore = class {
|
|
|
1255
1320
|
* not name an account (the catalog loader) still has to present the
|
|
1256
1321
|
* credential that is actually in play.
|
|
1257
1322
|
*/
|
|
1258
|
-
async credentialFor(accountId, fetchFn, force) {
|
|
1323
|
+
async credentialFor(accountId, fetchFn, force, expected) {
|
|
1259
1324
|
const data = await this.read();
|
|
1260
1325
|
const target = accountId === void 0 ? data.accounts.find((account) => account.id === data.activeAccountId) ?? data.accounts.find((account) => account.isPrimary) ?? data.accounts[0] : data.accounts.find((account) => account.id === accountId);
|
|
1261
1326
|
if (target === void 0) throw new LlmError(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`, "AUTH");
|
|
1262
1327
|
if (!this.isCredentialUsable(target)) throw new LlmError(`${this.hooks.displayName} 账号的凭据已被拒绝,需要重新登录。`, "AUTH", { status: 401 });
|
|
1263
|
-
if (
|
|
1264
|
-
|
|
1328
|
+
if (expected !== void 0 && !isDeepStrictEqual(expected, target.credentials)) return target.credentials;
|
|
1329
|
+
return (await this.freshAccount(target, fetchFn, force)).credentials;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Return one account's current row, refreshing it at most once per generation.
|
|
1333
|
+
*
|
|
1334
|
+
* Shared by every entry point — routing, auxiliary tools, the settings card
|
|
1335
|
+
* and forced renewals — across all instances of the pool file, because a
|
|
1336
|
+
* rotating refresh token may be spent once: two parallel exchanges both fail,
|
|
1337
|
+
* and the losing one gets recorded as a dead sign-in.
|
|
1338
|
+
*
|
|
1339
|
+
* Reading the current generation and registering/joining its flight happen in
|
|
1340
|
+
* one short transaction, so a commit cannot slip between the two decisions.
|
|
1341
|
+
* The network call is fired AFTER the lock is released, and the plan returned
|
|
1342
|
+
* from the lock is a plain value: awaiting a flight while holding the lock
|
|
1343
|
+
* would deadlock that flight's own commit.
|
|
1344
|
+
*/
|
|
1345
|
+
async freshAccount(target, fetchFn, force = false) {
|
|
1346
|
+
let launch;
|
|
1347
|
+
const plan = await this.serialize(async () => {
|
|
1348
|
+
const current = (await this.loadPoolData()).accounts.find((account) => account.id === target.id);
|
|
1349
|
+
if (current === void 0 || !this.isCredentialUsable(current)) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
|
|
1350
|
+
if (!isDeepStrictEqual(current.credentials, target.credentials)) return { account: current };
|
|
1351
|
+
if (!this.hooks.refresh || !force && !this.shouldRefreshCredential(current, Date.now())) return { account: current };
|
|
1352
|
+
const key = JSON.stringify([this.operationKey, current.id]);
|
|
1353
|
+
const pending = poolRefreshes.get(key);
|
|
1354
|
+
if (pending !== void 0 && isDeepStrictEqual(pending.credentials, current.credentials)) return { result: pending.result };
|
|
1355
|
+
const result = new Promise((resolve, reject) => {
|
|
1356
|
+
launch = () => {
|
|
1357
|
+
this.refreshAccount(current, fetchFn).then(resolve, reject);
|
|
1358
|
+
};
|
|
1359
|
+
});
|
|
1360
|
+
const flight = {
|
|
1361
|
+
credentials: structuredClone(current.credentials),
|
|
1362
|
+
result
|
|
1363
|
+
};
|
|
1364
|
+
poolRefreshes.set(key, flight);
|
|
1365
|
+
const release = () => {
|
|
1366
|
+
if (poolRefreshes.get(key) === flight) poolRefreshes.delete(key);
|
|
1367
|
+
};
|
|
1368
|
+
result.then(release, release);
|
|
1369
|
+
return { result };
|
|
1370
|
+
});
|
|
1371
|
+
launch?.();
|
|
1372
|
+
return plan.result ?? plan.account;
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Refresh one account: network outside the lock, then a conditional commit.
|
|
1376
|
+
*
|
|
1377
|
+
* Every write-back is checked against the credential the refresh started from,
|
|
1378
|
+
* which is what keeps a late response from restoring a spent token over a
|
|
1379
|
+
* re-login, and keeps a deleted account from coming back.
|
|
1380
|
+
*/
|
|
1381
|
+
async refreshAccount(target, fetchFn) {
|
|
1382
|
+
const expected = structuredClone(target.credentials);
|
|
1265
1383
|
const live = await this.hooks.liveCredentialsFor?.(target).catch(() => null);
|
|
1266
1384
|
if (live !== null && live !== void 0 && live.advanced) {
|
|
1267
|
-
target.
|
|
1268
|
-
|
|
1269
|
-
|
|
1385
|
+
const account = await this.updateAccountCredentials(target.id, live.credentials, expected).catch(() => ({
|
|
1386
|
+
...target,
|
|
1387
|
+
credentials: live.credentials
|
|
1388
|
+
}));
|
|
1389
|
+
this.assertUsableAccount(account);
|
|
1390
|
+
return account;
|
|
1270
1391
|
}
|
|
1271
1392
|
let refreshed;
|
|
1272
1393
|
try {
|
|
1273
1394
|
refreshed = await this.hooks.refresh(target.credentials, fetchFn);
|
|
1274
1395
|
} catch (error) {
|
|
1275
1396
|
const status = this.hooks.refreshFailureStatus?.(error);
|
|
1276
|
-
|
|
1397
|
+
let superseded;
|
|
1398
|
+
await this.updatePool((data) => {
|
|
1399
|
+
const current = data.accounts.find((account) => account.id === target.id);
|
|
1400
|
+
if (current === void 0) return;
|
|
1401
|
+
if (!isDeepStrictEqual(current.credentials, expected)) {
|
|
1402
|
+
superseded = current;
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (status !== void 0) {
|
|
1406
|
+
current.authStatus = status;
|
|
1407
|
+
current.authFailedReason = error instanceof Error ? error.message : String(error);
|
|
1408
|
+
}
|
|
1409
|
+
}).catch(() => void 0);
|
|
1410
|
+
if (superseded !== void 0) {
|
|
1411
|
+
this.assertUsableAccount(superseded);
|
|
1412
|
+
return superseded;
|
|
1413
|
+
}
|
|
1277
1414
|
throw error;
|
|
1278
1415
|
}
|
|
1279
|
-
await this.updateAccountCredentials(target.id, refreshed);
|
|
1280
|
-
|
|
1416
|
+
const account = await this.updateAccountCredentials(target.id, refreshed, expected);
|
|
1417
|
+
this.assertUsableAccount(account);
|
|
1418
|
+
return account;
|
|
1419
|
+
}
|
|
1420
|
+
assertUsableAccount(account) {
|
|
1421
|
+
if (account === void 0) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
|
|
1422
|
+
if (!this.isCredentialUsable(account)) throw new PoolAccountUnavailableError(this.hooks.displayName + " 账号的凭据已被拒绝,需要重新登录。", "AUTH", { status: 401 });
|
|
1281
1423
|
}
|
|
1282
1424
|
/** Whether one account's credential must be refreshed before it is handed out. */
|
|
1283
1425
|
shouldRefreshCredential(account, now) {
|
|
@@ -1321,37 +1463,34 @@ var AccountPoolCore = class {
|
|
|
1321
1463
|
* @param fetchFn - fetch used by the provider's token refresh.
|
|
1322
1464
|
* @throws LlmError with `RATE_LIMIT` when every account is cooling down.
|
|
1323
1465
|
*/
|
|
1324
|
-
async getEffectiveAccount(excludeIds, fetchFn = fetch) {
|
|
1466
|
+
async getEffectiveAccount(excludeIds, fetchFn = fetch, forceRefresh = false) {
|
|
1325
1467
|
const data = await this.read();
|
|
1326
1468
|
if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
|
|
1327
1469
|
const now = Date.now();
|
|
1328
1470
|
const eligible = data.accounts.filter((account) => this.isEligible(account, now, excludeIds));
|
|
1329
1471
|
if (eligible.length === 0) throw this.allUnavailableError(data, now);
|
|
1330
1472
|
const selected = this.selectAccount(eligible, data);
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
selected.lastUsedAt = now;
|
|
1350
|
-
data.activeAccountId = selected.id;
|
|
1351
|
-
await this.write(data).catch(() => void 0);
|
|
1473
|
+
let refreshed;
|
|
1474
|
+
try {
|
|
1475
|
+
refreshed = await this.freshAccount(selected, fetchFn, forceRefresh);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
if (this.hooks.refreshFailureStatus?.(error) === void 0 && !(error instanceof PoolAccountUnavailableError)) throw error;
|
|
1478
|
+
const nextExclude = new Set(excludeIds ?? []);
|
|
1479
|
+
nextExclude.add(selected.id);
|
|
1480
|
+
if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn, forceRefresh);
|
|
1481
|
+
throw error;
|
|
1482
|
+
}
|
|
1483
|
+
const account = await this.updatePool((current) => {
|
|
1484
|
+
const latest = current.accounts.find((entry) => entry.id === refreshed.id);
|
|
1485
|
+
if (latest === void 0 || !isDeepStrictEqual(latest.credentials, refreshed.credentials)) return latest;
|
|
1486
|
+
latest.lastUsedAt = now;
|
|
1487
|
+
if (current.activeAccountId === data.activeAccountId) current.activeAccountId = latest.id;
|
|
1488
|
+
return latest;
|
|
1489
|
+
}).catch(() => refreshed);
|
|
1490
|
+
this.assertUsableAccount(account);
|
|
1352
1491
|
return {
|
|
1353
|
-
account
|
|
1354
|
-
credentials:
|
|
1492
|
+
account,
|
|
1493
|
+
credentials: account.credentials
|
|
1355
1494
|
};
|
|
1356
1495
|
}
|
|
1357
1496
|
};
|
|
@@ -1724,8 +1863,7 @@ var OAuthService = class {
|
|
|
1724
1863
|
async refresh() {
|
|
1725
1864
|
this.assertAvailable();
|
|
1726
1865
|
if (this.pool !== null) {
|
|
1727
|
-
|
|
1728
|
-
await this.refreshAccount(credentials).then((refreshed) => this.pool.updateAccountCredentials(account.id, refreshed));
|
|
1866
|
+
await this.pool.getEffectiveAccount(void 0, this.fetchFn, true);
|
|
1729
1867
|
return this.status();
|
|
1730
1868
|
}
|
|
1731
1869
|
const stored = await this.loadAuthenticated();
|
|
@@ -1787,18 +1925,8 @@ var OAuthService = class {
|
|
|
1787
1925
|
*/
|
|
1788
1926
|
async credentials(forceRefresh = false, access = {}) {
|
|
1789
1927
|
if (this.pool !== null) {
|
|
1790
|
-
if (access.purpose === "tool")
|
|
1791
|
-
|
|
1792
|
-
if (!forceRefresh) return credentials;
|
|
1793
|
-
const refreshed = await this.refreshAccount(credentials, this.fetchFn);
|
|
1794
|
-
await this.pool.updateAccountCredentials(account.id, refreshed);
|
|
1795
|
-
return refreshed;
|
|
1796
|
-
}
|
|
1797
|
-
const { account, credentials } = await this.pool.getEffectiveAccount(void 0, this.fetchFn);
|
|
1798
|
-
if (!forceRefresh) return credentials;
|
|
1799
|
-
const refreshed = await this.refreshAccount(credentials, this.fetchFn);
|
|
1800
|
-
await this.pool.updateAccountCredentials(account.id, refreshed);
|
|
1801
|
-
return refreshed;
|
|
1928
|
+
if (access.purpose === "tool") return (await this.pool.getCredentialAccount(this.fetchFn, forceRefresh)).credentials;
|
|
1929
|
+
return (await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh)).credentials;
|
|
1802
1930
|
}
|
|
1803
1931
|
const stored = await this.loadAuthenticated();
|
|
1804
1932
|
if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
|
|
@@ -2251,11 +2379,8 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
2251
2379
|
async refreshAccountNow(accountId) {
|
|
2252
2380
|
const account = (await this.read()).accounts.find((entry) => entry.id === accountId);
|
|
2253
2381
|
if (account === void 0) throw new Error("ChatGPT 账号已不在号池中");
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
const refreshed = await refresher.refreshAccount(account.credentials);
|
|
2257
|
-
await this.updateAccountCredentials(accountId, refreshed);
|
|
2258
|
-
return refreshed;
|
|
2382
|
+
if (this.refresherRef.current === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
|
|
2383
|
+
return this.renewCredential(accountId, fetch, account.credentials);
|
|
2259
2384
|
}
|
|
2260
2385
|
/** The single-credential store this pool mirrors its primary account into. */
|
|
2261
2386
|
mirrorStore() {
|
|
@@ -21552,23 +21677,30 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21552
21677
|
* because that one genuinely needs a sign-in.
|
|
21553
21678
|
*/
|
|
21554
21679
|
async healStaleAuthFailures(current) {
|
|
21555
|
-
const
|
|
21556
|
-
if (
|
|
21680
|
+
const failedAccounts = (current ?? await this.read()).accounts.filter(isMarkedFailed);
|
|
21681
|
+
if (failedAccounts.length === 0) return false;
|
|
21682
|
+
const targets = new Map(failedAccounts.map((account) => [account.id, {
|
|
21683
|
+
credentials: account.credentials,
|
|
21684
|
+
authStatus: account.authStatus
|
|
21685
|
+
}]));
|
|
21557
21686
|
const live = await this.store.read().catch(() => null);
|
|
21558
21687
|
if (live === null) return false;
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
|
|
21568
|
-
|
|
21569
|
-
|
|
21570
|
-
|
|
21571
|
-
|
|
21688
|
+
return await this.updatePool((draft) => {
|
|
21689
|
+
let changed = false;
|
|
21690
|
+
for (const account of draft.accounts) {
|
|
21691
|
+
const expected = targets.get(account.id);
|
|
21692
|
+
if (!expected) continue;
|
|
21693
|
+
if (account.authStatus !== expected.authStatus) continue;
|
|
21694
|
+
if (!isDeepStrictEqual(account.credentials, expected.credentials)) continue;
|
|
21695
|
+
if (!minimaxCodeSameCredentialSession(live, account.credentials)) continue;
|
|
21696
|
+
if (!minimaxCodeCredentialAdvancedPast(live, account.credentials)) continue;
|
|
21697
|
+
account.authStatus = void 0;
|
|
21698
|
+
account.authFailedReason = void 0;
|
|
21699
|
+
account.credentials = live;
|
|
21700
|
+
changed = true;
|
|
21701
|
+
}
|
|
21702
|
+
return changed;
|
|
21703
|
+
});
|
|
21572
21704
|
}
|
|
21573
21705
|
/**
|
|
21574
21706
|
* Pick the account for the next request.
|
|
@@ -21582,7 +21714,13 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21582
21714
|
const effective = await this.getEffectiveAccount(excludeIds, fetchFn);
|
|
21583
21715
|
const live = await this.liveNativeCredential(effective.account);
|
|
21584
21716
|
if (live === null) return effective;
|
|
21585
|
-
if (live.accessToken !== effective.credentials.accessToken)
|
|
21717
|
+
if (live.accessToken !== effective.credentials.accessToken) {
|
|
21718
|
+
const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
|
|
21719
|
+
if (updated) return {
|
|
21720
|
+
account: updated,
|
|
21721
|
+
credentials: updated.credentials
|
|
21722
|
+
};
|
|
21723
|
+
}
|
|
21586
21724
|
return {
|
|
21587
21725
|
account: effective.account,
|
|
21588
21726
|
credentials: live
|
|
@@ -21644,7 +21782,7 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21644
21782
|
return this.addAccount(credentials, alias);
|
|
21645
21783
|
}
|
|
21646
21784
|
/**
|
|
21647
|
-
*
|
|
21785
|
+
* Remove one account inside a transaction.
|
|
21648
21786
|
*
|
|
21649
21787
|
* An account whose credential is the desktop app's own \`auth.json\` is refused:
|
|
21650
21788
|
* this plugin does not own that file, and removing the account would either
|
|
@@ -21652,9 +21790,9 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21652
21790
|
* file — sign the user out of MiniMax Code itself. Signing out there is the
|
|
21653
21791
|
* supported way to end it, and the card is told so by \`removable: false\`.
|
|
21654
21792
|
*/
|
|
21655
|
-
|
|
21656
|
-
if (
|
|
21657
|
-
|
|
21793
|
+
deleteAccountFromPool(data, accountId) {
|
|
21794
|
+
if (data.accounts.find((account) => account.id === accountId)?.source === "minimax-native") throw new Error("该账号来自 MiniMax Code 桌面端(~/.minimax/auth),本插件只能读取和续期,不能删除。请在 MiniMax Code 应用中退出登录。");
|
|
21795
|
+
super.deleteAccountFromPool(data, accountId);
|
|
21658
21796
|
}
|
|
21659
21797
|
/** The credential store this pool mirrors its primary account into. */
|
|
21660
21798
|
mirrorStore() {
|
|
@@ -26157,15 +26295,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26157
26295
|
this.options_selection = options.selection;
|
|
26158
26296
|
}
|
|
26159
26297
|
/**
|
|
26160
|
-
*
|
|
26298
|
+
* Load the pool, refreshing the legacy-key index on the way.
|
|
26161
26299
|
*
|
|
26162
|
-
* Every routing decision goes through this method before it
|
|
26163
|
-
* settings selection, so building the index here is what keeps
|
|
26164
|
-
* before the identity fix resolvable when
|
|
26165
|
-
* for it.
|
|
26300
|
+
* Every routing decision and mutation goes through this method before it
|
|
26301
|
+
* consults the settings selection, so building the index here is what keeps
|
|
26302
|
+
* a pin written before the identity fix resolvable when
|
|
26303
|
+
* {@link resolveSelectionId} is asked for it.
|
|
26166
26304
|
*/
|
|
26167
|
-
async
|
|
26168
|
-
const data = await super.
|
|
26305
|
+
async loadPoolData() {
|
|
26306
|
+
const data = await super.loadPoolData();
|
|
26169
26307
|
this.legacyIds.clear();
|
|
26170
26308
|
for (const account of data.accounts) for (const alias of workBuddyAccountIdAliases(account.credentials)) if (alias !== account.id) this.legacyIds.set(alias, account.id);
|
|
26171
26309
|
return data;
|
|
@@ -26185,20 +26323,34 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26185
26323
|
*/
|
|
26186
26324
|
async syncDesktopAccounts() {
|
|
26187
26325
|
const discovered = await this.store.discoverDesktopCredentials();
|
|
26188
|
-
|
|
26189
|
-
|
|
26190
|
-
|
|
26191
|
-
const
|
|
26192
|
-
|
|
26193
|
-
|
|
26194
|
-
|
|
26195
|
-
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26326
|
+
const candidates = [];
|
|
26327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26328
|
+
for (const raw of discovered) {
|
|
26329
|
+
const credentials = withResolvedIdentity({
|
|
26330
|
+
...raw,
|
|
26331
|
+
source: "desktop"
|
|
26332
|
+
});
|
|
26333
|
+
const id = workBuddyAccountId(credentials);
|
|
26334
|
+
if (seen.has(id)) continue;
|
|
26335
|
+
seen.add(id);
|
|
26336
|
+
candidates.push(credentials);
|
|
26337
|
+
}
|
|
26338
|
+
if (candidates.length > 0) await this.updatePool((data) => {
|
|
26339
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
26340
|
+
for (const account of data.accounts) {
|
|
26341
|
+
existingKeys.add(account.id);
|
|
26342
|
+
const key = this.hooks.dedupeKey?.(account.credentials);
|
|
26343
|
+
if (key !== void 0) existingKeys.add(key);
|
|
26200
26344
|
}
|
|
26201
|
-
|
|
26345
|
+
for (const credentials of candidates) {
|
|
26346
|
+
const key = this.hooks.dedupeKey?.(credentials);
|
|
26347
|
+
const id = this.hooks.accountId?.(credentials);
|
|
26348
|
+
if (key !== void 0 && existingKeys.has(key) || id !== void 0 && existingKeys.has(id)) continue;
|
|
26349
|
+
this.addAccountToPool(data, credentials);
|
|
26350
|
+
if (key !== void 0) existingKeys.add(key);
|
|
26351
|
+
if (id !== void 0) existingKeys.add(id);
|
|
26352
|
+
}
|
|
26353
|
+
});
|
|
26202
26354
|
return this.listAccounts();
|
|
26203
26355
|
}
|
|
26204
26356
|
/**
|
|
@@ -26214,7 +26366,13 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26214
26366
|
const live = await this.store.readDesktopFile(effective.account.sourceFile);
|
|
26215
26367
|
if (live === null) return effective;
|
|
26216
26368
|
if (isExpired(live)) return effective;
|
|
26217
|
-
if (live.accessToken !== effective.credentials.accessToken)
|
|
26369
|
+
if (live.accessToken !== effective.credentials.accessToken) {
|
|
26370
|
+
const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
|
|
26371
|
+
if (updated) return {
|
|
26372
|
+
account: updated,
|
|
26373
|
+
credentials: updated.credentials
|
|
26374
|
+
};
|
|
26375
|
+
}
|
|
26218
26376
|
return {
|
|
26219
26377
|
account: effective.account,
|
|
26220
26378
|
credentials: live
|
|
@@ -26251,15 +26409,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26251
26409
|
return super.addAccount(withResolvedIdentity(credentials), alias);
|
|
26252
26410
|
}
|
|
26253
26411
|
/**
|
|
26254
|
-
*
|
|
26412
|
+
* Remove one account inside a transaction.
|
|
26255
26413
|
*
|
|
26256
26414
|
* A desktop account is refused: the plugin does not own that credential file,
|
|
26257
26415
|
* so removing it from the pool would strand the IDE's session while telling
|
|
26258
26416
|
* the user their account was deleted. Hiding is the supported action there.
|
|
26259
26417
|
*/
|
|
26260
|
-
|
|
26261
|
-
if (
|
|
26262
|
-
|
|
26418
|
+
deleteAccountFromPool(data, accountId) {
|
|
26419
|
+
if (data.accounts.find((account) => account.id === accountId)?.source === "desktop") throw new Error("桌面账号由 CodeBuddy 客户端所有,只能隐藏,不能删除。");
|
|
26420
|
+
super.deleteAccountFromPool(data, accountId);
|
|
26263
26421
|
}
|
|
26264
26422
|
};
|
|
26265
26423
|
//#endregion
|
|
@@ -32577,7 +32735,7 @@ function isAdoptedCredentialExpired(credential, now = Date.now()) {
|
|
|
32577
32735
|
* caller in the process, so with a pool it would hand the second account the
|
|
32578
32736
|
* first account's freshly rotated credential — and, through the adapter above,
|
|
32579
32737
|
* write it into the second account's record. The pool therefore keeps its OWN
|
|
32580
|
-
* single-flight, per
|
|
32738
|
+
* single-flight, per pool file and account, which preserves the property
|
|
32581
32739
|
* the frozen module wanted (one rotation per expiry, no two callers spending the
|
|
32582
32740
|
* same rotating token) without the cross-account contamination.
|
|
32583
32741
|
*
|
|
@@ -32963,7 +33121,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
32963
33121
|
* marked expired immediately after a refresh that in fact succeeded. Keyed by
|
|
32964
33122
|
* the record's id — never by a token — so it cannot mix two accounts up.
|
|
32965
33123
|
*/
|
|
32966
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
32967
33124
|
constructor(options = {}) {
|
|
32968
33125
|
const store = options.store ?? new FileCredentialStore();
|
|
32969
33126
|
const refreshSeam = options.refreshCredentials ?? ((credentials, fetchFn) => refreshClaudeCredential(credentials, fetchFn));
|
|
@@ -33010,6 +33167,10 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33010
33167
|
if (record === void 0) return null;
|
|
33011
33168
|
return poolAccountForRecord(record, true);
|
|
33012
33169
|
},
|
|
33170
|
+
credentialsCommitted: async (account) => {
|
|
33171
|
+
if (isAdoptedPoolCredential(account.credentials)) return;
|
|
33172
|
+
await store.saveAccount(account.credentials, { internalId: account.id });
|
|
33173
|
+
},
|
|
33013
33174
|
mirrorPrimary: async (credentials) => {
|
|
33014
33175
|
if (credentials === null) {
|
|
33015
33176
|
await store.delete();
|
|
@@ -33071,8 +33232,8 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33071
33232
|
* {@link recordIdsFor} reads is kept in step with the rows that actually exist.
|
|
33072
33233
|
* See the recordIds field for why entries are not pruned.
|
|
33073
33234
|
*/
|
|
33074
|
-
async
|
|
33075
|
-
const data = await super.
|
|
33235
|
+
async loadPoolData() {
|
|
33236
|
+
const data = await super.loadPoolData();
|
|
33076
33237
|
for (const account of data.accounts) this.indexAccount(account);
|
|
33077
33238
|
return data;
|
|
33078
33239
|
}
|
|
@@ -33085,10 +33246,27 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33085
33246
|
* 'one account, one key' section.
|
|
33086
33247
|
*/
|
|
33087
33248
|
async addAccount(credentials, alias) {
|
|
33088
|
-
|
|
33249
|
+
return this.updatePool((data) => this.addManagedAccount(data, credentials, alias)).catch((error) => {
|
|
33250
|
+
this.recordIds.clear();
|
|
33251
|
+
throw error;
|
|
33252
|
+
});
|
|
33253
|
+
}
|
|
33254
|
+
async addManagedAccount(data, credentials, alias) {
|
|
33089
33255
|
if (isAdoptedPoolCredential(credentials)) return this.addAdoptedAccount(credentials, data, alias);
|
|
33256
|
+
parseClaudeCredentials(credentials);
|
|
33090
33257
|
const knownId = await this.recordIdFor(credentials);
|
|
33091
|
-
const
|
|
33258
|
+
const document = await this.store.read();
|
|
33259
|
+
const keys = identityKeysFor(credentials);
|
|
33260
|
+
const stored = document?.accounts.find((record) => record.identityKeys.some((key) => keys.includes(key)));
|
|
33261
|
+
const record = stored === void 0 ? {
|
|
33262
|
+
internalId: data.accounts.find((account) => account.id === knownId)?.adopted === false ? knownId ?? createInternalId() : createInternalId(),
|
|
33263
|
+
identityKeys: keys,
|
|
33264
|
+
credentials
|
|
33265
|
+
} : {
|
|
33266
|
+
...stored,
|
|
33267
|
+
identityKeys: mergeIdentityKeys(stored.identityKeys, keys),
|
|
33268
|
+
credentials
|
|
33269
|
+
};
|
|
33092
33270
|
this.indexRecord(record);
|
|
33093
33271
|
if (knownId !== void 0 && knownId !== record.internalId) {
|
|
33094
33272
|
const row = data.accounts.find((account) => account.id === knownId);
|
|
@@ -33100,10 +33278,9 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33100
33278
|
delete row.sourcePath;
|
|
33101
33279
|
row.identityKeys = mergeIdentityKeys(row.identityKeys, record.identityKeys);
|
|
33102
33280
|
if (data.activeAccountId === knownId) data.activeAccountId = record.internalId;
|
|
33103
|
-
await this.write(data);
|
|
33104
33281
|
}
|
|
33105
33282
|
}
|
|
33106
|
-
return
|
|
33283
|
+
return this.addAccountToPool(data, credentials, alias);
|
|
33107
33284
|
}
|
|
33108
33285
|
/**
|
|
33109
33286
|
* Add, or re-import, one borrowed Claude Code snapshot.
|
|
@@ -33132,9 +33309,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33132
33309
|
const candidateId = byIdentity ?? this.recordIds.get(sourceKey);
|
|
33133
33310
|
const existing = candidateId === void 0 ? void 0 : data.accounts.find((account) => account.id === candidateId);
|
|
33134
33311
|
if (existing !== void 0 && existing.adopted !== true) {
|
|
33135
|
-
const before = existing.identityKeys.length;
|
|
33136
33312
|
existing.identityKeys = mergeIdentityKeys(existing.identityKeys, identityKeysFor(credentials));
|
|
33137
|
-
if (existing.identityKeys.length !== before) await this.write(data);
|
|
33138
33313
|
return existing;
|
|
33139
33314
|
}
|
|
33140
33315
|
if (candidateId === void 0) {
|
|
@@ -33145,7 +33320,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33145
33320
|
this.recordIds.set(sourceKey, candidateId);
|
|
33146
33321
|
this.indexCredential(credentials, candidateId);
|
|
33147
33322
|
}
|
|
33148
|
-
return
|
|
33323
|
+
return this.addAccountToPool(data, credentials, alias);
|
|
33149
33324
|
}
|
|
33150
33325
|
/**
|
|
33151
33326
|
* Merge two rows that are one account.
|
|
@@ -33168,7 +33343,21 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33168
33343
|
*/
|
|
33169
33344
|
async mergeAccounts(keepAccountId, dropAccountId) {
|
|
33170
33345
|
if (keepAccountId === dropAccountId) throw new Error("A Claude pool merge needs two different accounts.");
|
|
33171
|
-
|
|
33346
|
+
let originals;
|
|
33347
|
+
return await this.updatePool(async (data) => {
|
|
33348
|
+
const keep = data.accounts.find((account) => account.id === keepAccountId);
|
|
33349
|
+
const drop = data.accounts.find((account) => account.id === dropAccountId);
|
|
33350
|
+
if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
|
|
33351
|
+
originals = {
|
|
33352
|
+
keep,
|
|
33353
|
+
drop
|
|
33354
|
+
};
|
|
33355
|
+
return this.mergeAccountsInPool(data, keepAccountId, dropAccountId);
|
|
33356
|
+
}, async (_data, result) => {
|
|
33357
|
+
if (originals !== void 0) await this.reconcileMergedDocument(originals.keep, originals.drop, result);
|
|
33358
|
+
});
|
|
33359
|
+
}
|
|
33360
|
+
async mergeAccountsInPool(data, keepAccountId, dropAccountId) {
|
|
33172
33361
|
const keep = data.accounts.find((account) => account.id === keepAccountId);
|
|
33173
33362
|
const drop = data.accounts.find((account) => account.id === dropAccountId);
|
|
33174
33363
|
if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
|
|
@@ -33217,9 +33406,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33217
33406
|
data.accounts[index] = merged;
|
|
33218
33407
|
data.accounts = data.accounts.filter((account) => account.id !== dropAccountId);
|
|
33219
33408
|
if (data.activeAccountId === dropAccountId) data.activeAccountId = merged.id;
|
|
33220
|
-
await this.write(data);
|
|
33221
|
-
if (merged.isPrimary) this.hooks.mirrorPrimary?.(merged.credentials).catch(() => void 0);
|
|
33222
|
-
await this.reconcileMergedDocument(keep, drop, merged);
|
|
33223
33409
|
return merged;
|
|
33224
33410
|
}
|
|
33225
33411
|
/**
|
|
@@ -33259,10 +33445,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33259
33445
|
* neither path.
|
|
33260
33446
|
*/
|
|
33261
33447
|
async removeImportedAccount(accountId) {
|
|
33262
|
-
|
|
33263
|
-
|
|
33264
|
-
|
|
33265
|
-
|
|
33448
|
+
return this.updatePool((data) => {
|
|
33449
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
33450
|
+
if (target === void 0) return;
|
|
33451
|
+
if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
|
|
33452
|
+
super.deleteAccountFromPool(data, accountId);
|
|
33453
|
+
});
|
|
33266
33454
|
}
|
|
33267
33455
|
/**
|
|
33268
33456
|
* Delete one plugin-owned account.
|
|
@@ -33275,9 +33463,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33275
33463
|
* back a credential the user just deleted.
|
|
33276
33464
|
*/
|
|
33277
33465
|
async deleteAccount(accountId) {
|
|
33278
|
-
|
|
33279
|
-
|
|
33280
|
-
|
|
33466
|
+
return this.updatePool((data) => {
|
|
33467
|
+
if (data.accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from Claude Code is not an account this plugin owns; remove the import instead.");
|
|
33468
|
+
super.deleteAccountFromPool(data, accountId);
|
|
33469
|
+
}, async () => {
|
|
33470
|
+
await this.store.deleteAccount(accountId);
|
|
33471
|
+
});
|
|
33281
33472
|
}
|
|
33282
33473
|
/**
|
|
33283
33474
|
* The immutable id of the record that owns a credential, or nothing.
|
|
@@ -33357,44 +33548,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33357
33548
|
*/
|
|
33358
33549
|
refreshCredentials(credentials, fetchFn) {
|
|
33359
33550
|
if (isAdoptedPoolCredential(credentials)) return Promise.reject(new ClaudeAdoptedCredentialError(adoptedCredentialExpired(credentials) ? ADOPTED_CREDENTIAL_EXPIRED_HINT : ADOPTED_NEVER_REFRESHED_MESSAGE));
|
|
33360
|
-
return this.
|
|
33361
|
-
}
|
|
33362
|
-
/**
|
|
33363
|
-
* The per-account single-flight, resolving the account's id first.
|
|
33364
|
-
*
|
|
33365
|
-
* Separated from the hook above only because resolving the id consults the
|
|
33366
|
-
* credential document, and a hook cannot await: the guard stays synchronous so
|
|
33367
|
-
* it can never be skipped by a caller that forgets to wait.
|
|
33368
|
-
*/
|
|
33369
|
-
async singleFlightRefresh(credentials, fetchFn) {
|
|
33370
|
-
const key = await this.recordIdFor(credentials);
|
|
33371
|
-
if (key !== void 0) {
|
|
33372
|
-
const shared = this.inFlight.get(key);
|
|
33373
|
-
if (shared !== void 0) return shared;
|
|
33374
|
-
}
|
|
33375
|
-
const pending = this.performRefresh(credentials, fetchFn);
|
|
33376
|
-
if (key !== void 0) {
|
|
33377
|
-
this.inFlight.set(key, pending);
|
|
33378
|
-
pending.then(() => void 0, () => void 0).then(() => {
|
|
33379
|
-
if (this.inFlight.get(key) === pending) this.inFlight.delete(key);
|
|
33380
|
-
});
|
|
33381
|
-
}
|
|
33382
|
-
return pending;
|
|
33383
|
-
}
|
|
33384
|
-
/**
|
|
33385
|
-
* Rotate one managed credential and keep the credential document in step.
|
|
33386
|
-
*
|
|
33387
|
-
* The document write is what stops the pre-pool projection from handing out a
|
|
33388
|
-
* token the rotation has already spent. Its failure is swallowed on purpose:
|
|
33389
|
-
* the credential WAS refreshed, the pool's own record holds it, and a mirror
|
|
33390
|
-
* that could not be written must not fail a request that can still be served —
|
|
33391
|
-
* the same rule the core applies to its own bookkeeping writes.
|
|
33392
|
-
*/
|
|
33393
|
-
async performRefresh(credentials, fetchFn) {
|
|
33394
|
-
const recordId = await this.recordIdFor(credentials);
|
|
33395
|
-
const refreshed = await this.refreshSeam(credentials, fetchFn);
|
|
33396
|
-
if (recordId !== void 0) await new ClaudeCredentialStoreAdapter(this.store, recordId).write(refreshed).catch(() => void 0);
|
|
33397
|
-
return refreshed;
|
|
33551
|
+
return this.refreshSeam(credentials, fetchFn);
|
|
33398
33552
|
}
|
|
33399
33553
|
};
|
|
33400
33554
|
//#endregion
|
|
@@ -37112,9 +37266,9 @@ function appendWindow(current, addition, limit) {
|
|
|
37112
37266
|
/**
|
|
37113
37267
|
* Install the reasoning-collapse guard on a Context.
|
|
37114
37268
|
*
|
|
37115
|
-
* The guard is inert unless the host exposes the
|
|
37116
|
-
*
|
|
37117
|
-
*
|
|
37269
|
+
* The guard is inert unless the host exposes the event bus, so a harness
|
|
37270
|
+
* without it keeps the built-in behaviour instead of failing this plugin's
|
|
37271
|
+
* load.
|
|
37118
37272
|
*
|
|
37119
37273
|
* Resuming needs the live Agent for `cancel` and `steer`, and no supported
|
|
37120
37274
|
* generation hands the agent to `llm/stream`. `agent/created` reports it on
|
|
@@ -37124,19 +37278,19 @@ function appendWindow(current, addition, limit) {
|
|
|
37124
37278
|
*
|
|
37125
37279
|
* @param ctx - plugin context.
|
|
37126
37280
|
* @param options - see {@link GuardOptions}; validated fail-loud.
|
|
37127
|
-
* @returns a disposer, or undefined when the
|
|
37281
|
+
* @returns a disposer removing both listeners, or undefined when the host
|
|
37282
|
+
* exposes no event bus.
|
|
37128
37283
|
*/
|
|
37129
37284
|
function installReasoningCollapseGuard(ctx, options = {}) {
|
|
37130
37285
|
const resolved = resolveGuardOptions(options);
|
|
37131
|
-
|
|
37132
|
-
if (runtime === void 0 || typeof runtime.stream !== "function") return void 0;
|
|
37133
|
-
const original = runtime.stream;
|
|
37286
|
+
if (typeof ctx.on !== "function") return void 0;
|
|
37134
37287
|
const watched = new Set(resolved.includeModels);
|
|
37135
37288
|
const breakers = /* @__PURE__ */ new WeakMap();
|
|
37136
37289
|
let currentAgent;
|
|
37137
|
-
const releaseAgent =
|
|
37138
|
-
currentAgent = agent;
|
|
37139
|
-
})
|
|
37290
|
+
const releaseAgent = ctx.on("agent/created", (payload) => {
|
|
37291
|
+
currentAgent = payload.agent;
|
|
37292
|
+
});
|
|
37293
|
+
const releaseStream = ctx.on("llm/stream", guardStream, { global: true });
|
|
37140
37294
|
function breakerFor(key) {
|
|
37141
37295
|
let state = breakers.get(key);
|
|
37142
37296
|
if (state === void 0) {
|
|
@@ -37174,7 +37328,7 @@ function installReasoningCollapseGuard(ctx, options = {}) {
|
|
|
37174
37328
|
agent.steer?.(createResumeMessage(text));
|
|
37175
37329
|
});
|
|
37176
37330
|
}
|
|
37177
|
-
|
|
37331
|
+
function guardStream(request, next) {
|
|
37178
37332
|
const state = breakerFor(request.signal ?? STATIC_KEY);
|
|
37179
37333
|
if (state.exhausted || !watchedModel(request.model)) return next();
|
|
37180
37334
|
async function* guarded() {
|
|
@@ -37196,9 +37350,9 @@ function installReasoningCollapseGuard(ctx, options = {}) {
|
|
|
37196
37350
|
}
|
|
37197
37351
|
}
|
|
37198
37352
|
return guarded();
|
|
37199
|
-
}
|
|
37353
|
+
}
|
|
37200
37354
|
return () => {
|
|
37201
|
-
|
|
37355
|
+
releaseListener(releaseStream);
|
|
37202
37356
|
releaseListener(releaseAgent);
|
|
37203
37357
|
};
|
|
37204
37358
|
}
|
|
@@ -37290,7 +37444,7 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
37290
37444
|
ctx.effect(() => {
|
|
37291
37445
|
const dispose = installReasoningCollapseGuard(ctx, guardOptions);
|
|
37292
37446
|
if (dispose === void 0) {
|
|
37293
|
-
ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no
|
|
37447
|
+
ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no event bus");
|
|
37294
37448
|
return () => void 0;
|
|
37295
37449
|
}
|
|
37296
37450
|
return dispose;
|