@opengeni/github 0.4.59 → 0.5.3-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,48 @@
1
1
  import type { Settings } from "@opengeni/config";
2
2
  import type { GitHubInstallationBindingCandidate, GitHubInstallationBindingProof, GitHubRepository, GitHubUserInstallationAccess } from "@opengeni/contracts";
3
+ /** Bound for the server-side repository-id lookup at turn start (mint + read). */
4
+ export declare const githubRepositoryLookupTimeoutMs = 10000;
3
5
  export declare const stateMaxAgeSeconds: number;
6
+ export declare const PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS: number;
7
+ export type PersonalGitHubGitBrokerRepositoryClaim = {
8
+ repositoryId: string;
9
+ fullName: string;
10
+ canonicalUrl: string;
11
+ ref: string;
12
+ access: "read" | "write";
13
+ selectionGeneration: number;
14
+ routeId: string;
15
+ };
16
+ export type PersonalGitHubGitBrokerClaims = {
17
+ version: 1;
18
+ accountId: string;
19
+ workspaceId: string;
20
+ sessionId: string;
21
+ rootSessionId: string;
22
+ turnId: string;
23
+ attemptId: string;
24
+ executionGeneration: number;
25
+ originWorkspaceId: string;
26
+ connectionId: string;
27
+ connectionAuthorityGeneration: number;
28
+ ownerSubjectId: string;
29
+ credentialBindingId: string;
30
+ selectionGeneration: number;
31
+ nonce: string;
32
+ issuedAt: number;
33
+ expiresAt: number;
34
+ };
35
+ export declare function personalGitHubGitBrokerRouteId(secret: string, input: Omit<PersonalGitHubGitBrokerClaims, "nonce" | "issuedAt" | "expiresAt"> & {
36
+ repository: Omit<PersonalGitHubGitBrokerRepositoryClaim, "routeId">;
37
+ }): string;
38
+ /**
39
+ * Seal exact Git broker authority into a confidential, authenticated bearer.
40
+ * The payload is encrypted rather than merely signed so tenant, session,
41
+ * connection, and repository identities are not readable from the sandbox's
42
+ * short-lived token file.
43
+ */
44
+ export declare function sealPersonalGitHubGitBrokerClaims(secret: string, claims: PersonalGitHubGitBrokerClaims): string;
45
+ export declare function openPersonalGitHubGitBrokerClaims(secret: string, token: string, nowSeconds?: number): PersonalGitHubGitBrokerClaims | null;
4
46
  export declare class GitHubAppConfigurationError extends Error {
5
47
  readonly missing: string[];
6
48
  constructor(missing: string[]);
@@ -29,6 +71,7 @@ export type GitHubSignedStatePayload = {
29
71
  [key: string]: unknown;
30
72
  };
31
73
  export declare function githubAppMissingSettings(settings: Settings): string[];
74
+ export type GitHubAppSigningSettings = Pick<Settings, "githubAppId" | "githubAppPrivateKey">;
32
75
  export declare function buildGitHubAppManifest(input: {
33
76
  appName: string;
34
77
  baseUrl: string;
@@ -94,6 +137,30 @@ export declare function discoverGitHubInstallationBindingCandidates(settings: Se
94
137
  export declare function listGitHubAppRepositories(settings: Settings, input?: {
95
138
  installationIds?: number[];
96
139
  }): Promise<GitHubRepository[]>;
140
+ /** List repositories for a separately registered App that needs only signing credentials. */
141
+ export declare function listGitHubAppRepositoriesWithSigningSettings(settings: GitHubAppSigningSettings, input?: {
142
+ installationIds?: number[];
143
+ }): Promise<GitHubRepository[]>;
144
+ export type GitHubAppInstallationRepositoryLookupInput = {
145
+ installationId: number;
146
+ owner: string;
147
+ name: string;
148
+ };
149
+ export type GitHubAppInstallationRepositoryLookup = (input: GitHubAppInstallationRepositoryLookupInput) => Promise<GitHubRepository | null>;
150
+ /**
151
+ * Resolve one `owner/name` repository through an exact App installation and
152
+ * return GitHub's stable repository identity, or null when that installation
153
+ * cannot see the repository. The server-side lookup token never leaves the
154
+ * caller and grants nothing by itself: the workspace allowlist decides whether
155
+ * the returned id may mint a sandbox-bound token.
156
+ */
157
+ export declare function getGitHubAppInstallationRepository(settings: Settings, input: GitHubAppInstallationRepositoryLookupInput): Promise<GitHubRepository | null>;
158
+ /**
159
+ * One lookup client that reuses a server-side installation token per
160
+ * installation for its lifetime (one worker turn), so several bare repository
161
+ * URIs from the same installation cost one mint plus one read each.
162
+ */
163
+ export declare function createGitHubAppInstallationRepositoryLookup(settings: Settings): GitHubAppInstallationRepositoryLookup;
97
164
  export declare function createGitHubAppInstallationToken(settings: Settings, input: {
98
165
  installationId: number;
99
166
  repositoryIds: number[];
@@ -106,6 +173,11 @@ export declare function createGitHubAppInstallationTokenWithExpiry(settings: Set
106
173
  installationId: number;
107
174
  repositoryIds: number[];
108
175
  }): Promise<GitHubAppInstallationToken>;
176
+ /** Mint for a separately registered App without requiring unrelated OAuth settings. */
177
+ export declare function createGitHubAppInstallationTokenWithSigningSettings(settings: GitHubAppSigningSettings, input: {
178
+ installationId: number;
179
+ repositoryIds: number[];
180
+ }): Promise<GitHubAppInstallationToken>;
109
181
  export declare function githubAppBotIdentity(settings: Settings): {
110
182
  name: string;
111
183
  email: string;
package/dist/index.js CHANGED
@@ -1,12 +1,148 @@
1
1
  // src/index.ts
2
- import { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from "crypto";
2
+ import {
3
+ createCipheriv,
4
+ createDecipheriv,
5
+ createHash,
6
+ createHmac,
7
+ createPrivateKey,
8
+ randomBytes,
9
+ timingSafeEqual
10
+ } from "crypto";
3
11
  import { SignJWT, importPKCS8 } from "jose";
4
12
  var githubApiBase = "https://api.github.com";
5
13
  var githubApiVersion = "2022-11-28";
6
14
  var githubTokenMintTimeoutMs = 6e4;
15
+ var githubRepositoryLookupTimeoutMs = 1e4;
7
16
  var stateMaxAgeSeconds = 60 * 60;
8
17
  var pkcs8PrivateKeyHeader = `-----BEGIN ${"PRIVATE KEY"}-----`;
9
18
  var rsaPrivateKeyHeader = `-----BEGIN ${"RSA PRIVATE KEY"}-----`;
19
+ var PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX = "oggh1";
20
+ var PERSONAL_GITHUB_GIT_BROKER_KEY_CONTEXT = "opengeni:personal-github:git-broker:v1";
21
+ var PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS = 5 * 60;
22
+ function personalGitHubGitBrokerRouteId(secret, input) {
23
+ const hmac = createHmac("sha256", personalGitHubGitBrokerKey(secret));
24
+ for (const value of [
25
+ String(input.version),
26
+ input.accountId,
27
+ input.workspaceId,
28
+ input.sessionId,
29
+ input.rootSessionId,
30
+ input.turnId,
31
+ input.attemptId,
32
+ String(input.executionGeneration),
33
+ input.originWorkspaceId,
34
+ input.connectionId,
35
+ String(input.connectionAuthorityGeneration),
36
+ input.ownerSubjectId,
37
+ input.credentialBindingId,
38
+ String(input.selectionGeneration),
39
+ input.repository.repositoryId,
40
+ input.repository.fullName,
41
+ input.repository.canonicalUrl,
42
+ input.repository.ref,
43
+ input.repository.access,
44
+ String(input.repository.selectionGeneration)
45
+ ]) {
46
+ const bytes = Buffer.from(value, "utf8");
47
+ hmac.update(Buffer.from(String(bytes.byteLength), "ascii"));
48
+ hmac.update(":");
49
+ hmac.update(bytes);
50
+ hmac.update(";");
51
+ }
52
+ return hmac.digest("base64url");
53
+ }
54
+ function sealPersonalGitHubGitBrokerClaims(secret, claims) {
55
+ assertPersonalGitHubGitBrokerClaims(claims);
56
+ const iv = randomBytes(12);
57
+ const cipher = createCipheriv("aes-256-gcm", personalGitHubGitBrokerKey(secret), iv);
58
+ cipher.setAAD(Buffer.from(PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX, "ascii"));
59
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(claims), "utf8"), cipher.final()]);
60
+ return [
61
+ PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX,
62
+ iv.toString("base64url"),
63
+ ciphertext.toString("base64url"),
64
+ cipher.getAuthTag().toString("base64url")
65
+ ].join(".");
66
+ }
67
+ function openPersonalGitHubGitBrokerClaims(secret, token, nowSeconds = Math.floor(Date.now() / 1e3)) {
68
+ const [prefix, encodedIv, encodedCiphertext, encodedTag, extra] = token.split(".");
69
+ if (prefix !== PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX || !encodedIv || !encodedCiphertext || !encodedTag || extra !== void 0) {
70
+ return null;
71
+ }
72
+ try {
73
+ const iv = Buffer.from(encodedIv, "base64url");
74
+ const ciphertext = Buffer.from(encodedCiphertext, "base64url");
75
+ const tag = Buffer.from(encodedTag, "base64url");
76
+ if (iv.byteLength !== 12 || tag.byteLength !== 16 || ciphertext.byteLength > 4096) {
77
+ return null;
78
+ }
79
+ const decipher = createDecipheriv("aes-256-gcm", personalGitHubGitBrokerKey(secret), iv);
80
+ decipher.setAAD(Buffer.from(PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX, "ascii"));
81
+ decipher.setAuthTag(tag);
82
+ const payload = JSON.parse(
83
+ Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8")
84
+ );
85
+ assertPersonalGitHubGitBrokerClaims(payload);
86
+ if (payload.issuedAt > nowSeconds + 60 || nowSeconds >= payload.expiresAt) return null;
87
+ if (payload.expiresAt - payload.issuedAt > PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS) {
88
+ return null;
89
+ }
90
+ return payload;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ function personalGitHubGitBrokerKey(secret) {
96
+ const normalized = secret.trim();
97
+ if (!normalized) throw new Error("personal GitHub Git broker signing secret is unavailable");
98
+ return createHash("sha256").update(PERSONAL_GITHUB_GIT_BROKER_KEY_CONTEXT, "utf8").update("\0", "utf8").update(normalized, "utf8").digest();
99
+ }
100
+ function assertPersonalGitHubGitBrokerClaims(value) {
101
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
102
+ throw new Error("invalid personal GitHub Git broker claims");
103
+ }
104
+ const claims = value;
105
+ const expectedKeys = /* @__PURE__ */ new Set([
106
+ "version",
107
+ "accountId",
108
+ "workspaceId",
109
+ "sessionId",
110
+ "rootSessionId",
111
+ "turnId",
112
+ "attemptId",
113
+ "executionGeneration",
114
+ "originWorkspaceId",
115
+ "connectionId",
116
+ "connectionAuthorityGeneration",
117
+ "ownerSubjectId",
118
+ "credentialBindingId",
119
+ "selectionGeneration",
120
+ "nonce",
121
+ "issuedAt",
122
+ "expiresAt"
123
+ ]);
124
+ const strings = [
125
+ "accountId",
126
+ "workspaceId",
127
+ "sessionId",
128
+ "rootSessionId",
129
+ "turnId",
130
+ "attemptId",
131
+ "originWorkspaceId",
132
+ "connectionId",
133
+ "ownerSubjectId",
134
+ "credentialBindingId",
135
+ "nonce"
136
+ ];
137
+ if (claims.version !== 1 || strings.some(
138
+ (field) => typeof claims[field] !== "string" || claims[field].length === 0 || claims[field].length > (field === "ownerSubjectId" ? 512 : 128)
139
+ ) || !positiveIntegerClaim(claims.executionGeneration) || !positiveIntegerClaim(claims.connectionAuthorityGeneration) || !positiveIntegerClaim(claims.selectionGeneration) || !positiveIntegerClaim(claims.issuedAt) || !positiveIntegerClaim(claims.expiresAt) || Object.keys(claims).some((key) => !expectedKeys.has(key))) {
140
+ throw new Error("invalid personal GitHub Git broker claims");
141
+ }
142
+ }
143
+ function positiveIntegerClaim(value) {
144
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
145
+ }
10
146
  var GitHubAppConfigurationError = class extends Error {
11
147
  constructor(missing) {
12
148
  super("GitHub App is not configured");
@@ -35,6 +171,13 @@ function githubAppMissingSettings(settings) {
35
171
  };
36
172
  return Object.entries(required).flatMap(([name, value]) => value && value.trim() ? [] : [name]);
37
173
  }
174
+ function githubAppTokenMissingSettings(settings) {
175
+ const required = {
176
+ OPENGENI_GITHUB_APP_ID: settings.githubAppId,
177
+ OPENGENI_GITHUB_APP_PRIVATE_KEY: settings.githubAppPrivateKey
178
+ };
179
+ return Object.entries(required).flatMap(([name, value]) => value && value.trim() ? [] : [name]);
180
+ }
38
181
  function buildGitHubAppManifest(input) {
39
182
  const base = input.baseUrl.replace(/\/+$/, "");
40
183
  const permissions = {
@@ -309,6 +452,13 @@ async function listGitHubAppRepositories(settings, input = {}) {
309
452
  if (missing.length > 0) {
310
453
  throw new GitHubAppConfigurationError(missing);
311
454
  }
455
+ return await listGitHubAppRepositoriesWithSigningSettings(settings, input);
456
+ }
457
+ async function listGitHubAppRepositoriesWithSigningSettings(settings, input = {}) {
458
+ const missing = githubAppTokenMissingSettings(settings);
459
+ if (missing.length > 0) {
460
+ throw new GitHubAppConfigurationError(missing);
461
+ }
312
462
  const allowedInstallations = input.installationIds ? new Set(input.installationIds) : null;
313
463
  if (allowedInstallations && allowedInstallations.size === 0) {
314
464
  return [];
@@ -336,6 +486,59 @@ async function listGitHubAppRepositories(settings, input = {}) {
336
486
  repositories.sort((left, right) => left.fullName.localeCompare(right.fullName));
337
487
  return repositories;
338
488
  }
489
+ async function getGitHubAppInstallationRepository(settings, input) {
490
+ return await createGitHubAppInstallationRepositoryLookup(settings)(input);
491
+ }
492
+ function createGitHubAppInstallationRepositoryLookup(settings) {
493
+ const missing = githubAppMissingSettings(settings);
494
+ if (missing.length > 0) {
495
+ throw new GitHubAppConfigurationError(missing);
496
+ }
497
+ const tokens = /* @__PURE__ */ new Map();
498
+ const installationToken = (installationId) => {
499
+ let pending = tokens.get(installationId);
500
+ if (!pending) {
501
+ pending = createGitHubAppJwt(settings).then(
502
+ (jwt) => createInstallationToken(jwt, {
503
+ installationId,
504
+ permissions: { metadata: "read" },
505
+ timeoutMs: githubRepositoryLookupTimeoutMs
506
+ })
507
+ );
508
+ pending.catch(() => tokens.delete(installationId));
509
+ tokens.set(installationId, pending);
510
+ }
511
+ return pending;
512
+ };
513
+ return async (input) => {
514
+ const owner = input.owner.trim();
515
+ const name = input.name.trim();
516
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/u.test(owner) || !/^[A-Za-z0-9._-]+$/u.test(name)) {
517
+ return null;
518
+ }
519
+ const token = await installationToken(input.installationId);
520
+ const response = await fetch(
521
+ `${githubApiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
522
+ {
523
+ headers: githubHeaders(token.token),
524
+ signal: AbortSignal.timeout(githubRepositoryLookupTimeoutMs)
525
+ }
526
+ );
527
+ if (response.status === 404) {
528
+ return null;
529
+ }
530
+ if (!response.ok) {
531
+ throw new GitHubAppApiError(await githubErrorMessage(response), response.status);
532
+ }
533
+ const payload = await response.json();
534
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
535
+ throw new GitHubAppApiError("GitHub returned an invalid repository payload");
536
+ }
537
+ const record = payload;
538
+ const account = record.owner && typeof record.owner === "object" && !Array.isArray(record.owner) ? record.owner : {};
539
+ return repositoryFromPayload(record, input.installationId, account);
540
+ };
541
+ }
339
542
  async function createGitHubAppInstallationToken(settings, input) {
340
543
  return (await createGitHubAppInstallationTokenWithExpiry(settings, input)).token;
341
544
  }
@@ -344,6 +547,13 @@ async function createGitHubAppInstallationTokenWithExpiry(settings, input) {
344
547
  if (missing.length > 0) {
345
548
  throw new GitHubAppConfigurationError(missing);
346
549
  }
550
+ return await createGitHubAppInstallationTokenWithSigningSettings(settings, input);
551
+ }
552
+ async function createGitHubAppInstallationTokenWithSigningSettings(settings, input) {
553
+ const missing = githubAppTokenMissingSettings(settings);
554
+ if (missing.length > 0) {
555
+ throw new GitHubAppConfigurationError(missing);
556
+ }
347
557
  if (!Array.isArray(input.repositoryIds)) {
348
558
  throw new GitHubAppApiError(
349
559
  "GitHub installation token mint requires an explicit, unique repository allowlist"
@@ -377,7 +587,7 @@ async function createGitHubAppJwt(settings) {
377
587
  const privateKey = normalizeGitHubAppPrivateKey(settings.githubAppPrivateKey ?? "");
378
588
  const appId = settings.githubAppId?.trim();
379
589
  if (!appId || !privateKey) {
380
- throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
590
+ throw new GitHubAppConfigurationError(githubAppTokenMissingSettings(settings));
381
591
  }
382
592
  const key = await importPKCS8(privateKey, "RS256");
383
593
  const now = Math.floor(Date.now() / 1e3);
@@ -527,7 +737,14 @@ async function listUserInstallationRepositories(token, installation) {
527
737
  }
528
738
  }
529
739
  async function createInstallationToken(appJwt, input) {
530
- const scoped = input.repositoryIds && input.repositoryIds.length > 0;
740
+ const body = {};
741
+ if (input.repositoryIds && input.repositoryIds.length > 0) {
742
+ body.repository_ids = input.repositoryIds;
743
+ }
744
+ if (input.permissions && Object.keys(input.permissions).length > 0) {
745
+ body.permissions = input.permissions;
746
+ }
747
+ const scoped = Object.keys(body).length > 0;
531
748
  const response = await fetch(
532
749
  `${githubApiBase}/app/installations/${input.installationId}/access_tokens`,
533
750
  {
@@ -536,8 +753,8 @@ async function createInstallationToken(appJwt, input) {
536
753
  ...githubHeaders(appJwt),
537
754
  ...scoped ? { "Content-Type": "application/json" } : {}
538
755
  },
539
- signal: AbortSignal.timeout(githubTokenMintTimeoutMs),
540
- ...scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}
756
+ signal: AbortSignal.timeout(input.timeoutMs ?? githubTokenMintTimeoutMs),
757
+ ...scoped ? { body: JSON.stringify(body) } : {}
541
758
  }
542
759
  );
543
760
  if (!response.ok) {
@@ -678,25 +895,34 @@ export {
678
895
  GitHubAppApiError,
679
896
  GitHubAppConfigurationError,
680
897
  GitHubInstallationAuthorityError,
898
+ PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS,
681
899
  authorizeGitHubAppUser,
682
900
  authorizeGitHubInstallationBinding,
683
901
  buildGitHubAppManifest,
684
902
  convertGitHubAppManifest,
903
+ createGitHubAppInstallationRepositoryLookup,
685
904
  createGitHubAppInstallationToken,
686
905
  createGitHubAppInstallationTokenWithExpiry,
906
+ createGitHubAppInstallationTokenWithSigningSettings,
687
907
  createSignedState,
688
908
  discoverGitHubInstallationBindingCandidates,
689
909
  envLinesFromGitHubManifestConversion,
910
+ getGitHubAppInstallationRepository,
690
911
  getGitHubAppInstallationSummary,
691
912
  githubAppBotIdentity,
692
913
  githubAppMissingSettings,
693
914
  githubOAuthAuthorizeUrl,
915
+ githubRepositoryLookupTimeoutMs,
694
916
  listGitHubAppInstallationSummaries,
695
917
  listGitHubAppRepositories,
918
+ listGitHubAppRepositoriesWithSigningSettings,
696
919
  normalizeGitHubAppPrivateKey,
920
+ openPersonalGitHubGitBrokerClaims,
697
921
  organizationAppManifestUrl,
698
922
  personalAppManifestUrl,
923
+ personalGitHubGitBrokerRouteId,
699
924
  readSignedState,
925
+ sealPersonalGitHubGitBrokerClaims,
700
926
  stateMaxAgeSeconds,
701
927
  verifyGitHubInstallationAccessForUser,
702
928
  verifySignedState
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport type {\n GitHubInstallationBindingCandidate,\n GitHubInstallationBindingProof,\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 constructor(\n message: string,\n readonly status: number | null = null,\n ) {\n super(message);\n }\n}\n\nexport type GitHubInstallationAuthorityFailure =\n | \"authority_denied\"\n | \"authority_unavailable\"\n | \"installation_missing\"\n | \"installation_suspended\"\n | \"repository_access_empty\";\n\nexport class GitHubInstallationAuthorityError extends GitHubAppApiError {\n constructor(\n readonly reason: GitHubInstallationAuthorityFailure,\n message: string,\n status: number | null = null,\n ) {\n super(message, status);\n }\n}\n\nexport type GitHubAppInstallationSummary = {\n installationId: number;\n accountId: 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 // Required for the authenticated-user membership endpoint that proves an\n // active organization owner. Existing installations must approve this\n // permission before organization-owner self-service can succeed.\n members: \"read\",\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 // A setup URL and OAuth-on-install are mutually exclusive in GitHub's App\n // contract. OpenGeni needs the setup callback to receive the installation\n // id, then starts its own exact user-authorization flow.\n request_oauth_on_install: !input.setupUrl,\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\n/**\n * Prove current GitHub installation authority without treating repository\n * administration or installation visibility as delegation authority.\n *\n * GitHub exposes an exact personal-account owner through the authenticated\n * user's immutable id. For organizations, GitHub's authenticated membership\n * endpoint exposes active owners as role=admin. GitHub does not expose an\n * equivalent current-authority receipt for App Managers, so that case remains\n * unsupported and fails closed.\n */\nexport async function authorizeGitHubInstallationBinding(\n settings: Settings,\n input: { code: string; installationId: number },\n): Promise<GitHubInstallationBindingProof> {\n const userToken = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const actor = await getAuthenticatedGitHubUser(userToken);\n const visibleInstallations = await listUserAccessibleInstallations(userToken);\n const visible = visibleInstallations.find(\n (installation) => installation.installationId === input.installationId,\n );\n if (!visible) {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"GitHub did not associate this installation with the authorized user\",\n );\n }\n\n const jwt = await createGitHubAppJwt(settings);\n const livePayload = (await listInstallations(jwt)).find(\n (installation) => asInt(installation.id) === input.installationId,\n );\n if (!livePayload) {\n throw new GitHubInstallationAuthorityError(\n \"installation_missing\",\n \"GitHub App installation was deleted or is not owned by this App\",\n );\n }\n const installation = installationSummaryFromPayload(livePayload);\n if (\n installation.accountId !== visible.accountId ||\n installation.accountLogin !== visible.accountLogin ||\n installation.accountType !== visible.accountType\n ) {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"GitHub installation identity changed during authorization\",\n );\n }\n if (installation.suspended) {\n throw new GitHubInstallationAuthorityError(\n \"installation_suspended\",\n \"GitHub App installation is suspended\",\n );\n }\n\n let authorityKind: GitHubInstallationBindingProof[\"authorityKind\"];\n if (installation.accountType === \"User\" && actor.id === installation.accountId) {\n authorityKind = \"personal_owner\";\n } else if (installation.accountType === \"Organization\" && installation.accountLogin) {\n await assertActiveOrganizationOwner(\n userToken,\n installation.accountId,\n installation.accountLogin,\n );\n authorityKind = \"organization_owner\";\n } else {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"Only a GitHub personal-account owner or organization owner may bind an installation\",\n );\n }\n\n const installationToken = await createInstallationToken(jwt, {\n installationId: installation.installationId,\n });\n const repositories = await listInstallationRepositories(\n installationToken.token,\n installation.installationId,\n { login: installation.accountLogin, type: installation.accountType },\n );\n if (repositories.length === 0) {\n throw new GitHubInstallationAuthorityError(\n \"repository_access_empty\",\n \"GitHub App installation does not currently grant access to any repositories\",\n );\n }\n if (authorityKind === \"organization_owner\") {\n // Repository enumeration is an async provider boundary. Re-read the live\n // owner tuple after it so a role revoked after the chooser proof cannot be\n // durably bound with a later, misleading authority timestamp.\n await assertActiveOrganizationOwner(\n userToken,\n installation.accountId,\n installation.accountLogin!,\n );\n }\n return {\n actorId: actor.id,\n actorLogin: actor.login,\n authorityKind,\n installation,\n repositories,\n };\n}\n\n/**\n * Discover existing installations that the freshly authorized GitHub human\n * can bind as an exact personal owner or active organization owner.\n *\n * `GET /user/installations` is discovery input only. Every candidate is\n * cross-checked against the App's live installation inventory and an\n * organization candidate requires a live `state=active, role=admin`\n * membership proof. The later exact authorization still re-runs the complete\n * proof immediately before the durable bind.\n */\nexport async function discoverGitHubInstallationBindingCandidates(\n settings: Settings,\n input: { code: string },\n): Promise<GitHubInstallationBindingCandidate[]> {\n const userToken = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const actor = await getAuthenticatedGitHubUser(userToken);\n const visibleInstallations = await listUserAccessibleInstallations(userToken);\n const jwt = await createGitHubAppJwt(settings);\n const liveInstallations = new Map(\n (await listInstallations(jwt)).map((payload) => {\n const installation = installationSummaryFromPayload(payload);\n return [installation.installationId, installation] as const;\n }),\n );\n const candidates: GitHubInstallationBindingCandidate[] = [];\n\n for (const visible of visibleInstallations) {\n const installation = liveInstallations.get(visible.installationId);\n if (\n !installation ||\n installation.suspended ||\n installation.accountId !== visible.accountId ||\n installation.accountLogin !== visible.accountLogin ||\n installation.accountType !== visible.accountType\n ) {\n continue;\n }\n if (installation.accountType === \"User\" && actor.id === installation.accountId) {\n candidates.push({ installation, authorityKind: \"personal_owner\" });\n continue;\n }\n if (installation.accountType !== \"Organization\" || !installation.accountLogin) {\n continue;\n }\n try {\n await assertActiveOrganizationOwner(\n userToken,\n installation.accountId,\n installation.accountLogin,\n );\n candidates.push({ installation, authorityKind: \"organization_owner\" });\n } catch (error) {\n if (\n error instanceof GitHubInstallationAuthorityError &&\n error.reason === \"authority_unavailable\"\n ) {\n continue;\n }\n if (\n error instanceof GitHubInstallationAuthorityError &&\n error.reason === \"authority_denied\"\n ) {\n continue;\n }\n throw error;\n }\n }\n\n return candidates.sort((left, right) =>\n (left.installation.accountLogin ?? \"\").localeCompare(right.installation.accountLogin ?? \"\"),\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 if (!Array.isArray(input.repositoryIds)) {\n throw new GitHubAppApiError(\n \"GitHub installation token mint requires an explicit, unique repository allowlist\",\n );\n }\n const repositoryIds = [...new Set(input.repositoryIds)];\n if (\n !Number.isSafeInteger(input.installationId) ||\n input.installationId <= 0 ||\n repositoryIds.length === 0 ||\n repositoryIds.length !== input.repositoryIds.length ||\n repositoryIds.some((id) => !Number.isSafeInteger(id) || id <= 0)\n ) {\n throw new GitHubAppApiError(\n \"GitHub installation token mint requires an explicit, unique repository allowlist\",\n );\n }\n const jwt = await createGitHubAppJwt(settings);\n return await createInstallationToken(jwt, {\n installationId: input.installationId,\n repositoryIds,\n });\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 getAuthenticatedGitHubUser(token: string): Promise<{ id: number; login: string }> {\n const payload = await githubGet(\"/user\", token);\n const id = payload && typeof payload === \"object\" ? asInt(payload.id) : null;\n const login =\n payload && typeof payload === \"object\" && typeof payload.login === \"string\"\n ? payload.login\n : null;\n if (id === null || !login) {\n throw new GitHubAppApiError(\"GitHub returned an invalid authenticated user payload\");\n }\n return { id, login };\n}\n\nasync function getAuthenticatedOrganizationMembership(\n token: string,\n organizationLogin: string,\n): Promise<{ organizationId: number; role: string; state: string }> {\n let payload: any;\n try {\n payload = await githubGet(\n `/user/memberships/orgs/${encodeURIComponent(organizationLogin)}`,\n token,\n );\n } catch (error) {\n if (error instanceof GitHubAppApiError) {\n throw new GitHubInstallationAuthorityError(\n \"authority_unavailable\",\n \"GitHub could not prove current organization-owner membership\",\n error.status,\n );\n }\n throw error;\n }\n const organization =\n payload && typeof payload === \"object\" && payload.organization ? payload.organization : null;\n const organizationId =\n organization && typeof organization === \"object\" ? asInt(organization.id) : null;\n if (\n organizationId === null ||\n typeof payload?.role !== \"string\" ||\n typeof payload?.state !== \"string\"\n ) {\n throw new GitHubInstallationAuthorityError(\n \"authority_unavailable\",\n \"GitHub returned an invalid organization membership proof\",\n );\n }\n return { organizationId, role: payload.role, state: payload.state };\n}\n\nasync function assertActiveOrganizationOwner(\n token: string,\n organizationId: number,\n organizationLogin: string,\n): Promise<void> {\n const membership = await getAuthenticatedOrganizationMembership(token, organizationLogin);\n if (\n membership.organizationId !== organizationId ||\n membership.state !== \"active\" ||\n membership.role !== \"admin\"\n ) {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"Only an active GitHub organization owner may bind this installation\",\n );\n }\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 const accountId = asInt(account.id);\n if (accountId === null) {\n throw new GitHubAppApiError(\"GitHub returned an installation without an account id\");\n }\n return {\n installationId,\n accountId,\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), response.status);\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":["installation"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport type {\n GitHubInstallationBindingCandidate,\n GitHubInstallationBindingProof,\n GitHubRepository,\n GitHubRepositoryPermissions,\n GitHubUserInstallationAccess,\n GitHubUserRepositoryAccess,\n} from \"@opengeni/contracts\";\nimport {\n createCipheriv,\n createDecipheriv,\n createHash,\n createHmac,\n createPrivateKey,\n randomBytes,\n timingSafeEqual,\n} from \"node:crypto\";\nimport { SignJWT, importPKCS8 } from \"jose\";\n\nconst githubApiBase = \"https://api.github.com\";\nconst githubApiVersion = \"2022-11-28\";\nconst githubTokenMintTimeoutMs = 60_000;\n/** Bound for the server-side repository-id lookup at turn start (mint + read). */\nexport const githubRepositoryLookupTimeoutMs = 10_000;\nexport const stateMaxAgeSeconds = 60 * 60;\nconst pkcs8PrivateKeyHeader = `-----BEGIN ${\"PRIVATE KEY\"}-----`;\nconst rsaPrivateKeyHeader = `-----BEGIN ${\"RSA PRIVATE KEY\"}-----`;\n\nconst PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX = \"oggh1\";\nconst PERSONAL_GITHUB_GIT_BROKER_KEY_CONTEXT = \"opengeni:personal-github:git-broker:v1\";\nexport const PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS = 5 * 60;\n\nexport type PersonalGitHubGitBrokerRepositoryClaim = {\n repositoryId: string;\n fullName: string;\n canonicalUrl: string;\n ref: string;\n access: \"read\" | \"write\";\n selectionGeneration: number;\n routeId: string;\n};\n\nexport type PersonalGitHubGitBrokerClaims = {\n version: 1;\n accountId: string;\n workspaceId: string;\n sessionId: string;\n rootSessionId: string;\n turnId: string;\n attemptId: string;\n executionGeneration: number;\n originWorkspaceId: string;\n connectionId: string;\n connectionAuthorityGeneration: number;\n ownerSubjectId: string;\n credentialBindingId: string;\n selectionGeneration: number;\n nonce: string;\n issuedAt: number;\n expiresAt: number;\n};\n\nexport function personalGitHubGitBrokerRouteId(\n secret: string,\n input: Omit<PersonalGitHubGitBrokerClaims, \"nonce\" | \"issuedAt\" | \"expiresAt\"> & {\n repository: Omit<PersonalGitHubGitBrokerRepositoryClaim, \"routeId\">;\n },\n): string {\n const hmac = createHmac(\"sha256\", personalGitHubGitBrokerKey(secret));\n for (const value of [\n String(input.version),\n input.accountId,\n input.workspaceId,\n input.sessionId,\n input.rootSessionId,\n input.turnId,\n input.attemptId,\n String(input.executionGeneration),\n input.originWorkspaceId,\n input.connectionId,\n String(input.connectionAuthorityGeneration),\n input.ownerSubjectId,\n input.credentialBindingId,\n String(input.selectionGeneration),\n input.repository.repositoryId,\n input.repository.fullName,\n input.repository.canonicalUrl,\n input.repository.ref,\n input.repository.access,\n String(input.repository.selectionGeneration),\n ]) {\n const bytes = Buffer.from(value, \"utf8\");\n hmac.update(Buffer.from(String(bytes.byteLength), \"ascii\"));\n hmac.update(\":\");\n hmac.update(bytes);\n hmac.update(\";\");\n }\n return hmac.digest(\"base64url\");\n}\n\n/**\n * Seal exact Git broker authority into a confidential, authenticated bearer.\n * The payload is encrypted rather than merely signed so tenant, session,\n * connection, and repository identities are not readable from the sandbox's\n * short-lived token file.\n */\nexport function sealPersonalGitHubGitBrokerClaims(\n secret: string,\n claims: PersonalGitHubGitBrokerClaims,\n): string {\n assertPersonalGitHubGitBrokerClaims(claims);\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", personalGitHubGitBrokerKey(secret), iv);\n cipher.setAAD(Buffer.from(PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX, \"ascii\"));\n const ciphertext = Buffer.concat([cipher.update(JSON.stringify(claims), \"utf8\"), cipher.final()]);\n return [\n PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX,\n iv.toString(\"base64url\"),\n ciphertext.toString(\"base64url\"),\n cipher.getAuthTag().toString(\"base64url\"),\n ].join(\".\");\n}\n\nexport function openPersonalGitHubGitBrokerClaims(\n secret: string,\n token: string,\n nowSeconds = Math.floor(Date.now() / 1_000),\n): PersonalGitHubGitBrokerClaims | null {\n const [prefix, encodedIv, encodedCiphertext, encodedTag, extra] = token.split(\".\");\n if (\n prefix !== PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX ||\n !encodedIv ||\n !encodedCiphertext ||\n !encodedTag ||\n extra !== undefined\n ) {\n return null;\n }\n try {\n const iv = Buffer.from(encodedIv, \"base64url\");\n const ciphertext = Buffer.from(encodedCiphertext, \"base64url\");\n const tag = Buffer.from(encodedTag, \"base64url\");\n if (iv.byteLength !== 12 || tag.byteLength !== 16 || ciphertext.byteLength > 4_096) {\n return null;\n }\n const decipher = createDecipheriv(\"aes-256-gcm\", personalGitHubGitBrokerKey(secret), iv);\n decipher.setAAD(Buffer.from(PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX, \"ascii\"));\n decipher.setAuthTag(tag);\n const payload = JSON.parse(\n Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString(\"utf8\"),\n ) as unknown;\n assertPersonalGitHubGitBrokerClaims(payload);\n if (payload.issuedAt > nowSeconds + 60 || nowSeconds >= payload.expiresAt) return null;\n if (payload.expiresAt - payload.issuedAt > PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS) {\n return null;\n }\n return payload;\n } catch {\n return null;\n }\n}\n\nfunction personalGitHubGitBrokerKey(secret: string): Buffer {\n const normalized = secret.trim();\n if (!normalized) throw new Error(\"personal GitHub Git broker signing secret is unavailable\");\n return createHash(\"sha256\")\n .update(PERSONAL_GITHUB_GIT_BROKER_KEY_CONTEXT, \"utf8\")\n .update(\"\\0\", \"utf8\")\n .update(normalized, \"utf8\")\n .digest();\n}\n\nfunction assertPersonalGitHubGitBrokerClaims(\n value: unknown,\n): asserts value is PersonalGitHubGitBrokerClaims {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(\"invalid personal GitHub Git broker claims\");\n }\n const claims = value as Record<string, unknown>;\n const expectedKeys = new Set([\n \"version\",\n \"accountId\",\n \"workspaceId\",\n \"sessionId\",\n \"rootSessionId\",\n \"turnId\",\n \"attemptId\",\n \"executionGeneration\",\n \"originWorkspaceId\",\n \"connectionId\",\n \"connectionAuthorityGeneration\",\n \"ownerSubjectId\",\n \"credentialBindingId\",\n \"selectionGeneration\",\n \"nonce\",\n \"issuedAt\",\n \"expiresAt\",\n ]);\n const strings = [\n \"accountId\",\n \"workspaceId\",\n \"sessionId\",\n \"rootSessionId\",\n \"turnId\",\n \"attemptId\",\n \"originWorkspaceId\",\n \"connectionId\",\n \"ownerSubjectId\",\n \"credentialBindingId\",\n \"nonce\",\n ];\n if (\n claims.version !== 1 ||\n strings.some(\n (field) =>\n typeof claims[field] !== \"string\" ||\n claims[field].length === 0 ||\n claims[field].length > (field === \"ownerSubjectId\" ? 512 : 128),\n ) ||\n !positiveIntegerClaim(claims.executionGeneration) ||\n !positiveIntegerClaim(claims.connectionAuthorityGeneration) ||\n !positiveIntegerClaim(claims.selectionGeneration) ||\n !positiveIntegerClaim(claims.issuedAt) ||\n !positiveIntegerClaim(claims.expiresAt) ||\n Object.keys(claims).some((key) => !expectedKeys.has(key))\n ) {\n throw new Error(\"invalid personal GitHub Git broker claims\");\n }\n}\n\nfunction positiveIntegerClaim(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\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 constructor(\n message: string,\n readonly status: number | null = null,\n ) {\n super(message);\n }\n}\n\nexport type GitHubInstallationAuthorityFailure =\n | \"authority_denied\"\n | \"authority_unavailable\"\n | \"installation_missing\"\n | \"installation_suspended\"\n | \"repository_access_empty\";\n\nexport class GitHubInstallationAuthorityError extends GitHubAppApiError {\n constructor(\n readonly reason: GitHubInstallationAuthorityFailure,\n message: string,\n status: number | null = null,\n ) {\n super(message, status);\n }\n}\n\nexport type GitHubAppInstallationSummary = {\n installationId: number;\n accountId: 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 type GitHubAppSigningSettings = Pick<Settings, \"githubAppId\" | \"githubAppPrivateKey\">;\n\nfunction githubAppTokenMissingSettings(settings: GitHubAppSigningSettings): string[] {\n const required: Record<string, string | undefined> = {\n OPENGENI_GITHUB_APP_ID: settings.githubAppId,\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 // Required for the authenticated-user membership endpoint that proves an\n // active organization owner. Existing installations must approve this\n // permission before organization-owner self-service can succeed.\n members: \"read\",\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 // A setup URL and OAuth-on-install are mutually exclusive in GitHub's App\n // contract. OpenGeni needs the setup callback to receive the installation\n // id, then starts its own exact user-authorization flow.\n request_oauth_on_install: !input.setupUrl,\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\n/**\n * Prove current GitHub installation authority without treating repository\n * administration or installation visibility as delegation authority.\n *\n * GitHub exposes an exact personal-account owner through the authenticated\n * user's immutable id. For organizations, GitHub's authenticated membership\n * endpoint exposes active owners as role=admin. GitHub does not expose an\n * equivalent current-authority receipt for App Managers, so that case remains\n * unsupported and fails closed.\n */\nexport async function authorizeGitHubInstallationBinding(\n settings: Settings,\n input: { code: string; installationId: number },\n): Promise<GitHubInstallationBindingProof> {\n const userToken = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const actor = await getAuthenticatedGitHubUser(userToken);\n const visibleInstallations = await listUserAccessibleInstallations(userToken);\n const visible = visibleInstallations.find(\n (installation) => installation.installationId === input.installationId,\n );\n if (!visible) {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"GitHub did not associate this installation with the authorized user\",\n );\n }\n\n const jwt = await createGitHubAppJwt(settings);\n const livePayload = (await listInstallations(jwt)).find(\n (installation) => asInt(installation.id) === input.installationId,\n );\n if (!livePayload) {\n throw new GitHubInstallationAuthorityError(\n \"installation_missing\",\n \"GitHub App installation was deleted or is not owned by this App\",\n );\n }\n const installation = installationSummaryFromPayload(livePayload);\n if (\n installation.accountId !== visible.accountId ||\n installation.accountLogin !== visible.accountLogin ||\n installation.accountType !== visible.accountType\n ) {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"GitHub installation identity changed during authorization\",\n );\n }\n if (installation.suspended) {\n throw new GitHubInstallationAuthorityError(\n \"installation_suspended\",\n \"GitHub App installation is suspended\",\n );\n }\n\n let authorityKind: GitHubInstallationBindingProof[\"authorityKind\"];\n if (installation.accountType === \"User\" && actor.id === installation.accountId) {\n authorityKind = \"personal_owner\";\n } else if (installation.accountType === \"Organization\" && installation.accountLogin) {\n await assertActiveOrganizationOwner(\n userToken,\n installation.accountId,\n installation.accountLogin,\n );\n authorityKind = \"organization_owner\";\n } else {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"Only a GitHub personal-account owner or organization owner may bind an installation\",\n );\n }\n\n const installationToken = await createInstallationToken(jwt, {\n installationId: installation.installationId,\n });\n const repositories = await listInstallationRepositories(\n installationToken.token,\n installation.installationId,\n { login: installation.accountLogin, type: installation.accountType },\n );\n if (repositories.length === 0) {\n throw new GitHubInstallationAuthorityError(\n \"repository_access_empty\",\n \"GitHub App installation does not currently grant access to any repositories\",\n );\n }\n if (authorityKind === \"organization_owner\") {\n // Repository enumeration is an async provider boundary. Re-read the live\n // owner tuple after it so a role revoked after the chooser proof cannot be\n // durably bound with a later, misleading authority timestamp.\n await assertActiveOrganizationOwner(\n userToken,\n installation.accountId,\n installation.accountLogin!,\n );\n }\n return {\n actorId: actor.id,\n actorLogin: actor.login,\n authorityKind,\n installation,\n repositories,\n };\n}\n\n/**\n * Discover existing installations that the freshly authorized GitHub human\n * can bind as an exact personal owner or active organization owner.\n *\n * `GET /user/installations` is discovery input only. Every candidate is\n * cross-checked against the App's live installation inventory and an\n * organization candidate requires a live `state=active, role=admin`\n * membership proof. The later exact authorization still re-runs the complete\n * proof immediately before the durable bind.\n */\nexport async function discoverGitHubInstallationBindingCandidates(\n settings: Settings,\n input: { code: string },\n): Promise<GitHubInstallationBindingCandidate[]> {\n const userToken = await exchangeGitHubOAuthCodeForUserToken(settings, input.code);\n const actor = await getAuthenticatedGitHubUser(userToken);\n const visibleInstallations = await listUserAccessibleInstallations(userToken);\n const jwt = await createGitHubAppJwt(settings);\n const liveInstallations = new Map(\n (await listInstallations(jwt)).map((payload) => {\n const installation = installationSummaryFromPayload(payload);\n return [installation.installationId, installation] as const;\n }),\n );\n const candidates: GitHubInstallationBindingCandidate[] = [];\n\n for (const visible of visibleInstallations) {\n const installation = liveInstallations.get(visible.installationId);\n if (\n !installation ||\n installation.suspended ||\n installation.accountId !== visible.accountId ||\n installation.accountLogin !== visible.accountLogin ||\n installation.accountType !== visible.accountType\n ) {\n continue;\n }\n if (installation.accountType === \"User\" && actor.id === installation.accountId) {\n candidates.push({ installation, authorityKind: \"personal_owner\" });\n continue;\n }\n if (installation.accountType !== \"Organization\" || !installation.accountLogin) {\n continue;\n }\n try {\n await assertActiveOrganizationOwner(\n userToken,\n installation.accountId,\n installation.accountLogin,\n );\n candidates.push({ installation, authorityKind: \"organization_owner\" });\n } catch (error) {\n if (\n error instanceof GitHubInstallationAuthorityError &&\n error.reason === \"authority_unavailable\"\n ) {\n continue;\n }\n if (\n error instanceof GitHubInstallationAuthorityError &&\n error.reason === \"authority_denied\"\n ) {\n continue;\n }\n throw error;\n }\n }\n\n return candidates.sort((left, right) =>\n (left.installation.accountLogin ?? \"\").localeCompare(right.installation.accountLogin ?? \"\"),\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 return await listGitHubAppRepositoriesWithSigningSettings(settings, input);\n}\n\n/** List repositories for a separately registered App that needs only signing credentials. */\nexport async function listGitHubAppRepositoriesWithSigningSettings(\n settings: GitHubAppSigningSettings,\n input: {\n installationIds?: number[];\n } = {},\n): Promise<GitHubRepository[]> {\n const missing = githubAppTokenMissingSettings(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 type GitHubAppInstallationRepositoryLookupInput = {\n installationId: number;\n owner: string;\n name: string;\n};\n\nexport type GitHubAppInstallationRepositoryLookup = (\n input: GitHubAppInstallationRepositoryLookupInput,\n) => Promise<GitHubRepository | null>;\n\n/**\n * Resolve one `owner/name` repository through an exact App installation and\n * return GitHub's stable repository identity, or null when that installation\n * cannot see the repository. The server-side lookup token never leaves the\n * caller and grants nothing by itself: the workspace allowlist decides whether\n * the returned id may mint a sandbox-bound token.\n */\nexport async function getGitHubAppInstallationRepository(\n settings: Settings,\n input: GitHubAppInstallationRepositoryLookupInput,\n): Promise<GitHubRepository | null> {\n return await createGitHubAppInstallationRepositoryLookup(settings)(input);\n}\n\n/**\n * One lookup client that reuses a server-side installation token per\n * installation for its lifetime (one worker turn), so several bare repository\n * URIs from the same installation cost one mint plus one read each.\n */\nexport function createGitHubAppInstallationRepositoryLookup(\n settings: Settings,\n): GitHubAppInstallationRepositoryLookup {\n const missing = githubAppMissingSettings(settings);\n if (missing.length > 0) {\n throw new GitHubAppConfigurationError(missing);\n }\n const tokens = new Map<number, Promise<GitHubAppInstallationToken>>();\n const installationToken = (installationId: number): Promise<GitHubAppInstallationToken> => {\n let pending = tokens.get(installationId);\n if (!pending) {\n // Metadata-read only: the lookup needs the repository id, never contents.\n // Bounded well below the sandbox mint timeout so a slow GitHub cannot\n // hold turn start; the caller proceeds bare on expiry.\n pending = createGitHubAppJwt(settings).then((jwt) =>\n createInstallationToken(jwt, {\n installationId,\n permissions: { metadata: \"read\" },\n timeoutMs: githubRepositoryLookupTimeoutMs,\n }),\n );\n pending.catch(() => tokens.delete(installationId));\n tokens.set(installationId, pending);\n }\n return pending;\n };\n return async (input) => {\n const owner = input.owner.trim();\n const name = input.name.trim();\n if (\n !/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/u.test(owner) ||\n !/^[A-Za-z0-9._-]+$/u.test(name)\n ) {\n return null;\n }\n const token = await installationToken(input.installationId);\n const response = await fetch(\n `${githubApiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,\n {\n headers: githubHeaders(token.token),\n signal: AbortSignal.timeout(githubRepositoryLookupTimeoutMs),\n },\n );\n if (response.status === 404) {\n return null;\n }\n if (!response.ok) {\n throw new GitHubAppApiError(await githubErrorMessage(response), response.status);\n }\n const payload = await response.json();\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) {\n throw new GitHubAppApiError(\"GitHub returned an invalid repository payload\");\n }\n const record = payload as Record<string, unknown>;\n const account =\n record.owner && typeof record.owner === \"object\" && !Array.isArray(record.owner)\n ? (record.owner as Record<string, unknown>)\n : {};\n return repositoryFromPayload(record, input.installationId, account);\n };\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 return await createGitHubAppInstallationTokenWithSigningSettings(settings, input);\n}\n\n/** Mint for a separately registered App without requiring unrelated OAuth settings. */\nexport async function createGitHubAppInstallationTokenWithSigningSettings(\n settings: GitHubAppSigningSettings,\n input: {\n installationId: number;\n repositoryIds: number[];\n },\n): Promise<GitHubAppInstallationToken> {\n const missing = githubAppTokenMissingSettings(settings);\n if (missing.length > 0) {\n throw new GitHubAppConfigurationError(missing);\n }\n if (!Array.isArray(input.repositoryIds)) {\n throw new GitHubAppApiError(\n \"GitHub installation token mint requires an explicit, unique repository allowlist\",\n );\n }\n const repositoryIds = [...new Set(input.repositoryIds)];\n if (\n !Number.isSafeInteger(input.installationId) ||\n input.installationId <= 0 ||\n repositoryIds.length === 0 ||\n repositoryIds.length !== input.repositoryIds.length ||\n repositoryIds.some((id) => !Number.isSafeInteger(id) || id <= 0)\n ) {\n throw new GitHubAppApiError(\n \"GitHub installation token mint requires an explicit, unique repository allowlist\",\n );\n }\n const jwt = await createGitHubAppJwt(settings);\n return await createInstallationToken(jwt, {\n installationId: input.installationId,\n repositoryIds,\n });\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: GitHubAppSigningSettings): Promise<string> {\n const privateKey = normalizeGitHubAppPrivateKey(settings.githubAppPrivateKey ?? \"\");\n const appId = settings.githubAppId?.trim();\n if (!appId || !privateKey) {\n throw new GitHubAppConfigurationError(githubAppTokenMissingSettings(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 getAuthenticatedGitHubUser(token: string): Promise<{ id: number; login: string }> {\n const payload = await githubGet(\"/user\", token);\n const id = payload && typeof payload === \"object\" ? asInt(payload.id) : null;\n const login =\n payload && typeof payload === \"object\" && typeof payload.login === \"string\"\n ? payload.login\n : null;\n if (id === null || !login) {\n throw new GitHubAppApiError(\"GitHub returned an invalid authenticated user payload\");\n }\n return { id, login };\n}\n\nasync function getAuthenticatedOrganizationMembership(\n token: string,\n organizationLogin: string,\n): Promise<{ organizationId: number; role: string; state: string }> {\n let payload: any;\n try {\n payload = await githubGet(\n `/user/memberships/orgs/${encodeURIComponent(organizationLogin)}`,\n token,\n );\n } catch (error) {\n if (error instanceof GitHubAppApiError) {\n throw new GitHubInstallationAuthorityError(\n \"authority_unavailable\",\n \"GitHub could not prove current organization-owner membership\",\n error.status,\n );\n }\n throw error;\n }\n const organization =\n payload && typeof payload === \"object\" && payload.organization ? payload.organization : null;\n const organizationId =\n organization && typeof organization === \"object\" ? asInt(organization.id) : null;\n if (\n organizationId === null ||\n typeof payload?.role !== \"string\" ||\n typeof payload?.state !== \"string\"\n ) {\n throw new GitHubInstallationAuthorityError(\n \"authority_unavailable\",\n \"GitHub returned an invalid organization membership proof\",\n );\n }\n return { organizationId, role: payload.role, state: payload.state };\n}\n\nasync function assertActiveOrganizationOwner(\n token: string,\n organizationId: number,\n organizationLogin: string,\n): Promise<void> {\n const membership = await getAuthenticatedOrganizationMembership(token, organizationLogin);\n if (\n membership.organizationId !== organizationId ||\n membership.state !== \"active\" ||\n membership.role !== \"admin\"\n ) {\n throw new GitHubInstallationAuthorityError(\n \"authority_denied\",\n \"Only an active GitHub organization owner may bind this installation\",\n );\n }\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 /** Narrow the token below the installation's granted permissions. */\n permissions?: Record<string, \"read\" | \"write\">;\n timeoutMs?: number;\n },\n): Promise<GitHubAppInstallationToken> {\n const body: Record<string, unknown> = {};\n if (input.repositoryIds && input.repositoryIds.length > 0) {\n body.repository_ids = input.repositoryIds;\n }\n if (input.permissions && Object.keys(input.permissions).length > 0) {\n body.permissions = input.permissions;\n }\n const scoped = Object.keys(body).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(input.timeoutMs ?? githubTokenMintTimeoutMs),\n ...(scoped ? { body: JSON.stringify(body) } : {}),\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 const accountId = asInt(account.id);\n if (accountId === null) {\n throw new GitHubAppApiError(\"GitHub returned an installation without an account id\");\n }\n return {\n installationId,\n accountId,\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), response.status);\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":["installation"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/github",
3
- "version": "0.4.59",
3
+ "version": "0.5.3-canary.0",
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.16.4",
35
- "@opengeni/contracts": "^1.0.1",
34
+ "@opengeni/config": "^0.19.1-canary.0",
35
+ "@opengeni/contracts": "^2.3.0-canary.0",
36
36
  "jose": "^6.1.3"
37
37
  }
38
38
  }
package/src/index.ts CHANGED
@@ -7,16 +7,232 @@ import type {
7
7
  GitHubUserInstallationAccess,
8
8
  GitHubUserRepositoryAccess,
9
9
  } from "@opengeni/contracts";
10
- import { createHmac, createPrivateKey, randomBytes, timingSafeEqual } from "node:crypto";
10
+ import {
11
+ createCipheriv,
12
+ createDecipheriv,
13
+ createHash,
14
+ createHmac,
15
+ createPrivateKey,
16
+ randomBytes,
17
+ timingSafeEqual,
18
+ } from "node:crypto";
11
19
  import { SignJWT, importPKCS8 } from "jose";
12
20
 
13
21
  const githubApiBase = "https://api.github.com";
14
22
  const githubApiVersion = "2022-11-28";
15
23
  const githubTokenMintTimeoutMs = 60_000;
24
+ /** Bound for the server-side repository-id lookup at turn start (mint + read). */
25
+ export const githubRepositoryLookupTimeoutMs = 10_000;
16
26
  export const stateMaxAgeSeconds = 60 * 60;
17
27
  const pkcs8PrivateKeyHeader = `-----BEGIN ${"PRIVATE KEY"}-----`;
18
28
  const rsaPrivateKeyHeader = `-----BEGIN ${"RSA PRIVATE KEY"}-----`;
19
29
 
30
+ const PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX = "oggh1";
31
+ const PERSONAL_GITHUB_GIT_BROKER_KEY_CONTEXT = "opengeni:personal-github:git-broker:v1";
32
+ export const PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS = 5 * 60;
33
+
34
+ export type PersonalGitHubGitBrokerRepositoryClaim = {
35
+ repositoryId: string;
36
+ fullName: string;
37
+ canonicalUrl: string;
38
+ ref: string;
39
+ access: "read" | "write";
40
+ selectionGeneration: number;
41
+ routeId: string;
42
+ };
43
+
44
+ export type PersonalGitHubGitBrokerClaims = {
45
+ version: 1;
46
+ accountId: string;
47
+ workspaceId: string;
48
+ sessionId: string;
49
+ rootSessionId: string;
50
+ turnId: string;
51
+ attemptId: string;
52
+ executionGeneration: number;
53
+ originWorkspaceId: string;
54
+ connectionId: string;
55
+ connectionAuthorityGeneration: number;
56
+ ownerSubjectId: string;
57
+ credentialBindingId: string;
58
+ selectionGeneration: number;
59
+ nonce: string;
60
+ issuedAt: number;
61
+ expiresAt: number;
62
+ };
63
+
64
+ export function personalGitHubGitBrokerRouteId(
65
+ secret: string,
66
+ input: Omit<PersonalGitHubGitBrokerClaims, "nonce" | "issuedAt" | "expiresAt"> & {
67
+ repository: Omit<PersonalGitHubGitBrokerRepositoryClaim, "routeId">;
68
+ },
69
+ ): string {
70
+ const hmac = createHmac("sha256", personalGitHubGitBrokerKey(secret));
71
+ for (const value of [
72
+ String(input.version),
73
+ input.accountId,
74
+ input.workspaceId,
75
+ input.sessionId,
76
+ input.rootSessionId,
77
+ input.turnId,
78
+ input.attemptId,
79
+ String(input.executionGeneration),
80
+ input.originWorkspaceId,
81
+ input.connectionId,
82
+ String(input.connectionAuthorityGeneration),
83
+ input.ownerSubjectId,
84
+ input.credentialBindingId,
85
+ String(input.selectionGeneration),
86
+ input.repository.repositoryId,
87
+ input.repository.fullName,
88
+ input.repository.canonicalUrl,
89
+ input.repository.ref,
90
+ input.repository.access,
91
+ String(input.repository.selectionGeneration),
92
+ ]) {
93
+ const bytes = Buffer.from(value, "utf8");
94
+ hmac.update(Buffer.from(String(bytes.byteLength), "ascii"));
95
+ hmac.update(":");
96
+ hmac.update(bytes);
97
+ hmac.update(";");
98
+ }
99
+ return hmac.digest("base64url");
100
+ }
101
+
102
+ /**
103
+ * Seal exact Git broker authority into a confidential, authenticated bearer.
104
+ * The payload is encrypted rather than merely signed so tenant, session,
105
+ * connection, and repository identities are not readable from the sandbox's
106
+ * short-lived token file.
107
+ */
108
+ export function sealPersonalGitHubGitBrokerClaims(
109
+ secret: string,
110
+ claims: PersonalGitHubGitBrokerClaims,
111
+ ): string {
112
+ assertPersonalGitHubGitBrokerClaims(claims);
113
+ const iv = randomBytes(12);
114
+ const cipher = createCipheriv("aes-256-gcm", personalGitHubGitBrokerKey(secret), iv);
115
+ cipher.setAAD(Buffer.from(PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX, "ascii"));
116
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(claims), "utf8"), cipher.final()]);
117
+ return [
118
+ PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX,
119
+ iv.toString("base64url"),
120
+ ciphertext.toString("base64url"),
121
+ cipher.getAuthTag().toString("base64url"),
122
+ ].join(".");
123
+ }
124
+
125
+ export function openPersonalGitHubGitBrokerClaims(
126
+ secret: string,
127
+ token: string,
128
+ nowSeconds = Math.floor(Date.now() / 1_000),
129
+ ): PersonalGitHubGitBrokerClaims | null {
130
+ const [prefix, encodedIv, encodedCiphertext, encodedTag, extra] = token.split(".");
131
+ if (
132
+ prefix !== PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX ||
133
+ !encodedIv ||
134
+ !encodedCiphertext ||
135
+ !encodedTag ||
136
+ extra !== undefined
137
+ ) {
138
+ return null;
139
+ }
140
+ try {
141
+ const iv = Buffer.from(encodedIv, "base64url");
142
+ const ciphertext = Buffer.from(encodedCiphertext, "base64url");
143
+ const tag = Buffer.from(encodedTag, "base64url");
144
+ if (iv.byteLength !== 12 || tag.byteLength !== 16 || ciphertext.byteLength > 4_096) {
145
+ return null;
146
+ }
147
+ const decipher = createDecipheriv("aes-256-gcm", personalGitHubGitBrokerKey(secret), iv);
148
+ decipher.setAAD(Buffer.from(PERSONAL_GITHUB_GIT_BROKER_TOKEN_PREFIX, "ascii"));
149
+ decipher.setAuthTag(tag);
150
+ const payload = JSON.parse(
151
+ Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8"),
152
+ ) as unknown;
153
+ assertPersonalGitHubGitBrokerClaims(payload);
154
+ if (payload.issuedAt > nowSeconds + 60 || nowSeconds >= payload.expiresAt) return null;
155
+ if (payload.expiresAt - payload.issuedAt > PERSONAL_GITHUB_GIT_BROKER_TOKEN_TTL_SECONDS) {
156
+ return null;
157
+ }
158
+ return payload;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ function personalGitHubGitBrokerKey(secret: string): Buffer {
165
+ const normalized = secret.trim();
166
+ if (!normalized) throw new Error("personal GitHub Git broker signing secret is unavailable");
167
+ return createHash("sha256")
168
+ .update(PERSONAL_GITHUB_GIT_BROKER_KEY_CONTEXT, "utf8")
169
+ .update("\0", "utf8")
170
+ .update(normalized, "utf8")
171
+ .digest();
172
+ }
173
+
174
+ function assertPersonalGitHubGitBrokerClaims(
175
+ value: unknown,
176
+ ): asserts value is PersonalGitHubGitBrokerClaims {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
178
+ throw new Error("invalid personal GitHub Git broker claims");
179
+ }
180
+ const claims = value as Record<string, unknown>;
181
+ const expectedKeys = new Set([
182
+ "version",
183
+ "accountId",
184
+ "workspaceId",
185
+ "sessionId",
186
+ "rootSessionId",
187
+ "turnId",
188
+ "attemptId",
189
+ "executionGeneration",
190
+ "originWorkspaceId",
191
+ "connectionId",
192
+ "connectionAuthorityGeneration",
193
+ "ownerSubjectId",
194
+ "credentialBindingId",
195
+ "selectionGeneration",
196
+ "nonce",
197
+ "issuedAt",
198
+ "expiresAt",
199
+ ]);
200
+ const strings = [
201
+ "accountId",
202
+ "workspaceId",
203
+ "sessionId",
204
+ "rootSessionId",
205
+ "turnId",
206
+ "attemptId",
207
+ "originWorkspaceId",
208
+ "connectionId",
209
+ "ownerSubjectId",
210
+ "credentialBindingId",
211
+ "nonce",
212
+ ];
213
+ if (
214
+ claims.version !== 1 ||
215
+ strings.some(
216
+ (field) =>
217
+ typeof claims[field] !== "string" ||
218
+ claims[field].length === 0 ||
219
+ claims[field].length > (field === "ownerSubjectId" ? 512 : 128),
220
+ ) ||
221
+ !positiveIntegerClaim(claims.executionGeneration) ||
222
+ !positiveIntegerClaim(claims.connectionAuthorityGeneration) ||
223
+ !positiveIntegerClaim(claims.selectionGeneration) ||
224
+ !positiveIntegerClaim(claims.issuedAt) ||
225
+ !positiveIntegerClaim(claims.expiresAt) ||
226
+ Object.keys(claims).some((key) => !expectedKeys.has(key))
227
+ ) {
228
+ throw new Error("invalid personal GitHub Git broker claims");
229
+ }
230
+ }
231
+
232
+ function positiveIntegerClaim(value: unknown): value is number {
233
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
234
+ }
235
+
20
236
  export class GitHubAppConfigurationError extends Error {
21
237
  constructor(readonly missing: string[]) {
22
238
  super("GitHub App is not configured");
@@ -76,6 +292,16 @@ export function githubAppMissingSettings(settings: Settings): string[] {
76
292
  return Object.entries(required).flatMap(([name, value]) => (value && value.trim() ? [] : [name]));
77
293
  }
78
294
 
295
+ export type GitHubAppSigningSettings = Pick<Settings, "githubAppId" | "githubAppPrivateKey">;
296
+
297
+ function githubAppTokenMissingSettings(settings: GitHubAppSigningSettings): string[] {
298
+ const required: Record<string, string | undefined> = {
299
+ OPENGENI_GITHUB_APP_ID: settings.githubAppId,
300
+ OPENGENI_GITHUB_APP_PRIVATE_KEY: settings.githubAppPrivateKey,
301
+ };
302
+ return Object.entries(required).flatMap(([name, value]) => (value && value.trim() ? [] : [name]));
303
+ }
304
+
79
305
  export function buildGitHubAppManifest(input: {
80
306
  appName: string;
81
307
  baseUrl: string;
@@ -472,6 +698,20 @@ export async function listGitHubAppRepositories(
472
698
  if (missing.length > 0) {
473
699
  throw new GitHubAppConfigurationError(missing);
474
700
  }
701
+ return await listGitHubAppRepositoriesWithSigningSettings(settings, input);
702
+ }
703
+
704
+ /** List repositories for a separately registered App that needs only signing credentials. */
705
+ export async function listGitHubAppRepositoriesWithSigningSettings(
706
+ settings: GitHubAppSigningSettings,
707
+ input: {
708
+ installationIds?: number[];
709
+ } = {},
710
+ ): Promise<GitHubRepository[]> {
711
+ const missing = githubAppTokenMissingSettings(settings);
712
+ if (missing.length > 0) {
713
+ throw new GitHubAppConfigurationError(missing);
714
+ }
475
715
  const allowedInstallations = input.installationIds ? new Set(input.installationIds) : null;
476
716
  if (allowedInstallations && allowedInstallations.size === 0) {
477
717
  return [];
@@ -503,6 +743,97 @@ export async function listGitHubAppRepositories(
503
743
  return repositories;
504
744
  }
505
745
 
746
+ export type GitHubAppInstallationRepositoryLookupInput = {
747
+ installationId: number;
748
+ owner: string;
749
+ name: string;
750
+ };
751
+
752
+ export type GitHubAppInstallationRepositoryLookup = (
753
+ input: GitHubAppInstallationRepositoryLookupInput,
754
+ ) => Promise<GitHubRepository | null>;
755
+
756
+ /**
757
+ * Resolve one `owner/name` repository through an exact App installation and
758
+ * return GitHub's stable repository identity, or null when that installation
759
+ * cannot see the repository. The server-side lookup token never leaves the
760
+ * caller and grants nothing by itself: the workspace allowlist decides whether
761
+ * the returned id may mint a sandbox-bound token.
762
+ */
763
+ export async function getGitHubAppInstallationRepository(
764
+ settings: Settings,
765
+ input: GitHubAppInstallationRepositoryLookupInput,
766
+ ): Promise<GitHubRepository | null> {
767
+ return await createGitHubAppInstallationRepositoryLookup(settings)(input);
768
+ }
769
+
770
+ /**
771
+ * One lookup client that reuses a server-side installation token per
772
+ * installation for its lifetime (one worker turn), so several bare repository
773
+ * URIs from the same installation cost one mint plus one read each.
774
+ */
775
+ export function createGitHubAppInstallationRepositoryLookup(
776
+ settings: Settings,
777
+ ): GitHubAppInstallationRepositoryLookup {
778
+ const missing = githubAppMissingSettings(settings);
779
+ if (missing.length > 0) {
780
+ throw new GitHubAppConfigurationError(missing);
781
+ }
782
+ const tokens = new Map<number, Promise<GitHubAppInstallationToken>>();
783
+ const installationToken = (installationId: number): Promise<GitHubAppInstallationToken> => {
784
+ let pending = tokens.get(installationId);
785
+ if (!pending) {
786
+ // Metadata-read only: the lookup needs the repository id, never contents.
787
+ // Bounded well below the sandbox mint timeout so a slow GitHub cannot
788
+ // hold turn start; the caller proceeds bare on expiry.
789
+ pending = createGitHubAppJwt(settings).then((jwt) =>
790
+ createInstallationToken(jwt, {
791
+ installationId,
792
+ permissions: { metadata: "read" },
793
+ timeoutMs: githubRepositoryLookupTimeoutMs,
794
+ }),
795
+ );
796
+ pending.catch(() => tokens.delete(installationId));
797
+ tokens.set(installationId, pending);
798
+ }
799
+ return pending;
800
+ };
801
+ return async (input) => {
802
+ const owner = input.owner.trim();
803
+ const name = input.name.trim();
804
+ if (
805
+ !/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/u.test(owner) ||
806
+ !/^[A-Za-z0-9._-]+$/u.test(name)
807
+ ) {
808
+ return null;
809
+ }
810
+ const token = await installationToken(input.installationId);
811
+ const response = await fetch(
812
+ `${githubApiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
813
+ {
814
+ headers: githubHeaders(token.token),
815
+ signal: AbortSignal.timeout(githubRepositoryLookupTimeoutMs),
816
+ },
817
+ );
818
+ if (response.status === 404) {
819
+ return null;
820
+ }
821
+ if (!response.ok) {
822
+ throw new GitHubAppApiError(await githubErrorMessage(response), response.status);
823
+ }
824
+ const payload = await response.json();
825
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
826
+ throw new GitHubAppApiError("GitHub returned an invalid repository payload");
827
+ }
828
+ const record = payload as Record<string, unknown>;
829
+ const account =
830
+ record.owner && typeof record.owner === "object" && !Array.isArray(record.owner)
831
+ ? (record.owner as Record<string, unknown>)
832
+ : {};
833
+ return repositoryFromPayload(record, input.installationId, account);
834
+ };
835
+ }
836
+
506
837
  export async function createGitHubAppInstallationToken(
507
838
  settings: Settings,
508
839
  input: {
@@ -529,6 +860,21 @@ export async function createGitHubAppInstallationTokenWithExpiry(
529
860
  if (missing.length > 0) {
530
861
  throw new GitHubAppConfigurationError(missing);
531
862
  }
863
+ return await createGitHubAppInstallationTokenWithSigningSettings(settings, input);
864
+ }
865
+
866
+ /** Mint for a separately registered App without requiring unrelated OAuth settings. */
867
+ export async function createGitHubAppInstallationTokenWithSigningSettings(
868
+ settings: GitHubAppSigningSettings,
869
+ input: {
870
+ installationId: number;
871
+ repositoryIds: number[];
872
+ },
873
+ ): Promise<GitHubAppInstallationToken> {
874
+ const missing = githubAppTokenMissingSettings(settings);
875
+ if (missing.length > 0) {
876
+ throw new GitHubAppConfigurationError(missing);
877
+ }
532
878
  if (!Array.isArray(input.repositoryIds)) {
533
879
  throw new GitHubAppApiError(
534
880
  "GitHub installation token mint requires an explicit, unique repository allowlist",
@@ -566,11 +912,11 @@ export function githubAppBotIdentity(settings: Settings): { name: string; email:
566
912
  };
567
913
  }
568
914
 
569
- async function createGitHubAppJwt(settings: Settings): Promise<string> {
915
+ async function createGitHubAppJwt(settings: GitHubAppSigningSettings): Promise<string> {
570
916
  const privateKey = normalizeGitHubAppPrivateKey(settings.githubAppPrivateKey ?? "");
571
917
  const appId = settings.githubAppId?.trim();
572
918
  if (!appId || !privateKey) {
573
- throw new GitHubAppConfigurationError(githubAppMissingSettings(settings));
919
+ throw new GitHubAppConfigurationError(githubAppTokenMissingSettings(settings));
574
920
  }
575
921
  const key = await importPKCS8(privateKey, "RS256");
576
922
  const now = Math.floor(Date.now() / 1000);
@@ -775,9 +1121,19 @@ async function createInstallationToken(
775
1121
  input: {
776
1122
  installationId: number;
777
1123
  repositoryIds?: number[];
1124
+ /** Narrow the token below the installation's granted permissions. */
1125
+ permissions?: Record<string, "read" | "write">;
1126
+ timeoutMs?: number;
778
1127
  },
779
1128
  ): Promise<GitHubAppInstallationToken> {
780
- const scoped = input.repositoryIds && input.repositoryIds.length > 0;
1129
+ const body: Record<string, unknown> = {};
1130
+ if (input.repositoryIds && input.repositoryIds.length > 0) {
1131
+ body.repository_ids = input.repositoryIds;
1132
+ }
1133
+ if (input.permissions && Object.keys(input.permissions).length > 0) {
1134
+ body.permissions = input.permissions;
1135
+ }
1136
+ const scoped = Object.keys(body).length > 0;
781
1137
  const response = await fetch(
782
1138
  `${githubApiBase}/app/installations/${input.installationId}/access_tokens`,
783
1139
  {
@@ -786,8 +1142,8 @@ async function createInstallationToken(
786
1142
  ...githubHeaders(appJwt),
787
1143
  ...(scoped ? { "Content-Type": "application/json" } : {}),
788
1144
  },
789
- signal: AbortSignal.timeout(githubTokenMintTimeoutMs),
790
- ...(scoped ? { body: JSON.stringify({ repository_ids: input.repositoryIds }) } : {}),
1145
+ signal: AbortSignal.timeout(input.timeoutMs ?? githubTokenMintTimeoutMs),
1146
+ ...(scoped ? { body: JSON.stringify(body) } : {}),
791
1147
  },
792
1148
  );
793
1149
  if (!response.ok) {