@theholocron/github-client 1.17.0 → 1.18.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,41 @@ 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
+
55
+ ## Webhooks
56
+
57
+ GitHub's own inbound-webhook mechanics — signature verification and
58
+ header/payload shapes — as standalone functions, not part of
59
+ `createGitHubClient()`: these verify a _delivery this package's consumer
60
+ received_, not an outbound REST call, so they need a webhook secret
61
+ instead of an API token.
62
+
63
+ ```ts
64
+ import {
65
+ parseGitHubWebhookHeaders,
66
+ verifyGitHubWebhookSignature,
67
+ type GitHubPushWebhookPayload,
68
+ } from "@theholocron/github-client";
69
+
70
+ const { event, delivery, signature } = parseGitHubWebhookHeaders(req.headers);
71
+ const ok = verifyGitHubWebhookSignature({ body: rawBody, signature, secret: webhookSecret });
72
+ ```
73
+
74
+ `verifyGitHubWebhookSignature({ body, signature, secret })` — `X-Hub-
75
+ Signature-256` verification (HMAC-SHA256 over the raw body,
76
+ `timingSafeEqual`-compared). Returns `false` for any failure to verify
77
+ (empty secret, missing/malformed signature, mismatch) — never throws;
78
+ the caller decides how to surface that.
79
+
80
+ `parseGitHubWebhookHeaders(headers)` — extracts `event`, `delivery`, and
81
+ `signature` from GitHub's three webhook headers, case-insensitively.
82
+
83
+ `GitHubInstallationWebhookPayload`, `GitHubPushWebhookPayload`,
84
+ `GitHubPullRequestWebhookPayload` — the delivery body shapes, scoped to
85
+ the fields a consumer reads today (not a full re-typing of every field
86
+ GitHub sends).
87
+
88
+ A consumer owns what to _do_ with a verified delivery — which event
89
+ categories matter, how to normalize them into its own domain shape.
90
+ `@theholocron/sentinel`'s `parseWebhookEvent()` is the reference
91
+ consumer.
package/dist/index.d.mts CHANGED
@@ -228,6 +228,73 @@ interface WorkflowRunFilter {
228
228
  status?: string;
229
229
  }
230
230
  //#endregion
231
+ //#region src/webhooks/webhooks.d.ts
232
+ /**
233
+ * GitHub's own webhook mechanics — signature verification and header/
234
+ * payload shapes. Pure functions, no REST call, no auth token: these
235
+ * operate on an *inbound* delivery, the mirror of everything else this
236
+ * package does (outbound REST calls), so they're standalone exports
237
+ * rather than part of `createGitHubClient()`'s returned object.
238
+ *
239
+ * A consumer (a GitHub App receiving webhooks) owns what to *do* with a
240
+ * verified delivery — which event categories matter, how to normalize
241
+ * them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
242
+ * between Svix verification and Clerk-specific `AuthEvent` normalization,
243
+ * just with the verification half living here instead of inline, since
244
+ * "how GitHub signs and shapes a webhook" is vendor knowledge this
245
+ * package already owns for every other GitHub API surface.
246
+ */
247
+ /**
248
+ * Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
249
+ * HMAC-SHA256 over the raw body, keyed by the webhook's configured
250
+ * secret, `timingSafeEqual`-compared.
251
+ * https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
252
+ *
253
+ * Returns `false` (never throws) for any failure to verify — a missing/
254
+ * empty secret, a missing/malformed signature header, or a mismatch —
255
+ * so the caller decides how to surface that (e.g. as its own error type).
256
+ */
257
+ declare function verifyGitHubWebhookSignature(input: {
258
+ body: string | Buffer;
259
+ signature: string | undefined;
260
+ secret: string;
261
+ }): boolean;
262
+ interface GitHubWebhookHeaders {
263
+ /** `X-GitHub-Event` — the event category, e.g. `"push"`, `"pull_request"`. */
264
+ event: string | undefined;
265
+ /** `X-GitHub-Delivery` — GitHub's per-delivery id, useful for idempotency/logging. */
266
+ delivery: string | undefined;
267
+ /** `X-Hub-Signature-256` — pass straight to `verifyGitHubWebhookSignature`. */
268
+ signature: string | undefined;
269
+ }
270
+ /** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
271
+ declare function parseGitHubWebhookHeaders(headers: Record<string, string | string[] | undefined>): GitHubWebhookHeaders;
272
+ interface GitHubInstallationWebhookPayload {
273
+ action: string;
274
+ installation: {
275
+ id: number;
276
+ };
277
+ }
278
+ interface GitHubPushWebhookPayload {
279
+ ref: string;
280
+ installation?: {
281
+ id: number;
282
+ };
283
+ repository: {
284
+ full_name: string;
285
+ default_branch: string;
286
+ };
287
+ }
288
+ interface GitHubPullRequestWebhookPayload {
289
+ action: string;
290
+ installation?: {
291
+ id: number;
292
+ };
293
+ repository: {
294
+ full_name: string;
295
+ };
296
+ }
297
+ //#endregion
231
298
  //#region src/index.d.ts
232
299
  declare function createGitHubClient(opts: GitHubClientOptions): {
233
300
  branches: {
@@ -332,4 +399,4 @@ declare function createGitHubClient(opts: GitHubClientOptions): {
332
399
  };
333
400
  type GitHubClient = ReturnType<typeof createGitHubClient>;
334
401
  //#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 };
402
+ export { type CodeScanningSetupResult, type CreatePagesPayload, type CreatePullInput, type GitBlob, type GitCommit, type GitContents, 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({
@@ -344,6 +345,52 @@ function workflows(rest) {
344
345
  };
345
346
  }
346
347
  //#endregion
348
+ //#region src/webhooks/webhooks.ts
349
+ /**
350
+ * GitHub's own webhook mechanics — signature verification and header/
351
+ * payload shapes. Pure functions, no REST call, no auth token: these
352
+ * operate on an *inbound* delivery, the mirror of everything else this
353
+ * package does (outbound REST calls), so they're standalone exports
354
+ * rather than part of `createGitHubClient()`'s returned object.
355
+ *
356
+ * A consumer (a GitHub App receiving webhooks) owns what to *do* with a
357
+ * verified delivery — which event categories matter, how to normalize
358
+ * them — the same split `holocron-plugin-clerk`'s `parseWebhook` draws
359
+ * between Svix verification and Clerk-specific `AuthEvent` normalization,
360
+ * just with the verification half living here instead of inline, since
361
+ * "how GitHub signs and shapes a webhook" is vendor knowledge this
362
+ * package already owns for every other GitHub API surface.
363
+ */
364
+ /**
365
+ * Verifies a GitHub webhook delivery's `X-Hub-Signature-256` header —
366
+ * HMAC-SHA256 over the raw body, keyed by the webhook's configured
367
+ * secret, `timingSafeEqual`-compared.
368
+ * https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
369
+ *
370
+ * Returns `false` (never throws) for any failure to verify — a missing/
371
+ * empty secret, a missing/malformed signature header, or a mismatch —
372
+ * so the caller decides how to surface that (e.g. as its own error type).
373
+ */
374
+ function verifyGitHubWebhookSignature(input) {
375
+ if (!input.secret || !input.signature || !input.signature.startsWith("sha256=")) return false;
376
+ const bodyStr = typeof input.body === "string" ? input.body : input.body.toString("utf8");
377
+ const provided = Buffer.from(input.signature.slice(7), "hex");
378
+ const computed = createHmac("sha256", input.secret).update(bodyStr).digest();
379
+ return provided.length === computed.length && timingSafeEqual(provided, computed);
380
+ }
381
+ /** Extracts GitHub's three webhook headers, case-insensitively (Node's raw headers may arrive lower-cased, or as an array when a header repeats). */
382
+ function parseGitHubWebhookHeaders(headers) {
383
+ const find = (name) => {
384
+ const target = name.toLowerCase();
385
+ for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === target) return Array.isArray(v) ? v[0] : v;
386
+ };
387
+ return {
388
+ event: find("x-github-event"),
389
+ delivery: find("x-github-delivery"),
390
+ signature: find("x-hub-signature-256")
391
+ };
392
+ }
393
+ //#endregion
347
394
  //#region src/index.ts
348
395
  function createGitHubClient(opts) {
349
396
  const rest = createGitHubRestClient(opts);
@@ -367,4 +414,4 @@ function createGitHubClient(opts) {
367
414
  };
368
415
  }
369
416
  //#endregion
370
- export { createGitHubClient };
417
+ 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.18.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.18.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@theholocron/cli": "5.0.0-alpha.3",