@opengeni/github 0.2.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.d.ts +63 -0
- package/dist/index.js +408 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
- package/src/index.ts +467 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Settings } from '@opengeni/config';
|
|
2
|
+
import { GitHubRepository } from '@opengeni/contracts';
|
|
3
|
+
|
|
4
|
+
declare const stateMaxAgeSeconds: number;
|
|
5
|
+
declare class GitHubAppConfigurationError extends Error {
|
|
6
|
+
readonly missing: string[];
|
|
7
|
+
constructor(missing: string[]);
|
|
8
|
+
}
|
|
9
|
+
declare class GitHubAppApiError extends Error {
|
|
10
|
+
}
|
|
11
|
+
type GitHubAppInstallationSummary = {
|
|
12
|
+
installationId: number;
|
|
13
|
+
accountLogin: string | null;
|
|
14
|
+
accountType: string | null;
|
|
15
|
+
suspended: boolean;
|
|
16
|
+
};
|
|
17
|
+
type GitHubSignedStatePayload = {
|
|
18
|
+
nonce: string;
|
|
19
|
+
iat: number;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
workspaceId?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
declare function githubAppMissingSettings(settings: Settings): string[];
|
|
25
|
+
declare function buildGitHubAppManifest(input: {
|
|
26
|
+
appName: string;
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
public: boolean;
|
|
29
|
+
includeCiPermissions: boolean;
|
|
30
|
+
setupUrl?: string;
|
|
31
|
+
}): Record<string, unknown>;
|
|
32
|
+
declare function personalAppManifestUrl(state: string): string;
|
|
33
|
+
declare function organizationAppManifestUrl(organization: string, state: string): string;
|
|
34
|
+
declare function githubOAuthAuthorizeUrl(input: {
|
|
35
|
+
clientId: string;
|
|
36
|
+
state: string;
|
|
37
|
+
redirectUri?: string;
|
|
38
|
+
}): string;
|
|
39
|
+
declare function createSignedState(secret: string, payloadOrNow?: Record<string, unknown> | number, nowArg?: number): string;
|
|
40
|
+
declare function readSignedState(state: string, secret: string, now?: number): GitHubSignedStatePayload | null;
|
|
41
|
+
declare function verifySignedState(state: string, secret: string, now?: number): boolean;
|
|
42
|
+
declare function envLinesFromGitHubManifestConversion(payload: Record<string, unknown>): string[];
|
|
43
|
+
declare function convertGitHubAppManifest(code: string): Promise<Record<string, unknown>>;
|
|
44
|
+
declare function listGitHubAppInstallationSummaries(settings: Settings): Promise<GitHubAppInstallationSummary[]>;
|
|
45
|
+
declare function getGitHubAppInstallationSummary(settings: Settings, installationId: number): Promise<GitHubAppInstallationSummary | null>;
|
|
46
|
+
declare function verifyGitHubInstallationAccessForUser(settings: Settings, input: {
|
|
47
|
+
code: string;
|
|
48
|
+
installationId: number;
|
|
49
|
+
}): Promise<GitHubAppInstallationSummary>;
|
|
50
|
+
declare function listGitHubAppRepositories(settings: Settings, input?: {
|
|
51
|
+
installationIds?: number[];
|
|
52
|
+
}): Promise<GitHubRepository[]>;
|
|
53
|
+
declare function createGitHubAppInstallationToken(settings: Settings, input: {
|
|
54
|
+
installationId: number;
|
|
55
|
+
repositoryIds?: number[];
|
|
56
|
+
}): Promise<string>;
|
|
57
|
+
declare function githubAppBotIdentity(settings: Settings): {
|
|
58
|
+
name: string;
|
|
59
|
+
email: string;
|
|
60
|
+
} | null;
|
|
61
|
+
declare function normalizeGitHubAppPrivateKey(value: string): string;
|
|
62
|
+
|
|
63
|
+
export { GitHubAppApiError, GitHubAppConfigurationError, type GitHubAppInstallationSummary, type GitHubSignedStatePayload, buildGitHubAppManifest, convertGitHubAppManifest, createGitHubAppInstallationToken, createSignedState, envLinesFromGitHubManifestConversion, getGitHubAppInstallationSummary, githubAppBotIdentity, githubAppMissingSettings, githubOAuthAuthorizeUrl, listGitHubAppInstallationSummaries, listGitHubAppRepositories, normalizeGitHubAppPrivateKey, organizationAppManifestUrl, personalAppManifestUrl, readSignedState, stateMaxAgeSeconds, verifyGitHubInstallationAccessForUser, verifySignedState };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from "crypto";
|
|
3
|
+
import { SignJWT, importPKCS8 } from "jose";
|
|
4
|
+
var githubApiBase = "https://api.github.com";
|
|
5
|
+
var githubApiVersion = "2022-11-28";
|
|
6
|
+
var stateMaxAgeSeconds = 60 * 60;
|
|
7
|
+
var pkcs8PrivateKeyHeader = `-----BEGIN ${"PRIVATE KEY"}-----`;
|
|
8
|
+
var rsaPrivateKeyHeader = `-----BEGIN ${"RSA PRIVATE KEY"}-----`;
|
|
9
|
+
var GitHubAppConfigurationError = class extends Error {
|
|
10
|
+
constructor(missing) {
|
|
11
|
+
super("GitHub App is not configured");
|
|
12
|
+
this.missing = missing;
|
|
13
|
+
}
|
|
14
|
+
missing;
|
|
15
|
+
};
|
|
16
|
+
var GitHubAppApiError = class extends Error {
|
|
17
|
+
};
|
|
18
|
+
function githubAppMissingSettings(settings) {
|
|
19
|
+
const required = {
|
|
20
|
+
OPENGENI_GITHUB_APP_ID: settings.githubAppId,
|
|
21
|
+
OPENGENI_GITHUB_CLIENT_ID: settings.githubClientId,
|
|
22
|
+
OPENGENI_GITHUB_CLIENT_SECRET: settings.githubClientSecret,
|
|
23
|
+
OPENGENI_GITHUB_APP_SLUG: settings.githubAppSlug,
|
|
24
|
+
OPENGENI_GITHUB_APP_PRIVATE_KEY: settings.githubAppPrivateKey
|
|
25
|
+
};
|
|
26
|
+
return Object.entries(required).flatMap(([name, value]) => value && value.trim() ? [] : [name]);
|
|
27
|
+
}
|
|
28
|
+
function buildGitHubAppManifest(input) {
|
|
29
|
+
const base = input.baseUrl.replace(/\/+$/, "");
|
|
30
|
+
const permissions = {
|
|
31
|
+
metadata: "read",
|
|
32
|
+
contents: "write",
|
|
33
|
+
pull_requests: "write"
|
|
34
|
+
};
|
|
35
|
+
if (input.includeCiPermissions) {
|
|
36
|
+
permissions.actions = "read";
|
|
37
|
+
permissions.checks = "read";
|
|
38
|
+
permissions.statuses = "write";
|
|
39
|
+
}
|
|
40
|
+
const manifest = {
|
|
41
|
+
name: input.appName,
|
|
42
|
+
url: base,
|
|
43
|
+
redirect_url: `${base}/v1/github/app-manifest/callback`,
|
|
44
|
+
public: input.public,
|
|
45
|
+
request_oauth_on_install: true,
|
|
46
|
+
default_permissions: permissions
|
|
47
|
+
};
|
|
48
|
+
if (input.setupUrl) {
|
|
49
|
+
manifest.setup_url = input.setupUrl;
|
|
50
|
+
manifest.setup_on_update = true;
|
|
51
|
+
}
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
54
|
+
function personalAppManifestUrl(state) {
|
|
55
|
+
return `https://github.com/settings/apps/new?state=${state}`;
|
|
56
|
+
}
|
|
57
|
+
function organizationAppManifestUrl(organization, state) {
|
|
58
|
+
return `https://github.com/organizations/${encodeURIComponent(organization)}/settings/apps/new?state=${state}`;
|
|
59
|
+
}
|
|
60
|
+
function githubOAuthAuthorizeUrl(input) {
|
|
61
|
+
const url = new URL("https://github.com/login/oauth/authorize");
|
|
62
|
+
url.searchParams.set("client_id", input.clientId);
|
|
63
|
+
url.searchParams.set("state", input.state);
|
|
64
|
+
if (input.redirectUri) {
|
|
65
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
66
|
+
}
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
function createSignedState(secret, payloadOrNow = {}, nowArg = Math.floor(Date.now() / 1e3)) {
|
|
70
|
+
const payloadInput = typeof payloadOrNow === "number" ? {} : payloadOrNow;
|
|
71
|
+
const now = typeof payloadOrNow === "number" ? payloadOrNow : nowArg;
|
|
72
|
+
const payload = {
|
|
73
|
+
...payloadInput,
|
|
74
|
+
nonce: randomBytes(16).toString("base64url"),
|
|
75
|
+
iat: now
|
|
76
|
+
};
|
|
77
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
78
|
+
return `${encoded}.${signStatePayload(encoded, secret)}`;
|
|
79
|
+
}
|
|
80
|
+
function readSignedState(state, secret, now = Math.floor(Date.now() / 1e3)) {
|
|
81
|
+
const [encoded, signature] = state.split(".", 2);
|
|
82
|
+
if (!encoded || !signature) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const expected = signStatePayload(encoded, secret);
|
|
86
|
+
if (!safeEqual(signature, expected)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
let payload;
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!payload || typeof payload !== "object" || typeof payload.iat !== "number" || typeof payload.nonce !== "string") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const age = now - payload.iat;
|
|
99
|
+
return age >= 0 && age <= stateMaxAgeSeconds ? payload : null;
|
|
100
|
+
}
|
|
101
|
+
function verifySignedState(state, secret, now = Math.floor(Date.now() / 1e3)) {
|
|
102
|
+
return readSignedState(state, secret, now) !== null;
|
|
103
|
+
}
|
|
104
|
+
function envLinesFromGitHubManifestConversion(payload) {
|
|
105
|
+
const privateKey = String(payload.pem ?? "").replace(/\n/g, "\\n");
|
|
106
|
+
return [
|
|
107
|
+
`OPENGENI_GITHUB_APP_ID=${payload.id ?? ""}`,
|
|
108
|
+
`OPENGENI_GITHUB_CLIENT_ID=${payload.client_id ?? ""}`,
|
|
109
|
+
`OPENGENI_GITHUB_CLIENT_SECRET=${payload.client_secret ?? ""}`,
|
|
110
|
+
`OPENGENI_GITHUB_APP_SLUG=${payload.slug ?? ""}`,
|
|
111
|
+
`OPENGENI_GITHUB_WEBHOOK_SECRET=${payload.webhook_secret ?? ""}`,
|
|
112
|
+
`OPENGENI_GITHUB_APP_PRIVATE_KEY="${privateKey}"`
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
async function convertGitHubAppManifest(code) {
|
|
116
|
+
const response = await fetch(`${githubApiBase}/app-manifests/${code}/conversions`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: githubHeaders(void 0)
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
122
|
+
}
|
|
123
|
+
const payload = await response.json();
|
|
124
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
125
|
+
throw new GitHubAppApiError("GitHub returned an invalid manifest conversion payload");
|
|
126
|
+
}
|
|
127
|
+
return payload;
|
|
128
|
+
}
|
|
129
|
+
async function listGitHubAppInstallationSummaries(settings) {
|
|
130
|
+
const missing = githubAppMissingSettings(settings);
|
|
131
|
+
if (missing.length > 0) {
|
|
132
|
+
throw new GitHubAppConfigurationError(missing);
|
|
133
|
+
}
|
|
134
|
+
const jwt = await createGitHubAppJwt(settings);
|
|
135
|
+
const installations = await listInstallations(jwt);
|
|
136
|
+
return installations.map(installationSummaryFromPayload);
|
|
137
|
+
}
|
|
138
|
+
async function getGitHubAppInstallationSummary(settings, installationId) {
|
|
139
|
+
const installations = await listGitHubAppInstallationSummaries(settings);
|
|
140
|
+
return installations.find((installation) => installation.installationId === installationId) ?? null;
|
|
141
|
+
}
|
|
142
|
+
async function verifyGitHubInstallationAccessForUser(settings, input) {
|
|
143
|
+
const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);
|
|
144
|
+
const installations = await listUserAccessibleInstallations(token);
|
|
145
|
+
const installation = installations.find((candidate) => candidate.installationId === input.installationId);
|
|
146
|
+
if (!installation) {
|
|
147
|
+
throw new GitHubAppApiError("GitHub installation is not accessible to the installing user");
|
|
148
|
+
}
|
|
149
|
+
return installation;
|
|
150
|
+
}
|
|
151
|
+
async function listGitHubAppRepositories(settings, input = {}) {
|
|
152
|
+
const missing = githubAppMissingSettings(settings);
|
|
153
|
+
if (missing.length > 0) {
|
|
154
|
+
throw new GitHubAppConfigurationError(missing);
|
|
155
|
+
}
|
|
156
|
+
const allowedInstallations = input.installationIds ? new Set(input.installationIds) : null;
|
|
157
|
+
if (allowedInstallations && allowedInstallations.size === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const jwt = await createGitHubAppJwt(settings);
|
|
161
|
+
const installations = await listInstallations(jwt);
|
|
162
|
+
const repositories = [];
|
|
163
|
+
for (const installation of installations) {
|
|
164
|
+
if (installation.suspended_at) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const installationId = asInt(installation.id);
|
|
168
|
+
if (installationId === null) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (allowedInstallations && !allowedInstallations.has(installationId)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const account = typeof installation.account === "object" && installation.account ? installation.account : {};
|
|
175
|
+
const token = await createInstallationToken(jwt, { installationId });
|
|
176
|
+
repositories.push(...await listInstallationRepositories(token, installationId, account));
|
|
177
|
+
}
|
|
178
|
+
repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));
|
|
179
|
+
return repositories;
|
|
180
|
+
}
|
|
181
|
+
async function createGitHubAppInstallationToken(settings, input) {
|
|
182
|
+
const missing = githubAppMissingSettings(settings);
|
|
183
|
+
if (missing.length > 0) {
|
|
184
|
+
throw new GitHubAppConfigurationError(missing);
|
|
185
|
+
}
|
|
186
|
+
const jwt = await createGitHubAppJwt(settings);
|
|
187
|
+
return await createInstallationToken(jwt, input);
|
|
188
|
+
}
|
|
189
|
+
function githubAppBotIdentity(settings) {
|
|
190
|
+
const appId = settings.githubAppId?.trim();
|
|
191
|
+
const slug = settings.githubAppSlug?.trim();
|
|
192
|
+
if (!appId || !slug) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const login = `${slug}[bot]`;
|
|
196
|
+
return {
|
|
197
|
+
name: login,
|
|
198
|
+
email: `${appId}+${login}@users.noreply.github.com`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function createGitHubAppJwt(settings) {
|
|
202
|
+
const privateKey = normalizeGitHubAppPrivateKey(settings.githubAppPrivateKey ?? "");
|
|
203
|
+
const appId = settings.githubAppId?.trim();
|
|
204
|
+
if (!appId || !privateKey) {
|
|
205
|
+
throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
|
|
206
|
+
}
|
|
207
|
+
const key = await importPKCS8(privateKey, "RS256");
|
|
208
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
209
|
+
return await new SignJWT({}).setProtectedHeader({ alg: "RS256" }).setIssuedAt(now - 60).setExpirationTime(now + 9 * 60).setIssuer(appId).sign(key);
|
|
210
|
+
}
|
|
211
|
+
async function listInstallations(token) {
|
|
212
|
+
const out = [];
|
|
213
|
+
for (let page = 1; ; page += 1) {
|
|
214
|
+
const payload = await githubGet("/app/installations", token, { per_page: "100", page: String(page) });
|
|
215
|
+
if (!Array.isArray(payload)) {
|
|
216
|
+
throw new GitHubAppApiError("GitHub returned an invalid installations payload");
|
|
217
|
+
}
|
|
218
|
+
out.push(...payload.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))));
|
|
219
|
+
if (payload.length < 100) {
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function exchangeGitHubOAuthCodeForUserToken(settings, code) {
|
|
225
|
+
if (!settings.githubClientId || !settings.githubClientSecret) {
|
|
226
|
+
throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
|
|
227
|
+
}
|
|
228
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
Accept: "application/json",
|
|
232
|
+
"Content-Type": "application/json"
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
client_id: settings.githubClientId,
|
|
236
|
+
client_secret: settings.githubClientSecret,
|
|
237
|
+
code
|
|
238
|
+
})
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
242
|
+
}
|
|
243
|
+
const payload = await response.json();
|
|
244
|
+
if (!payload || typeof payload !== "object" || typeof payload.access_token !== "string") {
|
|
245
|
+
throw new GitHubAppApiError("GitHub returned an invalid OAuth token payload");
|
|
246
|
+
}
|
|
247
|
+
return payload.access_token;
|
|
248
|
+
}
|
|
249
|
+
async function listUserAccessibleInstallations(token) {
|
|
250
|
+
const out = [];
|
|
251
|
+
for (let page = 1; ; page += 1) {
|
|
252
|
+
const payload = await githubGet("/user/installations", token, { per_page: "100", page: String(page) });
|
|
253
|
+
const installations = payload && typeof payload === "object" && Array.isArray(payload.installations) ? payload.installations : null;
|
|
254
|
+
if (!installations) {
|
|
255
|
+
throw new GitHubAppApiError("GitHub returned an invalid user installations payload");
|
|
256
|
+
}
|
|
257
|
+
out.push(...installations.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))).map(installationSummaryFromPayload));
|
|
258
|
+
if (installations.length < 100) {
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function createInstallationToken(appJwt, input) {
|
|
264
|
+
const scoped = input.repositoryIds && input.repositoryIds.length > 0;
|
|
265
|
+
const response = await fetch(`${githubApiBase}/app/installations/${input.installationId}/access_tokens`, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: {
|
|
268
|
+
...githubHeaders(appJwt),
|
|
269
|
+
...scoped ? { "Content-Type": "application/json" } : {}
|
|
270
|
+
},
|
|
271
|
+
...scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}
|
|
272
|
+
});
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
275
|
+
}
|
|
276
|
+
const payload = await response.json();
|
|
277
|
+
if (!payload || typeof payload !== "object" || typeof payload.token !== "string") {
|
|
278
|
+
throw new GitHubAppApiError("GitHub returned an invalid installation token payload");
|
|
279
|
+
}
|
|
280
|
+
return payload.token;
|
|
281
|
+
}
|
|
282
|
+
async function listInstallationRepositories(token, installationId, account) {
|
|
283
|
+
const out = [];
|
|
284
|
+
for (let page = 1; ; page += 1) {
|
|
285
|
+
const payload = await githubGet("/installation/repositories", token, { per_page: "100", page: String(page) });
|
|
286
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Array.isArray(payload.repositories)) {
|
|
287
|
+
throw new GitHubAppApiError("GitHub returned an invalid repositories payload");
|
|
288
|
+
}
|
|
289
|
+
for (const repo of payload.repositories) {
|
|
290
|
+
if (repo && typeof repo === "object" && !Array.isArray(repo)) {
|
|
291
|
+
out.push(repositoryFromPayload(repo, installationId, account));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (payload.repositories.length < 100) {
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function installationSummaryFromPayload(payload) {
|
|
300
|
+
const installationId = asInt(payload.id);
|
|
301
|
+
if (installationId === null) {
|
|
302
|
+
throw new GitHubAppApiError("GitHub returned an installation without id");
|
|
303
|
+
}
|
|
304
|
+
const account = typeof payload.account === "object" && payload.account ? payload.account : {};
|
|
305
|
+
return {
|
|
306
|
+
installationId,
|
|
307
|
+
accountLogin: typeof account.login === "string" ? account.login : null,
|
|
308
|
+
accountType: typeof account.type === "string" ? account.type : null,
|
|
309
|
+
suspended: Boolean(payload.suspended_at)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function githubGet(path, token, params) {
|
|
313
|
+
const url = new URL(`${githubApiBase}${path}`);
|
|
314
|
+
for (const [key, value] of Object.entries(params)) {
|
|
315
|
+
url.searchParams.set(key, value);
|
|
316
|
+
}
|
|
317
|
+
const response = await fetch(url, { headers: githubHeaders(token) });
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
320
|
+
}
|
|
321
|
+
return await response.json();
|
|
322
|
+
}
|
|
323
|
+
function repositoryFromPayload(payload, installationId, account) {
|
|
324
|
+
const id = asInt(payload.id);
|
|
325
|
+
const fullName = String(payload.full_name ?? "");
|
|
326
|
+
if (id === null || !fullName) {
|
|
327
|
+
throw new GitHubAppApiError("GitHub returned a repository without id/full_name");
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
id,
|
|
331
|
+
installationId,
|
|
332
|
+
fullName,
|
|
333
|
+
name: String(payload.name ?? fullName.split("/").at(-1) ?? fullName),
|
|
334
|
+
private: Boolean(payload.private),
|
|
335
|
+
htmlUrl: String(payload.html_url ?? `https://github.com/${fullName}`),
|
|
336
|
+
cloneUrl: String(payload.clone_url ?? `https://github.com/${fullName}.git`),
|
|
337
|
+
defaultBranch: String(payload.default_branch ?? "main"),
|
|
338
|
+
accountLogin: String(account.login ?? fullName.split("/", 1)[0]),
|
|
339
|
+
accountType: typeof account.type === "string" ? account.type : null
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function githubHeaders(token) {
|
|
343
|
+
return {
|
|
344
|
+
Accept: "application/vnd.github+json",
|
|
345
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
346
|
+
"X-GitHub-Api-Version": githubApiVersion
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function githubErrorMessage(response) {
|
|
350
|
+
try {
|
|
351
|
+
const payload = await response.json();
|
|
352
|
+
if (payload && typeof payload === "object" && "message" in payload) {
|
|
353
|
+
return `GitHub API ${response.status}: ${String(payload.message)}`;
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
return `GitHub API ${response.status}: ${await response.text()}`;
|
|
358
|
+
}
|
|
359
|
+
function normalizeGitHubAppPrivateKey(value) {
|
|
360
|
+
const privateKey = value.trim().replace(/\\n/g, "\n");
|
|
361
|
+
if (!privateKey || privateKey.startsWith(pkcs8PrivateKeyHeader)) {
|
|
362
|
+
return privateKey;
|
|
363
|
+
}
|
|
364
|
+
if (privateKey.startsWith(rsaPrivateKeyHeader)) {
|
|
365
|
+
return createPrivateKey(privateKey).export({ type: "pkcs8", format: "pem" }).toString();
|
|
366
|
+
}
|
|
367
|
+
return privateKey;
|
|
368
|
+
}
|
|
369
|
+
function signStatePayload(encoded, secret) {
|
|
370
|
+
return createHmac("sha256", secret).update(encoded).digest("base64url");
|
|
371
|
+
}
|
|
372
|
+
function safeEqual(left, right) {
|
|
373
|
+
const a = Buffer.from(left);
|
|
374
|
+
const b = Buffer.from(right);
|
|
375
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
376
|
+
}
|
|
377
|
+
function asInt(value) {
|
|
378
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
382
|
+
return Number(value);
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
export {
|
|
387
|
+
GitHubAppApiError,
|
|
388
|
+
GitHubAppConfigurationError,
|
|
389
|
+
buildGitHubAppManifest,
|
|
390
|
+
convertGitHubAppManifest,
|
|
391
|
+
createGitHubAppInstallationToken,
|
|
392
|
+
createSignedState,
|
|
393
|
+
envLinesFromGitHubManifestConversion,
|
|
394
|
+
getGitHubAppInstallationSummary,
|
|
395
|
+
githubAppBotIdentity,
|
|
396
|
+
githubAppMissingSettings,
|
|
397
|
+
githubOAuthAuthorizeUrl,
|
|
398
|
+
listGitHubAppInstallationSummaries,
|
|
399
|
+
listGitHubAppRepositories,
|
|
400
|
+
normalizeGitHubAppPrivateKey,
|
|
401
|
+
organizationAppManifestUrl,
|
|
402
|
+
personalAppManifestUrl,
|
|
403
|
+
readSignedState,
|
|
404
|
+
stateMaxAgeSeconds,
|
|
405
|
+
verifyGitHubInstallationAccessForUser,
|
|
406
|
+
verifySignedState
|
|
407
|
+
};
|
|
408
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport type { GitHubRepository } from \"@opengeni/contracts\";\nimport { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { SignJWT, importPKCS8 } from \"jose\";\n\nconst githubApiBase = \"https://api.github.com\";\nconst githubApiVersion = \"2022-11-28\";\nexport const stateMaxAgeSeconds = 60 * 60;\nconst pkcs8PrivateKeyHeader = `-----BEGIN ${\"PRIVATE KEY\"}-----`;\nconst rsaPrivateKeyHeader = `-----BEGIN ${\"RSA PRIVATE KEY\"}-----`;\n\nexport class GitHubAppConfigurationError extends Error {\n constructor(readonly missing: string[]) {\n super(\"GitHub App is not configured\");\n }\n}\n\nexport class GitHubAppApiError extends Error {}\n\nexport type GitHubAppInstallationSummary = {\n installationId: number;\n accountLogin: string | null;\n accountType: string | null;\n suspended: boolean;\n};\n\nexport type GitHubSignedStatePayload = {\n nonce: string;\n iat: number;\n accountId?: string;\n workspaceId?: string;\n [key: string]: unknown;\n};\n\nexport function githubAppMissingSettings(settings: Settings): string[] {\n const required: Record<string, string | undefined> = {\n OPENGENI_GITHUB_APP_ID: settings.githubAppId,\n OPENGENI_GITHUB_CLIENT_ID: settings.githubClientId,\n OPENGENI_GITHUB_CLIENT_SECRET: settings.githubClientSecret,\n OPENGENI_GITHUB_APP_SLUG: settings.githubAppSlug,\n OPENGENI_GITHUB_APP_PRIVATE_KEY: settings.githubAppPrivateKey,\n };\n return Object.entries(required).flatMap(([name, value]) => value && value.trim() ? [] : [name]);\n}\n\nexport function buildGitHubAppManifest(input: {\n appName: string;\n baseUrl: string;\n public: boolean;\n includeCiPermissions: boolean;\n setupUrl?: string;\n}): Record<string, unknown> {\n const base = input.baseUrl.replace(/\\/+$/, \"\");\n const permissions: Record<string, string> = {\n metadata: \"read\",\n contents: \"write\",\n pull_requests: \"write\",\n };\n if (input.includeCiPermissions) {\n permissions.actions = \"read\";\n permissions.checks = \"read\";\n permissions.statuses = \"write\";\n }\n const manifest: Record<string, unknown> = {\n name: input.appName,\n url: base,\n redirect_url: `${base}/v1/github/app-manifest/callback`,\n public: input.public,\n request_oauth_on_install: true,\n default_permissions: permissions,\n };\n if (input.setupUrl) {\n manifest.setup_url = input.setupUrl;\n manifest.setup_on_update = true;\n }\n return manifest;\n}\n\nexport function personalAppManifestUrl(state: string): string {\n return `https://github.com/settings/apps/new?state=${state}`;\n}\n\nexport function organizationAppManifestUrl(organization: string, state: string): string {\n return `https://github.com/organizations/${encodeURIComponent(organization)}/settings/apps/new?state=${state}`;\n}\n\nexport function githubOAuthAuthorizeUrl(input: {\n clientId: string;\n state: string;\n redirectUri?: string;\n}): string {\n const url = new URL(\"https://github.com/login/oauth/authorize\");\n url.searchParams.set(\"client_id\", input.clientId);\n url.searchParams.set(\"state\", input.state);\n if (input.redirectUri) {\n url.searchParams.set(\"redirect_uri\", input.redirectUri);\n }\n return url.toString();\n}\n\nexport function createSignedState(\n secret: string,\n payloadOrNow: Record<string, unknown> | number = {},\n nowArg = Math.floor(Date.now() / 1000),\n): string {\n const payloadInput = typeof payloadOrNow === \"number\" ? {} : payloadOrNow;\n const now = typeof payloadOrNow === \"number\" ? payloadOrNow : nowArg;\n const payload = {\n ...payloadInput,\n nonce: randomBytes(16).toString(\"base64url\"),\n iat: now,\n };\n const encoded = Buffer.from(JSON.stringify(payload)).toString(\"base64url\");\n return `${encoded}.${signStatePayload(encoded, secret)}`;\n}\n\nexport function readSignedState(state: string, secret: string, now = Math.floor(Date.now() / 1000)): GitHubSignedStatePayload | null {\n const [encoded, signature] = state.split(\".\", 2);\n if (!encoded || !signature) {\n return null;\n }\n const expected = signStatePayload(encoded, secret);\n if (!safeEqual(signature, expected)) {\n return null;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(Buffer.from(encoded, \"base64url\").toString(\"utf8\"));\n } catch {\n return null;\n }\n if (!payload || typeof payload !== \"object\" || typeof (payload as { iat?: unknown }).iat !== \"number\" || typeof (payload as { nonce?: unknown }).nonce !== \"string\") {\n return null;\n }\n const age = now - (payload as { iat: number }).iat;\n return age >= 0 && age <= stateMaxAgeSeconds ? payload as GitHubSignedStatePayload : null;\n}\n\nexport function verifySignedState(state: string, secret: string, now = Math.floor(Date.now() / 1000)): boolean {\n return readSignedState(state, secret, now) !== null;\n}\n\nexport function envLinesFromGitHubManifestConversion(payload: Record<string, unknown>): string[] {\n const privateKey = String(payload.pem ?? \"\").replace(/\\n/g, \"\\\\n\");\n return [\n `OPENGENI_GITHUB_APP_ID=${payload.id ?? \"\"}`,\n `OPENGENI_GITHUB_CLIENT_ID=${payload.client_id ?? \"\"}`,\n `OPENGENI_GITHUB_CLIENT_SECRET=${payload.client_secret ?? \"\"}`,\n `OPENGENI_GITHUB_APP_SLUG=${payload.slug ?? \"\"}`,\n `OPENGENI_GITHUB_WEBHOOK_SECRET=${payload.webhook_secret ?? \"\"}`,\n `OPENGENI_GITHUB_APP_PRIVATE_KEY=\"${privateKey}\"`,\n ];\n}\n\nexport async function convertGitHubAppManifest(code: string): Promise<Record<string, unknown>> {\n const response = await fetch(`${githubApiBase}/app-manifests/${code}/conversions`, {\n method: \"POST\",\n headers: githubHeaders(undefined),\n });\n if (!response.ok) {\n throw new GitHubAppApiError(await githubErrorMessage(response));\n }\n const payload = await response.json();\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) {\n throw new GitHubAppApiError(\"GitHub returned an invalid manifest conversion payload\");\n }\n return payload as Record<string, unknown>;\n}\n\nexport async function listGitHubAppInstallationSummaries(settings: Settings): Promise<GitHubAppInstallationSummary[]> {\n const missing = githubAppMissingSettings(settings);\n if (missing.length > 0) {\n throw new GitHubAppConfigurationError(missing);\n }\n const jwt = await createGitHubAppJwt(settings);\n const installations = await listInstallations(jwt);\n return installations.map(installationSummaryFromPayload);\n}\n\nexport async function getGitHubAppInstallationSummary(settings: Settings, installationId: number): Promise<GitHubAppInstallationSummary | null> {\n const installations = await listGitHubAppInstallationSummaries(settings);\n return installations.find((installation) => installation.installationId === installationId) ?? null;\n}\n\nexport async function verifyGitHubInstallationAccessForUser(settings: Settings, input: {\n code: string;\n installationId: number;\n}): Promise<GitHubAppInstallationSummary> {\n const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const installations = await listUserAccessibleInstallations(token);\n const installation = installations.find((candidate) => candidate.installationId === input.installationId);\n if (!installation) {\n throw new GitHubAppApiError(\"GitHub installation is not accessible to the installing user\");\n }\n return installation;\n}\n\nexport async function listGitHubAppRepositories(settings: Settings, input: {\n installationIds?: number[];\n} = {}): Promise<GitHubRepository[]> {\n const missing = githubAppMissingSettings(settings);\n if (missing.length > 0) {\n throw new GitHubAppConfigurationError(missing);\n }\n const allowedInstallations = input.installationIds ? new Set(input.installationIds) : null;\n if (allowedInstallations && allowedInstallations.size === 0) {\n return [];\n }\n const jwt = await createGitHubAppJwt(settings);\n const installations = await listInstallations(jwt);\n const repositories: GitHubRepository[] = [];\n for (const installation of installations) {\n if (installation.suspended_at) {\n continue;\n }\n const installationId = asInt(installation.id);\n if (installationId === null) {\n continue;\n }\n if (allowedInstallations && !allowedInstallations.has(installationId)) {\n continue;\n }\n const account = typeof installation.account === \"object\" && installation.account ? installation.account as Record<string, unknown> : {};\n const token = await createInstallationToken(jwt, { installationId });\n repositories.push(...await listInstallationRepositories(token, installationId, account));\n }\n repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));\n return repositories;\n}\n\nexport async function createGitHubAppInstallationToken(settings: Settings, input: {\n installationId: number;\n repositoryIds?: number[];\n}): Promise<string> {\n const missing = githubAppMissingSettings(settings);\n if (missing.length > 0) {\n throw new GitHubAppConfigurationError(missing);\n }\n const jwt = await createGitHubAppJwt(settings);\n return await createInstallationToken(jwt, input);\n}\n\nexport function githubAppBotIdentity(settings: Settings): { name: string; email: string } | null {\n const appId = settings.githubAppId?.trim();\n const slug = settings.githubAppSlug?.trim();\n if (!appId || !slug) {\n return null;\n }\n const login = `${slug}[bot]`;\n return {\n name: login,\n email: `${appId}+${login}@users.noreply.github.com`,\n };\n}\n\nasync function createGitHubAppJwt(settings: Settings): Promise<string> {\n const privateKey = normalizeGitHubAppPrivateKey(settings.githubAppPrivateKey ?? \"\");\n const appId = settings.githubAppId?.trim();\n if (!appId || !privateKey) {\n throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));\n }\n const key = await importPKCS8(privateKey, \"RS256\");\n const now = Math.floor(Date.now() / 1000);\n return await new SignJWT({})\n .setProtectedHeader({ alg: \"RS256\" })\n .setIssuedAt(now - 60)\n .setExpirationTime(now + 9 * 60)\n .setIssuer(appId)\n .sign(key);\n}\n\nasync function listInstallations(token: string): Promise<Array<Record<string, unknown>>> {\n const out: Array<Record<string, unknown>> = [];\n for (let page = 1; ; page += 1) {\n const payload = await githubGet(\"/app/installations\", token, { per_page: \"100\", page: String(page) });\n if (!Array.isArray(payload)) {\n throw new GitHubAppApiError(\"GitHub returned an invalid installations payload\");\n }\n out.push(...payload.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === \"object\" && !Array.isArray(item))));\n if (payload.length < 100) {\n return out;\n }\n }\n}\n\nasync function exchangeGitHubOAuthCodeForUserToken(settings: Settings, code: string): Promise<string> {\n if (!settings.githubClientId || !settings.githubClientSecret) {\n throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));\n }\n const response = await fetch(\"https://github.com/login/oauth/access_token\", {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n client_id: settings.githubClientId,\n client_secret: settings.githubClientSecret,\n code,\n }),\n });\n if (!response.ok) {\n throw new GitHubAppApiError(await githubErrorMessage(response));\n }\n const payload = await response.json();\n if (!payload || typeof payload !== \"object\" || typeof payload.access_token !== \"string\") {\n throw new GitHubAppApiError(\"GitHub returned an invalid OAuth token payload\");\n }\n return payload.access_token;\n}\n\nasync function listUserAccessibleInstallations(token: string): Promise<GitHubAppInstallationSummary[]> {\n const out: GitHubAppInstallationSummary[] = [];\n for (let page = 1; ; page += 1) {\n const payload = await githubGet(\"/user/installations\", token, { per_page: \"100\", page: String(page) });\n const installations: unknown[] | null = payload && typeof payload === \"object\" && Array.isArray(payload.installations)\n ? payload.installations as unknown[]\n : null;\n if (!installations) {\n throw new GitHubAppApiError(\"GitHub returned an invalid user installations payload\");\n }\n out.push(...installations\n .filter((item): item is Record<string, unknown> => Boolean(item && typeof item === \"object\" && !Array.isArray(item)))\n .map(installationSummaryFromPayload));\n if (installations.length < 100) {\n return out;\n }\n }\n}\n\nasync function createInstallationToken(appJwt: string, input: {\n installationId: number;\n repositoryIds?: number[];\n}): Promise<string> {\n const scoped = input.repositoryIds && input.repositoryIds.length > 0;\n const response = await fetch(`${githubApiBase}/app/installations/${input.installationId}/access_tokens`, {\n method: \"POST\",\n headers: {\n ...githubHeaders(appJwt),\n ...(scoped ? { \"Content-Type\": \"application/json\" } : {}),\n },\n ...(scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}),\n });\n if (!response.ok) {\n throw new GitHubAppApiError(await githubErrorMessage(response));\n }\n const payload = await response.json();\n if (!payload || typeof payload !== \"object\" || typeof payload.token !== \"string\") {\n throw new GitHubAppApiError(\"GitHub returned an invalid installation token payload\");\n }\n return payload.token;\n}\n\nasync function listInstallationRepositories(token: string, installationId: number, account: Record<string, unknown>): Promise<GitHubRepository[]> {\n const out: GitHubRepository[] = [];\n for (let page = 1; ; page += 1) {\n const payload = await githubGet(\"/installation/repositories\", token, { per_page: \"100\", page: String(page) });\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload) || !Array.isArray(payload.repositories)) {\n throw new GitHubAppApiError(\"GitHub returned an invalid repositories payload\");\n }\n for (const repo of payload.repositories) {\n if (repo && typeof repo === \"object\" && !Array.isArray(repo)) {\n out.push(repositoryFromPayload(repo as Record<string, unknown>, installationId, account));\n }\n }\n if (payload.repositories.length < 100) {\n return out;\n }\n }\n}\n\nfunction installationSummaryFromPayload(payload: Record<string, unknown>): GitHubAppInstallationSummary {\n const installationId = asInt(payload.id);\n if (installationId === null) {\n throw new GitHubAppApiError(\"GitHub returned an installation without id\");\n }\n const account = typeof payload.account === \"object\" && payload.account ? payload.account as Record<string, unknown> : {};\n return {\n installationId,\n accountLogin: typeof account.login === \"string\" ? account.login : null,\n accountType: typeof account.type === \"string\" ? account.type : null,\n suspended: Boolean(payload.suspended_at),\n };\n}\n\nasync function githubGet(path: string, token: string, params: Record<string, string>): Promise<any> {\n const url = new URL(`${githubApiBase}${path}`);\n for (const [key, value] of Object.entries(params)) {\n url.searchParams.set(key, value);\n }\n const response = await fetch(url, { headers: githubHeaders(token) });\n if (!response.ok) {\n throw new GitHubAppApiError(await githubErrorMessage(response));\n }\n return await response.json();\n}\n\nfunction repositoryFromPayload(payload: Record<string, unknown>, installationId: number, account: Record<string, unknown>): GitHubRepository {\n const id = asInt(payload.id);\n const fullName = String(payload.full_name ?? \"\");\n if (id === null || !fullName) {\n throw new GitHubAppApiError(\"GitHub returned a repository without id/full_name\");\n }\n return {\n id,\n installationId,\n fullName,\n name: String(payload.name ?? fullName.split(\"/\").at(-1) ?? fullName),\n private: Boolean(payload.private),\n htmlUrl: String(payload.html_url ?? `https://github.com/${fullName}`),\n cloneUrl: String(payload.clone_url ?? `https://github.com/${fullName}.git`),\n defaultBranch: String(payload.default_branch ?? \"main\"),\n accountLogin: String(account.login ?? fullName.split(\"/\", 1)[0]),\n accountType: typeof account.type === \"string\" ? account.type : null,\n };\n}\n\nfunction githubHeaders(token?: string): HeadersInit {\n return {\n Accept: \"application/vnd.github+json\",\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n \"X-GitHub-Api-Version\": githubApiVersion,\n };\n}\n\nasync function githubErrorMessage(response: Response): Promise<string> {\n try {\n const payload = await response.json();\n if (payload && typeof payload === \"object\" && \"message\" in payload) {\n return `GitHub API ${response.status}: ${String(payload.message)}`;\n }\n } catch {\n // fall through\n }\n return `GitHub API ${response.status}: ${await response.text()}`;\n}\n\nexport function normalizeGitHubAppPrivateKey(value: string): string {\n const privateKey = value.trim().replace(/\\\\n/g, \"\\n\");\n if (!privateKey || privateKey.startsWith(pkcs8PrivateKeyHeader)) {\n return privateKey;\n }\n if (privateKey.startsWith(rsaPrivateKeyHeader)) {\n return createPrivateKey(privateKey).export({ type: \"pkcs8\", format: \"pem\" }).toString();\n }\n return privateKey;\n}\n\nfunction signStatePayload(encoded: string, secret: string): string {\n return createHmac(\"sha256\", secret).update(encoded).digest(\"base64url\");\n}\n\nfunction safeEqual(left: string, right: string): boolean {\n const a = Buffer.from(left);\n const b = Buffer.from(right);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nfunction asInt(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isInteger(value)) {\n return value;\n }\n if (typeof value === \"string\" && /^\\d+$/.test(value)) {\n return Number(value);\n }\n return null;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengeni/github",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"provenance": true
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@opengeni/config": "^0.2.0",
|
|
28
|
+
"@opengeni/contracts": "^0.3.0",
|
|
29
|
+
"jose": "^6.1.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.5.0",
|
|
33
|
+
"typescript": "^6.0.3"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
|
38
|
+
"directory": "packages/github"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import type { GitHubRepository } from "@opengeni/contracts";
|
|
3
|
+
import { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { SignJWT, importPKCS8 } from "jose";
|
|
5
|
+
|
|
6
|
+
const githubApiBase = "https://api.github.com";
|
|
7
|
+
const githubApiVersion = "2022-11-28";
|
|
8
|
+
export const stateMaxAgeSeconds = 60 * 60;
|
|
9
|
+
const pkcs8PrivateKeyHeader = `-----BEGIN ${"PRIVATE KEY"}-----`;
|
|
10
|
+
const rsaPrivateKeyHeader = `-----BEGIN ${"RSA PRIVATE KEY"}-----`;
|
|
11
|
+
|
|
12
|
+
export class GitHubAppConfigurationError extends Error {
|
|
13
|
+
constructor(readonly missing: string[]) {
|
|
14
|
+
super("GitHub App is not configured");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class GitHubAppApiError extends Error {}
|
|
19
|
+
|
|
20
|
+
export type GitHubAppInstallationSummary = {
|
|
21
|
+
installationId: number;
|
|
22
|
+
accountLogin: string | null;
|
|
23
|
+
accountType: string | null;
|
|
24
|
+
suspended: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type GitHubSignedStatePayload = {
|
|
28
|
+
nonce: string;
|
|
29
|
+
iat: number;
|
|
30
|
+
accountId?: string;
|
|
31
|
+
workspaceId?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function githubAppMissingSettings(settings: Settings): string[] {
|
|
36
|
+
const required: Record<string, string | undefined> = {
|
|
37
|
+
OPENGENI_GITHUB_APP_ID: settings.githubAppId,
|
|
38
|
+
OPENGENI_GITHUB_CLIENT_ID: settings.githubClientId,
|
|
39
|
+
OPENGENI_GITHUB_CLIENT_SECRET: settings.githubClientSecret,
|
|
40
|
+
OPENGENI_GITHUB_APP_SLUG: settings.githubAppSlug,
|
|
41
|
+
OPENGENI_GITHUB_APP_PRIVATE_KEY: settings.githubAppPrivateKey,
|
|
42
|
+
};
|
|
43
|
+
return Object.entries(required).flatMap(([name, value]) => value && value.trim() ? [] : [name]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildGitHubAppManifest(input: {
|
|
47
|
+
appName: string;
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
public: boolean;
|
|
50
|
+
includeCiPermissions: boolean;
|
|
51
|
+
setupUrl?: string;
|
|
52
|
+
}): Record<string, unknown> {
|
|
53
|
+
const base = input.baseUrl.replace(/\/+$/, "");
|
|
54
|
+
const permissions: Record<string, string> = {
|
|
55
|
+
metadata: "read",
|
|
56
|
+
contents: "write",
|
|
57
|
+
pull_requests: "write",
|
|
58
|
+
};
|
|
59
|
+
if (input.includeCiPermissions) {
|
|
60
|
+
permissions.actions = "read";
|
|
61
|
+
permissions.checks = "read";
|
|
62
|
+
permissions.statuses = "write";
|
|
63
|
+
}
|
|
64
|
+
const manifest: Record<string, unknown> = {
|
|
65
|
+
name: input.appName,
|
|
66
|
+
url: base,
|
|
67
|
+
redirect_url: `${base}/v1/github/app-manifest/callback`,
|
|
68
|
+
public: input.public,
|
|
69
|
+
request_oauth_on_install: true,
|
|
70
|
+
default_permissions: permissions,
|
|
71
|
+
};
|
|
72
|
+
if (input.setupUrl) {
|
|
73
|
+
manifest.setup_url = input.setupUrl;
|
|
74
|
+
manifest.setup_on_update = true;
|
|
75
|
+
}
|
|
76
|
+
return manifest;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function personalAppManifestUrl(state: string): string {
|
|
80
|
+
return `https://github.com/settings/apps/new?state=${state}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function organizationAppManifestUrl(organization: string, state: string): string {
|
|
84
|
+
return `https://github.com/organizations/${encodeURIComponent(organization)}/settings/apps/new?state=${state}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function githubOAuthAuthorizeUrl(input: {
|
|
88
|
+
clientId: string;
|
|
89
|
+
state: string;
|
|
90
|
+
redirectUri?: string;
|
|
91
|
+
}): string {
|
|
92
|
+
const url = new URL("https://github.com/login/oauth/authorize");
|
|
93
|
+
url.searchParams.set("client_id", input.clientId);
|
|
94
|
+
url.searchParams.set("state", input.state);
|
|
95
|
+
if (input.redirectUri) {
|
|
96
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
97
|
+
}
|
|
98
|
+
return url.toString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createSignedState(
|
|
102
|
+
secret: string,
|
|
103
|
+
payloadOrNow: Record<string, unknown> | number = {},
|
|
104
|
+
nowArg = Math.floor(Date.now() / 1000),
|
|
105
|
+
): string {
|
|
106
|
+
const payloadInput = typeof payloadOrNow === "number" ? {} : payloadOrNow;
|
|
107
|
+
const now = typeof payloadOrNow === "number" ? payloadOrNow : nowArg;
|
|
108
|
+
const payload = {
|
|
109
|
+
...payloadInput,
|
|
110
|
+
nonce: randomBytes(16).toString("base64url"),
|
|
111
|
+
iat: now,
|
|
112
|
+
};
|
|
113
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
114
|
+
return `${encoded}.${signStatePayload(encoded, secret)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function readSignedState(state: string, secret: string, now = Math.floor(Date.now() / 1000)): GitHubSignedStatePayload | null {
|
|
118
|
+
const [encoded, signature] = state.split(".", 2);
|
|
119
|
+
if (!encoded || !signature) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const expected = signStatePayload(encoded, secret);
|
|
123
|
+
if (!safeEqual(signature, expected)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
let payload: unknown;
|
|
127
|
+
try {
|
|
128
|
+
payload = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (!payload || typeof payload !== "object" || typeof (payload as { iat?: unknown }).iat !== "number" || typeof (payload as { nonce?: unknown }).nonce !== "string") {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const age = now - (payload as { iat: number }).iat;
|
|
136
|
+
return age >= 0 && age <= stateMaxAgeSeconds ? payload as GitHubSignedStatePayload : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function verifySignedState(state: string, secret: string, now = Math.floor(Date.now() / 1000)): boolean {
|
|
140
|
+
return readSignedState(state, secret, now) !== null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function envLinesFromGitHubManifestConversion(payload: Record<string, unknown>): string[] {
|
|
144
|
+
const privateKey = String(payload.pem ?? "").replace(/\n/g, "\\n");
|
|
145
|
+
return [
|
|
146
|
+
`OPENGENI_GITHUB_APP_ID=${payload.id ?? ""}`,
|
|
147
|
+
`OPENGENI_GITHUB_CLIENT_ID=${payload.client_id ?? ""}`,
|
|
148
|
+
`OPENGENI_GITHUB_CLIENT_SECRET=${payload.client_secret ?? ""}`,
|
|
149
|
+
`OPENGENI_GITHUB_APP_SLUG=${payload.slug ?? ""}`,
|
|
150
|
+
`OPENGENI_GITHUB_WEBHOOK_SECRET=${payload.webhook_secret ?? ""}`,
|
|
151
|
+
`OPENGENI_GITHUB_APP_PRIVATE_KEY="${privateKey}"`,
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function convertGitHubAppManifest(code: string): Promise<Record<string, unknown>> {
|
|
156
|
+
const response = await fetch(`${githubApiBase}/app-manifests/${code}/conversions`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: githubHeaders(undefined),
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
162
|
+
}
|
|
163
|
+
const payload = await response.json();
|
|
164
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
165
|
+
throw new GitHubAppApiError("GitHub returned an invalid manifest conversion payload");
|
|
166
|
+
}
|
|
167
|
+
return payload as Record<string, unknown>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function listGitHubAppInstallationSummaries(settings: Settings): Promise<GitHubAppInstallationSummary[]> {
|
|
171
|
+
const missing = githubAppMissingSettings(settings);
|
|
172
|
+
if (missing.length > 0) {
|
|
173
|
+
throw new GitHubAppConfigurationError(missing);
|
|
174
|
+
}
|
|
175
|
+
const jwt = await createGitHubAppJwt(settings);
|
|
176
|
+
const installations = await listInstallations(jwt);
|
|
177
|
+
return installations.map(installationSummaryFromPayload);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function getGitHubAppInstallationSummary(settings: Settings, installationId: number): Promise<GitHubAppInstallationSummary | null> {
|
|
181
|
+
const installations = await listGitHubAppInstallationSummaries(settings);
|
|
182
|
+
return installations.find((installation) => installation.installationId === installationId) ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function verifyGitHubInstallationAccessForUser(settings: Settings, input: {
|
|
186
|
+
code: string;
|
|
187
|
+
installationId: number;
|
|
188
|
+
}): Promise<GitHubAppInstallationSummary> {
|
|
189
|
+
const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);
|
|
190
|
+
const installations = await listUserAccessibleInstallations(token);
|
|
191
|
+
const installation = installations.find((candidate) => candidate.installationId === input.installationId);
|
|
192
|
+
if (!installation) {
|
|
193
|
+
throw new GitHubAppApiError("GitHub installation is not accessible to the installing user");
|
|
194
|
+
}
|
|
195
|
+
return installation;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function listGitHubAppRepositories(settings: Settings, input: {
|
|
199
|
+
installationIds?: number[];
|
|
200
|
+
} = {}): Promise<GitHubRepository[]> {
|
|
201
|
+
const missing = githubAppMissingSettings(settings);
|
|
202
|
+
if (missing.length > 0) {
|
|
203
|
+
throw new GitHubAppConfigurationError(missing);
|
|
204
|
+
}
|
|
205
|
+
const allowedInstallations = input.installationIds ? new Set(input.installationIds) : null;
|
|
206
|
+
if (allowedInstallations && allowedInstallations.size === 0) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const jwt = await createGitHubAppJwt(settings);
|
|
210
|
+
const installations = await listInstallations(jwt);
|
|
211
|
+
const repositories: GitHubRepository[] = [];
|
|
212
|
+
for (const installation of installations) {
|
|
213
|
+
if (installation.suspended_at) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const installationId = asInt(installation.id);
|
|
217
|
+
if (installationId === null) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (allowedInstallations && !allowedInstallations.has(installationId)) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const account = typeof installation.account === "object" && installation.account ? installation.account as Record<string, unknown> : {};
|
|
224
|
+
const token = await createInstallationToken(jwt, { installationId });
|
|
225
|
+
repositories.push(...await listInstallationRepositories(token, installationId, account));
|
|
226
|
+
}
|
|
227
|
+
repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));
|
|
228
|
+
return repositories;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function createGitHubAppInstallationToken(settings: Settings, input: {
|
|
232
|
+
installationId: number;
|
|
233
|
+
repositoryIds?: number[];
|
|
234
|
+
}): Promise<string> {
|
|
235
|
+
const missing = githubAppMissingSettings(settings);
|
|
236
|
+
if (missing.length > 0) {
|
|
237
|
+
throw new GitHubAppConfigurationError(missing);
|
|
238
|
+
}
|
|
239
|
+
const jwt = await createGitHubAppJwt(settings);
|
|
240
|
+
return await createInstallationToken(jwt, input);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function githubAppBotIdentity(settings: Settings): { name: string; email: string } | null {
|
|
244
|
+
const appId = settings.githubAppId?.trim();
|
|
245
|
+
const slug = settings.githubAppSlug?.trim();
|
|
246
|
+
if (!appId || !slug) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const login = `${slug}[bot]`;
|
|
250
|
+
return {
|
|
251
|
+
name: login,
|
|
252
|
+
email: `${appId}+${login}@users.noreply.github.com`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function createGitHubAppJwt(settings: Settings): Promise<string> {
|
|
257
|
+
const privateKey = normalizeGitHubAppPrivateKey(settings.githubAppPrivateKey ?? "");
|
|
258
|
+
const appId = settings.githubAppId?.trim();
|
|
259
|
+
if (!appId || !privateKey) {
|
|
260
|
+
throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
|
|
261
|
+
}
|
|
262
|
+
const key = await importPKCS8(privateKey, "RS256");
|
|
263
|
+
const now = Math.floor(Date.now() / 1000);
|
|
264
|
+
return await new SignJWT({})
|
|
265
|
+
.setProtectedHeader({ alg: "RS256" })
|
|
266
|
+
.setIssuedAt(now - 60)
|
|
267
|
+
.setExpirationTime(now + 9 * 60)
|
|
268
|
+
.setIssuer(appId)
|
|
269
|
+
.sign(key);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function listInstallations(token: string): Promise<Array<Record<string, unknown>>> {
|
|
273
|
+
const out: Array<Record<string, unknown>> = [];
|
|
274
|
+
for (let page = 1; ; page += 1) {
|
|
275
|
+
const payload = await githubGet("/app/installations", token, { per_page: "100", page: String(page) });
|
|
276
|
+
if (!Array.isArray(payload)) {
|
|
277
|
+
throw new GitHubAppApiError("GitHub returned an invalid installations payload");
|
|
278
|
+
}
|
|
279
|
+
out.push(...payload.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === "object" && !Array.isArray(item))));
|
|
280
|
+
if (payload.length < 100) {
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function exchangeGitHubOAuthCodeForUserToken(settings: Settings, code: string): Promise<string> {
|
|
287
|
+
if (!settings.githubClientId || !settings.githubClientSecret) {
|
|
288
|
+
throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
|
|
289
|
+
}
|
|
290
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
Accept: "application/json",
|
|
294
|
+
"Content-Type": "application/json",
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
client_id: settings.githubClientId,
|
|
298
|
+
client_secret: settings.githubClientSecret,
|
|
299
|
+
code,
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
304
|
+
}
|
|
305
|
+
const payload = await response.json();
|
|
306
|
+
if (!payload || typeof payload !== "object" || typeof payload.access_token !== "string") {
|
|
307
|
+
throw new GitHubAppApiError("GitHub returned an invalid OAuth token payload");
|
|
308
|
+
}
|
|
309
|
+
return payload.access_token;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function listUserAccessibleInstallations(token: string): Promise<GitHubAppInstallationSummary[]> {
|
|
313
|
+
const out: GitHubAppInstallationSummary[] = [];
|
|
314
|
+
for (let page = 1; ; page += 1) {
|
|
315
|
+
const payload = await githubGet("/user/installations", token, { per_page: "100", page: String(page) });
|
|
316
|
+
const installations: unknown[] | null = payload && typeof payload === "object" && Array.isArray(payload.installations)
|
|
317
|
+
? payload.installations as unknown[]
|
|
318
|
+
: null;
|
|
319
|
+
if (!installations) {
|
|
320
|
+
throw new GitHubAppApiError("GitHub returned an invalid user installations payload");
|
|
321
|
+
}
|
|
322
|
+
out.push(...installations
|
|
323
|
+
.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === "object" && !Array.isArray(item)))
|
|
324
|
+
.map(installationSummaryFromPayload));
|
|
325
|
+
if (installations.length < 100) {
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function createInstallationToken(appJwt: string, input: {
|
|
332
|
+
installationId: number;
|
|
333
|
+
repositoryIds?: number[];
|
|
334
|
+
}): Promise<string> {
|
|
335
|
+
const scoped = input.repositoryIds && input.repositoryIds.length > 0;
|
|
336
|
+
const response = await fetch(`${githubApiBase}/app/installations/${input.installationId}/access_tokens`, {
|
|
337
|
+
method: "POST",
|
|
338
|
+
headers: {
|
|
339
|
+
...githubHeaders(appJwt),
|
|
340
|
+
...(scoped ? { "Content-Type": "application/json" } : {}),
|
|
341
|
+
},
|
|
342
|
+
...(scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}),
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
346
|
+
}
|
|
347
|
+
const payload = await response.json();
|
|
348
|
+
if (!payload || typeof payload !== "object" || typeof payload.token !== "string") {
|
|
349
|
+
throw new GitHubAppApiError("GitHub returned an invalid installation token payload");
|
|
350
|
+
}
|
|
351
|
+
return payload.token;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function listInstallationRepositories(token: string, installationId: number, account: Record<string, unknown>): Promise<GitHubRepository[]> {
|
|
355
|
+
const out: GitHubRepository[] = [];
|
|
356
|
+
for (let page = 1; ; page += 1) {
|
|
357
|
+
const payload = await githubGet("/installation/repositories", token, { per_page: "100", page: String(page) });
|
|
358
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Array.isArray(payload.repositories)) {
|
|
359
|
+
throw new GitHubAppApiError("GitHub returned an invalid repositories payload");
|
|
360
|
+
}
|
|
361
|
+
for (const repo of payload.repositories) {
|
|
362
|
+
if (repo && typeof repo === "object" && !Array.isArray(repo)) {
|
|
363
|
+
out.push(repositoryFromPayload(repo as Record<string, unknown>, installationId, account));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (payload.repositories.length < 100) {
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function installationSummaryFromPayload(payload: Record<string, unknown>): GitHubAppInstallationSummary {
|
|
373
|
+
const installationId = asInt(payload.id);
|
|
374
|
+
if (installationId === null) {
|
|
375
|
+
throw new GitHubAppApiError("GitHub returned an installation without id");
|
|
376
|
+
}
|
|
377
|
+
const account = typeof payload.account === "object" && payload.account ? payload.account as Record<string, unknown> : {};
|
|
378
|
+
return {
|
|
379
|
+
installationId,
|
|
380
|
+
accountLogin: typeof account.login === "string" ? account.login : null,
|
|
381
|
+
accountType: typeof account.type === "string" ? account.type : null,
|
|
382
|
+
suspended: Boolean(payload.suspended_at),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function githubGet(path: string, token: string, params: Record<string, string>): Promise<any> {
|
|
387
|
+
const url = new URL(`${githubApiBase}${path}`);
|
|
388
|
+
for (const [key, value] of Object.entries(params)) {
|
|
389
|
+
url.searchParams.set(key, value);
|
|
390
|
+
}
|
|
391
|
+
const response = await fetch(url, { headers: githubHeaders(token) });
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new GitHubAppApiError(await githubErrorMessage(response));
|
|
394
|
+
}
|
|
395
|
+
return await response.json();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function repositoryFromPayload(payload: Record<string, unknown>, installationId: number, account: Record<string, unknown>): GitHubRepository {
|
|
399
|
+
const id = asInt(payload.id);
|
|
400
|
+
const fullName = String(payload.full_name ?? "");
|
|
401
|
+
if (id === null || !fullName) {
|
|
402
|
+
throw new GitHubAppApiError("GitHub returned a repository without id/full_name");
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
id,
|
|
406
|
+
installationId,
|
|
407
|
+
fullName,
|
|
408
|
+
name: String(payload.name ?? fullName.split("/").at(-1) ?? fullName),
|
|
409
|
+
private: Boolean(payload.private),
|
|
410
|
+
htmlUrl: String(payload.html_url ?? `https://github.com/${fullName}`),
|
|
411
|
+
cloneUrl: String(payload.clone_url ?? `https://github.com/${fullName}.git`),
|
|
412
|
+
defaultBranch: String(payload.default_branch ?? "main"),
|
|
413
|
+
accountLogin: String(account.login ?? fullName.split("/", 1)[0]),
|
|
414
|
+
accountType: typeof account.type === "string" ? account.type : null,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function githubHeaders(token?: string): HeadersInit {
|
|
419
|
+
return {
|
|
420
|
+
Accept: "application/vnd.github+json",
|
|
421
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
422
|
+
"X-GitHub-Api-Version": githubApiVersion,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function githubErrorMessage(response: Response): Promise<string> {
|
|
427
|
+
try {
|
|
428
|
+
const payload = await response.json();
|
|
429
|
+
if (payload && typeof payload === "object" && "message" in payload) {
|
|
430
|
+
return `GitHub API ${response.status}: ${String(payload.message)}`;
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// fall through
|
|
434
|
+
}
|
|
435
|
+
return `GitHub API ${response.status}: ${await response.text()}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function normalizeGitHubAppPrivateKey(value: string): string {
|
|
439
|
+
const privateKey = value.trim().replace(/\\n/g, "\n");
|
|
440
|
+
if (!privateKey || privateKey.startsWith(pkcs8PrivateKeyHeader)) {
|
|
441
|
+
return privateKey;
|
|
442
|
+
}
|
|
443
|
+
if (privateKey.startsWith(rsaPrivateKeyHeader)) {
|
|
444
|
+
return createPrivateKey(privateKey).export({ type: "pkcs8", format: "pem" }).toString();
|
|
445
|
+
}
|
|
446
|
+
return privateKey;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function signStatePayload(encoded: string, secret: string): string {
|
|
450
|
+
return createHmac("sha256", secret).update(encoded).digest("base64url");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function safeEqual(left: string, right: string): boolean {
|
|
454
|
+
const a = Buffer.from(left);
|
|
455
|
+
const b = Buffer.from(right);
|
|
456
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function asInt(value: unknown): number | null {
|
|
460
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
464
|
+
return Number(value);
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|