@sentry/junior-github 0.181.1 → 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/chunk-JROTRF27.js +1731 -0
- package/dist/egress-policy.d.ts +11 -0
- package/dist/index.js +114 -763
- package/dist/plugin.d.ts +1 -1
- package/dist/resource-events/pull-request.d.ts +7 -1
- package/dist/testing.js +1 -1
- package/dist/webhooks/check-suite.d.ts +69 -0
- package/dist/webhooks/handler.d.ts +3 -2
- package/dist/webhooks/resource-events.d.ts +5 -37
- package/package.json +2 -2
- package/skills/github-code/SKILL.md +2 -2
- package/skills/github-code/SPEC.md +1 -1
- package/dist/chunk-GXZVNEIB.js +0 -825
- package/dist/webhooks/check-suite-enrichment.d.ts +0 -11
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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/
|
|
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
|
|
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" || !
|
|
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 (!
|
|
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 &&
|
|
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
|
-
|
|
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
|
})
|