@juspay/neurolink 9.86.4 → 9.87.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/CHANGELOG.md +12 -0
- package/dist/adapters/video/replicateVideoHandler.js +27 -17
- package/dist/auth/tokenStore.d.ts +6 -0
- package/dist/auth/tokenStore.js +36 -4
- package/dist/browser/neurolink.min.js +417 -417
- package/dist/cli/commands/auth.js +3 -0
- package/dist/cli/commands/proxy.js +310 -107
- package/dist/lib/adapters/video/replicateVideoHandler.js +27 -17
- package/dist/lib/auth/tokenStore.d.ts +6 -0
- package/dist/lib/auth/tokenStore.js +36 -4
- package/dist/lib/proxy/accountCooldown.d.ts +5 -0
- package/dist/lib/proxy/accountCooldown.js +93 -0
- package/dist/lib/proxy/accountQuota.d.ts +3 -4
- package/dist/lib/proxy/accountQuota.js +75 -44
- package/dist/lib/proxy/accountSelection.d.ts +8 -0
- package/dist/lib/proxy/accountSelection.js +26 -0
- package/dist/lib/proxy/proxyConfig.js +24 -0
- package/dist/lib/proxy/proxyPaths.js +2 -0
- package/dist/lib/proxy/requestLogger.js +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +1 -1
- package/dist/lib/proxy/sseInterceptor.js +16 -0
- package/dist/lib/proxy/streamOutcome.d.ts +3 -0
- package/dist/lib/proxy/streamOutcome.js +27 -0
- package/dist/lib/proxy/tokenRefresh.d.ts +8 -0
- package/dist/lib/proxy/tokenRefresh.js +92 -15
- package/dist/lib/proxy/updateState.js +17 -4
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +18 -3
- package/dist/lib/server/routes/claudeProxyRoutes.js +624 -223
- package/dist/lib/types/auth.d.ts +1 -1
- package/dist/lib/types/cli.d.ts +2 -0
- package/dist/lib/types/multimodal.d.ts +12 -0
- package/dist/lib/types/proxy.d.ts +49 -8
- package/dist/lib/types/subscription.d.ts +5 -0
- package/dist/proxy/accountCooldown.d.ts +5 -0
- package/dist/proxy/accountCooldown.js +92 -0
- package/dist/proxy/accountQuota.d.ts +3 -4
- package/dist/proxy/accountQuota.js +75 -44
- package/dist/proxy/accountSelection.d.ts +8 -0
- package/dist/proxy/accountSelection.js +25 -0
- package/dist/proxy/proxyConfig.js +24 -0
- package/dist/proxy/proxyPaths.js +2 -0
- package/dist/proxy/requestLogger.js +2 -0
- package/dist/proxy/snapshotPersistence.js +1 -1
- package/dist/proxy/sseInterceptor.js +16 -0
- package/dist/proxy/streamOutcome.d.ts +3 -0
- package/dist/proxy/streamOutcome.js +26 -0
- package/dist/proxy/tokenRefresh.d.ts +8 -0
- package/dist/proxy/tokenRefresh.js +92 -15
- package/dist/proxy/updateState.js +17 -4
- package/dist/server/routes/claudeProxyRoutes.d.ts +18 -3
- package/dist/server/routes/claudeProxyRoutes.js +624 -223
- package/dist/types/auth.d.ts +1 -1
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/multimodal.d.ts +12 -0
- package/dist/types/proxy.d.ts +49 -8
- package/dist/types/subscription.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
2
|
import { tokenStore } from "../auth/tokenStore.js";
|
|
3
|
+
import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
|
|
3
4
|
const REFRESH_URL = "https://api.anthropic.com/v1/oauth/token";
|
|
4
5
|
const REFRESH_URL_FALLBACK = "https://console.anthropic.com/v1/oauth/token";
|
|
5
6
|
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
6
|
-
const BUFFER_MS =
|
|
7
|
+
const BUFFER_MS = 5 * 60 * 1000;
|
|
8
|
+
const SUCCESS_CACHE_MS = 60_000;
|
|
7
9
|
const USER_AGENT = "claude-cli/2.1.80 (external, cli)";
|
|
10
|
+
const refreshesInFlight = new Map();
|
|
8
11
|
export function needsRefresh(account) {
|
|
9
12
|
return !!(account.expiresAt &&
|
|
10
13
|
account.expiresAt <= Date.now() + BUFFER_MS &&
|
|
11
14
|
account.refreshToken);
|
|
12
15
|
}
|
|
13
|
-
|
|
16
|
+
function isRefreshCredentialRejection(status) {
|
|
17
|
+
return status === 400 || status === 401 || status === 403 || status === 404;
|
|
18
|
+
}
|
|
19
|
+
export function isPermanentRefreshFailure(result) {
|
|
20
|
+
return isRefreshCredentialRejection(result.status);
|
|
21
|
+
}
|
|
22
|
+
async function performTokenRefresh(account) {
|
|
14
23
|
if (!account.refreshToken) {
|
|
15
24
|
return { success: false, error: "No refresh token available" };
|
|
16
25
|
}
|
|
@@ -24,6 +33,7 @@ export async function refreshToken(account) {
|
|
|
24
33
|
"User-Agent": USER_AGENT,
|
|
25
34
|
};
|
|
26
35
|
const urls = [REFRESH_URL, REFRESH_URL_FALLBACK];
|
|
36
|
+
let terminalFailure;
|
|
27
37
|
for (const url of urls) {
|
|
28
38
|
try {
|
|
29
39
|
const resp = await fetch(url, {
|
|
@@ -38,21 +48,26 @@ export async function refreshToken(account) {
|
|
|
38
48
|
status: resp.status,
|
|
39
49
|
error: errorBody.slice(0, 500),
|
|
40
50
|
});
|
|
41
|
-
|
|
51
|
+
const failure = {
|
|
52
|
+
success: false,
|
|
53
|
+
error: errorBody,
|
|
54
|
+
status: resp.status,
|
|
55
|
+
};
|
|
56
|
+
if (isRefreshCredentialRejection(resp.status)) {
|
|
57
|
+
terminalFailure = failure;
|
|
58
|
+
}
|
|
59
|
+
// If primary URL returned a non-ok status, try fallback.
|
|
42
60
|
if (url === REFRESH_URL) {
|
|
43
61
|
continue;
|
|
44
62
|
}
|
|
45
|
-
return
|
|
63
|
+
return terminalFailure ?? failure;
|
|
46
64
|
}
|
|
47
65
|
const data = (await resp.json());
|
|
48
|
-
const previousExpiresAt = account.expiresAt;
|
|
49
66
|
account.token = data.access_token;
|
|
50
67
|
account.expiresAt =
|
|
51
68
|
data.expires_in !== undefined
|
|
52
69
|
? Date.now() + data.expires_in * 1000
|
|
53
|
-
:
|
|
54
|
-
? previousExpiresAt
|
|
55
|
-
: Date.now() + 55 * 60 * 1000;
|
|
70
|
+
: Date.now() + 55 * 60 * 1000;
|
|
56
71
|
if (data.refresh_token) {
|
|
57
72
|
account.refreshToken = data.refresh_token;
|
|
58
73
|
}
|
|
@@ -67,11 +82,77 @@ export async function refreshToken(account) {
|
|
|
67
82
|
if (url === REFRESH_URL) {
|
|
68
83
|
continue;
|
|
69
84
|
}
|
|
70
|
-
return { success: false, error: String(e) };
|
|
85
|
+
return terminalFailure ?? { success: false, error: String(e) };
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
// Should not reach here, but guard against empty urls array
|
|
74
|
-
return
|
|
89
|
+
return (terminalFailure ?? {
|
|
90
|
+
success: false,
|
|
91
|
+
error: "No refresh URLs available",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Serialize refreshes by refresh-token value. OAuth refresh tokens may rotate,
|
|
96
|
+
* so concurrent refreshes with the same old token can make all but the first
|
|
97
|
+
* request fail with invalid_grant. A short success cache covers callers that
|
|
98
|
+
* loaded either the old token or the newly rotated token around persistence.
|
|
99
|
+
*/
|
|
100
|
+
export async function refreshToken(account) {
|
|
101
|
+
if (!account.refreshToken) {
|
|
102
|
+
return { success: false, error: "No refresh token available" };
|
|
103
|
+
}
|
|
104
|
+
const lockKey = account.refreshToken;
|
|
105
|
+
let sharedPromise = refreshesInFlight.get(lockKey);
|
|
106
|
+
if (!sharedPromise) {
|
|
107
|
+
const refreshAccount = { ...account };
|
|
108
|
+
const createdPromise = performTokenRefresh(refreshAccount).then((result) => {
|
|
109
|
+
const shared = {
|
|
110
|
+
result,
|
|
111
|
+
token: refreshAccount.token,
|
|
112
|
+
refreshToken: refreshAccount.refreshToken,
|
|
113
|
+
expiresAt: refreshAccount.expiresAt,
|
|
114
|
+
};
|
|
115
|
+
if (result.success) {
|
|
116
|
+
const cachedKeys = new Set([lockKey]);
|
|
117
|
+
const rotatedKey = refreshAccount.refreshToken;
|
|
118
|
+
if (rotatedKey &&
|
|
119
|
+
rotatedKey !== lockKey &&
|
|
120
|
+
!refreshesInFlight.has(rotatedKey)) {
|
|
121
|
+
refreshesInFlight.set(rotatedKey, createdPromise);
|
|
122
|
+
cachedKeys.add(rotatedKey);
|
|
123
|
+
}
|
|
124
|
+
const timer = setTimeout(() => {
|
|
125
|
+
for (const key of cachedKeys) {
|
|
126
|
+
if (refreshesInFlight.get(key) === createdPromise) {
|
|
127
|
+
refreshesInFlight.delete(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, SUCCESS_CACHE_MS);
|
|
131
|
+
timer.unref?.();
|
|
132
|
+
}
|
|
133
|
+
else if (refreshesInFlight.get(lockKey) === createdPromise) {
|
|
134
|
+
refreshesInFlight.delete(lockKey);
|
|
135
|
+
}
|
|
136
|
+
return shared;
|
|
137
|
+
}, (error) => {
|
|
138
|
+
if (refreshesInFlight.get(lockKey) === createdPromise) {
|
|
139
|
+
refreshesInFlight.delete(lockKey);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
});
|
|
143
|
+
refreshesInFlight.set(lockKey, createdPromise);
|
|
144
|
+
sharedPromise = createdPromise;
|
|
145
|
+
}
|
|
146
|
+
const shared = await sharedPromise;
|
|
147
|
+
if (shared.result.success) {
|
|
148
|
+
account.token = shared.token;
|
|
149
|
+
account.refreshToken = shared.refreshToken;
|
|
150
|
+
account.expiresAt = shared.expiresAt;
|
|
151
|
+
}
|
|
152
|
+
return shared.result;
|
|
153
|
+
}
|
|
154
|
+
export function clearRefreshStateForTests() {
|
|
155
|
+
refreshesInFlight.clear();
|
|
75
156
|
}
|
|
76
157
|
export async function persistTokens(target, account) {
|
|
77
158
|
if (typeof target !== "string" && "providerKey" in target) {
|
|
@@ -92,11 +173,7 @@ async function persistLegacyCredentials(credPath, account) {
|
|
|
92
173
|
refreshToken: account.refreshToken,
|
|
93
174
|
};
|
|
94
175
|
existing.updatedAt = Date.now();
|
|
95
|
-
|
|
96
|
-
await fs.writeFile(tmpPath, JSON.stringify(existing, null, 2), {
|
|
97
|
-
mode: 0o600,
|
|
98
|
-
});
|
|
99
|
-
await fs.rename(tmpPath, credPath);
|
|
176
|
+
await writeJsonSnapshotAtomically(credPath, existing, 0o600);
|
|
100
177
|
}
|
|
101
178
|
catch (err) {
|
|
102
179
|
logger.warn("[token-refresh] Failed to persist legacy credentials", {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import os from "os";
|
|
11
11
|
import path from "path";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
12
13
|
// ============================================
|
|
13
14
|
// Constants
|
|
14
15
|
// ============================================
|
|
@@ -92,10 +93,22 @@ export function loadUpdateState(stateFilePath) {
|
|
|
92
93
|
export function saveUpdateState(state, stateFilePath) {
|
|
93
94
|
const filePath = resolveStatePath(stateFilePath);
|
|
94
95
|
ensureParentDir(filePath);
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
// A process-unique temp path prevents concurrent update checks from renaming
|
|
97
|
+
// each other's file. The guard is single-owner now, but this also keeps
|
|
98
|
+
// explicit update commands safe when they overlap.
|
|
99
|
+
const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
102
|
+
fs.renameSync(tmpPath, filePath);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
try {
|
|
106
|
+
fs.rmSync(tmpPath, { force: true });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort cleanup after a successful rename or interrupted write.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
99
112
|
}
|
|
100
113
|
/**
|
|
101
114
|
* Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window).
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
|
|
13
13
|
import type { ModelRouter } from "../../proxy/modelRouter.js";
|
|
14
|
-
import
|
|
14
|
+
import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
|
|
15
|
+
import type { AccountAllowlist, AccountCooldownPlan, AccountQuota, ParsedClaudeError, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, StreamTerminalOutcome } from "../../types/index.js";
|
|
15
16
|
/** Resolve the configured primary's stable key to its current index in the
|
|
16
17
|
* request's enabledAccounts list. Returns 0 (insertion-order fallback) when
|
|
17
18
|
* no key is configured or the key cannot be matched (account disabled/
|
|
@@ -43,7 +44,7 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
|
|
|
43
44
|
* burst / acceleration limit) is transient: honor retry-after as a floor,
|
|
44
45
|
* allow a couple of jittered same-account retries, then a short cooldown.
|
|
45
46
|
*/
|
|
46
|
-
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number): AccountCooldownPlan;
|
|
47
|
+
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined): AccountCooldownPlan;
|
|
47
48
|
/**
|
|
48
49
|
* Seed each account's runtime quota from the persisted snapshots in
|
|
49
50
|
* ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
|
|
@@ -72,6 +73,15 @@ declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]):
|
|
|
72
73
|
* last resort.
|
|
73
74
|
*/
|
|
74
75
|
declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number): ProxyPassthroughAccount[];
|
|
76
|
+
declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>): {
|
|
77
|
+
stream: ReadableStream<Uint8Array>;
|
|
78
|
+
outcome: Promise<StreamTerminalOutcome>;
|
|
79
|
+
};
|
|
80
|
+
declare function getStreamFailureDetails(outcome: StreamTerminalOutcome): {
|
|
81
|
+
status: number;
|
|
82
|
+
errorType: string;
|
|
83
|
+
message: string;
|
|
84
|
+
} | undefined;
|
|
75
85
|
/**
|
|
76
86
|
* Create Claude-compatible proxy routes.
|
|
77
87
|
*
|
|
@@ -82,7 +92,7 @@ declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now:
|
|
|
82
92
|
* @param basePath - Base path prefix (default: "" since Claude API uses /v1/...).
|
|
83
93
|
* @returns RouteGroup with Claude-compatible endpoints.
|
|
84
94
|
*/
|
|
85
|
-
export declare function createClaudeProxyRoutes(modelRouter?: ModelRouter, basePath?: string, accountStrategy?: "round-robin" | "fill-first", passthroughMode?: boolean, primaryAccountKey?: string): RouteGroup;
|
|
95
|
+
export declare function createClaudeProxyRoutes(modelRouter?: ModelRouter, basePath?: string, accountStrategy?: "round-robin" | "fill-first", passthroughMode?: boolean, primaryAccountKey?: string, accountAllowlist?: AccountAllowlist): RouteGroup;
|
|
86
96
|
export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
|
|
87
97
|
/**
|
|
88
98
|
* Parse a Claude error payload when available.
|
|
@@ -107,12 +117,17 @@ export declare const __testHooks: {
|
|
|
107
117
|
resolveHomeIndex: typeof resolveHomeIndex;
|
|
108
118
|
maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
|
|
109
119
|
planCooldownFor429: typeof planCooldownFor429;
|
|
120
|
+
isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
|
|
121
|
+
getStreamFailureDetails: typeof getStreamFailureDetails;
|
|
122
|
+
trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
|
|
110
123
|
orderAccountsByQuota: typeof orderAccountsByQuota;
|
|
111
124
|
resetEpochToMs: typeof resetEpochToMs;
|
|
112
125
|
seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
|
|
113
126
|
getAccountRuntimeState: (key: string) => RuntimeAccountState | undefined;
|
|
114
127
|
setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
|
|
115
128
|
getConfiguredPrimaryAccountKey: () => string | undefined;
|
|
129
|
+
setConfiguredAccountAllowlist: (allowlist: AccountAllowlist | undefined) => void;
|
|
130
|
+
getConfiguredAccountAllowlist: () => string[] | undefined;
|
|
116
131
|
setPrimaryAccountIndex: (index: number) => void;
|
|
117
132
|
getPrimaryAccountIndex: () => number;
|
|
118
133
|
setAccountRuntimeState: (key: string, state: Partial<RuntimeAccountState>) => void;
|