@opengeni/github 0.4.59 → 0.5.2-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 +72 -0
- package/dist/index.js +231 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +362 -6
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 {
|
|
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(
|
|
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
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.5.2-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.
|
|
35
|
-
"@opengeni/contracts": "^
|
|
34
|
+
"@opengeni/config": "^0.19.0-canary.0",
|
|
35
|
+
"@opengeni/contracts": "^2.2.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 {
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
1145
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? githubTokenMintTimeoutMs),
|
|
1146
|
+
...(scoped ? { body: JSON.stringify(body) } : {}),
|
|
791
1147
|
},
|
|
792
1148
|
);
|
|
793
1149
|
if (!response.ok) {
|