@ex-machina/opencode-anthropic-auth 1.0.3 → 1.0.4
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/dist/index.js +67 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,6 +27,9 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
// Shared inflight refresh promise — prevents concurrent token refreshes
|
|
31
|
+
// from racing against each other (and causing 401 cascades with token rotation)
|
|
32
|
+
let refreshPromise = null;
|
|
30
33
|
return {
|
|
31
34
|
apiKey: '',
|
|
32
35
|
async fetch(input, init) {
|
|
@@ -34,64 +37,73 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
34
37
|
if (auth.type !== 'oauth')
|
|
35
38
|
return fetch(input, init);
|
|
36
39
|
if (!auth.access || !auth.expires || auth.expires < Date.now()) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
40
|
+
if (!refreshPromise) {
|
|
41
|
+
refreshPromise = (async () => {
|
|
42
|
+
const maxRetries = 2;
|
|
43
|
+
const baseDelayMs = 500;
|
|
44
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
if (attempt > 0) {
|
|
47
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
49
|
+
}
|
|
50
|
+
const response = await fetch(TOKEN_URL, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
Accept: 'application/json, text/plain, */*',
|
|
55
|
+
'User-Agent': 'axios/1.13.6',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
grant_type: 'refresh_token',
|
|
59
|
+
refresh_token: auth.refresh,
|
|
60
|
+
client_id: CLIENT_ID,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
65
|
+
await response.body?.cancel();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
const json = (await response.json());
|
|
71
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
72
|
+
await client.auth.set({
|
|
73
|
+
path: {
|
|
74
|
+
id: 'anthropic',
|
|
75
|
+
},
|
|
76
|
+
body: {
|
|
77
|
+
type: 'oauth',
|
|
78
|
+
refresh: json.refresh_token,
|
|
79
|
+
access: json.access_token,
|
|
80
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return json.access_token;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const isNetworkError = error instanceof Error &&
|
|
87
|
+
(error.message.includes('fetch failed') ||
|
|
88
|
+
('code' in error &&
|
|
89
|
+
(error.code === 'ECONNRESET' ||
|
|
90
|
+
error.code === 'ECONNREFUSED' ||
|
|
91
|
+
error.code === 'ETIMEDOUT' ||
|
|
92
|
+
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
93
|
+
if (attempt < maxRetries && isNetworkError) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
62
97
|
}
|
|
63
|
-
throw new Error(`Token refresh failed: ${response.status}`);
|
|
64
|
-
}
|
|
65
|
-
const json = (await response.json());
|
|
66
|
-
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
67
|
-
await client.auth.set({
|
|
68
|
-
path: {
|
|
69
|
-
id: 'anthropic',
|
|
70
|
-
},
|
|
71
|
-
body: {
|
|
72
|
-
type: 'oauth',
|
|
73
|
-
refresh: json.refresh_token,
|
|
74
|
-
access: json.access_token,
|
|
75
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
auth.access = json.access_token;
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
const isNetworkError = error instanceof Error &&
|
|
83
|
-
(error.message.includes('fetch failed') ||
|
|
84
|
-
('code' in error &&
|
|
85
|
-
(error.code === 'ECONNRESET' ||
|
|
86
|
-
error.code === 'ECONNREFUSED' ||
|
|
87
|
-
error.code === 'ETIMEDOUT' ||
|
|
88
|
-
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
89
|
-
if (attempt < maxRetries && isNetworkError) {
|
|
90
|
-
continue;
|
|
91
98
|
}
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
// Unreachable — each iteration either returns or throws.
|
|
100
|
+
// Kept as a TypeScript exhaustiveness guard.
|
|
101
|
+
throw new Error('Token refresh exhausted all retries');
|
|
102
|
+
})().finally(() => {
|
|
103
|
+
refreshPromise = null;
|
|
104
|
+
});
|
|
94
105
|
}
|
|
106
|
+
auth.access = await refreshPromise;
|
|
95
107
|
}
|
|
96
108
|
const requestHeaders = mergeHeaders(input, init);
|
|
97
109
|
// biome-ignore lint/style/noNonNullAssertion: access is guaranteed set above
|