@theholocron/github-client 1.17.0 → 1.19.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/README.md CHANGED
@@ -51,3 +51,42 @@ const blob = await client.git.createBlob("owner/name", "file contents");
51
51
  | `topics` | `setTopics` |
52
52
  | `properties` | `setProperties` |
53
53
  | `git` | `getRef`, `getCommit`, `getTree`, `getContents`, `createBlob`, `createTree`, `createCommit`, `createRef`, `updateRef`, `createPull` |
54
+ | `checks` | `createCheckRun` |
55
+
56
+ ## Webhooks
57
+
58
+ GitHub's own inbound-webhook mechanics — signature verification and
59
+ header/payload shapes — as standalone functions, not part of
60
+ `createGitHubClient()`: these verify a _delivery this package's consumer
61
+ received_, not an outbound REST call, so they need a webhook secret
62
+ instead of an API token.
63
+
64
+ ```ts
65
+ import {
66
+ parseGitHubWebhookHeaders,
67
+ verifyGitHubWebhookSignature,
68
+ type GitHubPushWebhookPayload,
69
+ } from "@theholocron/github-client";
70
+
71
+ const { event, delivery, signature } = parseGitHubWebhookHeaders(req.headers);
72
+ const ok = verifyGitHubWebhookSignature({ body: rawBody, signature, secret: webhookSecret });
73
+ ```
74
+
75
+ `verifyGitHubWebhookSignature({ body, signature, secret })` — `X-Hub-
76
+ Signature-256` verification (HMAC-SHA256 over the raw body,
77
+ `timingSafeEqual`-compared). Returns `false` for any failure to verify
78
+ (empty secret, missing/malformed signature, mismatch) — never throws;
79
+ the caller decides how to surface that.
80
+
81
+ `parseGitHubWebhookHeaders(headers)` — extracts `event`, `delivery`, and
82
+ `signature` from GitHub's three webhook headers, case-insensitively.
83
+
84
+ `GitHubInstallationWebhookPayload`, `GitHubPushWebhookPayload`,
85
+ `GitHubPullRequestWebhookPayload` — the delivery body shapes, scoped to
86
+ the fields a consumer reads today (not a full re-typing of every field
87
+ GitHub sends).
88
+
89
+ A consumer owns what to _do_ with a verified delivery — which event
90
+ categories matter, how to normalize them into its own domain shape.
91
+ `@theholocron/sentinel`'s `parseWebhookEvent()` is the reference
92
+ consumer.
package/dist/index.d.mts CHANGED
@@ -11,6 +11,33 @@ interface GitHubClientOptions {
11
11
  errors?: ErrorSink;
12
12
  }
13
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
+ }
32
+ interface GitHubCheckRun {
33
+ id: number;
34
+ name: string;
35
+ head_sha: string;
36
+ status: CheckRunStatus;
37
+ conclusion: CheckRunConclusion | null;
38
+ html_url: string;
39
+ }
40
+ //#endregion
14
41
  //#region src/environments/environments.d.ts
15
42
  interface GitHubEnvironment {
16
43
  name: string;
@@ -228,11 +255,81 @@ interface WorkflowRunFilter {
228
255
  status?: string;
229
256
  }
230
257
  //#endregion
258
+ //#region src/webhooks/webhooks.d.ts
259
+ /**
260
+ * GitHub's own webhook mechanics — signature verification and header/
261
+ * payload shapes. Pure functions, no REST call, no auth token: these
262
+ * operate on an *inbound* delivery, the mirror of everything else this
263
+ * package does (outbound REST calls), so they're standalone exports
264
+ * rather than part of `createGitHubClient()`'s returned object.
265
+ *
266
+ * A consumer (a GitHub App receiving webhooks) owns what to *do* with a
267
+ * verified delivery — which event categories matter, how to normalize
268
+ * them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
269
+ * between Svix verification and Clerk-specific `AuthEvent` normalization,
270
+ * just with the verification half living here instead of inline, since
271
+ * "how GitHub signs and shapes a webhook" is vendor knowledge this
272
+ * package already owns for every other GitHub API surface.
273
+ */
274
+ /**
275
+ * Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
276
+ * HMAC-SHA256 over the raw body, keyed by the webhook's configured
277
+ * secret, `timingSafeEqual`-compared.
278
+ * https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
279
+ *
280
+ * Returns `false` (never throws) for any failure to verify — a missing/
281
+ * empty secret, a missing/malformed signature header, or a mismatch —
282
+ * so the caller decides how to surface that (e.g. as its own error type).
283
+ */
284
+ declare function verifyGitHubWebhookSignature(input: {
285
+ body: string | Buffer;
286
+ signature: string | undefined;
287
+ secret: string;
288
+ }): boolean;
289
+ interface GitHubWebhookHeaders {
290
+ /** `X-GitHub-Event` — the event category, e.g. `"push"`, `"pull_request"`. */
291
+ event: string | undefined;
292
+ /** `X-GitHub-Delivery` — GitHub's per-delivery id, useful for idempotency/logging. */
293
+ delivery: string | undefined;
294
+ /** `X-Hub-Signature-256` — pass straight to `verifyGitHubWebhookSignature`. */
295
+ signature: string | undefined;
296
+ }
297
+ /** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
298
+ declare function parseGitHubWebhookHeaders(headers: Record<string, string | string[] | undefined>): GitHubWebhookHeaders;
299
+ interface GitHubInstallationWebhookPayload {
300
+ action: string;
301
+ installation: {
302
+ id: number;
303
+ };
304
+ }
305
+ interface GitHubPushWebhookPayload {
306
+ ref: string;
307
+ installation?: {
308
+ id: number;
309
+ };
310
+ repository: {
311
+ full_name: string;
312
+ default_branch: string;
313
+ };
314
+ }
315
+ interface GitHubPullRequestWebhookPayload {
316
+ action: string;
317
+ installation?: {
318
+ id: number;
319
+ };
320
+ repository: {
321
+ full_name: string;
322
+ };
323
+ }
324
+ //#endregion
231
325
  //#region src/index.d.ts
232
326
  declare function createGitHubClient(opts: GitHubClientOptions): {
233
327
  branches: {
234
328
  protectBranch: (repo: string, branch: string, payload: Record<string, unknown>) => Promise<void>;
235
329
  };
330
+ checks: {
331
+ createCheckRun: (repo: string, input: CreateCheckRunInput) => Promise<GitHubCheckRun>;
332
+ };
236
333
  environments: {
237
334
  listEnvironments: (repo: string) => Promise<GitHubEnvironment[]>;
238
335
  upsertEnvironment: (repo: string, name: string, body: Record<string, unknown>) => Promise<void>;
@@ -332,4 +429,4 @@ declare function createGitHubClient(opts: GitHubClientOptions): {
332
429
  };
333
430
  type GitHubClient = ReturnType<typeof createGitHubClient>;
334
431
  //#endregion
335
- export { type CodeScanningSetupResult, type CreatePagesPayload, type CreatePullInput, type GitBlob, type GitCommit, type GitContents, GitHubClient, type GitHubClientOptions, type GitHubContents, type GitHubEnvironment, type GitHubIssue, type GitHubLabel, type GitHubMilestone, type GitHubPages, type GitHubPublicKey, type GitHubPullRequest, type GitHubRepo, type GitHubRuleset, type GitHubUser, type GitHubWorkflowRun, type GitPull, type GitRef, type GitTree, type GitTreeItem, type IssueSearchParams, type PagesBuildStatus, type PagesBuildType, type SecretScope, type TeamPermission, type UpdatePagesPayload, type WorkflowRunFilter, createGitHubClient };
432
+ export { type CheckRunConclusion, type CheckRunOutput, type CheckRunStatus, type CodeScanningSetupResult, type CreateCheckRunInput, type CreatePagesPayload, type CreatePullInput, type GitBlob, type GitCommit, type GitContents, type GitHubCheckRun, GitHubClient, type GitHubClientOptions, type GitHubContents, type GitHubEnvironment, type GitHubInstallationWebhookPayload, type GitHubIssue, type GitHubLabel, type GitHubMilestone, type GitHubPages, type GitHubPublicKey, type GitHubPullRequest, type GitHubPullRequestWebhookPayload, type GitHubPushWebhookPayload, type GitHubRepo, type GitHubRuleset, type GitHubUser, type GitHubWebhookHeaders, type GitHubWorkflowRun, type GitPull, type GitRef, type GitTree, type GitTreeItem, type IssueSearchParams, type PagesBuildStatus, type PagesBuildType, type SecretScope, type TeamPermission, type UpdatePagesPayload, type WorkflowRunFilter, createGitHubClient, parseGitHubWebhookHeaders, verifyGitHubWebhookSignature };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ProviderApiError, createRestClient } from "@theholocron/http-client";
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
3
  //#region src/utils.ts
3
4
  function createGitHubRestClient(opts) {
4
5
  return createRestClient({
@@ -28,6 +29,14 @@ function branches(rest) {
28
29
  }) };
29
30
  }
30
31
  //#endregion
32
+ //#region src/checks/checks.ts
33
+ function checks(rest) {
34
+ return { createCheckRun: (repo, input) => rest.request(`${repoBase(repo)}/check-runs`, {
35
+ method: "POST",
36
+ body: input
37
+ }) };
38
+ }
39
+ //#endregion
31
40
  //#region src/environments/environments.ts
32
41
  function environments(rest) {
33
42
  return {
@@ -344,11 +353,58 @@ function workflows(rest) {
344
353
  };
345
354
  }
346
355
  //#endregion
356
+ //#region src/webhooks/webhooks.ts
357
+ /**
358
+ * GitHub's own webhook mechanics — signature verification and header/
359
+ * payload shapes. Pure functions, no REST call, no auth token: these
360
+ * operate on an *inbound* delivery, the mirror of everything else this
361
+ * package does (outbound REST calls), so they're standalone exports
362
+ * rather than part of `createGitHubClient()`'s returned object.
363
+ *
364
+ * A consumer (a GitHub App receiving webhooks) owns what to *do* with a
365
+ * verified delivery — which event categories matter, how to normalize
366
+ * them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
367
+ * between Svix verification and Clerk-specific `AuthEvent` normalization,
368
+ * just with the verification half living here instead of inline, since
369
+ * "how GitHub signs and shapes a webhook" is vendor knowledge this
370
+ * package already owns for every other GitHub API surface.
371
+ */
372
+ /**
373
+ * Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
374
+ * HMAC-SHA256 over the raw body, keyed by the webhook's configured
375
+ * secret, `timingSafeEqual`-compared.
376
+ * https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
377
+ *
378
+ * Returns `false` (never throws) for any failure to verify — a missing/
379
+ * empty secret, a missing/malformed signature header, or a mismatch —
380
+ * so the caller decides how to surface that (e.g. as its own error type).
381
+ */
382
+ function verifyGitHubWebhookSignature(input) {
383
+ if (!input.secret || !input.signature || !input.signature.startsWith("sha256=")) return false;
384
+ const bodyStr = typeof input.body === "string" ? input.body : input.body.toString("utf8");
385
+ const provided = Buffer.from(input.signature.slice(7), "hex");
386
+ const computed = createHmac("sha256", input.secret).update(bodyStr).digest();
387
+ return provided.length === computed.length && timingSafeEqual(provided, computed);
388
+ }
389
+ /** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
390
+ function parseGitHubWebhookHeaders(headers) {
391
+ const find = (name) => {
392
+ const target = name.toLowerCase();
393
+ for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === target) return Array.isArray(v) ? v[0] : v;
394
+ };
395
+ return {
396
+ event: find("x-github-event"),
397
+ delivery: find("x-github-delivery"),
398
+ signature: find("x-hub-signature-256")
399
+ };
400
+ }
401
+ //#endregion
347
402
  //#region src/index.ts
348
403
  function createGitHubClient(opts) {
349
404
  const rest = createGitHubRestClient(opts);
350
405
  return {
351
406
  branches: branches(rest),
407
+ checks: checks(rest),
352
408
  environments: environments(rest),
353
409
  git: git(rest),
354
410
  issues: issues(rest),
@@ -367,4 +423,4 @@ function createGitHubClient(opts) {
367
423
  };
368
424
  }
369
425
  //#endregion
370
- export { createGitHubClient };
426
+ export { createGitHubClient, parseGitHubWebhookHeaders, verifyGitHubWebhookSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/github-client",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "A TypeScript client for the GitHub REST API",
5
5
  "keywords": [
6
6
  "github",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@theholocron/observability": "^0.3.0",
36
- "@theholocron/http-client": "^1.17.0"
36
+ "@theholocron/http-client": "^1.19.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@theholocron/cli": "5.0.0-alpha.3",