@sentry/junior-github 0.74.1 → 0.76.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/index.d.ts +2 -4
- package/index.js +142 -51
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PluginRegistration } from "@sentry/junior-plugin-api";
|
|
2
2
|
|
|
3
3
|
export type GitHubAppPermissionLevel = "read" | "write" | "admin";
|
|
4
4
|
|
|
@@ -46,6 +46,4 @@ export interface GitHubPluginOptions {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** Register GitHub manifest content and runtime hooks. */
|
|
49
|
-
export function githubPlugin(
|
|
50
|
-
options?: GitHubPluginOptions,
|
|
51
|
-
): JuniorPluginRegistration;
|
|
49
|
+
export function githubPlugin(options?: GitHubPluginOptions): PluginRegistration;
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
|
|
|
13
13
|
const GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
|
|
14
14
|
const MAX_LEASE_MS = 60 * 60 * 1000;
|
|
15
15
|
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
16
|
+
const USER_REFRESH_TIMEOUT_MS = 20_000;
|
|
16
17
|
const GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
|
|
17
18
|
const HTTP_READ_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
18
19
|
const USER_TOKEN_GRANTS = new Set(["user-read", "user-write"]);
|
|
@@ -290,20 +291,27 @@ function buildOAuthTokenRequest(input) {
|
|
|
290
291
|
};
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
function
|
|
294
|
-
if (!
|
|
294
|
+
function parseOAuthResponseJson(responseText) {
|
|
295
|
+
if (!responseText.trim()) {
|
|
295
296
|
return undefined;
|
|
296
297
|
}
|
|
297
298
|
try {
|
|
298
|
-
|
|
299
|
-
return isRecord(parsed) && typeof parsed.error === "string"
|
|
300
|
-
? parsed.error
|
|
301
|
-
: undefined;
|
|
299
|
+
return JSON.parse(responseText);
|
|
302
300
|
} catch {
|
|
303
301
|
return undefined;
|
|
304
302
|
}
|
|
305
303
|
}
|
|
306
304
|
|
|
305
|
+
function oauthErrorCode(data) {
|
|
306
|
+
return isRecord(data) && typeof data.error === "string"
|
|
307
|
+
? data.error
|
|
308
|
+
: undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isRejectedRefreshError(errorCode) {
|
|
312
|
+
return errorCode === "bad_refresh_token" || errorCode === "invalid_grant";
|
|
313
|
+
}
|
|
314
|
+
|
|
307
315
|
function parseOAuthTokenResponse(data, requestedScope) {
|
|
308
316
|
if (!isRecord(data)) {
|
|
309
317
|
throw new Error("OAuth token response is invalid");
|
|
@@ -326,20 +334,30 @@ function parseOAuthTokenResponse(data, requestedScope) {
|
|
|
326
334
|
refreshToken: data.refresh_token,
|
|
327
335
|
...(scope ? { scope } : {}),
|
|
328
336
|
};
|
|
329
|
-
if (data.expires_in
|
|
330
|
-
|
|
337
|
+
if (data.expires_in !== undefined) {
|
|
338
|
+
if (
|
|
339
|
+
typeof data.expires_in !== "number" ||
|
|
340
|
+
!Number.isFinite(data.expires_in) ||
|
|
341
|
+
data.expires_in <= 0
|
|
342
|
+
) {
|
|
343
|
+
throw new Error("OAuth token response returned invalid expires_in");
|
|
344
|
+
}
|
|
345
|
+
result.expiresAt = Date.now() + data.expires_in * 1000;
|
|
331
346
|
}
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
347
|
+
if (data.refresh_token_expires_in !== undefined) {
|
|
348
|
+
if (
|
|
349
|
+
typeof data.refresh_token_expires_in !== "number" ||
|
|
350
|
+
!Number.isFinite(data.refresh_token_expires_in) ||
|
|
351
|
+
data.refresh_token_expires_in <= 0
|
|
352
|
+
) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"OAuth token response returned invalid refresh_token_expires_in",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
result.refreshTokenExpiresAt =
|
|
358
|
+
Date.now() + data.refresh_token_expires_in * 1000;
|
|
338
359
|
}
|
|
339
|
-
return
|
|
340
|
-
...result,
|
|
341
|
-
expiresAt: Date.now() + data.expires_in * 1000,
|
|
342
|
-
};
|
|
360
|
+
return result;
|
|
343
361
|
}
|
|
344
362
|
|
|
345
363
|
async function refreshUserAccessToken(input) {
|
|
@@ -357,19 +375,32 @@ async function refreshUserAccessToken(input) {
|
|
|
357
375
|
method: "POST",
|
|
358
376
|
headers: request.headers,
|
|
359
377
|
body: request.body,
|
|
378
|
+
signal: AbortSignal.timeout(USER_REFRESH_TIMEOUT_MS),
|
|
360
379
|
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
380
|
+
const responseText = await response.text();
|
|
381
|
+
const responseData = parseOAuthResponseJson(responseText);
|
|
382
|
+
const errorCode = oauthErrorCode(responseData);
|
|
383
|
+
if (isRejectedRefreshError(errorCode)) {
|
|
384
|
+
throw new GitHubUserRefreshRejectedError(
|
|
385
|
+
`GitHub user token refresh rejected: ${errorCode}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (!response.ok || errorCode) {
|
|
368
389
|
throw new Error(
|
|
369
390
|
`GitHub user token refresh failed: ${response.status}${errorCode ? ` ${errorCode}` : ""}`,
|
|
370
391
|
);
|
|
371
392
|
}
|
|
372
|
-
|
|
393
|
+
try {
|
|
394
|
+
return parseOAuthTokenResponse(responseData, input.requestedScope);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (
|
|
397
|
+
error instanceof Error &&
|
|
398
|
+
error.message === "OAuth token response missing access_token"
|
|
399
|
+
) {
|
|
400
|
+
throw new GitHubUserRefreshRejectedError(error.message);
|
|
401
|
+
}
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
373
404
|
}
|
|
374
405
|
|
|
375
406
|
function leaseExpiry(expiresAt) {
|
|
@@ -509,6 +540,83 @@ async function tokensWithAccount(tokenSlot, stored, scope) {
|
|
|
509
540
|
return { ok: true, tokens: updated };
|
|
510
541
|
}
|
|
511
542
|
|
|
543
|
+
function shouldRefreshUserToken(stored, now = Date.now()) {
|
|
544
|
+
return (
|
|
545
|
+
stored.expiresAt !== undefined && stored.expiresAt - now < REFRESH_BUFFER_MS
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function canUseStoredUserToken(stored) {
|
|
550
|
+
return (
|
|
551
|
+
stored.expiresAt === undefined ||
|
|
552
|
+
(stored.expiresAt > Date.now() && !shouldRefreshUserToken(stored))
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Re-read under the token-slot refresh gate so concurrent callers reuse the winner's rotated tokens. */
|
|
557
|
+
async function refreshUserTokensWithLock(tokenSlot, scope, options) {
|
|
558
|
+
return await tokenSlot.withRefresh(async () => {
|
|
559
|
+
const latest = await tokenSlot.get();
|
|
560
|
+
if (!latest) {
|
|
561
|
+
return {
|
|
562
|
+
ok: false,
|
|
563
|
+
result: credentialNeeded("Connect your GitHub account.", scope),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
if (!hasRequiredOAuthScope(latest.scope, scope)) {
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
result: credentialNeeded(
|
|
570
|
+
"Your GitHub authorization needs to be refreshed.",
|
|
571
|
+
scope,
|
|
572
|
+
),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (canUseStoredUserToken(latest)) {
|
|
576
|
+
return { ok: true, tokens: latest };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let refreshed;
|
|
580
|
+
try {
|
|
581
|
+
refreshed = await refreshUserAccessToken({
|
|
582
|
+
clientIdEnv: options.clientIdEnv,
|
|
583
|
+
clientSecretEnv: options.clientSecretEnv,
|
|
584
|
+
refreshToken: latest.refreshToken,
|
|
585
|
+
requestedScope: latest.scope ?? scope,
|
|
586
|
+
});
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (!(error instanceof GitHubUserRefreshRejectedError)) {
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
result: credentialNeeded(
|
|
594
|
+
"Your GitHub authorization has expired.",
|
|
595
|
+
scope,
|
|
596
|
+
),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (!hasRequiredOAuthScope(refreshed.scope, scope)) {
|
|
600
|
+
return {
|
|
601
|
+
ok: false,
|
|
602
|
+
result: credentialNeeded(
|
|
603
|
+
"Your GitHub authorization needs to be refreshed.",
|
|
604
|
+
scope,
|
|
605
|
+
),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const refreshedTokens = {
|
|
609
|
+
...(latest.refreshTokenExpiresAt
|
|
610
|
+
? { refreshTokenExpiresAt: latest.refreshTokenExpiresAt }
|
|
611
|
+
: {}),
|
|
612
|
+
...refreshed,
|
|
613
|
+
...(latest.account ? { account: latest.account } : {}),
|
|
614
|
+
};
|
|
615
|
+
await tokenSlot.set(refreshedTokens);
|
|
616
|
+
return { ok: true, tokens: refreshedTokens };
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
512
620
|
async function issueUserCredential(ctx, options) {
|
|
513
621
|
const scope = options.userScope;
|
|
514
622
|
const tokenSlot = ctx.tokens.currentUser ?? ctx.tokens.credentialSubject;
|
|
@@ -539,34 +647,17 @@ async function issueUserCredential(ctx, options) {
|
|
|
539
647
|
stored.expiresAt !== undefined &&
|
|
540
648
|
stored.expiresAt - now < REFRESH_BUFFER_MS
|
|
541
649
|
) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
} catch (error) {
|
|
551
|
-
if (!(error instanceof GitHubUserRefreshRejectedError)) {
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
return credentialNeeded("Your GitHub authorization has expired.", scope);
|
|
555
|
-
}
|
|
556
|
-
if (!hasRequiredOAuthScope(refreshed.scope, scope)) {
|
|
557
|
-
return credentialNeeded(
|
|
558
|
-
"Your GitHub authorization needs to be refreshed.",
|
|
559
|
-
scope,
|
|
560
|
-
);
|
|
650
|
+
const refreshResult = await refreshUserTokensWithLock(
|
|
651
|
+
tokenSlot,
|
|
652
|
+
scope,
|
|
653
|
+
options,
|
|
654
|
+
);
|
|
655
|
+
if (!refreshResult.ok) {
|
|
656
|
+
return refreshResult.result;
|
|
561
657
|
}
|
|
562
|
-
const refreshedTokens = {
|
|
563
|
-
...refreshed,
|
|
564
|
-
...(stored.account ? { account: stored.account } : {}),
|
|
565
|
-
};
|
|
566
|
-
await tokenSlot.set(refreshedTokens);
|
|
567
658
|
const withAccount = await tokensWithAccount(
|
|
568
659
|
tokenSlot,
|
|
569
|
-
|
|
660
|
+
refreshResult.tokens,
|
|
570
661
|
scope,
|
|
571
662
|
);
|
|
572
663
|
if (!withAccount.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"SETUP.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@sentry/junior-plugin-api": "0.
|
|
28
|
+
"@sentry/junior-plugin-api": "0.76.0"
|
|
29
29
|
}
|
|
30
30
|
}
|