@theholocron/github-client 1.31.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/github-client",
3
- "version": "1.31.0",
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.31.0"
36
+ "@theholocron/http-client": "^1.32.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@theholocron/cli": "5.0.0-alpha.33",
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.0",
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": "tsdown",
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,512 +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
- type CheckRunAnnotationLevel = "notice" | "warning" | "failure";
18
- interface CheckRunAnnotation {
19
- /** File path relative to the repo root. */
20
- path: string;
21
- /** 1-indexed. */
22
- start_line: number;
23
- end_line: number;
24
- /** Only valid when `start_line === end_line`. */
25
- start_column?: number;
26
- end_column?: number;
27
- annotation_level: CheckRunAnnotationLevel;
28
- /** Max 64KB. */
29
- message: string;
30
- /** Max 255 characters. */
31
- title?: string;
32
- /** Max 64KB. */
33
- raw_details?: string;
34
- }
35
- interface CheckRunOutput {
36
- title: string;
37
- summary: string;
38
- text?: string;
39
- /**
40
- * Inline PR annotations — up to 50 per request. Sending more requires a
41
- * follow-up `updateCheckRun()` PATCH per GitHub's own docs; this client
42
- * doesn't paginate them for you.
43
- */
44
- annotations?: CheckRunAnnotation[];
45
- }
46
- interface CreateCheckRunInput {
47
- name: string;
48
- /** The commit SHA the check run attaches to. */
49
- head_sha: string;
50
- /** Defaults to `"queued"` (GitHub's own default) when omitted. */
51
- status?: CheckRunStatus;
52
- /** Required when `status` is `"completed"`. */
53
- conclusion?: CheckRunConclusion;
54
- output?: CheckRunOutput;
55
- /** URL for the "Details" link on the check run. GitHub defaults to the check suite's own summary page when omitted. */
56
- details_url?: string;
57
- }
58
- interface GitHubCheckRun {
59
- id: number;
60
- name: string;
61
- head_sha: string;
62
- status: CheckRunStatus;
63
- conclusion: CheckRunConclusion | null;
64
- html_url: string;
65
- }
66
- interface UpdateCheckRunInput {
67
- status?: CheckRunStatus;
68
- /** Required when `status` is `"completed"`. */
69
- conclusion?: CheckRunConclusion;
70
- output?: CheckRunOutput;
71
- /** URL for the "Details" link on the check run. */
72
- details_url?: string;
73
- }
74
- //#endregion
75
- //#region src/environments/environments.d.ts
76
- interface GitHubEnvironment {
77
- name: string;
78
- wait_timer?: number;
79
- prevent_self_review?: boolean;
80
- protection_rules?: Array<{
81
- type: string;
82
- reviewers?: Array<{
83
- type: "User" | "Team";
84
- reviewer: {
85
- id: number;
86
- };
87
- }>;
88
- }>;
89
- }
90
- //#endregion
91
- //#region src/git/git.d.ts
92
- interface GitRef {
93
- object: {
94
- sha: string;
95
- type: string;
96
- url: string;
97
- };
98
- ref: string;
99
- url: string;
100
- }
101
- interface GitCommit {
102
- sha: string;
103
- tree: {
104
- sha: string;
105
- url: string;
106
- };
107
- }
108
- interface GitTreeItem {
109
- path: string;
110
- sha: string;
111
- type: string;
112
- mode?: string;
113
- size?: number;
114
- }
115
- interface GitTree {
116
- sha: string;
117
- tree: GitTreeItem[];
118
- truncated: boolean;
119
- }
120
- interface GitContents {
121
- content: string;
122
- encoding: string;
123
- sha: string;
124
- name: string;
125
- path: string;
126
- }
127
- interface GitBlob {
128
- sha: string;
129
- url: string;
130
- }
131
- interface GitPull {
132
- number: number;
133
- html_url: string;
134
- title: string;
135
- }
136
- interface CreatePullInput {
137
- title: string;
138
- head: string;
139
- base: string;
140
- body?: string;
141
- }
142
- //#endregion
143
- //#region src/issues/issues.d.ts
144
- interface GitHubIssue {
145
- id: number;
146
- number: number;
147
- title: string;
148
- body?: string | null;
149
- state: "open" | "closed";
150
- labels: Array<{
151
- name: string;
152
- }>;
153
- assignee: {
154
- login: string;
155
- name?: string | null;
156
- email?: string | null;
157
- } | null;
158
- updated_at: string;
159
- html_url: string;
160
- pull_request?: unknown;
161
- }
162
- interface GitHubMilestone {
163
- number: number;
164
- title: string;
165
- }
166
- interface IssueSearchParams {
167
- state?: "open" | "closed" | "all";
168
- sort?: string;
169
- direction?: string;
170
- per_page?: number;
171
- filter?: string;
172
- assignee?: string;
173
- }
174
- //#endregion
175
- //#region src/labels/labels.d.ts
176
- interface GitHubLabel {
177
- name: string;
178
- color: string;
179
- description: string | null;
180
- }
181
- //#endregion
182
- //#region src/pages/pages.d.ts
183
- type PagesBuildType = "workflow" | "legacy";
184
- type PagesBuildStatus = "built" | "building" | "errored" | "unknown";
185
- interface GitHubPages {
186
- status: PagesBuildStatus | null;
187
- build_type: PagesBuildType | null;
188
- source?: {
189
- branch: string;
190
- path: string;
191
- };
192
- https_enforced: boolean;
193
- custom_domain: string | null;
194
- }
195
- interface CreatePagesPayload {
196
- build_type: PagesBuildType;
197
- /** Required when build_type is "legacy". */
198
- source?: {
199
- branch: string;
200
- path: string;
201
- };
202
- }
203
- interface UpdatePagesPayload {
204
- build_type?: PagesBuildType;
205
- source?: {
206
- branch: string;
207
- path: string;
208
- };
209
- cname?: string | null;
210
- https_enforced?: boolean;
211
- }
212
- //#endregion
213
- //#region src/pulls/pulls.d.ts
214
- interface GitHubPullRequest {
215
- number: number;
216
- title: string;
217
- state: "open" | "closed";
218
- merged_at: string | null;
219
- html_url: string;
220
- head: {
221
- ref: string;
222
- };
223
- }
224
- /** One commit as returned by `GET /pulls/{pull_number}/commits` — only the fields any known consumer needs. */
225
- interface GitHubPullRequestCommit {
226
- sha: string;
227
- commit: {
228
- message: string;
229
- };
230
- }
231
- /** One file as returned by `GET /pulls/{pull_number}/files` — only the fields any known consumer needs. */
232
- interface GitHubPullRequestFile {
233
- filename: string;
234
- status: "added" | "removed" | "modified" | "renamed" | "copied" | "changed" | "unchanged";
235
- /** Set only when `status === "renamed"`. */
236
- previous_filename?: string;
237
- }
238
- //#endregion
239
- //#region src/repos/repos.d.ts
240
- interface GitHubRepo {
241
- default_branch: string;
242
- full_name: string;
243
- }
244
- interface GitHubContents {
245
- content: string;
246
- encoding: string;
247
- }
248
- //#endregion
249
- //#region src/rulesets/rulesets.d.ts
250
- interface GitHubRuleset {
251
- id: number;
252
- name: string;
253
- enforcement: string;
254
- }
255
- //#endregion
256
- //#region src/secrets/secrets.d.ts
257
- type SecretScope = {
258
- kind: "repo";
259
- } | {
260
- kind: "environment";
261
- name: string;
262
- } | {
263
- kind: "organization";
264
- org: string;
265
- };
266
- interface GitHubPublicKey {
267
- key_id: string;
268
- key: string;
269
- }
270
- //#endregion
271
- //#region src/security/security.d.ts
272
- interface CodeScanningSetupResult {
273
- run_id: number;
274
- run_url: string;
275
- }
276
- //#endregion
277
- //#region src/teams/teams.d.ts
278
- type TeamPermission = "pull" | "triage" | "push" | "maintain" | "admin";
279
- //#endregion
280
- //#region src/user/user.d.ts
281
- interface GitHubUser {
282
- login: string;
283
- name: string | null;
284
- email: string | null;
285
- }
286
- //#endregion
287
- //#region src/workflows/workflows.d.ts
288
- interface GitHubWorkflowRun {
289
- id: number;
290
- name: string | null;
291
- display_title: string;
292
- head_branch: string;
293
- head_sha: string;
294
- status: string;
295
- conclusion: string | null;
296
- html_url: string;
297
- created_at: string;
298
- updated_at: string;
299
- }
300
- interface WorkflowRunFilter {
301
- branch?: string;
302
- limit?: number;
303
- status?: string;
304
- }
305
- //#endregion
306
- //#region src/app-auth/app-auth.d.ts
307
- interface GitHubAppCredentials {
308
- /** The App's numeric id (not the client id). */
309
- appId: string;
310
- /** PEM-encoded RSA private key — PKCS#1 (GitHub's default download format) or PKCS#8, either works. */
311
- privateKey: string;
312
- }
313
- interface InstallationAccessToken {
314
- token: string;
315
- /** ISO 8601 — installation tokens are short-lived, about an hour. */
316
- expiresAt: string;
317
- }
318
- /**
319
- * Signs a JWT identifying this App (not a specific installation) — the
320
- * credential used only to request an installation access token, never
321
- * to call the REST API directly.
322
- */
323
- declare function createAppJWT(creds: GitHubAppCredentials, now?: number): Promise<string>;
324
- /**
325
- * Exchanges an App JWT for an installation access token — the credential
326
- * that actually authenticates REST calls, scoped to exactly one
327
- * installation (D10: never a hardcoded org — the caller supplies which
328
- * installation, from the webhook payload that triggered this).
329
- */
330
- declare function getInstallationAccessToken(creds: GitHubAppCredentials, installationId: number, opts?: Pick<GitHubClientOptions, "baseUrl" | "fetch">): Promise<InstallationAccessToken>;
331
- /** The whole flow in one call — an already-authenticated `GitHubClient` scoped to one installation. */
332
- declare function createInstallationClient(creds: GitHubAppCredentials, installationId: number, opts?: Pick<GitHubClientOptions, "baseUrl" | "fetch">): Promise<GitHubClient>;
333
- //#endregion
334
- //#region src/webhooks/webhooks.d.ts
335
- /**
336
- * GitHub's own webhook mechanics — signature verification and header/
337
- * payload shapes. Pure functions, no REST call, no auth token: these
338
- * operate on an *inbound* delivery, the mirror of everything else this
339
- * package does (outbound REST calls), so they're standalone exports
340
- * rather than part of `createGitHubClient()`'s returned object.
341
- *
342
- * A consumer (a GitHub App receiving webhooks) owns what to *do* with a
343
- * verified delivery — which event categories matter, how to normalize
344
- * them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
345
- * between Svix verification and Clerk-specific `AuthEvent` normalization,
346
- * just with the verification half living here instead of inline, since
347
- * "how GitHub signs and shapes a webhook" is vendor knowledge this
348
- * package already owns for every other GitHub API surface.
349
- */
350
- /**
351
- * Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
352
- * HMAC-SHA256 over the raw body, keyed by the webhook's configured
353
- * secret, `timingSafeEqual`-compared.
354
- * https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
355
- *
356
- * Returns `false` (never throws) for any failure to verify — a missing/
357
- * empty secret, a missing/malformed signature header, or a mismatch —
358
- * so the caller decides how to surface that (e.g. as its own error type).
359
- */
360
- declare function verifyGitHubWebhookSignature(input: {
361
- body: string | Buffer;
362
- signature: string | undefined;
363
- secret: string;
364
- }): boolean;
365
- interface GitHubWebhookHeaders {
366
- /** `X-GitHub-Event` — the event category, e.g. `"push"`, `"pull_request"`. */
367
- event: string | undefined;
368
- /** `X-GitHub-Delivery` — GitHub's per-delivery id, useful for idempotency/logging. */
369
- delivery: string | undefined;
370
- /** `X-Hub-Signature-256` — pass straight to `verifyGitHubWebhookSignature`. */
371
- signature: string | undefined;
372
- }
373
- /** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
374
- declare function parseGitHubWebhookHeaders(headers: Record<string, string | string[] | undefined>): GitHubWebhookHeaders;
375
- interface GitHubInstallationWebhookPayload {
376
- action: string;
377
- installation: {
378
- id: number;
379
- };
380
- }
381
- interface GitHubPushWebhookPayload {
382
- ref: string;
383
- installation?: {
384
- id: number;
385
- };
386
- repository: {
387
- full_name: string;
388
- default_branch: string;
389
- };
390
- }
391
- interface GitHubPullRequestWebhookPayload {
392
- action: string;
393
- installation?: {
394
- id: number;
395
- };
396
- repository: {
397
- full_name: string;
398
- };
399
- }
400
- //#endregion
401
- //#region src/index.d.ts
402
- declare function createGitHubClient(opts: GitHubClientOptions): {
403
- branches: {
404
- protectBranch: (repo: string, branch: string, payload: Record<string, unknown>) => Promise<void>;
405
- };
406
- checks: {
407
- createCheckRun: (repo: string, input: CreateCheckRunInput) => Promise<GitHubCheckRun>;
408
- updateCheckRun: (repo: string, checkRunId: number, input: UpdateCheckRunInput) => Promise<GitHubCheckRun>;
409
- };
410
- environments: {
411
- listEnvironments: (repo: string) => Promise<GitHubEnvironment[]>;
412
- upsertEnvironment: (repo: string, name: string, body: Record<string, unknown>) => Promise<void>;
413
- deleteEnvironment: (repo: string, name: string) => Promise<void>;
414
- };
415
- git: {
416
- getRef: (repo: string, branch: string) => Promise<GitRef>;
417
- getCommit: (repo: string, sha: string) => Promise<GitCommit>;
418
- getTree: (repo: string, sha: string, recursive?: boolean) => Promise<GitTree>;
419
- getContents: (repo: string, path: string, ref?: string) => Promise<GitContents>;
420
- createBlob: (repo: string, content: string, encoding?: string) => Promise<GitBlob>;
421
- createTree: (repo: string, tree: Array<{
422
- path: string;
423
- mode: string;
424
- type: string;
425
- sha: string;
426
- }>, baseTree?: string) => Promise<GitTree>;
427
- createCommit: (repo: string, message: string, tree: string, parents: string[]) => Promise<GitCommit>;
428
- createRef: (repo: string, ref: string, sha: string) => Promise<GitRef>;
429
- updateRef: (repo: string, ref: string, sha: string, force?: boolean) => Promise<GitRef>;
430
- createPull: (repo: string, input: CreatePullInput) => Promise<GitPull>;
431
- };
432
- issues: {
433
- listIssues: (repo: string, params?: IssueSearchParams) => Promise<GitHubIssue[]>;
434
- getIssue: (repo: string, number: number) => Promise<GitHubIssue>;
435
- createIssue: (repo: string, body: Record<string, unknown>) => Promise<GitHubIssue>;
436
- updateIssue: (repo: string, number: number, body: Record<string, unknown>) => Promise<GitHubIssue>;
437
- addLabels: (repo: string, number: number, labels: string[]) => Promise<void>;
438
- removeLabel: (repo: string, number: number, label: string) => Promise<void>;
439
- createComment: (repo: string, number: number, body: string) => Promise<void>;
440
- listMilestones: (repo: string, params?: {
441
- state?: "open" | "closed" | "all";
442
- }) => Promise<GitHubMilestone[]>;
443
- };
444
- labels: {
445
- listLabels: (repo: string) => Promise<GitHubLabel[]>;
446
- createLabel: (repo: string, body: {
447
- name: string;
448
- color: string;
449
- description: string;
450
- }) => Promise<GitHubLabel>;
451
- updateLabel: (repo: string, name: string, body: {
452
- color?: string;
453
- description?: string;
454
- }) => Promise<GitHubLabel>;
455
- deleteLabel: (repo: string, name: string) => Promise<void>;
456
- };
457
- pages: {
458
- getPages: (repo: string) => Promise<GitHubPages | null>;
459
- createPages: (repo: string, payload: CreatePagesPayload) => Promise<GitHubPages>;
460
- updatePages: (repo: string, payload: UpdatePagesPayload) => Promise<void>;
461
- };
462
- properties: {
463
- setProperties: (repo: string, values: Record<string, string | string[]>) => Promise<void>;
464
- };
465
- pulls: {
466
- getPullRequest: (repo: string, number: number) => Promise<GitHubPullRequest>;
467
- listCommits: (repo: string, number: number) => Promise<GitHubPullRequestCommit[]>;
468
- listFiles: (repo: string, number: number) => Promise<GitHubPullRequestFile[]>;
469
- };
470
- repos: {
471
- getRepo: (repo: string) => Promise<GitHubRepo>;
472
- updateRepo: (repo: string, settings: Record<string, unknown>) => Promise<void>;
473
- getContents: (repo: string, path: string) => Promise<GitHubContents>;
474
- };
475
- rulesets: {
476
- listRulesets: (repo: string) => Promise<GitHubRuleset[]>;
477
- createRuleset: (repo: string, payload: Record<string, unknown>) => Promise<GitHubRuleset>;
478
- updateRuleset: (repo: string, id: number, payload: Record<string, unknown>) => Promise<GitHubRuleset>;
479
- };
480
- secrets: {
481
- listSecrets: (repo: string, scope: SecretScope) => Promise<string[]>;
482
- getPublicKey: (repo: string, scope: SecretScope) => Promise<GitHubPublicKey>;
483
- putSecret: (repo: string, scope: SecretScope, name: string, body: Record<string, unknown>) => Promise<void>;
484
- deleteSecret: (repo: string, scope: SecretScope, name: string) => Promise<void>;
485
- };
486
- security: {
487
- enableVulnerabilityAlerts: (repo: string) => Promise<void>;
488
- enableAutomatedSecurityFixes: (repo: string) => Promise<void>;
489
- enableSecretScanning: (repo: string) => Promise<void>;
490
- enablePrivateVulnerabilityReporting: (repo: string) => Promise<void>;
491
- enableDependencyGraph: (repo: string) => Promise<void>;
492
- enableCodeScanning: (repo: string) => Promise<CodeScanningSetupResult>;
493
- disableDefaultCodeScanning: (repo: string) => Promise<void>;
494
- };
495
- teams: {
496
- addRepo: (org: string, teamSlug: string, owner: string, repo: string, permission: TeamPermission) => Promise<void>;
497
- };
498
- topics: {
499
- setTopics: (repo: string, names: string[]) => Promise<void>;
500
- };
501
- user: {
502
- getCurrentUser: () => Promise<GitHubUser>;
503
- };
504
- workflows: {
505
- listRuns: (repo: string, filter?: WorkflowRunFilter) => Promise<GitHubWorkflowRun[]>;
506
- getRun: (repo: string, id: string | number) => Promise<GitHubWorkflowRun>;
507
- createWorkflowDispatch: (repo: string, workflowFile: string, ref: string, inputs?: Record<string, string>) => Promise<void>;
508
- };
509
- };
510
- type GitHubClient = ReturnType<typeof createGitHubClient>;
511
- //#endregion
512
- export { type CheckRunAnnotation, type CheckRunAnnotationLevel, 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 };