@theholocron/github-client 1.30.0 → 1.32.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/package.json +5 -5
- package/dist/index.d.mts +0 -488
- package/dist/index.mjs +0 -631
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/github-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"description": "A TypeScript client for the GitHub REST API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@theholocron/observability": "^0.3.0",
|
|
36
|
-
"@theholocron/http-client": "^1.
|
|
36
|
+
"@theholocron/http-client": "^1.32.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
39
|
+
"@theholocron/cli": "5.0.0-alpha.79",
|
|
40
40
|
"@theholocron/eslint-config": "^8.4.3",
|
|
41
41
|
"@theholocron/tsconfig": "^8.2.0",
|
|
42
|
-
"@theholocron/tsdown-config": "^8.2
|
|
42
|
+
"@theholocron/tsdown-config": "^8.5.2",
|
|
43
43
|
"@theholocron/vitest-config": "^8.2.0",
|
|
44
44
|
"@types/node": "^26.4.1",
|
|
45
45
|
"@vitest/coverage-v8": "^5.0.0",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"releases": "https://github.com/theholocron/clients/releases",
|
|
62
62
|
"wiki": "https://github.com/theholocron/clients/wiki",
|
|
63
63
|
"scripts": {
|
|
64
|
-
"delivery.build": "
|
|
64
|
+
"delivery.build": "holocron run delivery.build -- && node ../../scripts/check-bundle-size.mjs",
|
|
65
65
|
"sourceQuality.staticAnalysis": "holocron run sourceQuality.staticAnalysis --",
|
|
66
66
|
"test:watch": "vitest",
|
|
67
67
|
"verification.typeSafety": "holocron run verification.typeSafety --",
|
package/dist/index.d.mts
DELETED
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
import "@theholocron/http-client";
|
|
2
|
-
import { ErrorSink, Logger } from "@theholocron/observability/core";
|
|
3
|
-
//#region src/utils.d.ts
|
|
4
|
-
interface GitHubClientOptions {
|
|
5
|
-
token: string;
|
|
6
|
-
baseUrl?: string;
|
|
7
|
-
fetch?: typeof fetch;
|
|
8
|
-
/** Structured logger for request/response diagnostics. Defaults to a no-op. */
|
|
9
|
-
logger?: Logger;
|
|
10
|
-
/** Reports transport failures and unexpected 5xx. Defaults to a no-op. */
|
|
11
|
-
errors?: ErrorSink;
|
|
12
|
-
}
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/checks/checks.d.ts
|
|
15
|
-
type CheckRunStatus = "queued" | "in_progress" | "completed";
|
|
16
|
-
type CheckRunConclusion = "success" | "failure" | "neutral" | "cancelled" | "timed_out" | "action_required" | "skipped";
|
|
17
|
-
interface CheckRunOutput {
|
|
18
|
-
title: string;
|
|
19
|
-
summary: string;
|
|
20
|
-
text?: string;
|
|
21
|
-
}
|
|
22
|
-
interface CreateCheckRunInput {
|
|
23
|
-
name: string;
|
|
24
|
-
/** The commit SHA the check run attaches to. */
|
|
25
|
-
head_sha: string;
|
|
26
|
-
/** Defaults to `"queued"` (GitHub's own default) when omitted. */
|
|
27
|
-
status?: CheckRunStatus;
|
|
28
|
-
/** Required when `status` is `"completed"`. */
|
|
29
|
-
conclusion?: CheckRunConclusion;
|
|
30
|
-
output?: CheckRunOutput;
|
|
31
|
-
/** URL for the "Details" link on the check run. GitHub defaults to the check suite's own summary page when omitted. */
|
|
32
|
-
details_url?: string;
|
|
33
|
-
}
|
|
34
|
-
interface GitHubCheckRun {
|
|
35
|
-
id: number;
|
|
36
|
-
name: string;
|
|
37
|
-
head_sha: string;
|
|
38
|
-
status: CheckRunStatus;
|
|
39
|
-
conclusion: CheckRunConclusion | null;
|
|
40
|
-
html_url: string;
|
|
41
|
-
}
|
|
42
|
-
interface UpdateCheckRunInput {
|
|
43
|
-
status?: CheckRunStatus;
|
|
44
|
-
/** Required when `status` is `"completed"`. */
|
|
45
|
-
conclusion?: CheckRunConclusion;
|
|
46
|
-
output?: CheckRunOutput;
|
|
47
|
-
/** URL for the "Details" link on the check run. */
|
|
48
|
-
details_url?: string;
|
|
49
|
-
}
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/environments/environments.d.ts
|
|
52
|
-
interface GitHubEnvironment {
|
|
53
|
-
name: string;
|
|
54
|
-
wait_timer?: number;
|
|
55
|
-
prevent_self_review?: boolean;
|
|
56
|
-
protection_rules?: Array<{
|
|
57
|
-
type: string;
|
|
58
|
-
reviewers?: Array<{
|
|
59
|
-
type: "User" | "Team";
|
|
60
|
-
reviewer: {
|
|
61
|
-
id: number;
|
|
62
|
-
};
|
|
63
|
-
}>;
|
|
64
|
-
}>;
|
|
65
|
-
}
|
|
66
|
-
//#endregion
|
|
67
|
-
//#region src/git/git.d.ts
|
|
68
|
-
interface GitRef {
|
|
69
|
-
object: {
|
|
70
|
-
sha: string;
|
|
71
|
-
type: string;
|
|
72
|
-
url: string;
|
|
73
|
-
};
|
|
74
|
-
ref: string;
|
|
75
|
-
url: string;
|
|
76
|
-
}
|
|
77
|
-
interface GitCommit {
|
|
78
|
-
sha: string;
|
|
79
|
-
tree: {
|
|
80
|
-
sha: string;
|
|
81
|
-
url: string;
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
interface GitTreeItem {
|
|
85
|
-
path: string;
|
|
86
|
-
sha: string;
|
|
87
|
-
type: string;
|
|
88
|
-
mode?: string;
|
|
89
|
-
size?: number;
|
|
90
|
-
}
|
|
91
|
-
interface GitTree {
|
|
92
|
-
sha: string;
|
|
93
|
-
tree: GitTreeItem[];
|
|
94
|
-
truncated: boolean;
|
|
95
|
-
}
|
|
96
|
-
interface GitContents {
|
|
97
|
-
content: string;
|
|
98
|
-
encoding: string;
|
|
99
|
-
sha: string;
|
|
100
|
-
name: string;
|
|
101
|
-
path: string;
|
|
102
|
-
}
|
|
103
|
-
interface GitBlob {
|
|
104
|
-
sha: string;
|
|
105
|
-
url: string;
|
|
106
|
-
}
|
|
107
|
-
interface GitPull {
|
|
108
|
-
number: number;
|
|
109
|
-
html_url: string;
|
|
110
|
-
title: string;
|
|
111
|
-
}
|
|
112
|
-
interface CreatePullInput {
|
|
113
|
-
title: string;
|
|
114
|
-
head: string;
|
|
115
|
-
base: string;
|
|
116
|
-
body?: string;
|
|
117
|
-
}
|
|
118
|
-
//#endregion
|
|
119
|
-
//#region src/issues/issues.d.ts
|
|
120
|
-
interface GitHubIssue {
|
|
121
|
-
id: number;
|
|
122
|
-
number: number;
|
|
123
|
-
title: string;
|
|
124
|
-
body?: string | null;
|
|
125
|
-
state: "open" | "closed";
|
|
126
|
-
labels: Array<{
|
|
127
|
-
name: string;
|
|
128
|
-
}>;
|
|
129
|
-
assignee: {
|
|
130
|
-
login: string;
|
|
131
|
-
name?: string | null;
|
|
132
|
-
email?: string | null;
|
|
133
|
-
} | null;
|
|
134
|
-
updated_at: string;
|
|
135
|
-
html_url: string;
|
|
136
|
-
pull_request?: unknown;
|
|
137
|
-
}
|
|
138
|
-
interface GitHubMilestone {
|
|
139
|
-
number: number;
|
|
140
|
-
title: string;
|
|
141
|
-
}
|
|
142
|
-
interface IssueSearchParams {
|
|
143
|
-
state?: "open" | "closed" | "all";
|
|
144
|
-
sort?: string;
|
|
145
|
-
direction?: string;
|
|
146
|
-
per_page?: number;
|
|
147
|
-
filter?: string;
|
|
148
|
-
assignee?: string;
|
|
149
|
-
}
|
|
150
|
-
//#endregion
|
|
151
|
-
//#region src/labels/labels.d.ts
|
|
152
|
-
interface GitHubLabel {
|
|
153
|
-
name: string;
|
|
154
|
-
color: string;
|
|
155
|
-
description: string | null;
|
|
156
|
-
}
|
|
157
|
-
//#endregion
|
|
158
|
-
//#region src/pages/pages.d.ts
|
|
159
|
-
type PagesBuildType = "workflow" | "legacy";
|
|
160
|
-
type PagesBuildStatus = "built" | "building" | "errored" | "unknown";
|
|
161
|
-
interface GitHubPages {
|
|
162
|
-
status: PagesBuildStatus | null;
|
|
163
|
-
build_type: PagesBuildType | null;
|
|
164
|
-
source?: {
|
|
165
|
-
branch: string;
|
|
166
|
-
path: string;
|
|
167
|
-
};
|
|
168
|
-
https_enforced: boolean;
|
|
169
|
-
custom_domain: string | null;
|
|
170
|
-
}
|
|
171
|
-
interface CreatePagesPayload {
|
|
172
|
-
build_type: PagesBuildType;
|
|
173
|
-
/** Required when build_type is "legacy". */
|
|
174
|
-
source?: {
|
|
175
|
-
branch: string;
|
|
176
|
-
path: string;
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
interface UpdatePagesPayload {
|
|
180
|
-
build_type?: PagesBuildType;
|
|
181
|
-
source?: {
|
|
182
|
-
branch: string;
|
|
183
|
-
path: string;
|
|
184
|
-
};
|
|
185
|
-
cname?: string | null;
|
|
186
|
-
https_enforced?: boolean;
|
|
187
|
-
}
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/pulls/pulls.d.ts
|
|
190
|
-
interface GitHubPullRequest {
|
|
191
|
-
number: number;
|
|
192
|
-
title: string;
|
|
193
|
-
state: "open" | "closed";
|
|
194
|
-
merged_at: string | null;
|
|
195
|
-
html_url: string;
|
|
196
|
-
head: {
|
|
197
|
-
ref: string;
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
/** One commit as returned by `GET /pulls/{pull_number}/commits` — only the fields any known consumer needs. */
|
|
201
|
-
interface GitHubPullRequestCommit {
|
|
202
|
-
sha: string;
|
|
203
|
-
commit: {
|
|
204
|
-
message: string;
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
/** One file as returned by `GET /pulls/{pull_number}/files` — only the fields any known consumer needs. */
|
|
208
|
-
interface GitHubPullRequestFile {
|
|
209
|
-
filename: string;
|
|
210
|
-
status: "added" | "removed" | "modified" | "renamed" | "copied" | "changed" | "unchanged";
|
|
211
|
-
/** Set only when `status === "renamed"`. */
|
|
212
|
-
previous_filename?: string;
|
|
213
|
-
}
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region src/repos/repos.d.ts
|
|
216
|
-
interface GitHubRepo {
|
|
217
|
-
default_branch: string;
|
|
218
|
-
full_name: string;
|
|
219
|
-
}
|
|
220
|
-
interface GitHubContents {
|
|
221
|
-
content: string;
|
|
222
|
-
encoding: string;
|
|
223
|
-
}
|
|
224
|
-
//#endregion
|
|
225
|
-
//#region src/rulesets/rulesets.d.ts
|
|
226
|
-
interface GitHubRuleset {
|
|
227
|
-
id: number;
|
|
228
|
-
name: string;
|
|
229
|
-
enforcement: string;
|
|
230
|
-
}
|
|
231
|
-
//#endregion
|
|
232
|
-
//#region src/secrets/secrets.d.ts
|
|
233
|
-
type SecretScope = {
|
|
234
|
-
kind: "repo";
|
|
235
|
-
} | {
|
|
236
|
-
kind: "environment";
|
|
237
|
-
name: string;
|
|
238
|
-
} | {
|
|
239
|
-
kind: "organization";
|
|
240
|
-
org: string;
|
|
241
|
-
};
|
|
242
|
-
interface GitHubPublicKey {
|
|
243
|
-
key_id: string;
|
|
244
|
-
key: string;
|
|
245
|
-
}
|
|
246
|
-
//#endregion
|
|
247
|
-
//#region src/security/security.d.ts
|
|
248
|
-
interface CodeScanningSetupResult {
|
|
249
|
-
run_id: number;
|
|
250
|
-
run_url: string;
|
|
251
|
-
}
|
|
252
|
-
//#endregion
|
|
253
|
-
//#region src/teams/teams.d.ts
|
|
254
|
-
type TeamPermission = "pull" | "triage" | "push" | "maintain" | "admin";
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region src/user/user.d.ts
|
|
257
|
-
interface GitHubUser {
|
|
258
|
-
login: string;
|
|
259
|
-
name: string | null;
|
|
260
|
-
email: string | null;
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region src/workflows/workflows.d.ts
|
|
264
|
-
interface GitHubWorkflowRun {
|
|
265
|
-
id: number;
|
|
266
|
-
name: string | null;
|
|
267
|
-
display_title: string;
|
|
268
|
-
head_branch: string;
|
|
269
|
-
head_sha: string;
|
|
270
|
-
status: string;
|
|
271
|
-
conclusion: string | null;
|
|
272
|
-
html_url: string;
|
|
273
|
-
created_at: string;
|
|
274
|
-
updated_at: string;
|
|
275
|
-
}
|
|
276
|
-
interface WorkflowRunFilter {
|
|
277
|
-
branch?: string;
|
|
278
|
-
limit?: number;
|
|
279
|
-
status?: string;
|
|
280
|
-
}
|
|
281
|
-
//#endregion
|
|
282
|
-
//#region src/app-auth/app-auth.d.ts
|
|
283
|
-
interface GitHubAppCredentials {
|
|
284
|
-
/** The App's numeric id (not the client id). */
|
|
285
|
-
appId: string;
|
|
286
|
-
/** PEM-encoded RSA private key — PKCS#1 (GitHub's default download format) or PKCS#8, either works. */
|
|
287
|
-
privateKey: string;
|
|
288
|
-
}
|
|
289
|
-
interface InstallationAccessToken {
|
|
290
|
-
token: string;
|
|
291
|
-
/** ISO 8601 — installation tokens are short-lived, about an hour. */
|
|
292
|
-
expiresAt: string;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Signs a JWT identifying this App (not a specific installation) — the
|
|
296
|
-
* credential used only to request an installation access token, never
|
|
297
|
-
* to call the REST API directly.
|
|
298
|
-
*/
|
|
299
|
-
declare function createAppJWT(creds: GitHubAppCredentials, now?: number): Promise<string>;
|
|
300
|
-
/**
|
|
301
|
-
* Exchanges an App JWT for an installation access token — the credential
|
|
302
|
-
* that actually authenticates REST calls, scoped to exactly one
|
|
303
|
-
* installation (D10: never a hardcoded org — the caller supplies which
|
|
304
|
-
* installation, from the webhook payload that triggered this).
|
|
305
|
-
*/
|
|
306
|
-
declare function getInstallationAccessToken(creds: GitHubAppCredentials, installationId: number, opts?: Pick<GitHubClientOptions, "baseUrl" | "fetch">): Promise<InstallationAccessToken>;
|
|
307
|
-
/** The whole flow in one call — an already-authenticated `GitHubClient` scoped to one installation. */
|
|
308
|
-
declare function createInstallationClient(creds: GitHubAppCredentials, installationId: number, opts?: Pick<GitHubClientOptions, "baseUrl" | "fetch">): Promise<GitHubClient>;
|
|
309
|
-
//#endregion
|
|
310
|
-
//#region src/webhooks/webhooks.d.ts
|
|
311
|
-
/**
|
|
312
|
-
* GitHub's own webhook mechanics — signature verification and header/
|
|
313
|
-
* payload shapes. Pure functions, no REST call, no auth token: these
|
|
314
|
-
* operate on an *inbound* delivery, the mirror of everything else this
|
|
315
|
-
* package does (outbound REST calls), so they're standalone exports
|
|
316
|
-
* rather than part of `createGitHubClient()`'s returned object.
|
|
317
|
-
*
|
|
318
|
-
* A consumer (a GitHub App receiving webhooks) owns what to *do* with a
|
|
319
|
-
* verified delivery — which event categories matter, how to normalize
|
|
320
|
-
* them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
|
|
321
|
-
* between Svix verification and Clerk-specific `AuthEvent` normalization,
|
|
322
|
-
* just with the verification half living here instead of inline, since
|
|
323
|
-
* "how GitHub signs and shapes a webhook" is vendor knowledge this
|
|
324
|
-
* package already owns for every other GitHub API surface.
|
|
325
|
-
*/
|
|
326
|
-
/**
|
|
327
|
-
* Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
|
|
328
|
-
* HMAC-SHA256 over the raw body, keyed by the webhook's configured
|
|
329
|
-
* secret, `timingSafeEqual`-compared.
|
|
330
|
-
* https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
|
|
331
|
-
*
|
|
332
|
-
* Returns `false` (never throws) for any failure to verify — a missing/
|
|
333
|
-
* empty secret, a missing/malformed signature header, or a mismatch —
|
|
334
|
-
* so the caller decides how to surface that (e.g. as its own error type).
|
|
335
|
-
*/
|
|
336
|
-
declare function verifyGitHubWebhookSignature(input: {
|
|
337
|
-
body: string | Buffer;
|
|
338
|
-
signature: string | undefined;
|
|
339
|
-
secret: string;
|
|
340
|
-
}): boolean;
|
|
341
|
-
interface GitHubWebhookHeaders {
|
|
342
|
-
/** `X-GitHub-Event` — the event category, e.g. `"push"`, `"pull_request"`. */
|
|
343
|
-
event: string | undefined;
|
|
344
|
-
/** `X-GitHub-Delivery` — GitHub's per-delivery id, useful for idempotency/logging. */
|
|
345
|
-
delivery: string | undefined;
|
|
346
|
-
/** `X-Hub-Signature-256` — pass straight to `verifyGitHubWebhookSignature`. */
|
|
347
|
-
signature: string | undefined;
|
|
348
|
-
}
|
|
349
|
-
/** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
|
|
350
|
-
declare function parseGitHubWebhookHeaders(headers: Record<string, string | string[] | undefined>): GitHubWebhookHeaders;
|
|
351
|
-
interface GitHubInstallationWebhookPayload {
|
|
352
|
-
action: string;
|
|
353
|
-
installation: {
|
|
354
|
-
id: number;
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
interface GitHubPushWebhookPayload {
|
|
358
|
-
ref: string;
|
|
359
|
-
installation?: {
|
|
360
|
-
id: number;
|
|
361
|
-
};
|
|
362
|
-
repository: {
|
|
363
|
-
full_name: string;
|
|
364
|
-
default_branch: string;
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
interface GitHubPullRequestWebhookPayload {
|
|
368
|
-
action: string;
|
|
369
|
-
installation?: {
|
|
370
|
-
id: number;
|
|
371
|
-
};
|
|
372
|
-
repository: {
|
|
373
|
-
full_name: string;
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
//#endregion
|
|
377
|
-
//#region src/index.d.ts
|
|
378
|
-
declare function createGitHubClient(opts: GitHubClientOptions): {
|
|
379
|
-
branches: {
|
|
380
|
-
protectBranch: (repo: string, branch: string, payload: Record<string, unknown>) => Promise<void>;
|
|
381
|
-
};
|
|
382
|
-
checks: {
|
|
383
|
-
createCheckRun: (repo: string, input: CreateCheckRunInput) => Promise<GitHubCheckRun>;
|
|
384
|
-
updateCheckRun: (repo: string, checkRunId: number, input: UpdateCheckRunInput) => Promise<GitHubCheckRun>;
|
|
385
|
-
};
|
|
386
|
-
environments: {
|
|
387
|
-
listEnvironments: (repo: string) => Promise<GitHubEnvironment[]>;
|
|
388
|
-
upsertEnvironment: (repo: string, name: string, body: Record<string, unknown>) => Promise<void>;
|
|
389
|
-
deleteEnvironment: (repo: string, name: string) => Promise<void>;
|
|
390
|
-
};
|
|
391
|
-
git: {
|
|
392
|
-
getRef: (repo: string, branch: string) => Promise<GitRef>;
|
|
393
|
-
getCommit: (repo: string, sha: string) => Promise<GitCommit>;
|
|
394
|
-
getTree: (repo: string, sha: string, recursive?: boolean) => Promise<GitTree>;
|
|
395
|
-
getContents: (repo: string, path: string, ref?: string) => Promise<GitContents>;
|
|
396
|
-
createBlob: (repo: string, content: string, encoding?: string) => Promise<GitBlob>;
|
|
397
|
-
createTree: (repo: string, tree: Array<{
|
|
398
|
-
path: string;
|
|
399
|
-
mode: string;
|
|
400
|
-
type: string;
|
|
401
|
-
sha: string;
|
|
402
|
-
}>, baseTree?: string) => Promise<GitTree>;
|
|
403
|
-
createCommit: (repo: string, message: string, tree: string, parents: string[]) => Promise<GitCommit>;
|
|
404
|
-
createRef: (repo: string, ref: string, sha: string) => Promise<GitRef>;
|
|
405
|
-
updateRef: (repo: string, ref: string, sha: string, force?: boolean) => Promise<GitRef>;
|
|
406
|
-
createPull: (repo: string, input: CreatePullInput) => Promise<GitPull>;
|
|
407
|
-
};
|
|
408
|
-
issues: {
|
|
409
|
-
listIssues: (repo: string, params?: IssueSearchParams) => Promise<GitHubIssue[]>;
|
|
410
|
-
getIssue: (repo: string, number: number) => Promise<GitHubIssue>;
|
|
411
|
-
createIssue: (repo: string, body: Record<string, unknown>) => Promise<GitHubIssue>;
|
|
412
|
-
updateIssue: (repo: string, number: number, body: Record<string, unknown>) => Promise<GitHubIssue>;
|
|
413
|
-
addLabels: (repo: string, number: number, labels: string[]) => Promise<void>;
|
|
414
|
-
removeLabel: (repo: string, number: number, label: string) => Promise<void>;
|
|
415
|
-
createComment: (repo: string, number: number, body: string) => Promise<void>;
|
|
416
|
-
listMilestones: (repo: string, params?: {
|
|
417
|
-
state?: "open" | "closed" | "all";
|
|
418
|
-
}) => Promise<GitHubMilestone[]>;
|
|
419
|
-
};
|
|
420
|
-
labels: {
|
|
421
|
-
listLabels: (repo: string) => Promise<GitHubLabel[]>;
|
|
422
|
-
createLabel: (repo: string, body: {
|
|
423
|
-
name: string;
|
|
424
|
-
color: string;
|
|
425
|
-
description: string;
|
|
426
|
-
}) => Promise<GitHubLabel>;
|
|
427
|
-
updateLabel: (repo: string, name: string, body: {
|
|
428
|
-
color?: string;
|
|
429
|
-
description?: string;
|
|
430
|
-
}) => Promise<GitHubLabel>;
|
|
431
|
-
deleteLabel: (repo: string, name: string) => Promise<void>;
|
|
432
|
-
};
|
|
433
|
-
pages: {
|
|
434
|
-
getPages: (repo: string) => Promise<GitHubPages | null>;
|
|
435
|
-
createPages: (repo: string, payload: CreatePagesPayload) => Promise<GitHubPages>;
|
|
436
|
-
updatePages: (repo: string, payload: UpdatePagesPayload) => Promise<void>;
|
|
437
|
-
};
|
|
438
|
-
properties: {
|
|
439
|
-
setProperties: (repo: string, values: Record<string, string | string[]>) => Promise<void>;
|
|
440
|
-
};
|
|
441
|
-
pulls: {
|
|
442
|
-
getPullRequest: (repo: string, number: number) => Promise<GitHubPullRequest>;
|
|
443
|
-
listCommits: (repo: string, number: number) => Promise<GitHubPullRequestCommit[]>;
|
|
444
|
-
listFiles: (repo: string, number: number) => Promise<GitHubPullRequestFile[]>;
|
|
445
|
-
};
|
|
446
|
-
repos: {
|
|
447
|
-
getRepo: (repo: string) => Promise<GitHubRepo>;
|
|
448
|
-
updateRepo: (repo: string, settings: Record<string, unknown>) => Promise<void>;
|
|
449
|
-
getContents: (repo: string, path: string) => Promise<GitHubContents>;
|
|
450
|
-
};
|
|
451
|
-
rulesets: {
|
|
452
|
-
listRulesets: (repo: string) => Promise<GitHubRuleset[]>;
|
|
453
|
-
createRuleset: (repo: string, payload: Record<string, unknown>) => Promise<GitHubRuleset>;
|
|
454
|
-
updateRuleset: (repo: string, id: number, payload: Record<string, unknown>) => Promise<GitHubRuleset>;
|
|
455
|
-
};
|
|
456
|
-
secrets: {
|
|
457
|
-
listSecrets: (repo: string, scope: SecretScope) => Promise<string[]>;
|
|
458
|
-
getPublicKey: (repo: string, scope: SecretScope) => Promise<GitHubPublicKey>;
|
|
459
|
-
putSecret: (repo: string, scope: SecretScope, name: string, body: Record<string, unknown>) => Promise<void>;
|
|
460
|
-
deleteSecret: (repo: string, scope: SecretScope, name: string) => Promise<void>;
|
|
461
|
-
};
|
|
462
|
-
security: {
|
|
463
|
-
enableVulnerabilityAlerts: (repo: string) => Promise<void>;
|
|
464
|
-
enableAutomatedSecurityFixes: (repo: string) => Promise<void>;
|
|
465
|
-
enableSecretScanning: (repo: string) => Promise<void>;
|
|
466
|
-
enablePrivateVulnerabilityReporting: (repo: string) => Promise<void>;
|
|
467
|
-
enableDependencyGraph: (repo: string) => Promise<void>;
|
|
468
|
-
enableCodeScanning: (repo: string) => Promise<CodeScanningSetupResult>;
|
|
469
|
-
disableDefaultCodeScanning: (repo: string) => Promise<void>;
|
|
470
|
-
};
|
|
471
|
-
teams: {
|
|
472
|
-
addRepo: (org: string, teamSlug: string, owner: string, repo: string, permission: TeamPermission) => Promise<void>;
|
|
473
|
-
};
|
|
474
|
-
topics: {
|
|
475
|
-
setTopics: (repo: string, names: string[]) => Promise<void>;
|
|
476
|
-
};
|
|
477
|
-
user: {
|
|
478
|
-
getCurrentUser: () => Promise<GitHubUser>;
|
|
479
|
-
};
|
|
480
|
-
workflows: {
|
|
481
|
-
listRuns: (repo: string, filter?: WorkflowRunFilter) => Promise<GitHubWorkflowRun[]>;
|
|
482
|
-
getRun: (repo: string, id: string | number) => Promise<GitHubWorkflowRun>;
|
|
483
|
-
createWorkflowDispatch: (repo: string, workflowFile: string, ref: string, inputs?: Record<string, string>) => Promise<void>;
|
|
484
|
-
};
|
|
485
|
-
};
|
|
486
|
-
type GitHubClient = ReturnType<typeof createGitHubClient>;
|
|
487
|
-
//#endregion
|
|
488
|
-
export { type CheckRunConclusion, type CheckRunOutput, type CheckRunStatus, type CodeScanningSetupResult, type CreateCheckRunInput, type CreatePagesPayload, type CreatePullInput, type GitBlob, type GitCommit, type GitContents, type GitHubAppCredentials, type GitHubCheckRun, GitHubClient, type GitHubClientOptions, type GitHubContents, type GitHubEnvironment, type GitHubInstallationWebhookPayload, type GitHubIssue, type GitHubLabel, type GitHubMilestone, type GitHubPages, type GitHubPublicKey, type GitHubPullRequest, type GitHubPullRequestCommit, type GitHubPullRequestFile, type GitHubPullRequestWebhookPayload, type GitHubPushWebhookPayload, type GitHubRepo, type GitHubRuleset, type GitHubUser, type GitHubWebhookHeaders, type GitHubWorkflowRun, type GitPull, type GitRef, type GitTree, type GitTreeItem, type InstallationAccessToken, type IssueSearchParams, type PagesBuildStatus, type PagesBuildType, type SecretScope, type TeamPermission, type UpdateCheckRunInput, type UpdatePagesPayload, type WorkflowRunFilter, createAppJWT, createGitHubClient, createInstallationClient, getInstallationAccessToken, parseGitHubWebhookHeaders, verifyGitHubWebhookSignature };
|
package/dist/index.mjs
DELETED
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
import { ProviderApiError, createRestClient } from "@theholocron/http-client";
|
|
2
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
-
//#region src/utils.ts
|
|
4
|
-
function createGitHubRestClient(opts) {
|
|
5
|
-
return createRestClient({
|
|
6
|
-
baseUrl: opts.baseUrl ?? "https://api.github.com",
|
|
7
|
-
token: opts.token,
|
|
8
|
-
extraHeaders: {
|
|
9
|
-
accept: "application/vnd.github+json",
|
|
10
|
-
"x-github-api-version": "2022-11-28"
|
|
11
|
-
},
|
|
12
|
-
vendor: "GitHub",
|
|
13
|
-
fetch: opts.fetch,
|
|
14
|
-
logger: opts.logger,
|
|
15
|
-
errors: opts.errors
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
/** `/repos/owner/name` prefix from a `"owner/name"` repo string. */
|
|
19
|
-
function repoBase(repo) {
|
|
20
|
-
const [owner, name] = repo.split("/", 2);
|
|
21
|
-
return `/repos/${owner}/${name}`;
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/branches/branches.ts
|
|
25
|
-
function branches(rest) {
|
|
26
|
-
return { protectBranch: (repo, branch, payload) => rest.request(`${repoBase(repo)}/branches/${encodeURIComponent(branch)}/protection`, {
|
|
27
|
-
method: "PUT",
|
|
28
|
-
body: payload
|
|
29
|
-
}) };
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/checks/checks.ts
|
|
33
|
-
function checks(rest) {
|
|
34
|
-
return {
|
|
35
|
-
createCheckRun: (repo, input) => rest.request(`${repoBase(repo)}/check-runs`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
body: input
|
|
38
|
-
}),
|
|
39
|
-
/** Patches an existing check run — used to move a `"queued"` run posted before a dispatched task ran to `"completed"` once it finishes. */
|
|
40
|
-
updateCheckRun: (repo, checkRunId, input) => rest.request(`${repoBase(repo)}/check-runs/${checkRunId}`, {
|
|
41
|
-
method: "PATCH",
|
|
42
|
-
body: input
|
|
43
|
-
})
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
|
-
//#region src/environments/environments.ts
|
|
48
|
-
function environments(rest) {
|
|
49
|
-
return {
|
|
50
|
-
listEnvironments: (repo) => rest.request(`${repoBase(repo)}/environments`).then((r) => r.environments),
|
|
51
|
-
upsertEnvironment: (repo, name, body) => rest.request(`${repoBase(repo)}/environments/${encodeURIComponent(name)}`, {
|
|
52
|
-
method: "PUT",
|
|
53
|
-
body
|
|
54
|
-
}),
|
|
55
|
-
deleteEnvironment: (repo, name) => rest.request(`${repoBase(repo)}/environments/${encodeURIComponent(name)}`, {
|
|
56
|
-
method: "DELETE",
|
|
57
|
-
expectNoContent: true
|
|
58
|
-
})
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region src/git/git.ts
|
|
63
|
-
function git(rest) {
|
|
64
|
-
return {
|
|
65
|
-
getRef: (repo, branch) => rest.request(`${repoBase(repo)}/git/ref/heads/${branch}`),
|
|
66
|
-
getCommit: (repo, sha) => rest.request(`${repoBase(repo)}/git/commits/${sha}`),
|
|
67
|
-
getTree: (repo, sha, recursive = false) => rest.request(`${repoBase(repo)}/git/trees/${sha}${recursive ? "?recursive=1" : ""}`),
|
|
68
|
-
/**
|
|
69
|
-
* `ref` defaults to the repo's default branch (GitHub's own default)
|
|
70
|
-
* when omitted — the security-relevant choice every existing caller
|
|
71
|
-
* relies on (D4/D6, `validateConfig()`'s own docstring). Pass it
|
|
72
|
-
* explicitly to read a specific commit/branch's content instead — a
|
|
73
|
-
* same-repo PR branch is no longer a new trust boundary for this org
|
|
74
|
-
* (D6-amended, `tech-sentinel-ci-runner.spec.md`), just never a fork's.
|
|
75
|
-
*/
|
|
76
|
-
getContents: (repo, path, ref) => rest.request(`${repoBase(repo)}/contents/${path}`, ref ? { query: { ref } } : void 0),
|
|
77
|
-
createBlob: (repo, content, encoding = "utf-8") => rest.request(`${repoBase(repo)}/git/blobs`, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
body: {
|
|
80
|
-
content,
|
|
81
|
-
encoding
|
|
82
|
-
}
|
|
83
|
-
}),
|
|
84
|
-
createTree: (repo, tree, baseTree) => rest.request(`${repoBase(repo)}/git/trees`, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
body: {
|
|
87
|
-
base_tree: baseTree,
|
|
88
|
-
tree
|
|
89
|
-
}
|
|
90
|
-
}),
|
|
91
|
-
createCommit: (repo, message, tree, parents) => rest.request(`${repoBase(repo)}/git/commits`, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
body: {
|
|
94
|
-
message,
|
|
95
|
-
tree,
|
|
96
|
-
parents
|
|
97
|
-
}
|
|
98
|
-
}),
|
|
99
|
-
createRef: (repo, ref, sha) => rest.request(`${repoBase(repo)}/git/refs`, {
|
|
100
|
-
method: "POST",
|
|
101
|
-
body: {
|
|
102
|
-
ref,
|
|
103
|
-
sha
|
|
104
|
-
}
|
|
105
|
-
}),
|
|
106
|
-
updateRef: (repo, ref, sha, force = false) => rest.request(`${repoBase(repo)}/git/refs/${ref}`, {
|
|
107
|
-
method: "PATCH",
|
|
108
|
-
body: {
|
|
109
|
-
sha,
|
|
110
|
-
force
|
|
111
|
-
}
|
|
112
|
-
}),
|
|
113
|
-
createPull: (repo, input) => rest.request(`${repoBase(repo)}/pulls`, {
|
|
114
|
-
method: "POST",
|
|
115
|
-
body: input
|
|
116
|
-
})
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
//#endregion
|
|
120
|
-
//#region src/issues/issues.ts
|
|
121
|
-
function issues(rest) {
|
|
122
|
-
return {
|
|
123
|
-
listIssues: (repo, params = {}) => {
|
|
124
|
-
const qs = new URLSearchParams();
|
|
125
|
-
if (params.state) qs.set("state", params.state);
|
|
126
|
-
if (params.sort) qs.set("sort", params.sort);
|
|
127
|
-
if (params.direction) qs.set("direction", params.direction);
|
|
128
|
-
if (params.per_page) qs.set("per_page", String(params.per_page));
|
|
129
|
-
if (params.filter) qs.set("filter", params.filter);
|
|
130
|
-
if (params.assignee) qs.set("assignee", params.assignee);
|
|
131
|
-
const q = qs.toString();
|
|
132
|
-
const path = q ? `${repoBase(repo)}/issues?${q}` : `${repoBase(repo)}/issues`;
|
|
133
|
-
return rest.request(path);
|
|
134
|
-
},
|
|
135
|
-
getIssue: (repo, number) => rest.request(`${repoBase(repo)}/issues/${number}`),
|
|
136
|
-
createIssue: (repo, body) => rest.request(`${repoBase(repo)}/issues`, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
body
|
|
139
|
-
}),
|
|
140
|
-
updateIssue: (repo, number, body) => rest.request(`${repoBase(repo)}/issues/${number}`, {
|
|
141
|
-
method: "PATCH",
|
|
142
|
-
body
|
|
143
|
-
}),
|
|
144
|
-
addLabels: (repo, number, labels) => rest.request(`${repoBase(repo)}/issues/${number}/labels`, {
|
|
145
|
-
method: "POST",
|
|
146
|
-
body: { labels }
|
|
147
|
-
}),
|
|
148
|
-
removeLabel: (repo, number, label) => rest.request(`${repoBase(repo)}/issues/${number}/labels/${encodeURIComponent(label)}`, { method: "DELETE" }),
|
|
149
|
-
createComment: (repo, number, body) => rest.request(`${repoBase(repo)}/issues/${number}/comments`, {
|
|
150
|
-
method: "POST",
|
|
151
|
-
body: { body }
|
|
152
|
-
}),
|
|
153
|
-
listMilestones: (repo, params = {}) => {
|
|
154
|
-
const qs = new URLSearchParams({ per_page: "100" });
|
|
155
|
-
if (params.state) qs.set("state", params.state);
|
|
156
|
-
return rest.request(`${repoBase(repo)}/milestones?${qs.toString()}`);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
//#endregion
|
|
161
|
-
//#region src/labels/labels.ts
|
|
162
|
-
function labels(rest) {
|
|
163
|
-
return {
|
|
164
|
-
listLabels: (repo) => rest.request(`${repoBase(repo)}/labels?per_page=100`),
|
|
165
|
-
createLabel: (repo, body) => rest.request(`${repoBase(repo)}/labels`, {
|
|
166
|
-
method: "POST",
|
|
167
|
-
body
|
|
168
|
-
}),
|
|
169
|
-
updateLabel: (repo, name, body) => rest.request(`${repoBase(repo)}/labels/${encodeURIComponent(name)}`, {
|
|
170
|
-
method: "PATCH",
|
|
171
|
-
body
|
|
172
|
-
}),
|
|
173
|
-
deleteLabel: (repo, name) => rest.request(`${repoBase(repo)}/labels/${encodeURIComponent(name)}`, { method: "DELETE" })
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
//#endregion
|
|
177
|
-
//#region src/pages/pages.ts
|
|
178
|
-
function pages(rest) {
|
|
179
|
-
return {
|
|
180
|
-
/** Returns null when Pages is not yet enabled (404). */
|
|
181
|
-
getPages: async (repo) => {
|
|
182
|
-
try {
|
|
183
|
-
return await rest.request(`${repoBase(repo)}/pages`);
|
|
184
|
-
} catch (err) {
|
|
185
|
-
if (err instanceof ProviderApiError && err.status === 404) return null;
|
|
186
|
-
throw err;
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
/**
|
|
190
|
-
* Enables Pages for the first time.
|
|
191
|
-
* Throws on all errors except 409 Conflict (already enabled — callers
|
|
192
|
-
* should catch that and fall through to updatePages).
|
|
193
|
-
*/
|
|
194
|
-
createPages: (repo, payload) => rest.request(`${repoBase(repo)}/pages`, {
|
|
195
|
-
method: "POST",
|
|
196
|
-
body: payload
|
|
197
|
-
}),
|
|
198
|
-
/**
|
|
199
|
-
* Updates Pages settings on an existing site (build type, cname, https).
|
|
200
|
-
* GitHub returns 204 No Content.
|
|
201
|
-
*/
|
|
202
|
-
updatePages: (repo, payload) => rest.request(`${repoBase(repo)}/pages`, {
|
|
203
|
-
method: "PUT",
|
|
204
|
-
body: payload
|
|
205
|
-
})
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
//#endregion
|
|
209
|
-
//#region src/properties/properties.ts
|
|
210
|
-
function properties(rest) {
|
|
211
|
-
return {
|
|
212
|
-
/**
|
|
213
|
-
* `value` is a plain string for `string`/`single_select`/`true_false`
|
|
214
|
-
* custom properties, or a string array for `multi_select` ones —
|
|
215
|
-
* GitHub's own PATCH body accepts either per property. A single-string
|
|
216
|
-
* comma-joined value has no length ceiling GitHub documents, but real
|
|
217
|
-
* usage hits one in practice (a `string`-type property with enough
|
|
218
|
-
* joined values 422s as "too long"); `multi_select` has none.
|
|
219
|
-
*/
|
|
220
|
-
setProperties: (repo, values) => {
|
|
221
|
-
const propertyList = Object.entries(values).map(([property_name, value]) => ({
|
|
222
|
-
property_name,
|
|
223
|
-
value
|
|
224
|
-
}));
|
|
225
|
-
return rest.request(`${repoBase(repo)}/properties/values`, {
|
|
226
|
-
method: "PATCH",
|
|
227
|
-
body: { properties: propertyList }
|
|
228
|
-
});
|
|
229
|
-
} };
|
|
230
|
-
}
|
|
231
|
-
//#endregion
|
|
232
|
-
//#region src/pulls/pulls.ts
|
|
233
|
-
function pulls(rest) {
|
|
234
|
-
return {
|
|
235
|
-
getPullRequest: (repo, number) => rest.request(`${repoBase(repo)}/pulls/${number}`),
|
|
236
|
-
/**
|
|
237
|
-
* A PR's own commits, in commit order. `per_page=100` — GitHub's max —
|
|
238
|
-
* covers every real PR in this org; a PR genuinely exceeding 100
|
|
239
|
-
* commits (GitHub caps this endpoint at 250 regardless of pagination)
|
|
240
|
-
* is an edge case no consumer here needs to paginate for today.
|
|
241
|
-
*/
|
|
242
|
-
listCommits: (repo, number) => rest.request(`${repoBase(repo)}/pulls/${number}/commits?per_page=100`),
|
|
243
|
-
/**
|
|
244
|
-
* A PR's own changed files. `per_page=100` — GitHub's max — covers
|
|
245
|
-
* every real PR in this org; a PR genuinely exceeding 100 changed
|
|
246
|
-
* files (GitHub caps this endpoint at 3000 regardless of pagination)
|
|
247
|
-
* is an edge case no consumer here needs to paginate for today.
|
|
248
|
-
*/
|
|
249
|
-
listFiles: (repo, number) => rest.request(`${repoBase(repo)}/pulls/${number}/files?per_page=100`)
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
//#endregion
|
|
253
|
-
//#region src/repos/repos.ts
|
|
254
|
-
function repos(rest) {
|
|
255
|
-
return {
|
|
256
|
-
getRepo: (repo) => rest.request(repoBase(repo)),
|
|
257
|
-
updateRepo: (repo, settings) => rest.request(repoBase(repo), {
|
|
258
|
-
method: "PATCH",
|
|
259
|
-
body: settings
|
|
260
|
-
}),
|
|
261
|
-
getContents: (repo, path) => rest.request(`${repoBase(repo)}/contents/${path}`)
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
//#endregion
|
|
265
|
-
//#region src/rulesets/rulesets.ts
|
|
266
|
-
function rulesets(rest) {
|
|
267
|
-
return {
|
|
268
|
-
listRulesets: (repo) => rest.request(`${repoBase(repo)}/rulesets`),
|
|
269
|
-
createRuleset: (repo, payload) => rest.request(`${repoBase(repo)}/rulesets`, {
|
|
270
|
-
method: "POST",
|
|
271
|
-
body: payload
|
|
272
|
-
}),
|
|
273
|
-
updateRuleset: (repo, id, payload) => rest.request(`${repoBase(repo)}/rulesets/${id}`, {
|
|
274
|
-
method: "PUT",
|
|
275
|
-
body: payload
|
|
276
|
-
})
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
//#endregion
|
|
280
|
-
//#region src/secrets/secrets.ts
|
|
281
|
-
function scopeBase(repo, scope) {
|
|
282
|
-
switch (scope.kind) {
|
|
283
|
-
case "repo": return `${repoBase(repo)}/actions`;
|
|
284
|
-
case "environment": return `${repoBase(repo)}/environments/${scope.name}`;
|
|
285
|
-
case "organization": return `/orgs/${scope.org}/actions`;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
function secrets(rest) {
|
|
289
|
-
return {
|
|
290
|
-
listSecrets: (repo, scope) => rest.request(`${scopeBase(repo, scope)}/secrets`).then((r) => r.secrets.map((s) => s.name)),
|
|
291
|
-
getPublicKey: (repo, scope) => rest.request(`${scopeBase(repo, scope)}/secrets/public-key`),
|
|
292
|
-
putSecret: (repo, scope, name, body) => rest.request(`${scopeBase(repo, scope)}/secrets/${name}`, {
|
|
293
|
-
method: "PUT",
|
|
294
|
-
body,
|
|
295
|
-
expectNoContent: true
|
|
296
|
-
}),
|
|
297
|
-
deleteSecret: (repo, scope, name) => rest.request(`${scopeBase(repo, scope)}/secrets/${name}`, {
|
|
298
|
-
method: "DELETE",
|
|
299
|
-
expectNoContent: true
|
|
300
|
-
})
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
//#endregion
|
|
304
|
-
//#region src/security/security.ts
|
|
305
|
-
function security(rest) {
|
|
306
|
-
return {
|
|
307
|
-
enableVulnerabilityAlerts: (repo) => rest.request(`${repoBase(repo)}/vulnerability-alerts`, {
|
|
308
|
-
method: "PUT",
|
|
309
|
-
expectNoContent: true
|
|
310
|
-
}),
|
|
311
|
-
enableAutomatedSecurityFixes: (repo) => rest.request(`${repoBase(repo)}/automated-security-fixes`, {
|
|
312
|
-
method: "PUT",
|
|
313
|
-
expectNoContent: true
|
|
314
|
-
}),
|
|
315
|
-
enableSecretScanning: (repo) => rest.request(repoBase(repo), {
|
|
316
|
-
method: "PATCH",
|
|
317
|
-
body: { security_and_analysis: {
|
|
318
|
-
secret_scanning: { status: "enabled" },
|
|
319
|
-
secret_scanning_push_protection: { status: "enabled" },
|
|
320
|
-
secret_scanning_validity_checks: { status: "enabled" },
|
|
321
|
-
secret_scanning_non_provider_patterns: { status: "enabled" }
|
|
322
|
-
} }
|
|
323
|
-
}),
|
|
324
|
-
enablePrivateVulnerabilityReporting: (repo) => rest.request(`${repoBase(repo)}/private-vulnerability-reporting`, {
|
|
325
|
-
method: "PUT",
|
|
326
|
-
expectNoContent: true
|
|
327
|
-
}),
|
|
328
|
-
enableDependencyGraph: (repo) => rest.request(repoBase(repo), {
|
|
329
|
-
method: "PATCH",
|
|
330
|
-
body: { security_and_analysis: {
|
|
331
|
-
dependency_graph: { status: "enabled" },
|
|
332
|
-
dependency_graph_autosubmit_action: { status: "enabled" }
|
|
333
|
-
} }
|
|
334
|
-
}),
|
|
335
|
-
enableCodeScanning: (repo) => rest.request(`${repoBase(repo)}/code-scanning/default-setup`, {
|
|
336
|
-
method: "PATCH",
|
|
337
|
-
body: {
|
|
338
|
-
state: "configured",
|
|
339
|
-
query_suite: "extended",
|
|
340
|
-
threat_model: "remote_and_local"
|
|
341
|
-
}
|
|
342
|
-
}),
|
|
343
|
-
disableDefaultCodeScanning: (repo) => rest.request(`${repoBase(repo)}/code-scanning/default-setup`, {
|
|
344
|
-
method: "PATCH",
|
|
345
|
-
body: { state: "not-configured" }
|
|
346
|
-
})
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
//#endregion
|
|
350
|
-
//#region src/teams/teams.ts
|
|
351
|
-
function teams(rest) {
|
|
352
|
-
return { addRepo: (org, teamSlug, owner, repo, permission) => rest.request(`/orgs/${org}/teams/${teamSlug}/repos/${owner}/${repo}`, {
|
|
353
|
-
method: "PUT",
|
|
354
|
-
body: { permission }
|
|
355
|
-
}) };
|
|
356
|
-
}
|
|
357
|
-
//#endregion
|
|
358
|
-
//#region src/topics/topics.ts
|
|
359
|
-
function topics(rest) {
|
|
360
|
-
return { setTopics: (repo, names) => rest.request(`${repoBase(repo)}/topics`, {
|
|
361
|
-
method: "PUT",
|
|
362
|
-
body: { names }
|
|
363
|
-
}) };
|
|
364
|
-
}
|
|
365
|
-
//#endregion
|
|
366
|
-
//#region src/user/user.ts
|
|
367
|
-
function user(rest) {
|
|
368
|
-
return { getCurrentUser: () => rest.request("/user") };
|
|
369
|
-
}
|
|
370
|
-
//#endregion
|
|
371
|
-
//#region src/workflows/workflows.ts
|
|
372
|
-
function workflows(rest) {
|
|
373
|
-
return {
|
|
374
|
-
listRuns: (repo, filter) => {
|
|
375
|
-
const params = new URLSearchParams();
|
|
376
|
-
if (filter?.branch) params.set("branch", filter.branch);
|
|
377
|
-
if (filter?.limit) params.set("per_page", String(filter.limit));
|
|
378
|
-
if (filter?.status) params.set("status", filter.status);
|
|
379
|
-
const qs = params.toString();
|
|
380
|
-
const path = qs ? `${repoBase(repo)}/actions/runs?${qs}` : `${repoBase(repo)}/actions/runs`;
|
|
381
|
-
return rest.request(path).then((r) => r.workflow_runs);
|
|
382
|
-
},
|
|
383
|
-
getRun: (repo, id) => rest.request(`${repoBase(repo)}/actions/runs/${id}`),
|
|
384
|
-
/**
|
|
385
|
-
* Triggers a `workflow_dispatch` run. `workflowFile` is the workflow's
|
|
386
|
-
* filename (e.g. `"platform.dispatchedCheck.yml"`) — GitHub accepts the
|
|
387
|
-
* filename directly, no numeric workflow ID lookup needed. Fire-and-forget:
|
|
388
|
-
* the API returns `204` with no run ID, so callers needing to correlate
|
|
389
|
-
* the resulting run back to this call must pass their own correlation
|
|
390
|
-
* value through `inputs` (visible in the Actions UI/logs — never a secret).
|
|
391
|
-
*/
|
|
392
|
-
createWorkflowDispatch: (repo, workflowFile, ref, inputs) => rest.request(`${repoBase(repo)}/actions/workflows/${workflowFile}/dispatches`, {
|
|
393
|
-
method: "POST",
|
|
394
|
-
body: {
|
|
395
|
-
ref,
|
|
396
|
-
inputs
|
|
397
|
-
},
|
|
398
|
-
expectNoContent: true
|
|
399
|
-
})
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
//#endregion
|
|
403
|
-
//#region src/app-auth/pem.ts
|
|
404
|
-
/**
|
|
405
|
-
* PEM/DER handling for GitHub App private keys — Web Crypto only (no
|
|
406
|
-
* `node:crypto`), so this runs unchanged on Cloudflare Workers (no
|
|
407
|
-
* `nodejs_compat` needed) and in Node.
|
|
408
|
-
*
|
|
409
|
-
* GitHub hands out App private keys in PKCS#1 ("-----BEGIN RSA PRIVATE
|
|
410
|
-
* KEY-----") by default; `SubtleCrypto.importKey("pkcs8", …)` only
|
|
411
|
-
* accepts PKCS#8 ("-----BEGIN PRIVATE KEY-----"). `pkcs1ToPkcs8` wraps a
|
|
412
|
-
* PKCS#1 `RSAPrivateKey` DER in the fixed `PrivateKeyInfo` DER structure
|
|
413
|
-
* PKCS#8 requires — a fixed `AlgorithmIdentifier` prefix for
|
|
414
|
-
* `rsaEncryption` (OID 1.2.840.113549.1.1.1) around the unchanged
|
|
415
|
-
* PKCS#1 bytes. Verified against a real generated key pair: imported,
|
|
416
|
-
* signed, and checked against `node:crypto`'s `verify()` on the
|
|
417
|
-
* original key's public half.
|
|
418
|
-
*/
|
|
419
|
-
function pemToDer(pem) {
|
|
420
|
-
const base64 = pem.replace(/-----BEGIN [^-]+-----/, "").replace(/-----END [^-]+-----/, "").replace(/\s/g, "");
|
|
421
|
-
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
|
422
|
-
}
|
|
423
|
-
function concat(...arrays) {
|
|
424
|
-
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
425
|
-
const out = new Uint8Array(total);
|
|
426
|
-
let offset = 0;
|
|
427
|
-
for (const a of arrays) {
|
|
428
|
-
out.set(a, offset);
|
|
429
|
-
offset += a.length;
|
|
430
|
-
}
|
|
431
|
-
return out;
|
|
432
|
-
}
|
|
433
|
-
function encodeDerLength(len) {
|
|
434
|
-
/* istanbul ignore next -- see comment above */
|
|
435
|
-
if (len < 128) return new Uint8Array([len]);
|
|
436
|
-
const bytes = [];
|
|
437
|
-
let n = len;
|
|
438
|
-
while (n > 0) {
|
|
439
|
-
bytes.unshift(n & 255);
|
|
440
|
-
n >>= 8;
|
|
441
|
-
}
|
|
442
|
-
return new Uint8Array([128 | bytes.length, ...bytes]);
|
|
443
|
-
}
|
|
444
|
-
function derEncode(tag, content) {
|
|
445
|
-
return concat(new Uint8Array([tag]), encodeDerLength(content.length), content);
|
|
446
|
-
}
|
|
447
|
-
/** SEQUENCE { OID rsaEncryption, NULL } — the fixed PKCS#8 AlgorithmIdentifier for RSA. */
|
|
448
|
-
const RSA_ALGORITHM_IDENTIFIER = new Uint8Array([
|
|
449
|
-
48,
|
|
450
|
-
13,
|
|
451
|
-
6,
|
|
452
|
-
9,
|
|
453
|
-
42,
|
|
454
|
-
134,
|
|
455
|
-
72,
|
|
456
|
-
134,
|
|
457
|
-
247,
|
|
458
|
-
13,
|
|
459
|
-
1,
|
|
460
|
-
1,
|
|
461
|
-
1,
|
|
462
|
-
5,
|
|
463
|
-
0
|
|
464
|
-
]);
|
|
465
|
-
function pkcs1ToPkcs8(pkcs1) {
|
|
466
|
-
const version = new Uint8Array([
|
|
467
|
-
2,
|
|
468
|
-
1,
|
|
469
|
-
0
|
|
470
|
-
]);
|
|
471
|
-
const octetString = derEncode(4, pkcs1);
|
|
472
|
-
return derEncode(48, concat(version, RSA_ALGORITHM_IDENTIFIER, octetString));
|
|
473
|
-
}
|
|
474
|
-
/** Imports a PEM-encoded RSA private key — PKCS#1 or PKCS#8, either works — for RS256 signing. */
|
|
475
|
-
async function importRsaPrivateKey(pem) {
|
|
476
|
-
const der = pemToDer(pem);
|
|
477
|
-
const pkcs8Der = (pem.includes("BEGIN RSA PRIVATE KEY") ? pkcs1ToPkcs8(der) : der).slice();
|
|
478
|
-
return crypto.subtle.importKey("pkcs8", pkcs8Der, {
|
|
479
|
-
name: "RSASSA-PKCS1-v1_5",
|
|
480
|
-
hash: "SHA-256"
|
|
481
|
-
}, false, ["sign"]);
|
|
482
|
-
}
|
|
483
|
-
/** Base64url (no padding) — the encoding both JWT segments and the signature use. */
|
|
484
|
-
function base64UrlEncode(bytes) {
|
|
485
|
-
let binary = "";
|
|
486
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
487
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
488
|
-
}
|
|
489
|
-
//#endregion
|
|
490
|
-
//#region src/app-auth/app-auth.ts
|
|
491
|
-
/**
|
|
492
|
-
* GitHub App authentication — a signed App JWT, exchanged for a
|
|
493
|
-
* short-lived installation access token, exchanged for a ready-to-use
|
|
494
|
-
* `GitHubClient`. Web Crypto only (see `pem.ts`), so this runs unchanged
|
|
495
|
-
* on Cloudflare Workers and in Node — no vendor-specific JWT library, no
|
|
496
|
-
* `nodejs_compat` flag.
|
|
497
|
-
*
|
|
498
|
-
* https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app
|
|
499
|
-
* https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
|
|
500
|
-
*/
|
|
501
|
-
const CLOCK_DRIFT_BUFFER_SECONDS = 60;
|
|
502
|
-
/** GitHub rejects an App JWT with more than 10 minutes between iat and exp. */
|
|
503
|
-
const JWT_LIFETIME_SECONDS = 600;
|
|
504
|
-
/**
|
|
505
|
-
* Signs a JWT identifying this App (not a specific installation) — the
|
|
506
|
-
* credential used only to request an installation access token, never
|
|
507
|
-
* to call the REST API directly.
|
|
508
|
-
*/
|
|
509
|
-
async function createAppJWT(creds, now = Date.now()) {
|
|
510
|
-
const key = await importRsaPrivateKey(creds.privateKey);
|
|
511
|
-
const iat = Math.floor(now / 1e3) - CLOCK_DRIFT_BUFFER_SECONDS;
|
|
512
|
-
const exp = iat + JWT_LIFETIME_SECONDS;
|
|
513
|
-
const signingInput = `${base64UrlEncode(new TextEncoder().encode(JSON.stringify({
|
|
514
|
-
alg: "RS256",
|
|
515
|
-
typ: "JWT"
|
|
516
|
-
})))}.${base64UrlEncode(new TextEncoder().encode(JSON.stringify({
|
|
517
|
-
iat,
|
|
518
|
-
exp,
|
|
519
|
-
iss: creds.appId
|
|
520
|
-
})))}`;
|
|
521
|
-
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(signingInput));
|
|
522
|
-
return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`;
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Exchanges an App JWT for an installation access token — the credential
|
|
526
|
-
* that actually authenticates REST calls, scoped to exactly one
|
|
527
|
-
* installation (D10: never a hardcoded org — the caller supplies which
|
|
528
|
-
* installation, from the webhook payload that triggered this).
|
|
529
|
-
*/
|
|
530
|
-
async function getInstallationAccessToken(creds, installationId, opts = {}) {
|
|
531
|
-
const jwt = await createAppJWT(creds);
|
|
532
|
-
const baseUrl = opts.baseUrl ?? "https://api.github.com";
|
|
533
|
-
const res = await (opts.fetch ?? globalThis.fetch)(`${baseUrl}/app/installations/${installationId}/access_tokens`, {
|
|
534
|
-
method: "POST",
|
|
535
|
-
headers: {
|
|
536
|
-
authorization: `Bearer ${jwt}`,
|
|
537
|
-
accept: "application/vnd.github+json",
|
|
538
|
-
"x-github-api-version": "2022-11-28"
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
if (!res.ok) {
|
|
542
|
-
const body = await res.text().catch(() => "");
|
|
543
|
-
throw new ProviderApiError(`GitHub POST /app/installations/${installationId}/access_tokens → ${res.status}`, res.status, body);
|
|
544
|
-
}
|
|
545
|
-
const json = await res.json();
|
|
546
|
-
return {
|
|
547
|
-
token: json.token,
|
|
548
|
-
expiresAt: json.expires_at
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
/** The whole flow in one call — an already-authenticated `GitHubClient` scoped to one installation. */
|
|
552
|
-
async function createInstallationClient(creds, installationId, opts = {}) {
|
|
553
|
-
const { token } = await getInstallationAccessToken(creds, installationId, opts);
|
|
554
|
-
return createGitHubClient({
|
|
555
|
-
token,
|
|
556
|
-
baseUrl: opts.baseUrl,
|
|
557
|
-
fetch: opts.fetch
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
//#endregion
|
|
561
|
-
//#region src/webhooks/webhooks.ts
|
|
562
|
-
/**
|
|
563
|
-
* GitHub's own webhook mechanics — signature verification and header/
|
|
564
|
-
* payload shapes. Pure functions, no REST call, no auth token: these
|
|
565
|
-
* operate on an *inbound* delivery, the mirror of everything else this
|
|
566
|
-
* package does (outbound REST calls), so they're standalone exports
|
|
567
|
-
* rather than part of `createGitHubClient()`'s returned object.
|
|
568
|
-
*
|
|
569
|
-
* A consumer (a GitHub App receiving webhooks) owns what to *do* with a
|
|
570
|
-
* verified delivery — which event categories matter, how to normalize
|
|
571
|
-
* them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
|
|
572
|
-
* between Svix verification and Clerk-specific `AuthEvent` normalization,
|
|
573
|
-
* just with the verification half living here instead of inline, since
|
|
574
|
-
* "how GitHub signs and shapes a webhook" is vendor knowledge this
|
|
575
|
-
* package already owns for every other GitHub API surface.
|
|
576
|
-
*/
|
|
577
|
-
/**
|
|
578
|
-
* Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
|
|
579
|
-
* HMAC-SHA256 over the raw body, keyed by the webhook's configured
|
|
580
|
-
* secret, `timingSafeEqual`-compared.
|
|
581
|
-
* https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
|
|
582
|
-
*
|
|
583
|
-
* Returns `false` (never throws) for any failure to verify — a missing/
|
|
584
|
-
* empty secret, a missing/malformed signature header, or a mismatch —
|
|
585
|
-
* so the caller decides how to surface that (e.g. as its own error type).
|
|
586
|
-
*/
|
|
587
|
-
function verifyGitHubWebhookSignature(input) {
|
|
588
|
-
if (!input.secret || !input.signature || !input.signature.startsWith("sha256=")) return false;
|
|
589
|
-
const bodyStr = typeof input.body === "string" ? input.body : input.body.toString("utf8");
|
|
590
|
-
const provided = Buffer.from(input.signature.slice(7), "hex");
|
|
591
|
-
const computed = createHmac("sha256", input.secret).update(bodyStr).digest();
|
|
592
|
-
return provided.length === computed.length && timingSafeEqual(provided, computed);
|
|
593
|
-
}
|
|
594
|
-
/** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
|
|
595
|
-
function parseGitHubWebhookHeaders(headers) {
|
|
596
|
-
const find = (name) => {
|
|
597
|
-
const target = name.toLowerCase();
|
|
598
|
-
for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === target) return Array.isArray(v) ? v[0] : v;
|
|
599
|
-
};
|
|
600
|
-
return {
|
|
601
|
-
event: find("x-github-event"),
|
|
602
|
-
delivery: find("x-github-delivery"),
|
|
603
|
-
signature: find("x-hub-signature-256")
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
//#endregion
|
|
607
|
-
//#region src/index.ts
|
|
608
|
-
function createGitHubClient(opts) {
|
|
609
|
-
const rest = createGitHubRestClient(opts);
|
|
610
|
-
return {
|
|
611
|
-
branches: branches(rest),
|
|
612
|
-
checks: checks(rest),
|
|
613
|
-
environments: environments(rest),
|
|
614
|
-
git: git(rest),
|
|
615
|
-
issues: issues(rest),
|
|
616
|
-
labels: labels(rest),
|
|
617
|
-
pages: pages(rest),
|
|
618
|
-
properties: properties(rest),
|
|
619
|
-
pulls: pulls(rest),
|
|
620
|
-
repos: repos(rest),
|
|
621
|
-
rulesets: rulesets(rest),
|
|
622
|
-
secrets: secrets(rest),
|
|
623
|
-
security: security(rest),
|
|
624
|
-
teams: teams(rest),
|
|
625
|
-
topics: topics(rest),
|
|
626
|
-
user: user(rest),
|
|
627
|
-
workflows: workflows(rest)
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
//#endregion
|
|
631
|
-
export { createAppJWT, createGitHubClient, createInstallationClient, getInstallationAccessToken, parseGitHubWebhookHeaders, verifyGitHubWebhookSignature };
|