ampcode-connector 0.1.14 → 0.1.15
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/auth/oauth.ts +98 -13
package/package.json
CHANGED
package/src/auth/oauth.ts
CHANGED
|
@@ -36,18 +36,8 @@ export async function token(config: OAuthConfig, account = 0): Promise<string |
|
|
|
36
36
|
|
|
37
37
|
if (store.fresh(creds)) return creds.accessToken;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch (err) {
|
|
42
|
-
logger.warn(`Token refresh failed for ${config.providerName}:${account}, retrying...`, { error: String(err) });
|
|
43
|
-
try {
|
|
44
|
-
await Bun.sleep(1000);
|
|
45
|
-
return (await refresh(config, creds.refreshToken, account)).accessToken;
|
|
46
|
-
} catch (retryErr) {
|
|
47
|
-
logger.error(`Token refresh retry failed for ${config.providerName}:${account}`, { error: String(retryErr) });
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
39
|
+
const refreshed = await refreshWithRetry(config, creds.refreshToken, account);
|
|
40
|
+
return refreshed?.accessToken ?? null;
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
export async function tokenFromAny(config: OAuthConfig): Promise<{ accessToken: string; account: number } | null> {
|
|
@@ -61,6 +51,7 @@ export async function tokenFromAny(config: OAuthConfig): Promise<{ accessToken:
|
|
|
61
51
|
const refreshed = await refresh(config, c.refreshToken, account);
|
|
62
52
|
return { accessToken: refreshed.accessToken, account };
|
|
63
53
|
} catch (err) {
|
|
54
|
+
handleRefreshFailure(config, account, err);
|
|
64
55
|
logger.debug(`${config.providerName}:${account} refresh failed in tokenFromAny`, { error: String(err) });
|
|
65
56
|
}
|
|
66
57
|
}
|
|
@@ -160,6 +151,29 @@ async function refresh(config: OAuthConfig, refreshToken: string, account = 0):
|
|
|
160
151
|
return credentials;
|
|
161
152
|
}
|
|
162
153
|
|
|
154
|
+
async function refreshWithRetry(
|
|
155
|
+
config: OAuthConfig,
|
|
156
|
+
refreshToken: string,
|
|
157
|
+
account: number,
|
|
158
|
+
): Promise<Credentials | null> {
|
|
159
|
+
try {
|
|
160
|
+
return await refresh(config, refreshToken, account);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (handleRefreshFailure(config, account, err)) return null;
|
|
163
|
+
|
|
164
|
+
logger.warn(`Token refresh failed for ${config.providerName}:${account}, retrying...`, { error: String(err) });
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await Bun.sleep(1000);
|
|
168
|
+
return await refresh(config, refreshToken, account);
|
|
169
|
+
} catch (retryErr) {
|
|
170
|
+
handleRefreshFailure(config, account, retryErr);
|
|
171
|
+
logger.error(`Token refresh retry failed for ${config.providerName}:${account}`, { error: String(retryErr) });
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
163
177
|
async function exchange(config: OAuthConfig, params: Record<string, string>): Promise<Record<string, unknown>> {
|
|
164
178
|
const all: Record<string, string> = { client_id: config.clientId, ...params };
|
|
165
179
|
if (config.clientSecret) all.client_secret = config.clientSecret;
|
|
@@ -173,12 +187,83 @@ async function exchange(config: OAuthConfig, params: Record<string, string>): Pr
|
|
|
173
187
|
|
|
174
188
|
if (!res.ok) {
|
|
175
189
|
const text = await res.text();
|
|
176
|
-
|
|
190
|
+
const oauthError = parseOAuthError(text);
|
|
191
|
+
throw new TokenExchangeError(config.providerName, res.status, text, oauthError.code, oauthError.description);
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
return (await res.json()) as Record<string, unknown>;
|
|
180
195
|
}
|
|
181
196
|
|
|
197
|
+
class TokenExchangeError extends Error {
|
|
198
|
+
readonly status: number;
|
|
199
|
+
readonly responseBody: string;
|
|
200
|
+
readonly errorCode: string | null;
|
|
201
|
+
readonly errorDescription: string | null;
|
|
202
|
+
|
|
203
|
+
constructor(
|
|
204
|
+
providerName: string,
|
|
205
|
+
status: number,
|
|
206
|
+
responseBody: string,
|
|
207
|
+
errorCode: string | null,
|
|
208
|
+
errorDescription: string | null,
|
|
209
|
+
) {
|
|
210
|
+
super(`${providerName} token exchange failed (${status}): ${responseBody}`);
|
|
211
|
+
this.name = "TokenExchangeError";
|
|
212
|
+
this.status = status;
|
|
213
|
+
this.responseBody = responseBody;
|
|
214
|
+
this.errorCode = errorCode;
|
|
215
|
+
this.errorDescription = errorDescription;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseOAuthError(responseBody: string): { code: string | null; description: string | null } {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(responseBody) as { error?: unknown; error_description?: unknown };
|
|
222
|
+
const code = typeof parsed.error === "string" ? parsed.error : null;
|
|
223
|
+
const description = typeof parsed.error_description === "string" ? parsed.error_description : null;
|
|
224
|
+
return { code, description };
|
|
225
|
+
} catch {
|
|
226
|
+
return { code: null, description: null };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Handles terminal refresh failures and returns true when retry should stop. */
|
|
231
|
+
function handleRefreshFailure(config: OAuthConfig, account: number, err: unknown): boolean {
|
|
232
|
+
if (!isInvalidRefreshTokenError(err)) return false;
|
|
233
|
+
if (!store.get(config.providerName, account)) return false;
|
|
234
|
+
|
|
235
|
+
store.remove(config.providerName, account);
|
|
236
|
+
logger.warn(`Removed invalid refresh token for ${config.providerName}:${account}; re-login required.`, {
|
|
237
|
+
error: String(err),
|
|
238
|
+
});
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isInvalidRefreshTokenError(err: unknown): boolean {
|
|
243
|
+
if (!(err instanceof TokenExchangeError)) return false;
|
|
244
|
+
if (err.status !== 400 && err.status !== 401) return false;
|
|
245
|
+
|
|
246
|
+
if (err.errorCode === "invalid_grant") return true;
|
|
247
|
+
|
|
248
|
+
const description = err.errorDescription?.toLowerCase() ?? "";
|
|
249
|
+
const hasRefreshTokenContext = description.includes("refresh token");
|
|
250
|
+
const indicatesInvalidState =
|
|
251
|
+
description.includes("invalid") ||
|
|
252
|
+
description.includes("not found") ||
|
|
253
|
+
description.includes("expired") ||
|
|
254
|
+
description.includes("revoked");
|
|
255
|
+
|
|
256
|
+
if (hasRefreshTokenContext && indicatesInvalidState) return true;
|
|
257
|
+
|
|
258
|
+
const body = err.responseBody.toLowerCase();
|
|
259
|
+
return (
|
|
260
|
+
body.includes("invalid_grant") ||
|
|
261
|
+
body.includes("invalid refresh token") ||
|
|
262
|
+
body.includes("refresh token not found") ||
|
|
263
|
+
body.includes("refresh token is invalid")
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
182
267
|
function parseTokenFields(raw: Record<string, unknown>, config: OAuthConfig): Credentials {
|
|
183
268
|
if (typeof raw.access_token !== "string" || !raw.access_token) {
|
|
184
269
|
throw new Error(`${config.providerName} token response missing access_token`);
|