@sentry/junior-github 0.68.0 → 0.69.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/SETUP.md +61 -17
- package/index.d.ts +41 -1
- package/index.js +842 -31
- package/package.json +3 -2
- package/permissions.js +77 -0
- package/skills/github-code/SKILL.md +4 -5
- package/skills/github-code/references/api-surface.md +43 -37
- package/skills/github-code/references/troubleshooting-workarounds.md +17 -13
- package/skills/github-issues/SKILL.md +2 -3
- package/skills/github-issues/references/api-surface.md +3 -3
package/index.js
CHANGED
|
@@ -1,8 +1,111 @@
|
|
|
1
|
+
import { createPrivateKey, createSign } from "node:crypto";
|
|
1
2
|
import { defineJuniorPlugin } from "@sentry/junior-plugin-api";
|
|
3
|
+
import {
|
|
4
|
+
normalizePermissions,
|
|
5
|
+
permissionCapabilities,
|
|
6
|
+
readGrantPermissions,
|
|
7
|
+
} from "./permissions.js";
|
|
8
|
+
|
|
9
|
+
const GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
|
|
10
|
+
const GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
|
|
11
|
+
const GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
|
|
12
|
+
const GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
|
|
13
|
+
const GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
|
|
14
|
+
const MAX_LEASE_MS = 60 * 60 * 1000;
|
|
15
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
16
|
+
const HTTP_READ_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
17
|
+
const USER_TOKEN_GRANTS = new Set(["user-read", "user-write"]);
|
|
18
|
+
const CONTENTS_WRITE_REQUIREMENTS = [
|
|
19
|
+
"GitHub App Contents: write on the target repository",
|
|
20
|
+
"requesting GitHub user write access to the repository",
|
|
21
|
+
];
|
|
22
|
+
const WORKFLOWS_WRITE_REQUIREMENTS = [
|
|
23
|
+
"GitHub App Contents: write and Workflows: write on the target repository",
|
|
24
|
+
"requesting GitHub user write access to the repository",
|
|
25
|
+
];
|
|
26
|
+
const ISSUES_WRITE_REQUIREMENTS = [
|
|
27
|
+
"GitHub App Issues: write on the target repository",
|
|
28
|
+
"requesting GitHub user issue access to the repository",
|
|
29
|
+
];
|
|
30
|
+
const PULL_REQUESTS_WRITE_REQUIREMENTS = [
|
|
31
|
+
"GitHub App Pull requests: write on the target repository",
|
|
32
|
+
"requesting GitHub user write access to the repository",
|
|
33
|
+
];
|
|
34
|
+
const FORK_CREATE_REQUIREMENTS = [
|
|
35
|
+
"GitHub App Administration: write and Contents: read",
|
|
36
|
+
"app installation access on the source and destination accounts",
|
|
37
|
+
"requesting GitHub user permission to fork the repository",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
class GitHubUserRefreshRejectedError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "GitHubUserRefreshRejectedError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class GitHubRequestError extends Error {
|
|
48
|
+
constructor(message, status) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "GitHubRequestError";
|
|
51
|
+
this.status = status;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class GitHubPluginSetupError extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "GitHubPluginSetupError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRecord(value) {
|
|
63
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
64
|
+
}
|
|
2
65
|
|
|
3
66
|
function readEnv(name) {
|
|
4
67
|
const value = process.env[name];
|
|
5
|
-
|
|
68
|
+
if (typeof value !== "string") {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const trimmed = value.trim();
|
|
72
|
+
return trimmed ? trimmed : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function requireEnv(name) {
|
|
76
|
+
const value = readEnv(name);
|
|
77
|
+
if (!value) {
|
|
78
|
+
throw new GitHubPluginSetupError(`Missing ${name}`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeScopeList(scopes) {
|
|
84
|
+
return [
|
|
85
|
+
...new Set(
|
|
86
|
+
(scopes ?? [])
|
|
87
|
+
.flatMap((scope) => String(scope).split(/\s+/))
|
|
88
|
+
.map((scope) => scope.trim())
|
|
89
|
+
.filter(Boolean),
|
|
90
|
+
),
|
|
91
|
+
].sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeOAuthScope(scope) {
|
|
95
|
+
const normalized = normalizeScopeList(scope ? [scope] : []);
|
|
96
|
+
return normalized.length ? normalized.join(" ") : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasRequiredOAuthScope(storedScope, requiredScope) {
|
|
100
|
+
const required = normalizeScopeList(requiredScope ? [requiredScope] : []);
|
|
101
|
+
if (required.length === 0) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const stored = new Set(normalizeScopeList(storedScope ? [storedScope] : []));
|
|
105
|
+
if (stored.size === 0) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return required.every((scope) => stored.has(scope));
|
|
6
109
|
}
|
|
7
110
|
|
|
8
111
|
function cleanIdentityPart(value) {
|
|
@@ -58,21 +161,21 @@ if [ -z "$message_file" ]; then
|
|
|
58
161
|
fi
|
|
59
162
|
|
|
60
163
|
if [ -z "\${JUNIOR_GIT_AUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_AUTHOR_EMAIL:-}" ]; then
|
|
61
|
-
echo "Junior GitHub plugin internal error:
|
|
164
|
+
echo "Junior GitHub plugin internal error: requester commit attribution was not injected by the host runtime. Do not set Git author env vars manually; report this configuration error." >&2
|
|
62
165
|
exit 1
|
|
63
166
|
fi
|
|
64
167
|
|
|
65
168
|
if [ "\${GIT_AUTHOR_NAME:-}" != "$JUNIOR_GIT_AUTHOR_NAME" ] || [ "\${GIT_AUTHOR_EMAIL:-}" != "$JUNIOR_GIT_AUTHOR_EMAIL" ]; then
|
|
66
|
-
echo "Junior GitHub plugin internal error: Git author was not set to the
|
|
169
|
+
echo "Junior GitHub plugin internal error: Git author was not set to the resolved requester identity. Do not override Git author manually; report this configuration error." >&2
|
|
67
170
|
exit 1
|
|
68
171
|
fi
|
|
69
172
|
|
|
70
173
|
if [ -z "\${JUNIOR_GIT_COAUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_COAUTHOR_EMAIL:-}" ]; then
|
|
71
|
-
echo "Junior GitHub plugin internal error:
|
|
174
|
+
echo "Junior GitHub plugin internal error: Junior coauthor identity was not injected by the host runtime. Do not set coauthor env vars manually; report this configuration error." >&2
|
|
72
175
|
exit 1
|
|
73
176
|
fi
|
|
74
177
|
|
|
75
|
-
trailer="Co-
|
|
178
|
+
trailer="Co-Authored-By: $JUNIOR_GIT_COAUTHOR_NAME <$JUNIOR_GIT_COAUTHOR_EMAIL>"
|
|
76
179
|
if grep -Fqx "$trailer" "$message_file"; then
|
|
77
180
|
exit 0
|
|
78
181
|
fi
|
|
@@ -93,10 +196,677 @@ async function configureGit(ctx, key, value) {
|
|
|
93
196
|
}
|
|
94
197
|
}
|
|
95
198
|
|
|
96
|
-
|
|
199
|
+
function base64Url(input) {
|
|
200
|
+
return Buffer.from(input)
|
|
201
|
+
.toString("base64")
|
|
202
|
+
.replace(/=/g, "")
|
|
203
|
+
.replace(/\+/g, "-")
|
|
204
|
+
.replace(/\//g, "_");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getPrivateKey(envName) {
|
|
208
|
+
const raw = requireEnv(envName);
|
|
209
|
+
let key;
|
|
210
|
+
try {
|
|
211
|
+
key = createPrivateKey({ key: raw, format: "pem" });
|
|
212
|
+
} catch {
|
|
213
|
+
throw new GitHubPluginSetupError(
|
|
214
|
+
`Invalid ${envName}: expected a PEM-encoded RSA private key`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (key.asymmetricKeyType !== "rsa") {
|
|
219
|
+
throw new GitHubPluginSetupError(
|
|
220
|
+
`Invalid ${envName}: GitHub App signing requires an RSA private key`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return key;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function createAppJwt(appId, privateKeyEnv) {
|
|
227
|
+
const now = Math.floor(Date.now() / 1000);
|
|
228
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
229
|
+
const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
|
|
230
|
+
const encodedHeader = base64Url(JSON.stringify(header));
|
|
231
|
+
const encodedPayload = base64Url(JSON.stringify(payload));
|
|
232
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
233
|
+
const signer = createSign("RSA-SHA256");
|
|
234
|
+
signer.update(signingInput);
|
|
235
|
+
signer.end();
|
|
236
|
+
const signature = signer
|
|
237
|
+
.sign(getPrivateKey(privateKeyEnv))
|
|
238
|
+
.toString("base64")
|
|
239
|
+
.replace(/=/g, "")
|
|
240
|
+
.replace(/\+/g, "-")
|
|
241
|
+
.replace(/\//g, "_");
|
|
242
|
+
return `${signingInput}.${signature}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function githubRequest(apiBase, path, params) {
|
|
246
|
+
const response = await fetch(`${apiBase}${path}`, {
|
|
247
|
+
method: params.method ?? "GET",
|
|
248
|
+
headers: {
|
|
249
|
+
Accept: "application/vnd.github+json",
|
|
250
|
+
Authorization: `Bearer ${params.token}`,
|
|
251
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
252
|
+
...(params.body ? { "Content-Type": "application/json" } : {}),
|
|
253
|
+
},
|
|
254
|
+
...(params.body ? { body: JSON.stringify(params.body) } : {}),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const text = await response.text();
|
|
258
|
+
let parsed;
|
|
259
|
+
if (text) {
|
|
260
|
+
try {
|
|
261
|
+
parsed = JSON.parse(text);
|
|
262
|
+
} catch {
|
|
263
|
+
parsed = undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
const message =
|
|
269
|
+
parsed && typeof parsed === "object" && typeof parsed.message === "string"
|
|
270
|
+
? parsed.message
|
|
271
|
+
: `GitHub API error ${response.status}`;
|
|
272
|
+
throw new GitHubRequestError(message, response.status);
|
|
273
|
+
}
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildOAuthTokenRequest(input) {
|
|
278
|
+
const payload = {
|
|
279
|
+
...input.payload,
|
|
280
|
+
client_id: input.clientId,
|
|
281
|
+
client_secret: input.clientSecret,
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
headers: {
|
|
285
|
+
Accept: "application/json",
|
|
286
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
287
|
+
},
|
|
288
|
+
body: new URLSearchParams(payload),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseOAuthError(text) {
|
|
293
|
+
if (!text.trim()) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const parsed = JSON.parse(text);
|
|
298
|
+
return isRecord(parsed) && typeof parsed.error === "string"
|
|
299
|
+
? parsed.error
|
|
300
|
+
: undefined;
|
|
301
|
+
} catch {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parseOAuthTokenResponse(data, requestedScope) {
|
|
307
|
+
if (!isRecord(data)) {
|
|
308
|
+
throw new Error("OAuth token response is invalid");
|
|
309
|
+
}
|
|
310
|
+
if (typeof data.access_token !== "string" || !data.access_token.trim()) {
|
|
311
|
+
throw new Error("OAuth token response missing access_token");
|
|
312
|
+
}
|
|
313
|
+
if (typeof data.refresh_token !== "string" || !data.refresh_token.trim()) {
|
|
314
|
+
throw new Error("OAuth token response missing refresh_token");
|
|
315
|
+
}
|
|
316
|
+
let scope = normalizeOAuthScope(requestedScope);
|
|
317
|
+
if (data.scope !== undefined) {
|
|
318
|
+
if (typeof data.scope !== "string") {
|
|
319
|
+
throw new Error("OAuth token response returned invalid scope");
|
|
320
|
+
}
|
|
321
|
+
scope = normalizeOAuthScope(data.scope) ?? scope;
|
|
322
|
+
}
|
|
323
|
+
const result = {
|
|
324
|
+
accessToken: data.access_token,
|
|
325
|
+
refreshToken: data.refresh_token,
|
|
326
|
+
...(scope ? { scope } : {}),
|
|
327
|
+
};
|
|
328
|
+
if (data.expires_in === undefined) {
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
if (
|
|
332
|
+
typeof data.expires_in !== "number" ||
|
|
333
|
+
!Number.isFinite(data.expires_in) ||
|
|
334
|
+
data.expires_in <= 0
|
|
335
|
+
) {
|
|
336
|
+
throw new Error("OAuth token response returned invalid expires_in");
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
...result,
|
|
340
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function refreshUserAccessToken(input) {
|
|
345
|
+
const clientId = requireEnv(input.clientIdEnv);
|
|
346
|
+
const clientSecret = requireEnv(input.clientSecretEnv);
|
|
347
|
+
const request = buildOAuthTokenRequest({
|
|
348
|
+
clientId,
|
|
349
|
+
clientSecret,
|
|
350
|
+
payload: {
|
|
351
|
+
grant_type: "refresh_token",
|
|
352
|
+
refresh_token: input.refreshToken,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: request.headers,
|
|
358
|
+
body: request.body,
|
|
359
|
+
});
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
const errorCode = parseOAuthError(await response.text());
|
|
362
|
+
if (errorCode === "bad_refresh_token" || errorCode === "invalid_grant") {
|
|
363
|
+
throw new GitHubUserRefreshRejectedError(
|
|
364
|
+
`GitHub user token refresh rejected: ${errorCode}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
throw new Error(
|
|
368
|
+
`GitHub user token refresh failed: ${response.status}${errorCode ? ` ${errorCode}` : ""}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return parseOAuthTokenResponse(await response.json(), input.requestedScope);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function leaseExpiry(expiresAt) {
|
|
375
|
+
return expiresAt
|
|
376
|
+
? Math.min(expiresAt, Date.now() + MAX_LEASE_MS)
|
|
377
|
+
: Date.now() + MAX_LEASE_MS;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isGitSmartHttpDomain(domain) {
|
|
381
|
+
return domain.toLowerCase() === "github.com";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function authorizationFor(domain, token) {
|
|
385
|
+
if (isGitSmartHttpDomain(domain)) {
|
|
386
|
+
return `Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
|
|
387
|
+
}
|
|
388
|
+
return `Bearer ${token}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function createCredentialLease(input) {
|
|
392
|
+
return {
|
|
393
|
+
type: "lease",
|
|
394
|
+
lease: {
|
|
395
|
+
...(input.account ? { account: input.account } : {}),
|
|
396
|
+
...(input.authorization ? { authorization: input.authorization } : {}),
|
|
397
|
+
expiresAt: new Date(input.expiresAtMs).toISOString(),
|
|
398
|
+
headerTransforms: ["api.github.com", "github.com"].map((domain) => ({
|
|
399
|
+
domain,
|
|
400
|
+
headers: {
|
|
401
|
+
Authorization: authorizationFor(domain, input.token),
|
|
402
|
+
},
|
|
403
|
+
})),
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function githubUserAuthorization(scope) {
|
|
409
|
+
return {
|
|
410
|
+
type: "oauth",
|
|
411
|
+
provider: "github",
|
|
412
|
+
...(scope ? { scope } : {}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function credentialNeeded(message, scope, allowAuthorization = true) {
|
|
417
|
+
return {
|
|
418
|
+
type: "needed",
|
|
419
|
+
message,
|
|
420
|
+
...(allowAuthorization
|
|
421
|
+
? { authorization: githubUserAuthorization(scope) }
|
|
422
|
+
: {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function credentialUnavailable(message) {
|
|
427
|
+
return {
|
|
428
|
+
type: "unavailable",
|
|
429
|
+
message,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function parseInstallationTokenResponse(data) {
|
|
434
|
+
if (!isRecord(data)) {
|
|
435
|
+
throw new Error("GitHub installation token response is invalid");
|
|
436
|
+
}
|
|
437
|
+
const token = data.token;
|
|
438
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
439
|
+
throw new Error("GitHub installation token response missing token");
|
|
440
|
+
}
|
|
441
|
+
const expiresAt = data.expires_at;
|
|
442
|
+
const expiresAtMs =
|
|
443
|
+
typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
|
|
444
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
"GitHub installation token response returned invalid expires_at",
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return { token, expiresAtMs };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readInstallationPermissions(installation) {
|
|
453
|
+
if (!isRecord(installation) || !isRecord(installation.permissions)) {
|
|
454
|
+
throw new Error("GitHub installation response missing permissions");
|
|
455
|
+
}
|
|
456
|
+
return readGrantPermissions(installation.permissions);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function resolveUserAccount(tokens) {
|
|
460
|
+
const account = await githubRequest("https://api.github.com", "/user", {
|
|
461
|
+
token: tokens.accessToken,
|
|
462
|
+
});
|
|
463
|
+
if (!isRecord(account)) {
|
|
464
|
+
throw new Error("GitHub user response is invalid");
|
|
465
|
+
}
|
|
466
|
+
const id = account.id;
|
|
467
|
+
const login = account.login;
|
|
468
|
+
if (
|
|
469
|
+
(typeof id !== "number" && typeof id !== "string") ||
|
|
470
|
+
typeof login !== "string" ||
|
|
471
|
+
!login.trim()
|
|
472
|
+
) {
|
|
473
|
+
throw new Error("GitHub user response missing id or login");
|
|
474
|
+
}
|
|
475
|
+
const url =
|
|
476
|
+
typeof account.html_url === "string" ? account.html_url : undefined;
|
|
477
|
+
return {
|
|
478
|
+
id: String(id),
|
|
479
|
+
label: login.trim(),
|
|
480
|
+
...(url ? { url } : {}),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function tokensWithAccount(tokenSlot, stored, scope) {
|
|
485
|
+
if (stored.account) {
|
|
486
|
+
return { ok: true, tokens: stored };
|
|
487
|
+
}
|
|
488
|
+
let account;
|
|
489
|
+
try {
|
|
490
|
+
account = await resolveUserAccount(stored);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (
|
|
493
|
+
error instanceof GitHubRequestError &&
|
|
494
|
+
(error.status === 401 || error.status === 403)
|
|
495
|
+
) {
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
result: credentialNeeded(
|
|
499
|
+
"Your GitHub authorization needs to be refreshed.",
|
|
500
|
+
scope,
|
|
501
|
+
),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
const updated = { ...stored, account };
|
|
507
|
+
await tokenSlot.set(updated);
|
|
508
|
+
return { ok: true, tokens: updated };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function issueUserCredential(ctx, options) {
|
|
512
|
+
const scope = options.userScope;
|
|
513
|
+
const tokenSlot = ctx.tokens.currentUser ?? ctx.tokens.credentialSubject;
|
|
514
|
+
if (!tokenSlot) {
|
|
515
|
+
return credentialNeeded(
|
|
516
|
+
"GitHub write access requires a current user or delegated user credential subject.",
|
|
517
|
+
scope,
|
|
518
|
+
false,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const stored = await tokenSlot.get();
|
|
523
|
+
if (!stored) {
|
|
524
|
+
return credentialNeeded(
|
|
525
|
+
"GitHub write access requires user authorization.",
|
|
526
|
+
scope,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (!hasRequiredOAuthScope(stored.scope, scope)) {
|
|
530
|
+
return credentialNeeded(
|
|
531
|
+
"Your GitHub authorization needs to be refreshed.",
|
|
532
|
+
scope,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const now = Date.now();
|
|
537
|
+
if (
|
|
538
|
+
stored.expiresAt !== undefined &&
|
|
539
|
+
stored.expiresAt - now < REFRESH_BUFFER_MS
|
|
540
|
+
) {
|
|
541
|
+
let refreshed;
|
|
542
|
+
try {
|
|
543
|
+
refreshed = await refreshUserAccessToken({
|
|
544
|
+
clientIdEnv: options.clientIdEnv,
|
|
545
|
+
clientSecretEnv: options.clientSecretEnv,
|
|
546
|
+
refreshToken: stored.refreshToken,
|
|
547
|
+
requestedScope: stored.scope ?? scope,
|
|
548
|
+
});
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (!(error instanceof GitHubUserRefreshRejectedError)) {
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
return credentialNeeded("Your GitHub authorization has expired.", scope);
|
|
554
|
+
}
|
|
555
|
+
if (!hasRequiredOAuthScope(refreshed.scope, scope)) {
|
|
556
|
+
return credentialNeeded(
|
|
557
|
+
"Your GitHub authorization needs to be refreshed.",
|
|
558
|
+
scope,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
const refreshedTokens = {
|
|
562
|
+
...refreshed,
|
|
563
|
+
...(stored.account ? { account: stored.account } : {}),
|
|
564
|
+
};
|
|
565
|
+
await tokenSlot.set(refreshedTokens);
|
|
566
|
+
const withAccount = await tokensWithAccount(
|
|
567
|
+
tokenSlot,
|
|
568
|
+
refreshedTokens,
|
|
569
|
+
scope,
|
|
570
|
+
);
|
|
571
|
+
if (!withAccount.ok) {
|
|
572
|
+
return withAccount.result;
|
|
573
|
+
}
|
|
574
|
+
return createCredentialLease({
|
|
575
|
+
account: withAccount.tokens.account,
|
|
576
|
+
token: withAccount.tokens.accessToken,
|
|
577
|
+
expiresAtMs: leaseExpiry(withAccount.tokens.expiresAt),
|
|
578
|
+
authorization: githubUserAuthorization(scope),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (stored.expiresAt === undefined || stored.expiresAt > Date.now()) {
|
|
583
|
+
const withAccount = await tokensWithAccount(tokenSlot, stored, scope);
|
|
584
|
+
if (!withAccount.ok) {
|
|
585
|
+
return withAccount.result;
|
|
586
|
+
}
|
|
587
|
+
return createCredentialLease({
|
|
588
|
+
account: withAccount.tokens.account,
|
|
589
|
+
token: withAccount.tokens.accessToken,
|
|
590
|
+
expiresAtMs: leaseExpiry(withAccount.tokens.expiresAt),
|
|
591
|
+
authorization: githubUserAuthorization(scope),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return credentialNeeded("Your GitHub authorization has expired.", scope);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function issueInstallationCredential(options) {
|
|
599
|
+
const appId = requireEnv(options.appIdEnv);
|
|
600
|
+
const installationIdRaw = requireEnv(options.installationIdEnv);
|
|
601
|
+
const installationId = Number(installationIdRaw);
|
|
602
|
+
if (!Number.isSafeInteger(installationId) || installationId <= 0) {
|
|
603
|
+
throw new GitHubPluginSetupError(`Invalid ${options.installationIdEnv}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const appJwt = createAppJwt(appId, options.privateKeyEnv);
|
|
607
|
+
let tokenPermissions = options.readPermissions;
|
|
608
|
+
if (!tokenPermissions) {
|
|
609
|
+
tokenPermissions = await options.loadReadPermissions({
|
|
610
|
+
appJwt,
|
|
611
|
+
installationId,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const accessTokenResponse = await githubRequest(
|
|
616
|
+
"https://api.github.com",
|
|
617
|
+
`/app/installations/${installationId}/access_tokens`,
|
|
618
|
+
{
|
|
619
|
+
method: "POST",
|
|
620
|
+
token: appJwt,
|
|
621
|
+
body: { permissions: tokenPermissions },
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
const parsedToken = parseInstallationTokenResponse(accessTokenResponse);
|
|
625
|
+
const expiresAtMs = Math.min(
|
|
626
|
+
parsedToken.expiresAtMs,
|
|
627
|
+
Date.now() + MAX_LEASE_MS,
|
|
628
|
+
);
|
|
629
|
+
return createCredentialLease({
|
|
630
|
+
token: parsedToken.token,
|
|
631
|
+
expiresAtMs,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function createPermissionCache() {
|
|
636
|
+
let cached;
|
|
637
|
+
let pending;
|
|
638
|
+
return async ({ appJwt, installationId }) => {
|
|
639
|
+
if (cached && cached.expiresAtMs > Date.now()) {
|
|
640
|
+
return cached.permissions;
|
|
641
|
+
}
|
|
642
|
+
pending ??= githubRequest(
|
|
643
|
+
"https://api.github.com",
|
|
644
|
+
`/app/installations/${installationId}`,
|
|
645
|
+
{ token: appJwt },
|
|
646
|
+
)
|
|
647
|
+
.then((installation) => {
|
|
648
|
+
const permissions = readInstallationPermissions(installation);
|
|
649
|
+
cached = {
|
|
650
|
+
expiresAtMs: Date.now() + MAX_LEASE_MS,
|
|
651
|
+
permissions,
|
|
652
|
+
};
|
|
653
|
+
return permissions;
|
|
654
|
+
})
|
|
655
|
+
.finally(() => {
|
|
656
|
+
pending = undefined;
|
|
657
|
+
});
|
|
658
|
+
return await pending;
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function githubSmartHttpAccess(upstreamUrl) {
|
|
663
|
+
const pathname = upstreamUrl.pathname.toLowerCase();
|
|
664
|
+
const service = upstreamUrl.searchParams.get("service")?.toLowerCase();
|
|
665
|
+
const isSmartHttpPath =
|
|
666
|
+
pathname.endsWith("/info/refs") ||
|
|
667
|
+
pathname.endsWith("/git-receive-pack") ||
|
|
668
|
+
pathname.endsWith("/git-upload-pack");
|
|
669
|
+
if (!isSmartHttpPath) {
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
672
|
+
if (
|
|
673
|
+
pathname.endsWith("/git-receive-pack") ||
|
|
674
|
+
service === "git-receive-pack"
|
|
675
|
+
) {
|
|
676
|
+
return "write";
|
|
677
|
+
}
|
|
678
|
+
if (pathname.endsWith("/git-upload-pack") || service === "git-upload-pack") {
|
|
679
|
+
return "read";
|
|
680
|
+
}
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function isGitHubGraphqlUrl(upstreamUrl) {
|
|
685
|
+
return (
|
|
686
|
+
upstreamUrl.hostname.toLowerCase() === "api.github.com" &&
|
|
687
|
+
upstreamUrl.pathname.toLowerCase().endsWith("/graphql")
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isGitHubApiUrl(upstreamUrl) {
|
|
692
|
+
return upstreamUrl.hostname.toLowerCase() === "api.github.com";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function githubUserReadReason(method, upstreamUrl) {
|
|
696
|
+
if (method !== "GET" || !isGitHubApiUrl(upstreamUrl)) {
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
return upstreamUrl.pathname.toLowerCase() === "/user"
|
|
700
|
+
? "github.user-read"
|
|
701
|
+
: undefined;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function githubGraphqlAccess(method, upstreamUrl) {
|
|
705
|
+
if (!isGitHubGraphqlUrl(upstreamUrl)) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
if (HTTP_READ_METHODS.has(method)) {
|
|
709
|
+
return "read";
|
|
710
|
+
}
|
|
711
|
+
// GitHub GraphQL POST bodies can be read queries or write mutations. The
|
|
712
|
+
// egress hook intentionally does not read sandbox request bodies, so non-read
|
|
713
|
+
// methods require user-write attribution rather than risking an unattributed
|
|
714
|
+
// mutation through an installation-read token.
|
|
715
|
+
return "write";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function githubApiWriteReason(method, upstreamUrl) {
|
|
719
|
+
const pathname = upstreamUrl.pathname.toLowerCase();
|
|
720
|
+
if (!isGitHubApiUrl(upstreamUrl)) {
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(pathname)) {
|
|
724
|
+
return "github.issue-create";
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
method === "POST" &&
|
|
728
|
+
/^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+\/comments$/.test(pathname)
|
|
729
|
+
) {
|
|
730
|
+
return "github.issues-write";
|
|
731
|
+
}
|
|
732
|
+
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(pathname)) {
|
|
733
|
+
return "github.pull-create";
|
|
734
|
+
}
|
|
735
|
+
if (
|
|
736
|
+
method === "PATCH" &&
|
|
737
|
+
/^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+$/.test(pathname)
|
|
738
|
+
) {
|
|
739
|
+
return "github.pull-requests-write";
|
|
740
|
+
}
|
|
741
|
+
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/forks$/.test(pathname)) {
|
|
742
|
+
return "github.fork-create";
|
|
743
|
+
}
|
|
744
|
+
if (
|
|
745
|
+
/^\/repos\/[^/]+\/[^/]+\/contents(?:\/|$)/.test(pathname) &&
|
|
746
|
+
(method === "PUT" || method === "DELETE")
|
|
747
|
+
) {
|
|
748
|
+
return pathname.includes("/.github/workflows/")
|
|
749
|
+
? "github.workflows-write"
|
|
750
|
+
: "github.contents-write";
|
|
751
|
+
}
|
|
752
|
+
if (
|
|
753
|
+
method === "POST" &&
|
|
754
|
+
/^\/repos\/[^/]+\/[^/]+\/git\/(blobs|trees|commits)$/.test(pathname)
|
|
755
|
+
) {
|
|
756
|
+
return "github.contents-write";
|
|
757
|
+
}
|
|
758
|
+
if (
|
|
759
|
+
method === "POST" &&
|
|
760
|
+
/^\/repos\/[^/]+\/[^/]+\/git\/refs$/.test(pathname)
|
|
761
|
+
) {
|
|
762
|
+
return "github.contents-write";
|
|
763
|
+
}
|
|
764
|
+
if (
|
|
765
|
+
(method === "PATCH" || method === "DELETE") &&
|
|
766
|
+
/^\/repos\/[^/]+\/[^/]+\/git\/refs\/.+/.test(pathname)
|
|
767
|
+
) {
|
|
768
|
+
return "github.contents-write";
|
|
769
|
+
}
|
|
770
|
+
if (
|
|
771
|
+
method === "PUT" &&
|
|
772
|
+
/^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/merge$/.test(pathname)
|
|
773
|
+
) {
|
|
774
|
+
return "github.contents-write";
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function grantRequirements(reason) {
|
|
780
|
+
if (reason === "github.git-write" || reason === "github.contents-write") {
|
|
781
|
+
return CONTENTS_WRITE_REQUIREMENTS;
|
|
782
|
+
}
|
|
783
|
+
if (reason === "github.workflows-write") {
|
|
784
|
+
return WORKFLOWS_WRITE_REQUIREMENTS;
|
|
785
|
+
}
|
|
786
|
+
if (reason === "github.issue-create" || reason === "github.issues-write") {
|
|
787
|
+
return ISSUES_WRITE_REQUIREMENTS;
|
|
788
|
+
}
|
|
789
|
+
if (
|
|
790
|
+
reason === "github.pull-create" ||
|
|
791
|
+
reason === "github.pull-requests-write"
|
|
792
|
+
) {
|
|
793
|
+
return PULL_REQUESTS_WRITE_REQUIREMENTS;
|
|
794
|
+
}
|
|
795
|
+
if (reason === "github.fork-create") {
|
|
796
|
+
return FORK_CREATE_REQUIREMENTS;
|
|
797
|
+
}
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function grantForAccess(access, reason, name) {
|
|
802
|
+
const requirements = grantRequirements(reason);
|
|
803
|
+
return {
|
|
804
|
+
name,
|
|
805
|
+
access,
|
|
806
|
+
reason,
|
|
807
|
+
...(requirements ? { requirements } : {}),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function githubGrantForEgress(ctx) {
|
|
812
|
+
const method = ctx.request.method.toUpperCase();
|
|
813
|
+
const upstreamUrl = new URL(ctx.request.url);
|
|
814
|
+
const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
|
|
815
|
+
if (smartHttpAccess) {
|
|
816
|
+
return grantForAccess(
|
|
817
|
+
smartHttpAccess,
|
|
818
|
+
smartHttpAccess === "write" ? "github.git-write" : "github.git-read",
|
|
819
|
+
smartHttpAccess === "write" ? "user-write" : "installation-read",
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const userReadReason = githubUserReadReason(method, upstreamUrl);
|
|
824
|
+
if (userReadReason) {
|
|
825
|
+
return grantForAccess("read", userReadReason, "user-read");
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const writeReason = githubApiWriteReason(method, upstreamUrl);
|
|
829
|
+
if (writeReason) {
|
|
830
|
+
return grantForAccess("write", writeReason, "user-write");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const graphqlAccess = githubGraphqlAccess(method, upstreamUrl);
|
|
834
|
+
if (graphqlAccess) {
|
|
835
|
+
return grantForAccess(
|
|
836
|
+
graphqlAccess,
|
|
837
|
+
graphqlAccess === "write"
|
|
838
|
+
? "github.graphql-write"
|
|
839
|
+
: "github.graphql-read",
|
|
840
|
+
graphqlAccess === "write" ? "user-write" : "installation-read",
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const access = HTTP_READ_METHODS.has(method) ? "read" : "write";
|
|
845
|
+
return grantForAccess(
|
|
846
|
+
access,
|
|
847
|
+
access === "write" ? "github.api-write" : "github.api-read",
|
|
848
|
+
access === "write" ? "user-write" : "installation-read",
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Register GitHub runtime hooks for repository workflows. */
|
|
97
853
|
export function githubPlugin(options = {}) {
|
|
98
854
|
const botNameEnv = options.botNameEnv ?? "GITHUB_APP_BOT_NAME";
|
|
99
855
|
const botEmailEnv = options.botEmailEnv ?? "GITHUB_APP_BOT_EMAIL";
|
|
856
|
+
const clientIdEnv = options.clientIdEnv ?? "GITHUB_APP_CLIENT_ID";
|
|
857
|
+
const clientSecretEnv = options.clientSecretEnv ?? "GITHUB_APP_CLIENT_SECRET";
|
|
858
|
+
const appIdEnv = options.appIdEnv ?? GITHUB_APP_ID_ENV;
|
|
859
|
+
const privateKeyEnv = options.privateKeyEnv ?? GITHUB_APP_PRIVATE_KEY_ENV;
|
|
860
|
+
const installationIdEnv =
|
|
861
|
+
options.installationIdEnv ?? GITHUB_INSTALLATION_ID_ENV;
|
|
862
|
+
const appPermissions = normalizePermissions(options.appPermissions);
|
|
863
|
+
const appReadPermissions = appPermissions
|
|
864
|
+
? readGrantPermissions(appPermissions)
|
|
865
|
+
: undefined;
|
|
866
|
+
const loadReadPermissions = createPermissionCache();
|
|
867
|
+
const appCapabilities = permissionCapabilities(appPermissions);
|
|
868
|
+
const userScopes = normalizeScopeList(options.additionalUserScopes);
|
|
869
|
+
const userScope = userScopes.length ? userScopes.join(" ") : undefined;
|
|
100
870
|
|
|
101
871
|
return defineJuniorPlugin({
|
|
102
872
|
packageName: "@sentry/junior-github",
|
|
@@ -104,25 +874,32 @@ export function githubPlugin(options = {}) {
|
|
|
104
874
|
name: "github",
|
|
105
875
|
description:
|
|
106
876
|
"GitHub issue, pull request, and repository workflows via GitHub App",
|
|
877
|
+
...(appCapabilities ? { capabilities: appCapabilities } : {}),
|
|
107
878
|
configKeys: ["org", "repo"],
|
|
879
|
+
domains: ["api.github.com", "github.com"],
|
|
108
880
|
envVars: {
|
|
109
|
-
|
|
110
|
-
|
|
881
|
+
[appIdEnv]: {},
|
|
882
|
+
[privateKeyEnv]: {},
|
|
883
|
+
[installationIdEnv]: {},
|
|
884
|
+
[clientIdEnv]: {},
|
|
885
|
+
[clientSecretEnv]: {},
|
|
886
|
+
[botNameEnv]: { exposeToCommandEnv: true },
|
|
887
|
+
[botEmailEnv]: { exposeToCommandEnv: true },
|
|
111
888
|
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
889
|
+
oauth: {
|
|
890
|
+
clientIdEnv,
|
|
891
|
+
clientSecretEnv,
|
|
892
|
+
authorizeEndpoint: "https://github.com/login/oauth/authorize",
|
|
893
|
+
tokenEndpoint: "https://github.com/login/oauth/access_token",
|
|
894
|
+
// GitHub App user-to-server tokens always return scope: "" regardless
|
|
895
|
+
// of what was requested; treat empty response scope as unreported.
|
|
896
|
+
treatEmptyScopeAsUnreported: true,
|
|
897
|
+
...(userScope ? { scope: userScope } : {}),
|
|
120
898
|
},
|
|
121
899
|
commandEnv: {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
GIT_COMMITTER_EMAIL: "${GITHUB_APP_BOT_EMAIL}",
|
|
900
|
+
[GITHUB_AUTH_TOKEN_ENV]: GITHUB_AUTH_TOKEN_PLACEHOLDER,
|
|
901
|
+
GIT_COMMITTER_NAME: `\${${botNameEnv}}`,
|
|
902
|
+
GIT_COMMITTER_EMAIL: `\${${botEmailEnv}}`,
|
|
126
903
|
},
|
|
127
904
|
target: {
|
|
128
905
|
type: "repo",
|
|
@@ -170,24 +947,58 @@ export function githubPlugin(options = {}) {
|
|
|
170
947
|
if (!botName || !botEmail) {
|
|
171
948
|
return;
|
|
172
949
|
}
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
if ((!
|
|
950
|
+
const authorName = requesterName(ctx.requester);
|
|
951
|
+
const authorEmail = requesterEmail(ctx.requester);
|
|
952
|
+
if ((!authorName || !authorEmail) && isGitCommitCommand(command)) {
|
|
176
953
|
ctx.decision.deny(
|
|
177
|
-
"Junior GitHub plugin could not determine a resolved requester name and email for commit attribution. This is an internal request-context error; do not set
|
|
954
|
+
"Junior GitHub plugin could not determine a resolved requester name and email for commit attribution. This is an internal request-context error; do not set author env vars manually.",
|
|
178
955
|
);
|
|
179
956
|
return;
|
|
180
957
|
}
|
|
181
|
-
|
|
182
|
-
|
|
958
|
+
if (authorName && authorEmail) {
|
|
959
|
+
ctx.env.set("GIT_AUTHOR_NAME", authorName);
|
|
960
|
+
ctx.env.set("GIT_AUTHOR_EMAIL", authorEmail);
|
|
961
|
+
ctx.env.set("JUNIOR_GIT_AUTHOR_NAME", authorName);
|
|
962
|
+
ctx.env.set("JUNIOR_GIT_AUTHOR_EMAIL", authorEmail);
|
|
963
|
+
}
|
|
183
964
|
ctx.env.set("GIT_COMMITTER_NAME", botName);
|
|
184
965
|
ctx.env.set("GIT_COMMITTER_EMAIL", botEmail);
|
|
185
|
-
ctx.env.set("
|
|
186
|
-
ctx.env.set("
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
966
|
+
ctx.env.set("JUNIOR_GIT_COAUTHOR_NAME", botName);
|
|
967
|
+
ctx.env.set("JUNIOR_GIT_COAUTHOR_EMAIL", botEmail);
|
|
968
|
+
},
|
|
969
|
+
grantForEgress(ctx) {
|
|
970
|
+
return githubGrantForEgress(ctx);
|
|
971
|
+
},
|
|
972
|
+
async resolveOAuthAccount(ctx) {
|
|
973
|
+
return await resolveUserAccount(ctx.tokens);
|
|
974
|
+
},
|
|
975
|
+
async issueCredential(ctx) {
|
|
976
|
+
try {
|
|
977
|
+
if (ctx.grant.name === "installation-read") {
|
|
978
|
+
return await issueInstallationCredential({
|
|
979
|
+
appIdEnv,
|
|
980
|
+
privateKeyEnv,
|
|
981
|
+
installationIdEnv,
|
|
982
|
+
readPermissions: appReadPermissions,
|
|
983
|
+
loadReadPermissions,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
if (USER_TOKEN_GRANTS.has(ctx.grant.name)) {
|
|
987
|
+
return await issueUserCredential(ctx, {
|
|
988
|
+
clientIdEnv,
|
|
989
|
+
clientSecretEnv,
|
|
990
|
+
userScope,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
} catch (error) {
|
|
994
|
+
if (error instanceof GitHubPluginSetupError) {
|
|
995
|
+
return credentialUnavailable(error.message);
|
|
996
|
+
}
|
|
997
|
+
throw error;
|
|
190
998
|
}
|
|
999
|
+
throw new Error(
|
|
1000
|
+
`GitHub plugin cannot issue unknown grant "${ctx.grant.name}".`,
|
|
1001
|
+
);
|
|
191
1002
|
},
|
|
192
1003
|
},
|
|
193
1004
|
});
|