maxpool 1.5.57 → 1.5.59
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/package.json +1 -1
- package/src/oauth.js +31 -10
- package/src/server.js +10 -1
package/package.json
CHANGED
package/src/oauth.js
CHANGED
|
@@ -27,14 +27,17 @@ export function tokenFingerprint(token) {
|
|
|
27
27
|
* Refresh an expired OAuth access token using the refresh token.
|
|
28
28
|
* Retries on 5xx and network errors with exponential backoff.
|
|
29
29
|
*/
|
|
30
|
+
// How long to wait for the user to finish the browser login. Env-overridable.
|
|
31
|
+
const LOGIN_TIMEOUT_MS = Math.max(60_000, Number(process.env.MAXPOOL_LOGIN_TIMEOUT_MS) || 300_000);
|
|
32
|
+
|
|
30
33
|
export async function refreshAccessToken(refreshToken, endpoint = DEFAULT_TOKEN_ENDPOINT) {
|
|
31
34
|
const maxRetries = 2;
|
|
32
35
|
const baseDelayMs = 500;
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
const perAttemptTimeoutMs = 10_000;
|
|
36
|
+
// 30s, not 10s. A refresh aborted by OUR timeout is the dangerous case (see below), so
|
|
37
|
+
// the timeout must be generous enough that a merely slow network never triggers it.
|
|
38
|
+
// Measured 2026-08-03: refreshes aborted at 10s during a degraded-network window, and
|
|
39
|
+
// the retries that followed destroyed 4 accounts.
|
|
40
|
+
const perAttemptTimeoutMs = Math.max(10_000, Number(process.env.MAXPOOL_TOKEN_REFRESH_TIMEOUT_MS) || 30_000);
|
|
38
41
|
|
|
39
42
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
40
43
|
try {
|
|
@@ -83,10 +86,26 @@ export async function refreshAccessToken(refreshToken, endpoint = DEFAULT_TOKEN_
|
|
|
83
86
|
(err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED' ||
|
|
84
87
|
err.code === 'ETIMEDOUT' || err.code === 'UND_ERR_CONNECT_TIMEOUT'));
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
// NEVER re-send a single-use refresh token after an AMBIGUOUS failure. A timeout,
|
|
90
|
+
// an abort, or a reset AFTER the request left us means the server may have processed
|
|
91
|
+
// it and rotated the token — retrying then spends a token that is already dead and
|
|
92
|
+
// the account is destroyed with invalid_grant. Only a failure that provably happened
|
|
93
|
+
// BEFORE the server saw the request (connection refused, DNS) is safe to retry.
|
|
94
|
+
//
|
|
95
|
+
// Measured 2026-08-03: a degraded network aborted refreshes at 10s; the retry loop
|
|
96
|
+
// re-sent each token up to 3 times within seconds, and 4 accounts died with
|
|
97
|
+
// "Refresh token not found or invalid" — requiring a manual re-login each.
|
|
98
|
+
const sentToServer = !(err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND'
|
|
99
|
+
|| err.code === 'EAI_AGAIN' || err.code === 'UND_ERR_CONNECT_TIMEOUT');
|
|
100
|
+
if (attempt < maxRetries && isNetworkError && !sentToServer) {
|
|
87
101
|
continue;
|
|
88
102
|
}
|
|
89
|
-
if (isNetworkError)
|
|
103
|
+
if (isNetworkError) {
|
|
104
|
+
err.retryable = true;
|
|
105
|
+
// Tell the caller the token's fate is UNKNOWN, so it cools the account and tries
|
|
106
|
+
// once later instead of treating this as a clean failure.
|
|
107
|
+
err.ambiguousRefresh = sentToServer;
|
|
108
|
+
}
|
|
90
109
|
throw err;
|
|
91
110
|
}
|
|
92
111
|
}
|
|
@@ -555,11 +574,13 @@ function startCallbackServer(expectedState) {
|
|
|
555
574
|
});
|
|
556
575
|
server.on('error', reject);
|
|
557
576
|
|
|
558
|
-
//
|
|
577
|
+
// Bounded so a forgotten browser tab can't pin the process. 5 minutes, not 2:
|
|
578
|
+
// reported 2026-08-03 that a real login (password + 2FA, several accounts in a row)
|
|
579
|
+
// repeatedly overran 2 minutes and had to be restarted from scratch.
|
|
559
580
|
const timer = setTimeout(() => {
|
|
560
|
-
rejectCode(new Error(
|
|
581
|
+
rejectCode(new Error(`Login timed out after ${Math.round(LOGIN_TIMEOUT_MS / 60_000)} minutes`));
|
|
561
582
|
server.close();
|
|
562
|
-
},
|
|
583
|
+
}, LOGIN_TIMEOUT_MS);
|
|
563
584
|
timer.unref();
|
|
564
585
|
});
|
|
565
586
|
}
|
package/src/server.js
CHANGED
|
@@ -1332,11 +1332,20 @@ async function forwardRequest(
|
|
|
1332
1332
|
// Record the user-facing failure explicitly so it's greppable in the event
|
|
1333
1333
|
// log (the in-memory TUI feed scrolls away). `err` is the last network error.
|
|
1334
1334
|
console.error(`[Maxpool] Returned connection_unavailable (503) after network errors on all routes (last: "${account.name}" — ${err.code || err.message})`);
|
|
1335
|
+
// Name what actually happened. "Check your internet connection" sent the user
|
|
1336
|
+
// hunting a fault that wasn't there: measured 2026-08-04, 10 requests failed this
|
|
1337
|
+
// way within one hour while every other session kept working — the connection to
|
|
1338
|
+
// Anthropic dropped mid-flight (`terminated`) on each account tried, which is not
|
|
1339
|
+
// the same as the machine being offline. Carrying the real code makes it greppable
|
|
1340
|
+
// and stops the misdiagnosis.
|
|
1341
|
+
const lastCode = err.code || err.cause?.code || (err.message || '').slice(0, 40);
|
|
1335
1342
|
sendErrorResponse(res, requestInfo, 503, {
|
|
1336
1343
|
type: 'error',
|
|
1337
1344
|
error: {
|
|
1338
1345
|
type: 'connection_unavailable',
|
|
1339
|
-
message:
|
|
1346
|
+
message: `The connection to Claude dropped on every account maxpool tried (${lastCode}). `
|
|
1347
|
+
+ 'This is not a quota problem, and it is usually brief — sending the message again normally works. '
|
|
1348
|
+
+ 'If it keeps happening, the link between this machine and Anthropic is unstable.',
|
|
1340
1349
|
},
|
|
1341
1350
|
});
|
|
1342
1351
|
return;
|