@sentry/junior-github 0.75.0 → 0.76.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.
Files changed (2) hide show
  1. package/index.js +109 -37
  2. package/package.json +2 -2
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"]);
@@ -333,20 +334,30 @@ function parseOAuthTokenResponse(data, requestedScope) {
333
334
  refreshToken: data.refresh_token,
334
335
  ...(scope ? { scope } : {}),
335
336
  };
336
- if (data.expires_in === undefined) {
337
- return result;
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;
338
346
  }
339
- if (
340
- typeof data.expires_in !== "number" ||
341
- !Number.isFinite(data.expires_in) ||
342
- data.expires_in <= 0
343
- ) {
344
- throw new Error("OAuth token response returned invalid expires_in");
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;
345
359
  }
346
- return {
347
- ...result,
348
- expiresAt: Date.now() + data.expires_in * 1000,
349
- };
360
+ return result;
350
361
  }
351
362
 
352
363
  async function refreshUserAccessToken(input) {
@@ -364,6 +375,7 @@ async function refreshUserAccessToken(input) {
364
375
  method: "POST",
365
376
  headers: request.headers,
366
377
  body: request.body,
378
+ signal: AbortSignal.timeout(USER_REFRESH_TIMEOUT_MS),
367
379
  });
368
380
  const responseText = await response.text();
369
381
  const responseData = parseOAuthResponseJson(responseText);
@@ -528,6 +540,83 @@ async function tokensWithAccount(tokenSlot, stored, scope) {
528
540
  return { ok: true, tokens: updated };
529
541
  }
530
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
+
531
620
  async function issueUserCredential(ctx, options) {
532
621
  const scope = options.userScope;
533
622
  const tokenSlot = ctx.tokens.currentUser ?? ctx.tokens.credentialSubject;
@@ -558,34 +647,17 @@ async function issueUserCredential(ctx, options) {
558
647
  stored.expiresAt !== undefined &&
559
648
  stored.expiresAt - now < REFRESH_BUFFER_MS
560
649
  ) {
561
- let refreshed;
562
- try {
563
- refreshed = await refreshUserAccessToken({
564
- clientIdEnv: options.clientIdEnv,
565
- clientSecretEnv: options.clientSecretEnv,
566
- refreshToken: stored.refreshToken,
567
- requestedScope: stored.scope ?? scope,
568
- });
569
- } catch (error) {
570
- if (!(error instanceof GitHubUserRefreshRejectedError)) {
571
- throw error;
572
- }
573
- return credentialNeeded("Your GitHub authorization has expired.", scope);
574
- }
575
- if (!hasRequiredOAuthScope(refreshed.scope, scope)) {
576
- return credentialNeeded(
577
- "Your GitHub authorization needs to be refreshed.",
578
- scope,
579
- );
650
+ const refreshResult = await refreshUserTokensWithLock(
651
+ tokenSlot,
652
+ scope,
653
+ options,
654
+ );
655
+ if (!refreshResult.ok) {
656
+ return refreshResult.result;
580
657
  }
581
- const refreshedTokens = {
582
- ...refreshed,
583
- ...(stored.account ? { account: stored.account } : {}),
584
- };
585
- await tokenSlot.set(refreshedTokens);
586
658
  const withAccount = await tokensWithAccount(
587
659
  tokenSlot,
588
- refreshedTokens,
660
+ refreshResult.tokens,
589
661
  scope,
590
662
  );
591
663
  if (!withAccount.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.75.0",
3
+ "version": "0.76.1",
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.75.0"
28
+ "@sentry/junior-plugin-api": "0.76.1"
29
29
  }
30
30
  }