@opengeni/github 0.2.8 → 0.2.9

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 CHANGED
@@ -54,10 +54,18 @@ declare function createGitHubAppInstallationToken(settings: Settings, input: {
54
54
  installationId: number;
55
55
  repositoryIds?: number[];
56
56
  }): Promise<string>;
57
+ type GitHubAppInstallationToken = {
58
+ token: string;
59
+ expiresAt: string | null;
60
+ };
61
+ declare function createGitHubAppInstallationTokenWithExpiry(settings: Settings, input: {
62
+ installationId: number;
63
+ repositoryIds?: number[];
64
+ }): Promise<GitHubAppInstallationToken>;
57
65
  declare function githubAppBotIdentity(settings: Settings): {
58
66
  name: string;
59
67
  email: string;
60
68
  } | null;
61
69
  declare function normalizeGitHubAppPrivateKey(value: string): string;
62
70
 
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 };
71
+ export { GitHubAppApiError, GitHubAppConfigurationError, type GitHubAppInstallationSummary, type GitHubAppInstallationToken, type GitHubSignedStatePayload, buildGitHubAppManifest, convertGitHubAppManifest, createGitHubAppInstallationToken, createGitHubAppInstallationTokenWithExpiry, createSignedState, envLinesFromGitHubManifestConversion, getGitHubAppInstallationSummary, githubAppBotIdentity, githubAppMissingSettings, githubOAuthAuthorizeUrl, listGitHubAppInstallationSummaries, listGitHubAppRepositories, normalizeGitHubAppPrivateKey, organizationAppManifestUrl, personalAppManifestUrl, readSignedState, stateMaxAgeSeconds, verifyGitHubInstallationAccessForUser, verifySignedState };
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from "cryp
3
3
  import { SignJWT, importPKCS8 } from "jose";
4
4
  var githubApiBase = "https://api.github.com";
5
5
  var githubApiVersion = "2022-11-28";
6
+ var githubTokenMintTimeoutMs = 6e4;
6
7
  var stateMaxAgeSeconds = 60 * 60;
7
8
  var pkcs8PrivateKeyHeader = `-----BEGIN ${"PRIVATE KEY"}-----`;
8
9
  var rsaPrivateKeyHeader = `-----BEGIN ${"RSA PRIVATE KEY"}-----`;
@@ -11,7 +12,6 @@ var GitHubAppConfigurationError = class extends Error {
11
12
  super("GitHub App is not configured");
12
13
  this.missing = missing;
13
14
  }
14
- missing;
15
15
  };
16
16
  var GitHubAppApiError = class extends Error {
17
17
  };
@@ -142,7 +142,9 @@ async function getGitHubAppInstallationSummary(settings, installationId) {
142
142
  async function verifyGitHubInstallationAccessForUser(settings, input) {
143
143
  const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);
144
144
  const installations = await listUserAccessibleInstallations(token);
145
- const installation = installations.find((candidate) => candidate.installationId === input.installationId);
145
+ const installation = installations.find(
146
+ (candidate) => candidate.installationId === input.installationId
147
+ );
146
148
  if (!installation) {
147
149
  throw new GitHubAppApiError("GitHub installation is not accessible to the installing user");
148
150
  }
@@ -173,12 +175,17 @@ async function listGitHubAppRepositories(settings, input = {}) {
173
175
  }
174
176
  const account = typeof installation.account === "object" && installation.account ? installation.account : {};
175
177
  const token = await createInstallationToken(jwt, { installationId });
176
- repositories.push(...await listInstallationRepositories(token, installationId, account));
178
+ repositories.push(
179
+ ...await listInstallationRepositories(token.token, installationId, account)
180
+ );
177
181
  }
178
182
  repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));
179
183
  return repositories;
180
184
  }
181
185
  async function createGitHubAppInstallationToken(settings, input) {
186
+ return (await createGitHubAppInstallationTokenWithExpiry(settings, input)).token;
187
+ }
188
+ async function createGitHubAppInstallationTokenWithExpiry(settings, input) {
182
189
  const missing = githubAppMissingSettings(settings);
183
190
  if (missing.length > 0) {
184
191
  throw new GitHubAppConfigurationError(missing);
@@ -211,11 +218,18 @@ async function createGitHubAppJwt(settings) {
211
218
  async function listInstallations(token) {
212
219
  const out = [];
213
220
  for (let page = 1; ; page += 1) {
214
- const payload = await githubGet("/app/installations", token, { per_page: "100", page: String(page) });
221
+ const payload = await githubGet("/app/installations", token, {
222
+ per_page: "100",
223
+ page: String(page)
224
+ });
215
225
  if (!Array.isArray(payload)) {
216
226
  throw new GitHubAppApiError("GitHub returned an invalid installations payload");
217
227
  }
218
- out.push(...payload.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))));
228
+ out.push(
229
+ ...payload.filter(
230
+ (item) => Boolean(item && typeof item === "object" && !Array.isArray(item))
231
+ )
232
+ );
219
233
  if (payload.length < 100) {
220
234
  return out;
221
235
  }
@@ -249,12 +263,19 @@ async function exchangeGitHubOAuthCodeForUserToken(settings, code) {
249
263
  async function listUserAccessibleInstallations(token) {
250
264
  const out = [];
251
265
  for (let page = 1; ; page += 1) {
252
- const payload = await githubGet("/user/installations", token, { per_page: "100", page: String(page) });
266
+ const payload = await githubGet("/user/installations", token, {
267
+ per_page: "100",
268
+ page: String(page)
269
+ });
253
270
  const installations = payload && typeof payload === "object" && Array.isArray(payload.installations) ? payload.installations : null;
254
271
  if (!installations) {
255
272
  throw new GitHubAppApiError("GitHub returned an invalid user installations payload");
256
273
  }
257
- out.push(...installations.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))).map(installationSummaryFromPayload));
274
+ out.push(
275
+ ...installations.filter(
276
+ (item) => Boolean(item && typeof item === "object" && !Array.isArray(item))
277
+ ).map(installationSummaryFromPayload)
278
+ );
258
279
  if (installations.length < 100) {
259
280
  return out;
260
281
  }
@@ -262,14 +283,18 @@ async function listUserAccessibleInstallations(token) {
262
283
  }
263
284
  async function createInstallationToken(appJwt, input) {
264
285
  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
- });
286
+ const response = await fetch(
287
+ `${githubApiBase}/app/installations/${input.installationId}/access_tokens`,
288
+ {
289
+ method: "POST",
290
+ headers: {
291
+ ...githubHeaders(appJwt),
292
+ ...scoped ? { "Content-Type": "application/json" } : {}
293
+ },
294
+ signal: AbortSignal.timeout(githubTokenMintTimeoutMs),
295
+ ...scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}
296
+ }
297
+ );
273
298
  if (!response.ok) {
274
299
  throw new GitHubAppApiError(await githubErrorMessage(response));
275
300
  }
@@ -277,12 +302,18 @@ async function createInstallationToken(appJwt, input) {
277
302
  if (!payload || typeof payload !== "object" || typeof payload.token !== "string") {
278
303
  throw new GitHubAppApiError("GitHub returned an invalid installation token payload");
279
304
  }
280
- return payload.token;
305
+ return {
306
+ token: payload.token,
307
+ expiresAt: typeof payload.expires_at === "string" ? payload.expires_at : null
308
+ };
281
309
  }
282
310
  async function listInstallationRepositories(token, installationId, account) {
283
311
  const out = [];
284
312
  for (let page = 1; ; page += 1) {
285
- const payload = await githubGet("/installation/repositories", token, { per_page: "100", page: String(page) });
313
+ const payload = await githubGet("/installation/repositories", token, {
314
+ per_page: "100",
315
+ page: String(page)
316
+ });
286
317
  if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Array.isArray(payload.repositories)) {
287
318
  throw new GitHubAppApiError("GitHub returned an invalid repositories payload");
288
319
  }
@@ -389,6 +420,7 @@ export {
389
420
  buildGitHubAppManifest,
390
421
  convertGitHubAppManifest,
391
422
  createGitHubAppInstallationToken,
423
+ createGitHubAppInstallationTokenWithExpiry,
392
424
  createSignedState,
393
425
  envLinesFromGitHubManifestConversion,
394
426
  getGitHubAppInstallationSummary,
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
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\";\nconst githubTokenMintTimeoutMs = 60_000;\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(\n state: string,\n secret: string,\n now = Math.floor(Date.now() / 1000),\n): 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 (\n !payload ||\n typeof payload !== \"object\" ||\n typeof (payload as { iat?: unknown }).iat !== \"number\" ||\n typeof (payload as { nonce?: unknown }).nonce !== \"string\"\n ) {\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(\n state: string,\n secret: string,\n now = Math.floor(Date.now() / 1000),\n): 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(\n settings: Settings,\n): 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(\n settings: Settings,\n installationId: number,\n): Promise<GitHubAppInstallationSummary | null> {\n const installations = await listGitHubAppInstallationSummaries(settings);\n return (\n installations.find((installation) => installation.installationId === installationId) ?? null\n );\n}\n\nexport async function verifyGitHubInstallationAccessForUser(\n settings: Settings,\n input: {\n code: string;\n installationId: number;\n },\n): Promise<GitHubAppInstallationSummary> {\n const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const installations = await listUserAccessibleInstallations(token);\n const installation = installations.find(\n (candidate) => candidate.installationId === input.installationId,\n );\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(\n settings: Settings,\n input: {\n installationIds?: number[];\n } = {},\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 =\n typeof installation.account === \"object\" && installation.account\n ? (installation.account as Record<string, unknown>)\n : {};\n const token = await createInstallationToken(jwt, { installationId });\n repositories.push(\n ...(await listInstallationRepositories(token.token, installationId, account)),\n );\n }\n repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));\n return repositories;\n}\n\nexport async function createGitHubAppInstallationToken(\n settings: Settings,\n input: {\n installationId: number;\n repositoryIds?: number[];\n },\n): Promise<string> {\n return (await createGitHubAppInstallationTokenWithExpiry(settings, input)).token;\n}\n\nexport type GitHubAppInstallationToken = {\n token: string;\n expiresAt: string | null;\n};\n\nexport async function createGitHubAppInstallationTokenWithExpiry(\n settings: Settings,\n input: {\n installationId: number;\n repositoryIds?: number[];\n },\n): Promise<GitHubAppInstallationToken> {\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, {\n per_page: \"100\",\n page: String(page),\n });\n if (!Array.isArray(payload)) {\n throw new GitHubAppApiError(\"GitHub returned an invalid installations payload\");\n }\n out.push(\n ...payload.filter((item): item is Record<string, unknown> =>\n Boolean(item && typeof item === \"object\" && !Array.isArray(item)),\n ),\n );\n if (payload.length < 100) {\n return out;\n }\n }\n}\n\nasync function exchangeGitHubOAuthCodeForUserToken(\n settings: Settings,\n code: string,\n): 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(\n token: string,\n): Promise<GitHubAppInstallationSummary[]> {\n const out: GitHubAppInstallationSummary[] = [];\n for (let page = 1; ; page += 1) {\n const payload = await githubGet(\"/user/installations\", token, {\n per_page: \"100\",\n page: String(page),\n });\n const installations: unknown[] | null =\n 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(\n ...installations\n .filter((item): item is Record<string, unknown> =>\n Boolean(item && typeof item === \"object\" && !Array.isArray(item)),\n )\n .map(installationSummaryFromPayload),\n );\n if (installations.length < 100) {\n return out;\n }\n }\n}\n\nasync function createInstallationToken(\n appJwt: string,\n input: {\n installationId: number;\n repositoryIds?: number[];\n },\n): Promise<GitHubAppInstallationToken> {\n const scoped = input.repositoryIds && input.repositoryIds.length > 0;\n const response = await fetch(\n `${githubApiBase}/app/installations/${input.installationId}/access_tokens`,\n {\n method: \"POST\",\n headers: {\n ...githubHeaders(appJwt),\n ...(scoped ? { \"Content-Type\": \"application/json\" } : {}),\n },\n signal: AbortSignal.timeout(githubTokenMintTimeoutMs),\n ...(scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}),\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.token !== \"string\") {\n throw new GitHubAppApiError(\"GitHub returned an invalid installation token payload\");\n }\n return {\n token: payload.token,\n expiresAt: typeof payload.expires_at === \"string\" ? payload.expires_at : null,\n };\n}\n\nasync function listInstallationRepositories(\n token: string,\n installationId: number,\n account: Record<string, unknown>,\n): Promise<GitHubRepository[]> {\n const out: GitHubRepository[] = [];\n for (let page = 1; ; page += 1) {\n const payload = await githubGet(\"/installation/repositories\", token, {\n per_page: \"100\",\n page: String(page),\n });\n if (\n !payload ||\n typeof payload !== \"object\" ||\n Array.isArray(payload) ||\n !Array.isArray(payload.repositories)\n ) {\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(\n payload: Record<string, unknown>,\n): 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 =\n typeof payload.account === \"object\" && payload.account\n ? (payload.account as Record<string, unknown>)\n : {};\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(\n path: string,\n token: string,\n params: Record<string, string>,\n): 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(\n payload: Record<string, unknown>,\n installationId: number,\n account: Record<string, unknown>,\n): 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":";AAEA,SAAS,YAAY,kBAAkB,aAAa,uBAAuB;AAC3E,SAAS,SAAS,mBAAmB;AAErC,IAAM,gBAAgB;AACtB,IAAM,mBAAmB;AACzB,IAAM,2BAA2B;AAC1B,IAAM,qBAAqB,KAAK;AACvC,IAAM,wBAAwB,cAAc,aAAa;AACzD,IAAM,sBAAsB,cAAc,iBAAiB;AAEpD,IAAM,8BAAN,cAA0C,MAAM;AAAA,EACrD,YAAqB,SAAmB;AACtC,UAAM,8BAA8B;AADjB;AAAA,EAErB;AACF;AAEO,IAAM,oBAAN,cAAgC,MAAM;AAAC;AAiBvC,SAAS,yBAAyB,UAA8B;AACrE,QAAM,WAA+C;AAAA,IACnD,wBAAwB,SAAS;AAAA,IACjC,2BAA2B,SAAS;AAAA,IACpC,+BAA+B,SAAS;AAAA,IACxC,0BAA0B,SAAS;AAAA,IACnC,iCAAiC,SAAS;AAAA,EAC5C;AACA,SAAO,OAAO,QAAQ,QAAQ,EAAE,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAO,SAAS,MAAM,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAE;AAClG;AAEO,SAAS,uBAAuB,OAMX;AAC1B,QAAM,OAAO,MAAM,QAAQ,QAAQ,QAAQ,EAAE;AAC7C,QAAM,cAAsC;AAAA,IAC1C,UAAU;AAAA,IACV,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AACA,MAAI,MAAM,sBAAsB;AAC9B,gBAAY,UAAU;AACtB,gBAAY,SAAS;AACrB,gBAAY,WAAW;AAAA,EACzB;AACA,QAAM,WAAoC;AAAA,IACxC,MAAM,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,cAAc,GAAG,IAAI;AAAA,IACrB,QAAQ,MAAM;AAAA,IACd,0BAA0B;AAAA,IAC1B,qBAAqB;AAAA,EACvB;AACA,MAAI,MAAM,UAAU;AAClB,aAAS,YAAY,MAAM;AAC3B,aAAS,kBAAkB;AAAA,EAC7B;AACA,SAAO;AACT;AAEO,SAAS,uBAAuB,OAAuB;AAC5D,SAAO,8CAA8C,KAAK;AAC5D;AAEO,SAAS,2BAA2B,cAAsB,OAAuB;AACtF,SAAO,oCAAoC,mBAAmB,YAAY,CAAC,4BAA4B,KAAK;AAC9G;AAEO,SAAS,wBAAwB,OAI7B;AACT,QAAM,MAAM,IAAI,IAAI,0CAA0C;AAC9D,MAAI,aAAa,IAAI,aAAa,MAAM,QAAQ;AAChD,MAAI,aAAa,IAAI,SAAS,MAAM,KAAK;AACzC,MAAI,MAAM,aAAa;AACrB,QAAI,aAAa,IAAI,gBAAgB,MAAM,WAAW;AAAA,EACxD;AACA,SAAO,IAAI,SAAS;AACtB;AAEO,SAAS,kBACd,QACA,eAAiD,CAAC,GAClD,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAC7B;AACR,QAAM,eAAe,OAAO,iBAAiB,WAAW,CAAC,IAAI;AAC7D,QAAM,MAAM,OAAO,iBAAiB,WAAW,eAAe;AAC9D,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,OAAO,YAAY,EAAE,EAAE,SAAS,WAAW;AAAA,IAC3C,KAAK;AAAA,EACP;AACA,QAAM,UAAU,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,EAAE,SAAS,WAAW;AACzE,SAAO,GAAG,OAAO,IAAI,iBAAiB,SAAS,MAAM,CAAC;AACxD;AAEO,SAAS,gBACd,OACA,QACA,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GACD;AACjC,QAAM,CAAC,SAAS,SAAS,IAAI,MAAM,MAAM,KAAK,CAAC;AAC/C,MAAI,CAAC,WAAW,CAAC,WAAW;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,iBAAiB,SAAS,MAAM;AACjD,MAAI,CAAC,UAAU,WAAW,QAAQ,GAAG;AACnC,WAAO;AAAA,EACT;AACA,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,EAAE,SAAS,MAAM,CAAC;AAAA,EACzE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MACE,CAAC,WACD,OAAO,YAAY,YACnB,OAAQ,QAA8B,QAAQ,YAC9C,OAAQ,QAAgC,UAAU,UAClD;AACA,WAAO;AAAA,EACT;AACA,QAAM,MAAM,MAAO,QAA4B;AAC/C,SAAO,OAAO,KAAK,OAAO,qBAAsB,UAAuC;AACzF;AAEO,SAAS,kBACd,OACA,QACA,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GACzB;AACT,SAAO,gBAAgB,OAAO,QAAQ,GAAG,MAAM;AACjD;AAEO,SAAS,qCAAqC,SAA4C;AAC/F,QAAM,aAAa,OAAO,QAAQ,OAAO,EAAE,EAAE,QAAQ,OAAO,KAAK;AACjE,SAAO;AAAA,IACL,0BAA0B,QAAQ,MAAM,EAAE;AAAA,IAC1C,6BAA6B,QAAQ,aAAa,EAAE;AAAA,IACpD,iCAAiC,QAAQ,iBAAiB,EAAE;AAAA,IAC5D,4BAA4B,QAAQ,QAAQ,EAAE;AAAA,IAC9C,kCAAkC,QAAQ,kBAAkB,EAAE;AAAA,IAC9D,oCAAoC,UAAU;AAAA,EAChD;AACF;AAEA,eAAsB,yBAAyB,MAAgD;AAC7F,QAAM,WAAW,MAAM,MAAM,GAAG,aAAa,kBAAkB,IAAI,gBAAgB;AAAA,IACjF,QAAQ;AAAA,IACR,SAAS,cAAc,MAAS;AAAA,EAClC,CAAC;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,kBAAkB,MAAM,mBAAmB,QAAQ,CAAC;AAAA,EAChE;AACA,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,MAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAAG;AACrE,UAAM,IAAI,kBAAkB,wDAAwD;AAAA,EACtF;AACA,SAAO;AACT;AAEA,eAAsB,mCACpB,UACyC;AACzC,QAAM,UAAU,yBAAyB,QAAQ;AACjD,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI,4BAA4B,OAAO;AAAA,EAC/C;AACA,QAAM,MAAM,MAAM,mBAAmB,QAAQ;AAC7C,QAAM,gBAAgB,MAAM,kBAAkB,GAAG;AACjD,SAAO,cAAc,IAAI,8BAA8B;AACzD;AAEA,eAAsB,gCACpB,UACA,gBAC8C;AAC9C,QAAM,gBAAgB,MAAM,mCAAmC,QAAQ;AACvE,SACE,cAAc,KAAK,CAAC,iBAAiB,aAAa,mBAAmB,cAAc,KAAK;AAE5F;AAEA,eAAsB,sCACpB,UACA,OAIuC;AACvC,QAAM,QAAQ,MAAM,oCAAoC,UAAU,MAAM,IAAI;AAC5E,QAAM,gBAAgB,MAAM,gCAAgC,KAAK;AACjE,QAAM,eAAe,cAAc;AAAA,IACjC,CAAC,cAAc,UAAU,mBAAmB,MAAM;AAAA,EACpD;AACA,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI,kBAAkB,8DAA8D;AAAA,EAC5F;AACA,SAAO;AACT;AAEA,eAAsB,0BACpB,UACA,QAEI,CAAC,GACwB;AAC7B,QAAM,UAAU,yBAAyB,QAAQ;AACjD,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI,4BAA4B,OAAO;AAAA,EAC/C;AACA,QAAM,uBAAuB,MAAM,kBAAkB,IAAI,IAAI,MAAM,eAAe,IAAI;AACtF,MAAI,wBAAwB,qBAAqB,SAAS,GAAG;AAC3D,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAM,MAAM,mBAAmB,QAAQ;AAC7C,QAAM,gBAAgB,MAAM,kBAAkB,GAAG;AACjD,QAAM,eAAmC,CAAC;AAC1C,aAAW,gBAAgB,eAAe;AACxC,QAAI,aAAa,cAAc;AAC7B;AAAA,IACF;AACA,UAAM,iBAAiB,MAAM,aAAa,EAAE;AAC5C,QAAI,mBAAmB,MAAM;AAC3B;AAAA,IACF;AACA,QAAI,wBAAwB,CAAC,qBAAqB,IAAI,cAAc,GAAG;AACrE;AAAA,IACF;AACA,UAAM,UACJ,OAAO,aAAa,YAAY,YAAY,aAAa,UACpD,aAAa,UACd,CAAC;AACP,UAAM,QAAQ,MAAM,wBAAwB,KAAK,EAAE,eAAe,CAAC;AACnE,iBAAa;AAAA,MACX,GAAI,MAAM,6BAA6B,MAAM,OAAO,gBAAgB,OAAO;AAAA,IAC7E;AAAA,EACF;AACA,eAAa,KAAK,CAAC,MAAM,UAAU,KAAK,SAAS,cAAc,MAAM,QAAQ,CAAC;AAC9E,SAAO;AACT;AAEA,eAAsB,iCACpB,UACA,OAIiB;AACjB,UAAQ,MAAM,2CAA2C,UAAU,KAAK,GAAG;AAC7E;AAOA,eAAsB,2CACpB,UACA,OAIqC;AACrC,QAAM,UAAU,yBAAyB,QAAQ;AACjD,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI,4BAA4B,OAAO;AAAA,EAC/C;AACA,QAAM,MAAM,MAAM,mBAAmB,QAAQ;AAC7C,SAAO,MAAM,wBAAwB,KAAK,KAAK;AACjD;AAEO,SAAS,qBAAqB,UAA4D;AAC/F,QAAM,QAAQ,SAAS,aAAa,KAAK;AACzC,QAAM,OAAO,SAAS,eAAe,KAAK;AAC1C,MAAI,CAAC,SAAS,CAAC,MAAM;AACnB,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,GAAG,IAAI;AACrB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,GAAG,KAAK,IAAI,KAAK;AAAA,EAC1B;AACF;AAEA,eAAe,mBAAmB,UAAqC;AACrE,QAAM,aAAa,6BAA6B,SAAS,uBAAuB,EAAE;AAClF,QAAM,QAAQ,SAAS,aAAa,KAAK;AACzC,MAAI,CAAC,SAAS,CAAC,YAAY;AACzB,UAAM,IAAI,4BAA4B,yBAAyB,QAAQ,CAAC;AAAA,EAC1E;AACA,QAAM,MAAM,MAAM,YAAY,YAAY,OAAO;AACjD,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,SAAO,MAAM,IAAI,QAAQ,CAAC,CAAC,EACxB,mBAAmB,EAAE,KAAK,QAAQ,CAAC,EACnC,YAAY,MAAM,EAAE,EACpB,kBAAkB,MAAM,IAAI,EAAE,EAC9B,UAAU,KAAK,EACf,KAAK,GAAG;AACb;AAEA,eAAe,kBAAkB,OAAwD;AACvF,QAAM,MAAsC,CAAC;AAC7C,WAAS,OAAO,KAAK,QAAQ,GAAG;AAC9B,UAAM,UAAU,MAAM,UAAU,sBAAsB,OAAO;AAAA,MAC3D,UAAU;AAAA,MACV,MAAM,OAAO,IAAI;AAAA,IACnB,CAAC;AACD,QAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAC3B,YAAM,IAAI,kBAAkB,kDAAkD;AAAA,IAChF;AACA,QAAI;AAAA,MACF,GAAG,QAAQ;AAAA,QAAO,CAAC,SACjB,QAAQ,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,CAAC;AAAA,MAClE;AAAA,IACF;AACA,QAAI,QAAQ,SAAS,KAAK;AACxB,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,eAAe,oCACb,UACA,MACiB;AACjB,MAAI,CAAC,SAAS,kBAAkB,CAAC,SAAS,oBAAoB;AAC5D,UAAM,IAAI,4BAA4B,yBAAyB,QAAQ,CAAC;AAAA,EAC1E;AACA,QAAM,WAAW,MAAM,MAAM,+CAA+C;AAAA,IAC1E,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,QAAQ;AAAA,MACR,gBAAgB;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB,WAAW,SAAS;AAAA,MACpB,eAAe,SAAS;AAAA,MACxB;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,kBAAkB,MAAM,mBAAmB,QAAQ,CAAC;AAAA,EAChE;AACA,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,MAAI,CAAC,WAAW,OAAO,YAAY,YAAY,OAAO,QAAQ,iBAAiB,UAAU;AACvF,UAAM,IAAI,kBAAkB,gDAAgD;AAAA,EAC9E;AACA,SAAO,QAAQ;AACjB;AAEA,eAAe,gCACb,OACyC;AACzC,QAAM,MAAsC,CAAC;AAC7C,WAAS,OAAO,KAAK,QAAQ,GAAG;AAC9B,UAAM,UAAU,MAAM,UAAU,uBAAuB,OAAO;AAAA,MAC5D,UAAU;AAAA,MACV,MAAM,OAAO,IAAI;AAAA,IACnB,CAAC;AACD,UAAM,gBACJ,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,QAAQ,aAAa,IACxE,QAAQ,gBACT;AACN,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI,kBAAkB,uDAAuD;AAAA,IACrF;AACA,QAAI;AAAA,MACF,GAAG,cACA;AAAA,QAAO,CAAC,SACP,QAAQ,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,CAAC;AAAA,MAClE,EACC,IAAI,8BAA8B;AAAA,IACvC;AACA,QAAI,cAAc,SAAS,KAAK;AAC9B,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,eAAe,wBACb,QACA,OAIqC;AACrC,QAAM,SAAS,MAAM,iBAAiB,MAAM,cAAc,SAAS;AACnE,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,aAAa,sBAAsB,MAAM,cAAc;AAAA,IAC1D;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,GAAG,cAAc,MAAM;AAAA,QACvB,GAAI,SAAS,EAAE,gBAAgB,mBAAmB,IAAI,CAAC;AAAA,MACzD;AAAA,MACA,QAAQ,YAAY,QAAQ,wBAAwB;AAAA,MACpD,GAAI,SAAS,EAAE,MAAM,KAAK,UAAU,EAAE,gBAAgB,MAAM,cAAc,CAAC,EAAE,IAAI,CAAC;AAAA,IACpF;AAAA,EACF;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,kBAAkB,MAAM,mBAAmB,QAAQ,CAAC;AAAA,EAChE;AACA,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,MAAI,CAAC,WAAW,OAAO,YAAY,YAAY,OAAO,QAAQ,UAAU,UAAU;AAChF,UAAM,IAAI,kBAAkB,uDAAuD;AAAA,EACrF;AACA,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf,WAAW,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EAC3E;AACF;AAEA,eAAe,6BACb,OACA,gBACA,SAC6B;AAC7B,QAAM,MAA0B,CAAC;AACjC,WAAS,OAAO,KAAK,QAAQ,GAAG;AAC9B,UAAM,UAAU,MAAM,UAAU,8BAA8B,OAAO;AAAA,MACnE,UAAU;AAAA,MACV,MAAM,OAAO,IAAI;AAAA,IACnB,CAAC;AACD,QACE,CAAC,WACD,OAAO,YAAY,YACnB,MAAM,QAAQ,OAAO,KACrB,CAAC,MAAM,QAAQ,QAAQ,YAAY,GACnC;AACA,YAAM,IAAI,kBAAkB,iDAAiD;AAAA,IAC/E;AACA,eAAW,QAAQ,QAAQ,cAAc;AACvC,UAAI,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,GAAG;AAC5D,YAAI,KAAK,sBAAsB,MAAiC,gBAAgB,OAAO,CAAC;AAAA,MAC1F;AAAA,IACF;AACA,QAAI,QAAQ,aAAa,SAAS,KAAK;AACrC,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,+BACP,SAC8B;AAC9B,QAAM,iBAAiB,MAAM,QAAQ,EAAE;AACvC,MAAI,mBAAmB,MAAM;AAC3B,UAAM,IAAI,kBAAkB,4CAA4C;AAAA,EAC1E;AACA,QAAM,UACJ,OAAO,QAAQ,YAAY,YAAY,QAAQ,UAC1C,QAAQ,UACT,CAAC;AACP,SAAO;AAAA,IACL;AAAA,IACA,cAAc,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,IAClE,aAAa,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAAA,IAC/D,WAAW,QAAQ,QAAQ,YAAY;AAAA,EACzC;AACF;AAEA,eAAe,UACb,MACA,OACA,QACc;AACd,QAAM,MAAM,IAAI,IAAI,GAAG,aAAa,GAAG,IAAI,EAAE;AAC7C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,aAAa,IAAI,KAAK,KAAK;AAAA,EACjC;AACA,QAAM,WAAW,MAAM,MAAM,KAAK,EAAE,SAAS,cAAc,KAAK,EAAE,CAAC;AACnE,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,kBAAkB,MAAM,mBAAmB,QAAQ,CAAC;AAAA,EAChE;AACA,SAAO,MAAM,SAAS,KAAK;AAC7B;AAEA,SAAS,sBACP,SACA,gBACA,SACkB;AAClB,QAAM,KAAK,MAAM,QAAQ,EAAE;AAC3B,QAAM,WAAW,OAAO,QAAQ,aAAa,EAAE;AAC/C,MAAI,OAAO,QAAQ,CAAC,UAAU;AAC5B,UAAM,IAAI,kBAAkB,mDAAmD;AAAA,EACjF;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,OAAO,QAAQ,QAAQ,SAAS,MAAM,GAAG,EAAE,GAAG,EAAE,KAAK,QAAQ;AAAA,IACnE,SAAS,QAAQ,QAAQ,OAAO;AAAA,IAChC,SAAS,OAAO,QAAQ,YAAY,sBAAsB,QAAQ,EAAE;AAAA,IACpE,UAAU,OAAO,QAAQ,aAAa,sBAAsB,QAAQ,MAAM;AAAA,IAC1E,eAAe,OAAO,QAAQ,kBAAkB,MAAM;AAAA,IACtD,cAAc,OAAO,QAAQ,SAAS,SAAS,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC;AAAA,IAC/D,aAAa,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAAA,EACjE;AACF;AAEA,SAAS,cAAc,OAA6B;AAClD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,GAAI,QAAQ,EAAE,eAAe,UAAU,KAAK,GAAG,IAAI,CAAC;AAAA,IACpD,wBAAwB;AAAA,EAC1B;AACF;AAEA,eAAe,mBAAmB,UAAqC;AACrE,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,QAAI,WAAW,OAAO,YAAY,YAAY,aAAa,SAAS;AAClE,aAAO,cAAc,SAAS,MAAM,KAAK,OAAO,QAAQ,OAAO,CAAC;AAAA,IAClE;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,cAAc,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK,CAAC;AAChE;AAEO,SAAS,6BAA6B,OAAuB;AAClE,QAAM,aAAa,MAAM,KAAK,EAAE,QAAQ,QAAQ,IAAI;AACpD,MAAI,CAAC,cAAc,WAAW,WAAW,qBAAqB,GAAG;AAC/D,WAAO;AAAA,EACT;AACA,MAAI,WAAW,WAAW,mBAAmB,GAAG;AAC9C,WAAO,iBAAiB,UAAU,EAAE,OAAO,EAAE,MAAM,SAAS,QAAQ,MAAM,CAAC,EAAE,SAAS;AAAA,EACxF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAiB,QAAwB;AACjE,SAAO,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,WAAW;AACxE;AAEA,SAAS,UAAU,MAAc,OAAwB;AACvD,QAAM,IAAI,OAAO,KAAK,IAAI;AAC1B,QAAM,IAAI,OAAO,KAAK,KAAK;AAC3B,SAAO,EAAE,WAAW,EAAE,UAAU,gBAAgB,GAAG,CAAC;AACtD;AAEA,SAAS,MAAM,OAA+B;AAC5C,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,GAAG;AACxD,WAAO;AAAA,EACT;AACA,MAAI,OAAO,UAAU,YAAY,QAAQ,KAAK,KAAK,GAAG;AACpD,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@opengeni/github",
3
- "version": "0.2.8",
4
- "type": "module",
3
+ "version": "0.2.9",
5
4
  "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
8
+ "directory": "packages/github"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "type": "module",
6
15
  "main": "./dist/index.js",
7
16
  "module": "./dist/index.js",
8
17
  "types": "./dist/index.d.ts",
@@ -12,27 +21,18 @@
12
21
  "import": "./dist/index.js"
13
22
  }
14
23
  },
15
- "files": [
16
- "dist",
17
- "src"
18
- ],
19
24
  "publishConfig": {
20
25
  "access": "public",
21
26
  "provenance": true
22
27
  },
23
28
  "scripts": {
24
29
  "build": "tsup",
25
- "typecheck": "tsc --noEmit",
30
+ "typecheck": "tsgo --noEmit",
26
31
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
27
32
  },
28
33
  "dependencies": {
29
- "@opengeni/config": "^0.4.0",
30
- "@opengeni/contracts": "^0.9.0",
34
+ "@opengeni/config": "^0.5.0",
35
+ "@opengeni/contracts": "^0.10.0",
31
36
  "jose": "^6.1.3"
32
- },
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
36
- "directory": "packages/github"
37
37
  }
38
38
  }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { SignJWT, importPKCS8 } from "jose";
5
5
 
6
6
  const githubApiBase = "https://api.github.com";
7
7
  const githubApiVersion = "2022-11-28";
8
+ const githubTokenMintTimeoutMs = 60_000;
8
9
  export const stateMaxAgeSeconds = 60 * 60;
9
10
  const pkcs8PrivateKeyHeader = `-----BEGIN ${"PRIVATE KEY"}-----`;
10
11
  const rsaPrivateKeyHeader = `-----BEGIN ${"RSA PRIVATE KEY"}-----`;
@@ -40,7 +41,7 @@ export function githubAppMissingSettings(settings: Settings): string[] {
40
41
  OPENGENI_GITHUB_APP_SLUG: settings.githubAppSlug,
41
42
  OPENGENI_GITHUB_APP_PRIVATE_KEY: settings.githubAppPrivateKey,
42
43
  };
43
- return Object.entries(required).flatMap(([name, value]) => value && value.trim() ? [] : [name]);
44
+ return Object.entries(required).flatMap(([name, value]) => (value && value.trim() ? [] : [name]));
44
45
  }
45
46
 
46
47
  export function buildGitHubAppManifest(input: {
@@ -114,7 +115,11 @@ export function createSignedState(
114
115
  return `${encoded}.${signStatePayload(encoded, secret)}`;
115
116
  }
116
117
 
117
- export function readSignedState(state: string, secret: string, now = Math.floor(Date.now() / 1000)): GitHubSignedStatePayload | null {
118
+ export function readSignedState(
119
+ state: string,
120
+ secret: string,
121
+ now = Math.floor(Date.now() / 1000),
122
+ ): GitHubSignedStatePayload | null {
118
123
  const [encoded, signature] = state.split(".", 2);
119
124
  if (!encoded || !signature) {
120
125
  return null;
@@ -129,14 +134,23 @@ export function readSignedState(state: string, secret: string, now = Math.floor(
129
134
  } catch {
130
135
  return null;
131
136
  }
132
- if (!payload || typeof payload !== "object" || typeof (payload as { iat?: unknown }).iat !== "number" || typeof (payload as { nonce?: unknown }).nonce !== "string") {
137
+ if (
138
+ !payload ||
139
+ typeof payload !== "object" ||
140
+ typeof (payload as { iat?: unknown }).iat !== "number" ||
141
+ typeof (payload as { nonce?: unknown }).nonce !== "string"
142
+ ) {
133
143
  return null;
134
144
  }
135
145
  const age = now - (payload as { iat: number }).iat;
136
- return age >= 0 && age <= stateMaxAgeSeconds ? payload as GitHubSignedStatePayload : null;
146
+ return age >= 0 && age <= stateMaxAgeSeconds ? (payload as GitHubSignedStatePayload) : null;
137
147
  }
138
148
 
139
- export function verifySignedState(state: string, secret: string, now = Math.floor(Date.now() / 1000)): boolean {
149
+ export function verifySignedState(
150
+ state: string,
151
+ secret: string,
152
+ now = Math.floor(Date.now() / 1000),
153
+ ): boolean {
140
154
  return readSignedState(state, secret, now) !== null;
141
155
  }
142
156
 
@@ -167,7 +181,9 @@ export async function convertGitHubAppManifest(code: string): Promise<Record<str
167
181
  return payload as Record<string, unknown>;
168
182
  }
169
183
 
170
- export async function listGitHubAppInstallationSummaries(settings: Settings): Promise<GitHubAppInstallationSummary[]> {
184
+ export async function listGitHubAppInstallationSummaries(
185
+ settings: Settings,
186
+ ): Promise<GitHubAppInstallationSummary[]> {
171
187
  const missing = githubAppMissingSettings(settings);
172
188
  if (missing.length > 0) {
173
189
  throw new GitHubAppConfigurationError(missing);
@@ -177,27 +193,40 @@ export async function listGitHubAppInstallationSummaries(settings: Settings): Pr
177
193
  return installations.map(installationSummaryFromPayload);
178
194
  }
179
195
 
180
- export async function getGitHubAppInstallationSummary(settings: Settings, installationId: number): Promise<GitHubAppInstallationSummary | null> {
196
+ export async function getGitHubAppInstallationSummary(
197
+ settings: Settings,
198
+ installationId: number,
199
+ ): Promise<GitHubAppInstallationSummary | null> {
181
200
  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> {
201
+ return (
202
+ installations.find((installation) => installation.installationId === installationId) ?? null
203
+ );
204
+ }
205
+
206
+ export async function verifyGitHubInstallationAccessForUser(
207
+ settings: Settings,
208
+ input: {
209
+ code: string;
210
+ installationId: number;
211
+ },
212
+ ): Promise<GitHubAppInstallationSummary> {
189
213
  const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);
190
214
  const installations = await listUserAccessibleInstallations(token);
191
- const installation = installations.find((candidate) => candidate.installationId === input.installationId);
215
+ const installation = installations.find(
216
+ (candidate) => candidate.installationId === input.installationId,
217
+ );
192
218
  if (!installation) {
193
219
  throw new GitHubAppApiError("GitHub installation is not accessible to the installing user");
194
220
  }
195
221
  return installation;
196
222
  }
197
223
 
198
- export async function listGitHubAppRepositories(settings: Settings, input: {
199
- installationIds?: number[];
200
- } = {}): Promise<GitHubRepository[]> {
224
+ export async function listGitHubAppRepositories(
225
+ settings: Settings,
226
+ input: {
227
+ installationIds?: number[];
228
+ } = {},
229
+ ): Promise<GitHubRepository[]> {
201
230
  const missing = githubAppMissingSettings(settings);
202
231
  if (missing.length > 0) {
203
232
  throw new GitHubAppConfigurationError(missing);
@@ -220,18 +249,41 @@ export async function listGitHubAppRepositories(settings: Settings, input: {
220
249
  if (allowedInstallations && !allowedInstallations.has(installationId)) {
221
250
  continue;
222
251
  }
223
- const account = typeof installation.account === "object" && installation.account ? installation.account as Record<string, unknown> : {};
252
+ const account =
253
+ typeof installation.account === "object" && installation.account
254
+ ? (installation.account as Record<string, unknown>)
255
+ : {};
224
256
  const token = await createInstallationToken(jwt, { installationId });
225
- repositories.push(...await listInstallationRepositories(token, installationId, account));
257
+ repositories.push(
258
+ ...(await listInstallationRepositories(token.token, installationId, account)),
259
+ );
226
260
  }
227
261
  repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));
228
262
  return repositories;
229
263
  }
230
264
 
231
- export async function createGitHubAppInstallationToken(settings: Settings, input: {
232
- installationId: number;
233
- repositoryIds?: number[];
234
- }): Promise<string> {
265
+ export async function createGitHubAppInstallationToken(
266
+ settings: Settings,
267
+ input: {
268
+ installationId: number;
269
+ repositoryIds?: number[];
270
+ },
271
+ ): Promise<string> {
272
+ return (await createGitHubAppInstallationTokenWithExpiry(settings, input)).token;
273
+ }
274
+
275
+ export type GitHubAppInstallationToken = {
276
+ token: string;
277
+ expiresAt: string | null;
278
+ };
279
+
280
+ export async function createGitHubAppInstallationTokenWithExpiry(
281
+ settings: Settings,
282
+ input: {
283
+ installationId: number;
284
+ repositoryIds?: number[];
285
+ },
286
+ ): Promise<GitHubAppInstallationToken> {
235
287
  const missing = githubAppMissingSettings(settings);
236
288
  if (missing.length > 0) {
237
289
  throw new GitHubAppConfigurationError(missing);
@@ -272,18 +324,28 @@ async function createGitHubAppJwt(settings: Settings): Promise<string> {
272
324
  async function listInstallations(token: string): Promise<Array<Record<string, unknown>>> {
273
325
  const out: Array<Record<string, unknown>> = [];
274
326
  for (let page = 1; ; page += 1) {
275
- const payload = await githubGet("/app/installations", token, { per_page: "100", page: String(page) });
327
+ const payload = await githubGet("/app/installations", token, {
328
+ per_page: "100",
329
+ page: String(page),
330
+ });
276
331
  if (!Array.isArray(payload)) {
277
332
  throw new GitHubAppApiError("GitHub returned an invalid installations payload");
278
333
  }
279
- out.push(...payload.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === "object" && !Array.isArray(item))));
334
+ out.push(
335
+ ...payload.filter((item): item is Record<string, unknown> =>
336
+ Boolean(item && typeof item === "object" && !Array.isArray(item)),
337
+ ),
338
+ );
280
339
  if (payload.length < 100) {
281
340
  return out;
282
341
  }
283
342
  }
284
343
  }
285
344
 
286
- async function exchangeGitHubOAuthCodeForUserToken(settings: Settings, code: string): Promise<string> {
345
+ async function exchangeGitHubOAuthCodeForUserToken(
346
+ settings: Settings,
347
+ code: string,
348
+ ): Promise<string> {
287
349
  if (!settings.githubClientId || !settings.githubClientSecret) {
288
350
  throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
289
351
  }
@@ -309,38 +371,55 @@ async function exchangeGitHubOAuthCodeForUserToken(settings: Settings, code: str
309
371
  return payload.access_token;
310
372
  }
311
373
 
312
- async function listUserAccessibleInstallations(token: string): Promise<GitHubAppInstallationSummary[]> {
374
+ async function listUserAccessibleInstallations(
375
+ token: string,
376
+ ): Promise<GitHubAppInstallationSummary[]> {
313
377
  const out: GitHubAppInstallationSummary[] = [];
314
378
  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;
379
+ const payload = await githubGet("/user/installations", token, {
380
+ per_page: "100",
381
+ page: String(page),
382
+ });
383
+ const installations: unknown[] | null =
384
+ payload && typeof payload === "object" && Array.isArray(payload.installations)
385
+ ? (payload.installations as unknown[])
386
+ : null;
319
387
  if (!installations) {
320
388
  throw new GitHubAppApiError("GitHub returned an invalid user installations payload");
321
389
  }
322
- out.push(...installations
323
- .filter((item): item is Record<string, unknown> => Boolean(item && typeof item === "object" && !Array.isArray(item)))
324
- .map(installationSummaryFromPayload));
390
+ out.push(
391
+ ...installations
392
+ .filter((item): item is Record<string, unknown> =>
393
+ Boolean(item && typeof item === "object" && !Array.isArray(item)),
394
+ )
395
+ .map(installationSummaryFromPayload),
396
+ );
325
397
  if (installations.length < 100) {
326
398
  return out;
327
399
  }
328
400
  }
329
401
  }
330
402
 
331
- async function createInstallationToken(appJwt: string, input: {
332
- installationId: number;
333
- repositoryIds?: number[];
334
- }): Promise<string> {
403
+ async function createInstallationToken(
404
+ appJwt: string,
405
+ input: {
406
+ installationId: number;
407
+ repositoryIds?: number[];
408
+ },
409
+ ): Promise<GitHubAppInstallationToken> {
335
410
  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" } : {}),
411
+ const response = await fetch(
412
+ `${githubApiBase}/app/installations/${input.installationId}/access_tokens`,
413
+ {
414
+ method: "POST",
415
+ headers: {
416
+ ...githubHeaders(appJwt),
417
+ ...(scoped ? { "Content-Type": "application/json" } : {}),
418
+ },
419
+ signal: AbortSignal.timeout(githubTokenMintTimeoutMs),
420
+ ...(scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}),
341
421
  },
342
- ...(scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}),
343
- });
422
+ );
344
423
  if (!response.ok) {
345
424
  throw new GitHubAppApiError(await githubErrorMessage(response));
346
425
  }
@@ -348,14 +427,29 @@ async function createInstallationToken(appJwt: string, input: {
348
427
  if (!payload || typeof payload !== "object" || typeof payload.token !== "string") {
349
428
  throw new GitHubAppApiError("GitHub returned an invalid installation token payload");
350
429
  }
351
- return payload.token;
430
+ return {
431
+ token: payload.token,
432
+ expiresAt: typeof payload.expires_at === "string" ? payload.expires_at : null,
433
+ };
352
434
  }
353
435
 
354
- async function listInstallationRepositories(token: string, installationId: number, account: Record<string, unknown>): Promise<GitHubRepository[]> {
436
+ async function listInstallationRepositories(
437
+ token: string,
438
+ installationId: number,
439
+ account: Record<string, unknown>,
440
+ ): Promise<GitHubRepository[]> {
355
441
  const out: GitHubRepository[] = [];
356
442
  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)) {
443
+ const payload = await githubGet("/installation/repositories", token, {
444
+ per_page: "100",
445
+ page: String(page),
446
+ });
447
+ if (
448
+ !payload ||
449
+ typeof payload !== "object" ||
450
+ Array.isArray(payload) ||
451
+ !Array.isArray(payload.repositories)
452
+ ) {
359
453
  throw new GitHubAppApiError("GitHub returned an invalid repositories payload");
360
454
  }
361
455
  for (const repo of payload.repositories) {
@@ -369,12 +463,17 @@ async function listInstallationRepositories(token: string, installationId: numbe
369
463
  }
370
464
  }
371
465
 
372
- function installationSummaryFromPayload(payload: Record<string, unknown>): GitHubAppInstallationSummary {
466
+ function installationSummaryFromPayload(
467
+ payload: Record<string, unknown>,
468
+ ): GitHubAppInstallationSummary {
373
469
  const installationId = asInt(payload.id);
374
470
  if (installationId === null) {
375
471
  throw new GitHubAppApiError("GitHub returned an installation without id");
376
472
  }
377
- const account = typeof payload.account === "object" && payload.account ? payload.account as Record<string, unknown> : {};
473
+ const account =
474
+ typeof payload.account === "object" && payload.account
475
+ ? (payload.account as Record<string, unknown>)
476
+ : {};
378
477
  return {
379
478
  installationId,
380
479
  accountLogin: typeof account.login === "string" ? account.login : null,
@@ -383,7 +482,11 @@ function installationSummaryFromPayload(payload: Record<string, unknown>): GitHu
383
482
  };
384
483
  }
385
484
 
386
- async function githubGet(path: string, token: string, params: Record<string, string>): Promise<any> {
485
+ async function githubGet(
486
+ path: string,
487
+ token: string,
488
+ params: Record<string, string>,
489
+ ): Promise<any> {
387
490
  const url = new URL(`${githubApiBase}${path}`);
388
491
  for (const [key, value] of Object.entries(params)) {
389
492
  url.searchParams.set(key, value);
@@ -395,7 +498,11 @@ async function githubGet(path: string, token: string, params: Record<string, str
395
498
  return await response.json();
396
499
  }
397
500
 
398
- function repositoryFromPayload(payload: Record<string, unknown>, installationId: number, account: Record<string, unknown>): GitHubRepository {
501
+ function repositoryFromPayload(
502
+ payload: Record<string, unknown>,
503
+ installationId: number,
504
+ account: Record<string, unknown>,
505
+ ): GitHubRepository {
399
506
  const id = asInt(payload.id);
400
507
  const fullName = String(payload.full_name ?? "");
401
508
  if (id === null || !fullName) {