@opengeni/github 0.2.10 → 0.3.10

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
@@ -1,5 +1,5 @@
1
1
  import { Settings } from '@opengeni/config';
2
- import { GitHubRepository } from '@opengeni/contracts';
2
+ import { GitHubUserInstallationAccess, GitHubRepository } from '@opengeni/contracts';
3
3
 
4
4
  declare const stateMaxAgeSeconds: number;
5
5
  declare class GitHubAppConfigurationError extends Error {
@@ -47,6 +47,16 @@ declare function verifyGitHubInstallationAccessForUser(settings: Settings, input
47
47
  code: string;
48
48
  installationId: number;
49
49
  }): Promise<GitHubAppInstallationSummary>;
50
+ /**
51
+ * Exchange a GitHub App user-authorization code and discover the installations
52
+ * and repositories the user can explicitly access. This is compatibility
53
+ * discovery metadata only: visibility and repository permission bits do not
54
+ * prove that the human may install, configure, or bind the App installation.
55
+ * No production binding path may treat this result as authority.
56
+ */
57
+ declare function authorizeGitHubAppUser(settings: Settings, input: {
58
+ code: string;
59
+ }): Promise<GitHubUserInstallationAccess[]>;
50
60
  declare function listGitHubAppRepositories(settings: Settings, input?: {
51
61
  installationIds?: number[];
52
62
  }): Promise<GitHubRepository[]>;
@@ -68,4 +78,4 @@ declare function githubAppBotIdentity(settings: Settings): {
68
78
  } | null;
69
79
  declare function normalizeGitHubAppPrivateKey(value: string): string;
70
80
 
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 };
81
+ export { GitHubAppApiError, GitHubAppConfigurationError, type GitHubAppInstallationSummary, type GitHubAppInstallationToken, type GitHubSignedStatePayload, authorizeGitHubAppUser, buildGitHubAppManifest, convertGitHubAppManifest, createGitHubAppInstallationToken, createGitHubAppInstallationTokenWithExpiry, createSignedState, envLinesFromGitHubManifestConversion, getGitHubAppInstallationSummary, githubAppBotIdentity, githubAppMissingSettings, githubOAuthAuthorizeUrl, listGitHubAppInstallationSummaries, listGitHubAppRepositories, normalizeGitHubAppPrivateKey, organizationAppManifestUrl, personalAppManifestUrl, readSignedState, stateMaxAgeSeconds, verifyGitHubInstallationAccessForUser, verifySignedState };
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ function buildGitHubAppManifest(input) {
41
41
  name: input.appName,
42
42
  url: base,
43
43
  redirect_url: `${base}/v1/github/app-manifest/callback`,
44
+ callback_urls: [`${base}/v1/github/oauth/callback`],
44
45
  public: input.public,
45
46
  request_oauth_on_install: true,
46
47
  default_permissions: permissions
@@ -150,6 +151,16 @@ async function verifyGitHubInstallationAccessForUser(settings, input) {
150
151
  }
151
152
  return installation;
152
153
  }
154
+ async function authorizeGitHubAppUser(settings, input) {
155
+ const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);
156
+ const installations = await listUserAccessibleInstallations(token);
157
+ return await Promise.all(
158
+ installations.map(async (installation) => ({
159
+ ...installation,
160
+ repositories: installation.suspended ? [] : await listUserInstallationRepositories(token, installation)
161
+ }))
162
+ );
163
+ }
153
164
  async function listGitHubAppRepositories(settings, input = {}) {
154
165
  const missing = githubAppMissingSettings(settings);
155
166
  if (missing.length > 0) {
@@ -281,6 +292,38 @@ async function listUserAccessibleInstallations(token) {
281
292
  }
282
293
  }
283
294
  }
295
+ async function listUserInstallationRepositories(token, installation) {
296
+ const out = [];
297
+ const account = {
298
+ ...installation.accountLogin ? { login: installation.accountLogin } : {},
299
+ ...installation.accountType ? { type: installation.accountType } : {}
300
+ };
301
+ for (let page = 1; ; page += 1) {
302
+ const payload = await githubGet(
303
+ `/user/installations/${installation.installationId}/repositories`,
304
+ token,
305
+ { per_page: "100", page: String(page) }
306
+ );
307
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Array.isArray(payload.repositories)) {
308
+ throw new GitHubAppApiError(
309
+ "GitHub returned an invalid user installation repositories payload"
310
+ );
311
+ }
312
+ for (const repository of payload.repositories) {
313
+ if (!repository || typeof repository !== "object" || Array.isArray(repository)) {
314
+ continue;
315
+ }
316
+ const record = repository;
317
+ out.push({
318
+ ...repositoryFromPayload(record, installation.installationId, account),
319
+ permissions: repositoryPermissionsFromPayload(record.permissions)
320
+ });
321
+ }
322
+ if (payload.repositories.length < 100) {
323
+ return out;
324
+ }
325
+ }
326
+ }
284
327
  async function createInstallationToken(appJwt, input) {
285
328
  const scoped = input.repositoryIds && input.repositoryIds.length > 0;
286
329
  const response = await fetch(
@@ -370,6 +413,16 @@ function repositoryFromPayload(payload, installationId, account) {
370
413
  accountType: typeof account.type === "string" ? account.type : null
371
414
  };
372
415
  }
416
+ function repositoryPermissionsFromPayload(payload) {
417
+ const permissions = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
418
+ return {
419
+ admin: permissions.admin === true,
420
+ maintain: permissions.maintain === true,
421
+ push: permissions.push === true,
422
+ triage: permissions.triage === true,
423
+ pull: permissions.pull === true
424
+ };
425
+ }
373
426
  function githubHeaders(token) {
374
427
  return {
375
428
  Accept: "application/vnd.github+json",
@@ -417,6 +470,7 @@ function asInt(value) {
417
470
  export {
418
471
  GitHubAppApiError,
419
472
  GitHubAppConfigurationError,
473
+ authorizeGitHubAppUser,
420
474
  buildGitHubAppManifest,
421
475
  convertGitHubAppManifest,
422
476
  createGitHubAppInstallationToken,
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\";\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":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport type {\n GitHubRepository,\n GitHubRepositoryPermissions,\n GitHubUserInstallationAccess,\n GitHubUserRepositoryAccess,\n} 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 callback_urls: [`${base}/v1/github/oauth/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\n/**\n * Exchange a GitHub App user-authorization code and discover the installations\n * and repositories the user can explicitly access. This is compatibility\n * discovery metadata only: visibility and repository permission bits do not\n * prove that the human may install, configure, or bind the App installation.\n * No production binding path may treat this result as authority.\n */\nexport async function authorizeGitHubAppUser(\n settings: Settings,\n input: { code: string },\n): Promise<GitHubUserInstallationAccess[]> {\n const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const installations = await listUserAccessibleInstallations(token);\n return await Promise.all(\n installations.map(async (installation) => ({\n ...installation,\n repositories: installation.suspended\n ? []\n : await listUserInstallationRepositories(token, installation),\n })),\n );\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 listUserInstallationRepositories(\n token: string,\n installation: GitHubAppInstallationSummary,\n): Promise<GitHubUserRepositoryAccess[]> {\n const out: GitHubUserRepositoryAccess[] = [];\n const account = {\n ...(installation.accountLogin ? { login: installation.accountLogin } : {}),\n ...(installation.accountType ? { type: installation.accountType } : {}),\n };\n for (let page = 1; ; page += 1) {\n const payload = await githubGet(\n `/user/installations/${installation.installationId}/repositories`,\n token,\n { per_page: \"100\", 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(\n \"GitHub returned an invalid user installation repositories payload\",\n );\n }\n for (const repository of payload.repositories) {\n if (!repository || typeof repository !== \"object\" || Array.isArray(repository)) {\n continue;\n }\n const record = repository as Record<string, unknown>;\n out.push({\n ...repositoryFromPayload(record, installation.installationId, account),\n permissions: repositoryPermissionsFromPayload(record.permissions),\n });\n }\n if (payload.repositories.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 repositoryPermissionsFromPayload(payload: unknown): GitHubRepositoryPermissions {\n const permissions =\n payload && typeof payload === \"object\" && !Array.isArray(payload)\n ? (payload as Record<string, unknown>)\n : {};\n return {\n admin: permissions.admin === true,\n maintain: permissions.maintain === true,\n push: permissions.push === true,\n triage: permissions.triage === true,\n pull: permissions.pull === true,\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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/github",
3
- "version": "0.2.10",
3
+ "version": "0.3.10",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
32
32
  },
33
33
  "dependencies": {
34
- "@opengeni/config": "^0.5.1",
35
- "@opengeni/contracts": "^0.10.0",
34
+ "@opengeni/config": "^0.6.9",
35
+ "@opengeni/contracts": "^0.18.0",
36
36
  "jose": "^6.1.3"
37
37
  }
38
38
  }
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { Settings } from "@opengeni/config";
2
- import type { GitHubRepository } from "@opengeni/contracts";
2
+ import type {
3
+ GitHubRepository,
4
+ GitHubRepositoryPermissions,
5
+ GitHubUserInstallationAccess,
6
+ GitHubUserRepositoryAccess,
7
+ } from "@opengeni/contracts";
3
8
  import { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from "node:crypto";
4
9
  import { SignJWT, importPKCS8 } from "jose";
5
10
 
@@ -66,6 +71,7 @@ export function buildGitHubAppManifest(input: {
66
71
  name: input.appName,
67
72
  url: base,
68
73
  redirect_url: `${base}/v1/github/app-manifest/callback`,
74
+ callback_urls: [`${base}/v1/github/oauth/callback`],
69
75
  public: input.public,
70
76
  request_oauth_on_install: true,
71
77
  default_permissions: permissions,
@@ -221,6 +227,29 @@ export async function verifyGitHubInstallationAccessForUser(
221
227
  return installation;
222
228
  }
223
229
 
230
+ /**
231
+ * Exchange a GitHub App user-authorization code and discover the installations
232
+ * and repositories the user can explicitly access. This is compatibility
233
+ * discovery metadata only: visibility and repository permission bits do not
234
+ * prove that the human may install, configure, or bind the App installation.
235
+ * No production binding path may treat this result as authority.
236
+ */
237
+ export async function authorizeGitHubAppUser(
238
+ settings: Settings,
239
+ input: { code: string },
240
+ ): Promise<GitHubUserInstallationAccess[]> {
241
+ const token = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);
242
+ const installations = await listUserAccessibleInstallations(token);
243
+ return await Promise.all(
244
+ installations.map(async (installation) => ({
245
+ ...installation,
246
+ repositories: installation.suspended
247
+ ? []
248
+ : await listUserInstallationRepositories(token, installation),
249
+ })),
250
+ );
251
+ }
252
+
224
253
  export async function listGitHubAppRepositories(
225
254
  settings: Settings,
226
255
  input: {
@@ -400,6 +429,47 @@ async function listUserAccessibleInstallations(
400
429
  }
401
430
  }
402
431
 
432
+ async function listUserInstallationRepositories(
433
+ token: string,
434
+ installation: GitHubAppInstallationSummary,
435
+ ): Promise<GitHubUserRepositoryAccess[]> {
436
+ const out: GitHubUserRepositoryAccess[] = [];
437
+ const account = {
438
+ ...(installation.accountLogin ? { login: installation.accountLogin } : {}),
439
+ ...(installation.accountType ? { type: installation.accountType } : {}),
440
+ };
441
+ for (let page = 1; ; page += 1) {
442
+ const payload = await githubGet(
443
+ `/user/installations/${installation.installationId}/repositories`,
444
+ token,
445
+ { per_page: "100", page: String(page) },
446
+ );
447
+ if (
448
+ !payload ||
449
+ typeof payload !== "object" ||
450
+ Array.isArray(payload) ||
451
+ !Array.isArray(payload.repositories)
452
+ ) {
453
+ throw new GitHubAppApiError(
454
+ "GitHub returned an invalid user installation repositories payload",
455
+ );
456
+ }
457
+ for (const repository of payload.repositories) {
458
+ if (!repository || typeof repository !== "object" || Array.isArray(repository)) {
459
+ continue;
460
+ }
461
+ const record = repository as Record<string, unknown>;
462
+ out.push({
463
+ ...repositoryFromPayload(record, installation.installationId, account),
464
+ permissions: repositoryPermissionsFromPayload(record.permissions),
465
+ });
466
+ }
467
+ if (payload.repositories.length < 100) {
468
+ return out;
469
+ }
470
+ }
471
+ }
472
+
403
473
  async function createInstallationToken(
404
474
  appJwt: string,
405
475
  input: {
@@ -522,6 +592,20 @@ function repositoryFromPayload(
522
592
  };
523
593
  }
524
594
 
595
+ function repositoryPermissionsFromPayload(payload: unknown): GitHubRepositoryPermissions {
596
+ const permissions =
597
+ payload && typeof payload === "object" && !Array.isArray(payload)
598
+ ? (payload as Record<string, unknown>)
599
+ : {};
600
+ return {
601
+ admin: permissions.admin === true,
602
+ maintain: permissions.maintain === true,
603
+ push: permissions.push === true,
604
+ triage: permissions.triage === true,
605
+ pull: permissions.pull === true,
606
+ };
607
+ }
608
+
525
609
  function githubHeaders(token?: string): HeadersInit {
526
610
  return {
527
611
  Accept: "application/vnd.github+json",