@sentry/junior-github 0.181.0 → 0.182.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/dist/index.js CHANGED
@@ -1,728 +1,54 @@
1
1
  import {
2
+ CREATE_TOOL_ROUTING_GUIDANCE,
3
+ GITHUB_APP_ID_ENV,
4
+ GITHUB_APP_PRIVATE_KEY_ENV,
5
+ GITHUB_AUTH_TOKEN_ENV,
6
+ GITHUB_AUTH_TOKEN_PLACEHOLDER,
2
7
  GITHUB_DEPLOYMENT_EVENTS,
3
8
  GITHUB_DEPLOYMENT_SUGGESTED_EVENTS,
9
+ GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES,
10
+ GITHUB_INSTALLATION_ID_ENV,
4
11
  GITHUB_ISSUE_EVENTS,
5
12
  GITHUB_ISSUE_SUGGESTED_EVENTS,
6
13
  GITHUB_PULL_REQUEST_EVENTS,
14
+ GITHUB_PULL_REQUEST_MATCH_FIELDS,
7
15
  GITHUB_PULL_REQUEST_SUGGESTED_EVENTS,
8
16
  GITHUB_RELEASE_EVENTS,
9
17
  GITHUB_RELEASE_SUGGESTED_EVENTS,
18
+ GitHubPluginSetupError,
19
+ HTTP_READ_METHODS,
20
+ USER_TOKEN_GRANTS,
21
+ USER_WRITE_REQUIREMENTS,
22
+ createPermissionCache,
23
+ credentialUnavailable,
10
24
  gitHubDeploymentSourceSubscribable,
11
25
  gitHubIssueSubscribable,
12
26
  gitHubPullRequestSubscribable,
13
27
  gitHubReleaseSourceSubscribable,
14
28
  gitHubRepositorySubscribable,
29
+ githubRepositoryFromLeaseScope,
30
+ githubRepositoryFromUrl,
31
+ githubRepositoryLeaseScope,
32
+ githubRequest,
33
+ isRecord,
34
+ issueInstallationCredential,
35
+ issueInstallationToken,
36
+ issueUserCredential,
37
+ loadCheckSuiteFacts,
15
38
  normalizeGitHubResourceEvents,
16
- parseCheckSuiteEnrichmentTarget,
17
- selectFailingChecks
18
- } from "./chunk-GXZVNEIB.js";
39
+ normalizePermissions,
40
+ normalizeScopeList,
41
+ readEnv,
42
+ readGrantPermissions,
43
+ requireEnv,
44
+ resolveUserAccount
45
+ } from "./chunk-JROTRF27.js";
19
46
 
20
47
  // src/plugin.ts
21
48
  import {
22
- defineJuniorPlugin,
23
- EgressPolicyDenied as EgressPolicyDenied2,
24
- enforceEgressPolicy
49
+ defineJuniorPlugin
25
50
  } from "@sentry/junior-plugin-api";
26
51
 
27
- // src/permissions.ts
28
- var LEVELS = /* @__PURE__ */ new Set(["read", "write", "admin"]);
29
- var WRITE_ONLY_PERMISSIONS = /* @__PURE__ */ new Set(["profile", "workflows"]);
30
- function isLevel(value) {
31
- return LEVELS.has(value);
32
- }
33
- function normalizeScope(rawScope) {
34
- return String(rawScope).trim().replace(/-/g, "_");
35
- }
36
- function normalizePermissions(permissions) {
37
- if (permissions === void 0) {
38
- return void 0;
39
- }
40
- const entries = Object.entries(permissions);
41
- if (entries.length === 0) {
42
- throw new Error(
43
- "githubPlugin appPermissions must contain at least one permission when provided."
44
- );
45
- }
46
- const request = {};
47
- for (const [rawScope, rawLevel] of entries) {
48
- const normalizedScope = normalizeScope(rawScope);
49
- if (!normalizedScope) {
50
- throw new Error(
51
- "githubPlugin appPermissions contains an empty permission name."
52
- );
53
- }
54
- if (!/^[a-z][a-z0-9_]*$/.test(normalizedScope)) {
55
- throw new Error(
56
- `githubPlugin appPermissions contains invalid permission "${rawScope}".`
57
- );
58
- }
59
- if (!isLevel(rawLevel)) {
60
- throw new Error(
61
- `githubPlugin appPermissions.${rawScope} must be "read", "write", or "admin".`
62
- );
63
- }
64
- request[normalizedScope] = rawLevel;
65
- }
66
- return request;
67
- }
68
- function readGrantPermissions(permissions) {
69
- const readOnly = { metadata: "read" };
70
- for (const [scope, level] of Object.entries(permissions ?? {})) {
71
- if (!isLevel(level)) {
72
- throw new Error(
73
- `GitHub permission "${scope}" returned invalid level "${String(level)}".`
74
- );
75
- }
76
- if (!WRITE_ONLY_PERMISSIONS.has(scope)) {
77
- readOnly[scope] = "read";
78
- }
79
- }
80
- return readOnly;
81
- }
82
-
83
- // src/pull-request-review-policy.ts
84
- import { EgressPolicyDenied } from "@sentry/junior-plugin-api";
85
-
86
- // src/credential-support.ts
87
- import { createPrivateKey, createSign } from "crypto";
88
- var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
89
- var GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
90
- var GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
91
- var GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
92
- var GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
93
- var MAX_LEASE_MS = 60 * 60 * 1e3;
94
- var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
95
- var USER_REFRESH_TIMEOUT_MS = 2e4;
96
- var GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
97
- var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
98
- var USER_TOKEN_GRANTS = /* @__PURE__ */ new Set(["user-read", "user-write"]);
99
- var CREATE_TOOL_ROUTING_GUIDANCE = "This is a Junior tool-routing denial, not a GitHub permission failure. Do not ask the user for GitHub permissions; retry with the required Junior tool.";
100
- var USER_WRITE_REQUIREMENTS = [
101
- "requesting GitHub user permission to perform this operation"
102
- ];
103
- var GITHUB_CREDENTIAL_DOMAINS = ["api.github.com", "github.com"];
104
- var GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS = [
105
- ...GITHUB_CREDENTIAL_DOMAINS,
106
- "uploads.github.com"
107
- ];
108
- var GitHubUserRefreshRejectedError = class extends Error {
109
- constructor(message) {
110
- super(message);
111
- this.name = "GitHubUserRefreshRejectedError";
112
- }
113
- };
114
- var GitHubRequestError = class extends Error {
115
- status;
116
- constructor(message, status) {
117
- super(message);
118
- this.name = "GitHubRequestError";
119
- this.status = status;
120
- }
121
- };
122
- var GitHubPluginSetupError = class extends Error {
123
- constructor(message) {
124
- super(message);
125
- this.name = "GitHubPluginSetupError";
126
- }
127
- };
128
- function isRecord(value) {
129
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
130
- }
131
- function readEnv(name) {
132
- const value = process.env[name];
133
- if (typeof value !== "string") {
134
- return void 0;
135
- }
136
- const trimmed = value.trim();
137
- return trimmed ? trimmed : void 0;
138
- }
139
- function requireEnv(name) {
140
- const value = readEnv(name);
141
- if (!value) {
142
- throw new GitHubPluginSetupError(`Missing ${name}`);
143
- }
144
- return value;
145
- }
146
- function normalizeScopeList(scopes) {
147
- return [
148
- ...new Set(
149
- (scopes ?? []).flatMap((scope) => String(scope).split(/\s+/)).map((scope) => scope.trim()).filter(Boolean)
150
- )
151
- ].sort();
152
- }
153
- function normalizeOAuthScope(scope) {
154
- const normalized = normalizeScopeList(scope ? [scope] : []);
155
- return normalized.length ? normalized.join(" ") : void 0;
156
- }
157
- function hasRequiredOAuthScope(storedScope, requiredScope) {
158
- const required = normalizeScopeList(requiredScope ? [requiredScope] : []);
159
- if (required.length === 0) {
160
- return true;
161
- }
162
- const stored = new Set(normalizeScopeList(storedScope ? [storedScope] : []));
163
- if (stored.size === 0) {
164
- return false;
165
- }
166
- return required.every((scope) => stored.has(scope));
167
- }
168
- function isGitHubApiUrl(upstreamUrl) {
169
- return upstreamUrl.hostname.toLowerCase() === "api.github.com";
170
- }
171
- function base64Url(input) {
172
- return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
173
- }
174
- function getPrivateKey(envName) {
175
- const raw = requireEnv(envName);
176
- let key;
177
- try {
178
- key = createPrivateKey({ key: raw, format: "pem" });
179
- } catch {
180
- throw new GitHubPluginSetupError(
181
- `Invalid ${envName}: expected a PEM-encoded RSA private key`
182
- );
183
- }
184
- if (key.asymmetricKeyType !== "rsa") {
185
- throw new GitHubPluginSetupError(
186
- `Invalid ${envName}: GitHub App signing requires an RSA private key`
187
- );
188
- }
189
- return key;
190
- }
191
- function createAppJwt(appId, privateKeyEnv) {
192
- const now = Math.floor(Date.now() / 1e3);
193
- const header = { alg: "RS256", typ: "JWT" };
194
- const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
195
- const encodedHeader = base64Url(JSON.stringify(header));
196
- const encodedPayload = base64Url(JSON.stringify(payload));
197
- const signingInput = `${encodedHeader}.${encodedPayload}`;
198
- const signer = createSign("RSA-SHA256");
199
- signer.update(signingInput);
200
- signer.end();
201
- const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
202
- return `${signingInput}.${signature}`;
203
- }
204
- async function githubRequest(apiBase, path, params) {
205
- const response = await fetch(`${apiBase}${path}`, {
206
- method: params.method ?? "GET",
207
- headers: {
208
- Accept: "application/vnd.github+json",
209
- Authorization: `Bearer ${params.token}`,
210
- "X-GitHub-Api-Version": "2022-11-28",
211
- ...params.body ? { "Content-Type": "application/json" } : void 0
212
- },
213
- ...params.body ? { body: JSON.stringify(params.body) } : void 0
214
- });
215
- const text2 = await response.text();
216
- let parsed;
217
- if (text2) {
218
- try {
219
- parsed = JSON.parse(text2);
220
- } catch {
221
- parsed = void 0;
222
- }
223
- }
224
- if (!response.ok) {
225
- const message = isRecord(parsed) && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
226
- throw new GitHubRequestError(message, response.status);
227
- }
228
- return parsed;
229
- }
230
- function buildOAuthTokenRequest(input) {
231
- const payload = {
232
- ...input.payload,
233
- client_id: input.clientId,
234
- client_secret: input.clientSecret
235
- };
236
- return {
237
- headers: {
238
- Accept: "application/json",
239
- "Content-Type": "application/x-www-form-urlencoded"
240
- },
241
- body: new URLSearchParams(payload)
242
- };
243
- }
244
- function parseOAuthResponseJson(responseText) {
245
- if (!responseText.trim()) {
246
- return void 0;
247
- }
248
- try {
249
- return JSON.parse(responseText);
250
- } catch {
251
- return void 0;
252
- }
253
- }
254
- function oauthErrorCode(data) {
255
- return isRecord(data) && typeof data.error === "string" ? data.error : void 0;
256
- }
257
- function isRejectedRefreshError(errorCode) {
258
- return errorCode === "bad_refresh_token" || errorCode === "invalid_grant";
259
- }
260
- function parseOAuthTokenResponse(data, requestedScope) {
261
- if (!isRecord(data)) {
262
- throw new Error("OAuth token response is invalid");
263
- }
264
- if (typeof data.access_token !== "string" || !data.access_token.trim()) {
265
- throw new Error("OAuth token response missing access_token");
266
- }
267
- if (typeof data.refresh_token !== "string" || !data.refresh_token.trim()) {
268
- throw new Error("OAuth token response missing refresh_token");
269
- }
270
- let scope = normalizeOAuthScope(requestedScope);
271
- if (data.scope !== void 0) {
272
- if (typeof data.scope !== "string") {
273
- throw new Error("OAuth token response returned invalid scope");
274
- }
275
- scope = normalizeOAuthScope(data.scope) ?? scope;
276
- }
277
- const result = {
278
- accessToken: data.access_token,
279
- refreshToken: data.refresh_token,
280
- ...scope ? { scope } : void 0
281
- };
282
- if (data.expires_in !== void 0) {
283
- if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
284
- throw new Error("OAuth token response returned invalid expires_in");
285
- }
286
- result.expiresAt = Date.now() + data.expires_in * 1e3;
287
- }
288
- if (data.refresh_token_expires_in !== void 0) {
289
- if (typeof data.refresh_token_expires_in !== "number" || !Number.isFinite(data.refresh_token_expires_in) || data.refresh_token_expires_in <= 0) {
290
- throw new Error(
291
- "OAuth token response returned invalid refresh_token_expires_in"
292
- );
293
- }
294
- result.refreshTokenExpiresAt = Date.now() + data.refresh_token_expires_in * 1e3;
295
- }
296
- return result;
297
- }
298
- async function refreshUserAccessToken(input) {
299
- const clientId = requireEnv(input.clientIdEnv);
300
- const clientSecret = requireEnv(input.clientSecretEnv);
301
- const request = buildOAuthTokenRequest({
302
- clientId,
303
- clientSecret,
304
- payload: {
305
- grant_type: "refresh_token",
306
- refresh_token: input.refreshToken
307
- }
308
- });
309
- const response = await fetch("https://github.com/login/oauth/access_token", {
310
- method: "POST",
311
- headers: request.headers,
312
- body: request.body,
313
- signal: AbortSignal.timeout(USER_REFRESH_TIMEOUT_MS)
314
- });
315
- const responseText = await response.text();
316
- const responseData = parseOAuthResponseJson(responseText);
317
- const errorCode = oauthErrorCode(responseData);
318
- if (isRejectedRefreshError(errorCode)) {
319
- throw new GitHubUserRefreshRejectedError(
320
- `GitHub user token refresh rejected: ${errorCode}`
321
- );
322
- }
323
- if (!response.ok || errorCode) {
324
- throw new Error(
325
- `GitHub user token refresh failed: ${response.status}${errorCode ? ` ${errorCode}` : ""}`
326
- );
327
- }
328
- try {
329
- return parseOAuthTokenResponse(responseData, input.requestedScope);
330
- } catch (error) {
331
- if (error instanceof Error && error.message === "OAuth token response missing access_token") {
332
- throw new GitHubUserRefreshRejectedError(error.message);
333
- }
334
- throw error;
335
- }
336
- }
337
- function leaseExpiry(expiresAt) {
338
- return expiresAt ? Math.min(expiresAt, Date.now() + MAX_LEASE_MS) : Date.now() + MAX_LEASE_MS;
339
- }
340
- function isGitSmartHttpDomain(domain) {
341
- return domain.toLowerCase() === "github.com";
342
- }
343
- function authorizationFor(domain, token) {
344
- if (isGitSmartHttpDomain(domain)) {
345
- return `Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
346
- }
347
- return `Bearer ${token}`;
348
- }
349
- function createCredentialLease(input) {
350
- return {
351
- type: "lease",
352
- lease: {
353
- ...input.account ? { account: input.account } : void 0,
354
- ...input.authorization ? { authorization: input.authorization } : void 0,
355
- expiresAt: new Date(input.expiresAtMs).toISOString(),
356
- headerTransforms: (input.domains ?? (input.authorization ? GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS : GITHUB_CREDENTIAL_DOMAINS)).map((domain) => ({
357
- domain,
358
- headers: {
359
- Authorization: authorizationFor(domain, input.token)
360
- }
361
- }))
362
- }
363
- };
364
- }
365
- function githubUserAuthorization(scope) {
366
- return {
367
- type: "oauth",
368
- provider: "github",
369
- ...scope ? { scope } : void 0
370
- };
371
- }
372
- function credentialNeeded(message, scope, allowAuthorization = true) {
373
- return {
374
- type: "needed",
375
- message,
376
- ...allowAuthorization ? { authorization: githubUserAuthorization(scope) } : void 0
377
- };
378
- }
379
- function credentialUnavailable(message) {
380
- return {
381
- type: "unavailable",
382
- message
383
- };
384
- }
385
- function parseInstallationTokenResponse(data) {
386
- if (!isRecord(data)) {
387
- throw new Error("GitHub installation token response is invalid");
388
- }
389
- const token = data.token;
390
- if (typeof token !== "string" || !token.trim()) {
391
- throw new Error("GitHub installation token response missing token");
392
- }
393
- const expiresAt = data.expires_at;
394
- const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
395
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
396
- throw new Error(
397
- "GitHub installation token response returned invalid expires_at"
398
- );
399
- }
400
- return { token, expiresAtMs };
401
- }
402
- function readInstallationPermissions(installation) {
403
- if (!isRecord(installation) || !isRecord(installation.permissions)) {
404
- throw new Error("GitHub installation response missing permissions");
405
- }
406
- return readGrantPermissions(installation.permissions);
407
- }
408
- function decodeGitHubPathSegment(value) {
409
- try {
410
- const decoded = decodeURIComponent(value).trim();
411
- return decoded && !decoded.includes("/") ? decoded : void 0;
412
- } catch {
413
- return void 0;
414
- }
415
- }
416
- function githubRepositoryFromUrl(upstreamUrl) {
417
- const segments = upstreamUrl.pathname.split("/").filter(Boolean);
418
- if (isGitHubApiUrl(upstreamUrl) && segments[0]?.toLowerCase() === "repos") {
419
- const owner2 = segments[1] ? decodeGitHubPathSegment(segments[1]) : void 0;
420
- const name2 = segments[2] ? decodeGitHubPathSegment(segments[2]) : void 0;
421
- return owner2 && name2 ? { owner: owner2, name: name2 } : void 0;
422
- }
423
- if (upstreamUrl.hostname.toLowerCase() !== "github.com") {
424
- return void 0;
425
- }
426
- const owner = segments[0] ? decodeGitHubPathSegment(segments[0]) : void 0;
427
- const rawName = segments[1]?.replace(/\.git$/i, "");
428
- const name = rawName ? decodeGitHubPathSegment(rawName) : void 0;
429
- return owner && name ? { owner, name } : void 0;
430
- }
431
- function githubRepositoryLeaseScope(repository) {
432
- return `repository:${repository.owner.toLowerCase()}/${repository.name.toLowerCase()}`;
433
- }
434
- function githubRepositoryFromLeaseScope(leaseScope) {
435
- const match = /^repository:([^/]+)\/([^/]+)$/.exec(leaseScope ?? "");
436
- if (!match?.[1] || !match[2]) {
437
- throw new GitHubPluginSetupError(
438
- "GitHub installation write grant is missing a repository lease scope."
439
- );
440
- }
441
- return { owner: match[1], name: match[2] };
442
- }
443
- async function resolveUserAccount(tokens) {
444
- const account = await githubRequest("https://api.github.com", "/user", {
445
- token: tokens.accessToken
446
- });
447
- if (!isRecord(account)) {
448
- throw new Error("GitHub user response is invalid");
449
- }
450
- const id = account.id;
451
- const login = account.login;
452
- if (typeof id !== "number" && typeof id !== "string" || typeof login !== "string" || !login.trim()) {
453
- throw new Error("GitHub user response missing id or login");
454
- }
455
- const url = typeof account.html_url === "string" ? account.html_url : void 0;
456
- return {
457
- handle: login.trim(),
458
- id: String(id),
459
- label: login.trim(),
460
- ...url ? { url } : void 0
461
- };
462
- }
463
- async function tokensWithAccount(tokenSlot, stored, scope) {
464
- if (stored.account) {
465
- return { ok: true, tokens: stored };
466
- }
467
- let account;
468
- try {
469
- account = await resolveUserAccount(stored);
470
- } catch (error) {
471
- if (error instanceof GitHubRequestError && (error.status === 401 || error.status === 403)) {
472
- return {
473
- ok: false,
474
- result: credentialNeeded(
475
- "Your GitHub authorization needs to be refreshed.",
476
- scope
477
- )
478
- };
479
- }
480
- throw error;
481
- }
482
- const updated = { ...stored, account };
483
- await tokenSlot.set(updated);
484
- return { ok: true, tokens: updated };
485
- }
486
- function shouldRefreshUserToken(stored, now = Date.now()) {
487
- return stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS;
488
- }
489
- function canUseStoredUserToken(stored) {
490
- return stored.expiresAt === void 0 || stored.expiresAt > Date.now() && !shouldRefreshUserToken(stored);
491
- }
492
- async function refreshUserTokensWithLock(tokenSlot, scope, options) {
493
- return await tokenSlot.withRefresh(async () => {
494
- const latest = await tokenSlot.get();
495
- if (!latest) {
496
- return {
497
- ok: false,
498
- result: credentialNeeded("Connect your GitHub account.", scope)
499
- };
500
- }
501
- if (!hasRequiredOAuthScope(latest.scope, scope)) {
502
- return {
503
- ok: false,
504
- result: credentialNeeded(
505
- "Your GitHub authorization needs to be refreshed.",
506
- scope
507
- )
508
- };
509
- }
510
- if (canUseStoredUserToken(latest)) {
511
- return { ok: true, tokens: latest };
512
- }
513
- let refreshed;
514
- try {
515
- refreshed = await refreshUserAccessToken({
516
- clientIdEnv: options.clientIdEnv,
517
- clientSecretEnv: options.clientSecretEnv,
518
- refreshToken: latest.refreshToken,
519
- requestedScope: latest.scope ?? scope
520
- });
521
- } catch (error) {
522
- if (!(error instanceof GitHubUserRefreshRejectedError)) {
523
- throw error;
524
- }
525
- return {
526
- ok: false,
527
- result: credentialNeeded(
528
- "Your GitHub authorization has expired.",
529
- scope
530
- )
531
- };
532
- }
533
- if (!hasRequiredOAuthScope(refreshed.scope, scope)) {
534
- return {
535
- ok: false,
536
- result: credentialNeeded(
537
- "Your GitHub authorization needs to be refreshed.",
538
- scope
539
- )
540
- };
541
- }
542
- const refreshedTokens = {
543
- ...latest.refreshTokenExpiresAt ? { refreshTokenExpiresAt: latest.refreshTokenExpiresAt } : void 0,
544
- ...refreshed,
545
- ...latest.account ? { account: latest.account } : void 0
546
- };
547
- await tokenSlot.set(refreshedTokens);
548
- return { ok: true, tokens: refreshedTokens };
549
- });
550
- }
551
- async function issueUserCredential(ctx, options) {
552
- const scope = options.userScope;
553
- const tokenSlot = ctx.tokens.currentUser ?? ctx.tokens.credentialSubject;
554
- if (!tokenSlot) {
555
- return credentialNeeded(
556
- "GitHub write access requires a current user or delegated user credential subject.",
557
- scope,
558
- false
559
- );
560
- }
561
- const stored = await tokenSlot.get();
562
- if (!stored) {
563
- return credentialNeeded(
564
- "GitHub write access requires user authorization.",
565
- scope
566
- );
567
- }
568
- if (!hasRequiredOAuthScope(stored.scope, scope)) {
569
- return credentialNeeded(
570
- "Your GitHub authorization needs to be refreshed.",
571
- scope
572
- );
573
- }
574
- const now = Date.now();
575
- if (stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS) {
576
- const refreshResult = await refreshUserTokensWithLock(
577
- tokenSlot,
578
- scope,
579
- options
580
- );
581
- if (!refreshResult.ok) {
582
- return refreshResult.result;
583
- }
584
- const withAccount = await tokensWithAccount(
585
- tokenSlot,
586
- refreshResult.tokens,
587
- scope
588
- );
589
- if (!withAccount.ok) {
590
- return withAccount.result;
591
- }
592
- return createCredentialLease({
593
- account: withAccount.tokens.account,
594
- token: withAccount.tokens.accessToken,
595
- expiresAtMs: leaseExpiry(withAccount.tokens.expiresAt),
596
- authorization: githubUserAuthorization(scope)
597
- });
598
- }
599
- if (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) {
600
- const withAccount = await tokensWithAccount(tokenSlot, stored, scope);
601
- if (!withAccount.ok) {
602
- return withAccount.result;
603
- }
604
- return createCredentialLease({
605
- account: withAccount.tokens.account,
606
- token: withAccount.tokens.accessToken,
607
- expiresAtMs: leaseExpiry(withAccount.tokens.expiresAt),
608
- authorization: githubUserAuthorization(scope)
609
- });
610
- }
611
- return credentialNeeded("Your GitHub authorization has expired.", scope);
612
- }
613
- async function issueInstallationToken(options) {
614
- const appId = requireEnv(options.appIdEnv);
615
- const installationIdRaw = requireEnv(options.installationIdEnv);
616
- const installationId = Number(installationIdRaw);
617
- if (!Number.isSafeInteger(installationId) || installationId <= 0) {
618
- throw new GitHubPluginSetupError(`Invalid ${options.installationIdEnv}`);
619
- }
620
- const appJwt = createAppJwt(appId, options.privateKeyEnv);
621
- const permissions = "permissions" in options ? options.permissions : typeof options.loadPermissions === "function" ? await options.loadPermissions({ appJwt, installationId }) : void 0;
622
- const body = {
623
- ...permissions ? { permissions } : void 0,
624
- ..."repositories" in options ? { repositories: options.repositories } : void 0
625
- };
626
- const accessTokenResponse = await githubRequest(
627
- "https://api.github.com",
628
- `/app/installations/${installationId}/access_tokens`,
629
- {
630
- method: "POST",
631
- token: appJwt,
632
- body
633
- }
634
- );
635
- const parsedToken = parseInstallationTokenResponse(accessTokenResponse);
636
- return {
637
- expiresAtMs: Math.min(parsedToken.expiresAtMs, Date.now() + MAX_LEASE_MS),
638
- token: parsedToken.token
639
- };
640
- }
641
- async function issueInstallationCredential(options) {
642
- const token = await issueInstallationToken(options);
643
- return createCredentialLease({
644
- token: token.token,
645
- expiresAtMs: token.expiresAtMs
646
- });
647
- }
648
- function createPermissionCache() {
649
- let cached;
650
- let pending;
651
- return async ({ appJwt, installationId }) => {
652
- if (cached && cached.expiresAtMs > Date.now()) {
653
- return cached.permissions;
654
- }
655
- pending ??= githubRequest(
656
- "https://api.github.com",
657
- `/app/installations/${installationId}`,
658
- { token: appJwt }
659
- ).then((installation) => {
660
- const permissions = readInstallationPermissions(installation);
661
- cached = {
662
- expiresAtMs: Date.now() + MAX_LEASE_MS,
663
- permissions
664
- };
665
- return permissions;
666
- }).finally(() => {
667
- pending = void 0;
668
- });
669
- return await pending;
670
- };
671
- }
672
-
673
- // src/pull-request-review-policy.ts
674
- function assertGitHubPullRequestApprovalDenied(input) {
675
- if (input.method !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") {
676
- return;
677
- }
678
- const match = input.upstreamUrl.pathname.toLowerCase().match(
679
- /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/(events))?$/
680
- );
681
- if (!match) return;
682
- const isEventsPath = match[1] === "events";
683
- const bodyText = input.bodyText?.trim() ?? "";
684
- if (!bodyText) {
685
- if (isEventsPath) {
686
- throw new EgressPolicyDenied(
687
- "GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
688
- );
689
- }
690
- return;
691
- }
692
- let body;
693
- try {
694
- body = JSON.parse(bodyText);
695
- } catch {
696
- throw new EgressPolicyDenied(
697
- "GitHub pull request review requests must use JSON bodies so Junior can enforce the no-approve policy."
698
- );
699
- }
700
- if (!isRecord(body)) {
701
- throw new EgressPolicyDenied(
702
- "GitHub pull request review requests must use JSON object bodies so Junior can enforce the no-approve policy."
703
- );
704
- }
705
- let event;
706
- if ("event" in body) {
707
- if (typeof body.event !== "string" || body.event.trim().length === 0) {
708
- throw new EgressPolicyDenied(
709
- "GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
710
- );
711
- }
712
- event = body.event.trim().toUpperCase();
713
- }
714
- if (event === "APPROVE") {
715
- throw new EgressPolicyDenied(
716
- "Junior cannot approve GitHub pull requests. Request changes, leave a comment review, or dismiss Junior's own review instead."
717
- );
718
- }
719
- if (isEventsPath && event === void 0) {
720
- throw new EgressPolicyDenied(
721
- "GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
722
- );
723
- }
724
- }
725
-
726
52
  // src/tools/clone-repository.ts
727
53
  import {
728
54
  definePluginTool,
@@ -3683,12 +3009,18 @@ function createGitHubWebhookRoute(args) {
3683
3009
  args.db,
3684
3010
  pullRequestLinkedIssues
3685
3011
  ) : false;
3686
- const failingChecks = eventName === "check_suite" && args.loadFailingChecks ? await args.loadFailingChecks(body) : void 0;
3012
+ const checkSuiteFacts = eventName === "check_suite" ? await loadCheckSuiteFacts({
3013
+ appIdEnv: args.appIdEnv,
3014
+ body,
3015
+ installationIdEnv: args.installationIdEnv,
3016
+ log: args.log,
3017
+ privateKeyEnv: args.privateKeyEnv
3018
+ }) : void 0;
3687
3019
  const resourceEvents = normalizeGitHubResourceEvents({
3688
3020
  body,
3021
+ ...checkSuiteFacts ? { checkSuiteFacts } : void 0,
3689
3022
  deliveryId,
3690
- eventName,
3691
- failingChecks
3023
+ eventName
3692
3024
  });
3693
3025
  for (const event of resourceEvents) {
3694
3026
  await args.resourceEvents.publish(event);
@@ -4852,44 +4184,6 @@ function githubSidebarAnnotations(annotations) {
4852
4184
  }).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map(({ annotation }) => annotation);
4853
4185
  }
4854
4186
 
4855
- // src/webhooks/check-suite-enrichment.ts
4856
- function checkRunsFromResponse(value) {
4857
- if (!value || typeof value !== "object" || Array.isArray(value)) {
4858
- return [];
4859
- }
4860
- const checkRuns = value.check_runs;
4861
- return Array.isArray(checkRuns) ? checkRuns : [];
4862
- }
4863
- async function loadFailingChecksForSuite(args) {
4864
- const target = parseCheckSuiteEnrichmentTarget(args.body);
4865
- if (!target) return void 0;
4866
- try {
4867
- const token = await issueInstallationToken({
4868
- appIdEnv: args.appIdEnv,
4869
- installationIdEnv: args.installationIdEnv,
4870
- permissions: { checks: "read" },
4871
- privateKeyEnv: args.privateKeyEnv,
4872
- repositories: [target.repoName]
4873
- });
4874
- const response = await githubRequest(
4875
- "https://api.github.com",
4876
- `/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repoName)}/check-suites/${target.checkSuiteId}/check-runs?filter=latest&per_page=100`,
4877
- { token: token.token }
4878
- );
4879
- const failing = selectFailingChecks(checkRunsFromResponse(response), {
4880
- checkSuiteId: target.checkSuiteId
4881
- });
4882
- return failing.length > 0 ? failing : void 0;
4883
- } catch (error) {
4884
- args.log?.error("GitHub check suite enrichment failed", {
4885
- checkSuiteId: target.checkSuiteId,
4886
- errorType: error instanceof Error ? error.name : "UnknownError",
4887
- repository: `${target.owner}/${target.repoName}`
4888
- });
4889
- return void 0;
4890
- }
4891
- }
4892
-
4893
4187
  // src/git-config.ts
4894
4188
  function cleanIdentityPart(value) {
4895
4189
  return String(value ?? "").replaceAll("\n", " ").replaceAll("\r", " ").replace(/[<>]/g, "").trim();
@@ -5438,7 +4732,67 @@ async function prepareWorkspace(ctx) {
5438
4732
  }
5439
4733
  }
5440
4734
 
5441
- // src/plugin.ts
4735
+ // src/egress-policy.ts
4736
+ import {
4737
+ EgressPolicyDenied as EgressPolicyDenied2,
4738
+ enforceEgressPolicy
4739
+ } from "@sentry/junior-plugin-api";
4740
+
4741
+ // src/pull-request-review-policy.ts
4742
+ import { EgressPolicyDenied } from "@sentry/junior-plugin-api";
4743
+ function assertGitHubPullRequestApprovalDenied(input) {
4744
+ if (input.method !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") {
4745
+ return;
4746
+ }
4747
+ const match = input.upstreamUrl.pathname.toLowerCase().match(
4748
+ /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/(events))?$/
4749
+ );
4750
+ if (!match) return;
4751
+ const isEventsPath = match[1] === "events";
4752
+ const bodyText = input.bodyText?.trim() ?? "";
4753
+ if (!bodyText) {
4754
+ if (isEventsPath) {
4755
+ throw new EgressPolicyDenied(
4756
+ "GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
4757
+ );
4758
+ }
4759
+ return;
4760
+ }
4761
+ let body;
4762
+ try {
4763
+ body = JSON.parse(bodyText);
4764
+ } catch {
4765
+ throw new EgressPolicyDenied(
4766
+ "GitHub pull request review requests must use JSON bodies so Junior can enforce the no-approve policy."
4767
+ );
4768
+ }
4769
+ if (!isRecord(body)) {
4770
+ throw new EgressPolicyDenied(
4771
+ "GitHub pull request review requests must use JSON object bodies so Junior can enforce the no-approve policy."
4772
+ );
4773
+ }
4774
+ let event;
4775
+ if ("event" in body) {
4776
+ if (typeof body.event !== "string" || body.event.trim().length === 0) {
4777
+ throw new EgressPolicyDenied(
4778
+ "GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
4779
+ );
4780
+ }
4781
+ event = body.event.trim().toUpperCase();
4782
+ }
4783
+ if (event === "APPROVE") {
4784
+ throw new EgressPolicyDenied(
4785
+ "Junior cannot approve GitHub pull requests. Request changes, leave a comment review, or dismiss Junior's own review instead."
4786
+ );
4787
+ }
4788
+ if (isEventsPath && event === void 0) {
4789
+ throw new EgressPolicyDenied(
4790
+ "GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
4791
+ );
4792
+ }
4793
+ }
4794
+
4795
+ // src/egress-policy.ts
5442
4796
  function githubSmartHttpAccess(upstreamUrl) {
5443
4797
  const pathname = upstreamUrl.pathname.toLowerCase();
5444
4798
  const service = upstreamUrl.searchParams.get("service")?.toLowerCase();
@@ -5457,14 +4811,14 @@ function githubSmartHttpAccess(upstreamUrl) {
5457
4811
  function isGitHubGraphqlUrl(upstreamUrl) {
5458
4812
  return upstreamUrl.hostname.toLowerCase() === "api.github.com" && upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
5459
4813
  }
5460
- function isGitHubApiUrl2(upstreamUrl) {
4814
+ function isGitHubApiUrl(upstreamUrl) {
5461
4815
  return upstreamUrl.hostname.toLowerCase() === "api.github.com";
5462
4816
  }
5463
4817
  function isGitHubAssetUploadRequest(method, upstreamUrl) {
5464
4818
  return method === "POST" && upstreamUrl.hostname.toLowerCase() === "uploads.github.com" && upstreamUrl.pathname === "/user-attachments/assets";
5465
4819
  }
5466
4820
  function githubUserReadReason(method, upstreamUrl) {
5467
- if (method !== "GET" || !isGitHubApiUrl2(upstreamUrl)) {
4821
+ if (method !== "GET" || !isGitHubApiUrl(upstreamUrl)) {
5468
4822
  return void 0;
5469
4823
  }
5470
4824
  return upstreamUrl.pathname.toLowerCase() === "/user" ? "github.user-read" : void 0;
@@ -5622,7 +4976,7 @@ var GITHUB_BODY_WRITES = [
5622
4976
  ];
5623
4977
  function githubApiWriteGrantName(method, upstreamUrl) {
5624
4978
  const pathname = upstreamUrl.pathname.toLowerCase();
5625
- if (!isGitHubApiUrl2(upstreamUrl)) {
4979
+ if (!isGitHubApiUrl(upstreamUrl)) {
5626
4980
  return void 0;
5627
4981
  }
5628
4982
  if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/actions\/workflows\/[^/]+\/dispatches$/.test(
@@ -5710,7 +5064,7 @@ function applyGitHubEgressPolicy(input) {
5710
5064
  });
5711
5065
  const pathname = input.upstreamUrl.pathname.toLowerCase();
5712
5066
  const write = GITHUB_BODY_WRITES.find(
5713
- (candidate) => candidate.method === input.method && isGitHubApiUrl2(input.upstreamUrl) && candidate.restPath.test(pathname) || isGitHubGraphqlMutation(
5067
+ (candidate) => candidate.method === input.method && isGitHubApiUrl(input.upstreamUrl) && candidate.restPath.test(pathname) || isGitHubGraphqlMutation(
5714
5068
  input.method,
5715
5069
  input.upstreamUrl,
5716
5070
  input.bodyText,
@@ -5821,6 +5175,8 @@ async function githubGrantForEgress(ctx) {
5821
5175
  }
5822
5176
  return grantForAccess(access, "github.api-read", "installation-read");
5823
5177
  }
5178
+
5179
+ // src/plugin.ts
5824
5180
  function githubPlugin(options = {}) {
5825
5181
  const botNameEnv = options.botNameEnv ?? "GITHUB_APP_BOT_NAME";
5826
5182
  const botEmailEnv = options.botEmailEnv ?? "GITHUB_APP_BOT_EMAIL";
@@ -5852,6 +5208,7 @@ function githubPlugin(options = {}) {
5852
5208
  type: "pull_request",
5853
5209
  supportedEvents: [...GITHUB_PULL_REQUEST_EVENTS],
5854
5210
  suggestedEvents: [...GITHUB_PULL_REQUEST_SUGGESTED_EVENTS],
5211
+ matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS,
5855
5212
  ...options.pullRequestEvents?.guidance ? { guidance: options.pullRequestEvents.guidance } : void 0
5856
5213
  },
5857
5214
  {
@@ -5861,16 +5218,14 @@ function githubPlugin(options = {}) {
5861
5218
  },
5862
5219
  {
5863
5220
  type: "repository",
5864
- supportedEvents: [
5865
- ...GITHUB_ISSUE_EVENTS,
5866
- ...GITHUB_PULL_REQUEST_EVENTS
5867
- ],
5221
+ supportedEvents: [...GITHUB_ISSUE_EVENTS, ...GITHUB_PULL_REQUEST_EVENTS],
5868
5222
  suggestedEvents: [
5869
5223
  "issue.opened",
5870
5224
  "pull_request.opened",
5871
5225
  ...GITHUB_ISSUE_SUGGESTED_EVENTS,
5872
5226
  ...GITHUB_PULL_REQUEST_SUGGESTED_EVENTS
5873
- ]
5227
+ ],
5228
+ matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS
5874
5229
  }
5875
5230
  ],
5876
5231
  isEnabled: () => Boolean(readEnv("GITHUB_WEBHOOK_SECRET")),
@@ -5959,6 +5314,7 @@ function githubPlugin(options = {}) {
5959
5314
  return [
5960
5315
  createGitHubWebhookRoute({
5961
5316
  annotations: ctx.annotations,
5317
+ appIdEnv,
5962
5318
  botEmail: () => readEnv(botEmailEnv),
5963
5319
  classifyPullRequestCommits: async ({
5964
5320
  number,
@@ -5988,14 +5344,9 @@ function githubPlugin(options = {}) {
5988
5344
  },
5989
5345
  db: ctx.db,
5990
5346
  installationId: () => readEnv(installationIdEnv),
5991
- loadFailingChecks: async (body) => await loadFailingChecksForSuite({
5992
- appIdEnv,
5993
- body,
5994
- installationIdEnv,
5995
- log: ctx.log,
5996
- privateKeyEnv
5997
- }),
5347
+ installationIdEnv,
5998
5348
  log: ctx.log,
5349
+ privateKeyEnv,
5999
5350
  resourceEvents: ctx.resourceEvents,
6000
5351
  webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET")
6001
5352
  })