@theholocron/github-client 1.16.1 → 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: {
@@ -286,7 +353,7 @@ declare function createGitHubClient(opts: GitHubClientOptions): {
286
353
  updatePages: (repo: string, payload: UpdatePagesPayload) => Promise<void>;
287
354
  };
288
355
  properties: {
289
- setProperties: (repo: string, values: Record<string, string>) => Promise<void>;
356
+ setProperties: (repo: string, values: Record<string, string | string[]>) => Promise<void>;
290
357
  };
291
358
  pulls: {
292
359
  getPullRequest: (repo: string, number: number) => Promise<GitHubPullRequest>;
@@ -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({
@@ -184,7 +185,16 @@ function pages(rest) {
184
185
  //#endregion
185
186
  //#region src/properties/properties.ts
186
187
  function properties(rest) {
187
- return { setProperties: (repo, values) => {
188
+ return {
189
+ /**
190
+ * `value` is a plain string for `string`/`single_select`/`true_false`
191
+ * custom properties, or a string array for `multi_select` ones —
192
+ * GitHub's own PATCH body accepts either per property. A single-string
193
+ * comma-joined value has no length ceiling GitHub documents, but real
194
+ * usage hits one in practice (a `string`-type property with enough
195
+ * joined values 422s as "too long"); `multi_select` has none.
196
+ */
197
+ setProperties: (repo, values) => {
188
198
  const propertyList = Object.entries(values).map(([property_name, value]) => ({
189
199
  property_name,
190
200
  value
@@ -335,6 +345,52 @@ function workflows(rest) {
335
345
  };
336
346
  }
337
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
338
394
  //#region src/index.ts
339
395
  function createGitHubClient(opts) {
340
396
  const rest = createGitHubRestClient(opts);
@@ -358,4 +414,4 @@ function createGitHubClient(opts) {
358
414
  };
359
415
  }
360
416
  //#endregion
361
- 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.16.1",
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.16.1"
36
+ "@theholocron/http-client": "^1.18.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@theholocron/cli": "5.0.0-alpha.3",