@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 +38 -0
- package/dist/index.d.mts +69 -2
- package/dist/index.mjs +58 -2
- package/package.json +2 -2
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 {
|
|
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.
|
|
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.
|
|
36
|
+
"@theholocron/http-client": "^1.18.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@theholocron/cli": "5.0.0-alpha.3",
|