pi-anthropic-oauth 0.1.0 → 0.1.1
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 -9
- package/src/auth.ts +89 -40
package/package.json
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anthropic-oauth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Claude Pro/Max OAuth extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"pi-package",
|
|
9
|
-
"pi",
|
|
10
|
-
"anthropic",
|
|
11
|
-
"claude",
|
|
12
|
-
"oauth",
|
|
13
|
-
"extension"
|
|
14
|
-
],
|
|
15
7
|
"files": [
|
|
16
8
|
"src",
|
|
17
9
|
"README.md",
|
package/src/auth.ts
CHANGED
|
@@ -16,8 +16,10 @@ const SCOPES = [
|
|
|
16
16
|
"user:mcp_servers",
|
|
17
17
|
"user:file_upload",
|
|
18
18
|
].join(" ");
|
|
19
|
-
const USER_AGENT = "claude-code/2.1.
|
|
19
|
+
const USER_AGENT = "claude-code/2.1.97";
|
|
20
20
|
const LOCAL_CALLBACK_TIMEOUT = 5 * 60 * 1000;
|
|
21
|
+
const MAX_TOKEN_RETRIES = 2;
|
|
22
|
+
const INITIAL_RETRY_DELAY_MS = 5000;
|
|
21
23
|
|
|
22
24
|
export { USER_AGENT };
|
|
23
25
|
|
|
@@ -71,25 +73,23 @@ export async function loginAnthropic(
|
|
|
71
73
|
if (!parsed) throw new Error("Could not parse authorization callback input.");
|
|
72
74
|
if (parsed.state !== state) throw new Error("OAuth state mismatch.");
|
|
73
75
|
|
|
74
|
-
const tokenResponse = await
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
}
|
|
76
|
+
const tokenResponse = await fetchWithRetry(
|
|
77
|
+
TOKEN_URL,
|
|
78
|
+
{
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: makeTokenHeaders(),
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
grant_type: "authorization_code",
|
|
83
|
+
client_id: CLIENT_ID,
|
|
84
|
+
code: parsed.code,
|
|
85
|
+
state: parsed.state,
|
|
86
|
+
redirect_uri: redirectUri,
|
|
87
|
+
code_verifier: verifier,
|
|
88
|
+
}),
|
|
89
|
+
signal: callbacks.signal,
|
|
90
|
+
},
|
|
91
|
+
"Token exchange",
|
|
92
|
+
);
|
|
93
93
|
|
|
94
94
|
const data = (await tokenResponse.json()) as {
|
|
95
95
|
access_token: string;
|
|
@@ -107,20 +107,27 @@ export async function loginAnthropic(
|
|
|
107
107
|
export async function refreshAnthropicToken(
|
|
108
108
|
credentials: OAuthCredentials,
|
|
109
109
|
): Promise<OAuthCredentials> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
let response: Response;
|
|
111
|
+
try {
|
|
112
|
+
response = await fetchWithRetry(
|
|
113
|
+
TOKEN_URL,
|
|
114
|
+
{
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: makeTokenHeaders(),
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
grant_type: "refresh_token",
|
|
119
|
+
client_id: CLIENT_ID,
|
|
120
|
+
refresh_token: credentials.refresh,
|
|
121
|
+
scope: SCOPES,
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
"Token refresh",
|
|
123
125
|
);
|
|
126
|
+
} catch {
|
|
127
|
+
if (credentials.expires > Date.now()) {
|
|
128
|
+
return { ...credentials, expires: Date.now() + 30_000 };
|
|
129
|
+
}
|
|
130
|
+
throw new Error("Token refresh failed and token has expired.");
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
const data = (await response.json()) as {
|
|
@@ -155,11 +162,50 @@ function makeAuthorizeUrl(
|
|
|
155
162
|
return `${AUTHORIZE_URL}?${authParams.toString()}`;
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
async function fetchWithRetry(
|
|
166
|
+
url: string,
|
|
167
|
+
init: RequestInit,
|
|
168
|
+
label: string,
|
|
169
|
+
): Promise<Response> {
|
|
170
|
+
let lastError: Error | undefined;
|
|
171
|
+
|
|
172
|
+
for (let attempt = 0; attempt <= MAX_TOKEN_RETRIES; attempt++) {
|
|
173
|
+
const response = await fetch(url, init);
|
|
174
|
+
|
|
175
|
+
if (response.ok) return response;
|
|
176
|
+
|
|
177
|
+
const bodyText = await response.text();
|
|
178
|
+
|
|
179
|
+
const shouldRetry = response.headers.get("x-should-retry");
|
|
180
|
+
if (shouldRetry === "false") {
|
|
181
|
+
throw new Error(`${label} failed: ${response.status} ${bodyText}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
attempt < MAX_TOKEN_RETRIES &&
|
|
186
|
+
(response.status === 429 || response.status >= 500)
|
|
187
|
+
) {
|
|
188
|
+
const retryAfter = response.headers.get("retry-after");
|
|
189
|
+
const delayMs = retryAfter
|
|
190
|
+
? Math.min(Number(retryAfter) * 1000, 30_000)
|
|
191
|
+
: INITIAL_RETRY_DELAY_MS * 2 ** attempt;
|
|
192
|
+
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
194
|
+
lastError = new Error(
|
|
195
|
+
`${label} failed: ${response.status} ${bodyText}`,
|
|
196
|
+
);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error(`${label} failed: ${response.status} ${bodyText}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw lastError ?? new Error(`${label} failed after retries`);
|
|
204
|
+
}
|
|
205
|
+
|
|
158
206
|
function makeTokenHeaders(): HeadersInit {
|
|
159
207
|
return {
|
|
160
|
-
|
|
161
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
162
|
-
"User-Agent": USER_AGENT,
|
|
208
|
+
"Content-Type": "application/json",
|
|
163
209
|
};
|
|
164
210
|
}
|
|
165
211
|
|
|
@@ -180,11 +226,11 @@ async function createLocalAuthorization(
|
|
|
180
226
|
if (done) return;
|
|
181
227
|
done = true;
|
|
182
228
|
if (timer) clearTimeout(timer);
|
|
229
|
+
complete(value);
|
|
183
230
|
if (server.listening) {
|
|
184
|
-
server.
|
|
185
|
-
|
|
231
|
+
server.closeAllConnections();
|
|
232
|
+
server.close();
|
|
186
233
|
}
|
|
187
|
-
complete(value);
|
|
188
234
|
};
|
|
189
235
|
|
|
190
236
|
server.on("request", (req, res) => {
|
|
@@ -214,7 +260,10 @@ async function createLocalAuthorization(
|
|
|
214
260
|
return;
|
|
215
261
|
}
|
|
216
262
|
|
|
217
|
-
res.writeHead(200, {
|
|
263
|
+
res.writeHead(200, {
|
|
264
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
265
|
+
Connection: "close",
|
|
266
|
+
});
|
|
218
267
|
res.end(makeCallbackPage());
|
|
219
268
|
finish(url.toString());
|
|
220
269
|
});
|