@async/github-app 0.1.1

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/dist/github.js ADDED
@@ -0,0 +1,258 @@
1
+ import { validateChangeFiles } from "./safety.js";
2
+ import { redactSensitive, utf8ToBase64 } from "./util.js";
3
+ export class GitHubApiError extends Error {
4
+ status;
5
+ method;
6
+ path;
7
+ constructor(message, status, method, path) {
8
+ super(message);
9
+ this.status = status;
10
+ this.method = method;
11
+ this.path = path;
12
+ this.name = "GitHubApiError";
13
+ }
14
+ }
15
+ export function createGitHubClient(auth) {
16
+ async function request(method, path, body) {
17
+ const token = await auth.getToken();
18
+ const init = {
19
+ method,
20
+ headers: {
21
+ accept: "application/vnd.github+json",
22
+ authorization: `Bearer ${token}`,
23
+ "content-type": "application/json",
24
+ "x-github-api-version": "2022-11-28"
25
+ }
26
+ };
27
+ if (body !== undefined) {
28
+ init.body = JSON.stringify(body);
29
+ }
30
+ const response = await fetch(`${auth.baseUrl}${path}`, init);
31
+ if (response.status === 204) {
32
+ return undefined;
33
+ }
34
+ const text = await response.text();
35
+ if (!response.ok) {
36
+ throw new GitHubApiError(`GitHub ${method} ${path} failed with ${response.status}: ${redactSensitive(text)}`, response.status, method, path);
37
+ }
38
+ return (text ? JSON.parse(text) : undefined);
39
+ }
40
+ return {
41
+ request,
42
+ ensureBranch: (options) => ensureBranch(request, options),
43
+ commitChangeSet: (options) => commitChangeSet(request, options),
44
+ openOrUpdatePullRequest: (options) => openOrUpdatePullRequest(request, options),
45
+ getTreeSnapshot: (options) => getTreeSnapshot(request, options),
46
+ compareBranch: (options) => compareBranch(request, options)
47
+ };
48
+ }
49
+ export function parseGitHubRepo(input) {
50
+ if (typeof input !== "string") {
51
+ return input;
52
+ }
53
+ const [owner, repo, extra] = input.split("/");
54
+ if (!owner || !repo || extra) {
55
+ throw new Error(`Expected GitHub repo as owner/name, received "${input}".`);
56
+ }
57
+ return { owner, repo };
58
+ }
59
+ export function formatGitHubRepo(input) {
60
+ const repo = parseGitHubRepo(input);
61
+ return `${repo.owner}/${repo.repo}`;
62
+ }
63
+ async function ensureBranch(request, options) {
64
+ const repo = parseGitHubRepo(options.repo);
65
+ const repoName = formatGitHubRepo(repo);
66
+ const branchPath = `/repos/${repo.owner}/${repo.repo}/git/ref/heads/${encodeURIComponent(options.branch)}`;
67
+ try {
68
+ const existing = await request("GET", branchPath);
69
+ return {
70
+ repo: repoName,
71
+ branch: options.branch,
72
+ sha: existing.object.sha,
73
+ created: false
74
+ };
75
+ }
76
+ catch (error) {
77
+ if (!(error instanceof GitHubApiError) || error.status !== 404) {
78
+ throw error;
79
+ }
80
+ }
81
+ const base = await request("GET", `/repos/${repo.owner}/${repo.repo}/git/ref/heads/${encodeURIComponent(options.from)}`);
82
+ await request("POST", `/repos/${repo.owner}/${repo.repo}/git/refs`, {
83
+ ref: `refs/heads/${options.branch}`,
84
+ sha: base.object.sha
85
+ });
86
+ return {
87
+ repo: repoName,
88
+ branch: options.branch,
89
+ sha: base.object.sha,
90
+ created: true
91
+ };
92
+ }
93
+ async function commitChangeSet(request, options) {
94
+ validateChangeFiles(options.files, {
95
+ allowWorkflowPaths: options.allowWorkflowPaths,
96
+ allowedPathGlobs: options.allowedPathGlobs
97
+ });
98
+ const repo = parseGitHubRepo(options.repo);
99
+ const repoName = formatGitHubRepo(repo);
100
+ const receipts = [];
101
+ const commitShas = [];
102
+ for (const file of options.files) {
103
+ const receipt = await commitOneFile(request, repo, options.branch, options.message, file, {
104
+ author: options.author,
105
+ committer: options.committer
106
+ });
107
+ receipts.push(receipt);
108
+ if (receipt.commitSha) {
109
+ commitShas.push(receipt.commitSha);
110
+ }
111
+ }
112
+ return {
113
+ id: options.changeSetId,
114
+ repo: repoName,
115
+ branch: options.branch,
116
+ baseBranch: options.baseBranch,
117
+ commitSha: commitShas.at(-1),
118
+ commitShas,
119
+ files: receipts,
120
+ indexHints: extractIndexHints(options.metadata),
121
+ metadata: options.metadata
122
+ };
123
+ }
124
+ async function commitOneFile(request, repo, branch, message, file, identity) {
125
+ const contentPath = `/repos/${repo.owner}/${repo.repo}/contents/${encodeContentPath(file.path)}`;
126
+ const sha = file.previousSha ?? await getContentSha(request, contentPath, branch);
127
+ if (file.action === "delete") {
128
+ if (!sha) {
129
+ throw new GitHubApiError(`Cannot delete ${file.path}; GitHub did not return an existing sha.`, 404, "GET", contentPath);
130
+ }
131
+ const deleted = await request("DELETE", contentPath, {
132
+ message,
133
+ sha,
134
+ branch,
135
+ author: identity.author,
136
+ committer: identity.committer
137
+ });
138
+ return {
139
+ path: file.path,
140
+ action: "delete",
141
+ commitSha: deleted.commit.sha,
142
+ contentSha: sha
143
+ };
144
+ }
145
+ const updated = await request("PUT", contentPath, {
146
+ message,
147
+ content: file.encoding === "base64" ? file.content : utf8ToBase64(file.content ?? ""),
148
+ sha,
149
+ branch,
150
+ author: identity.author,
151
+ committer: identity.committer
152
+ });
153
+ return {
154
+ path: file.path,
155
+ action: "upsert",
156
+ commitSha: updated.commit.sha,
157
+ contentSha: updated.content?.sha
158
+ };
159
+ }
160
+ async function getContentSha(request, contentPath, branch) {
161
+ try {
162
+ const current = await request("GET", `${contentPath}?ref=${encodeURIComponent(branch)}`);
163
+ return current.sha;
164
+ }
165
+ catch (error) {
166
+ if (error instanceof GitHubApiError && error.status === 404) {
167
+ return undefined;
168
+ }
169
+ throw error;
170
+ }
171
+ }
172
+ async function openOrUpdatePullRequest(request, options) {
173
+ const repo = parseGitHubRepo(options.repo);
174
+ const headForSearch = options.head.includes(":") ? options.head : `${repo.owner}:${options.head}`;
175
+ const existing = await request("GET", `/repos/${repo.owner}/${repo.repo}/pulls?state=open&head=${encodeURIComponent(headForSearch)}&base=${encodeURIComponent(options.base)}`);
176
+ if (existing[0]) {
177
+ const updated = await request("PATCH", `/repos/${repo.owner}/${repo.repo}/pulls/${existing[0].number}`, {
178
+ title: options.title,
179
+ body: options.body
180
+ });
181
+ return {
182
+ number: updated.number,
183
+ url: updated.html_url,
184
+ head: options.head,
185
+ base: options.base,
186
+ created: false
187
+ };
188
+ }
189
+ const created = await request("POST", `/repos/${repo.owner}/${repo.repo}/pulls`, {
190
+ title: options.title,
191
+ body: options.body,
192
+ head: options.head,
193
+ base: options.base,
194
+ draft: options.draft
195
+ });
196
+ return {
197
+ number: created.number,
198
+ url: created.html_url,
199
+ head: options.head,
200
+ base: options.base,
201
+ created: true
202
+ };
203
+ }
204
+ async function getTreeSnapshot(request, options) {
205
+ const repo = parseGitHubRepo(options.repo);
206
+ const entries = [];
207
+ if (options.paths?.length) {
208
+ for (const path of options.paths) {
209
+ const item = await request("GET", `/repos/${repo.owner}/${repo.repo}/contents/${encodeContentPath(path)}?ref=${encodeURIComponent(options.ref)}`);
210
+ const items = Array.isArray(item) ? item : [item];
211
+ for (const entry of items) {
212
+ entries.push({
213
+ path: entry.path,
214
+ sha: entry.sha,
215
+ type: normalizeContentType(entry.type),
216
+ size: entry.size
217
+ });
218
+ }
219
+ }
220
+ }
221
+ else {
222
+ const tree = await request("GET", `/repos/${repo.owner}/${repo.repo}/git/trees/${encodeURIComponent(options.ref)}?recursive=1`);
223
+ for (const entry of tree.tree) {
224
+ entries.push({
225
+ path: entry.path,
226
+ sha: entry.sha,
227
+ type: entry.type,
228
+ size: entry.size
229
+ });
230
+ }
231
+ }
232
+ return {
233
+ repo: formatGitHubRepo(repo),
234
+ ref: options.ref,
235
+ entries
236
+ };
237
+ }
238
+ async function compareBranch(request, options) {
239
+ const repo = parseGitHubRepo(options.repo);
240
+ const compared = await request("GET", `/repos/${repo.owner}/${repo.repo}/compare/${encodeURIComponent(options.base)}...${encodeURIComponent(options.head)}`);
241
+ return {
242
+ status: compared.status,
243
+ aheadBy: compared.ahead_by,
244
+ behindBy: compared.behind_by,
245
+ commits: compared.commits.map((commit) => ({ sha: commit.sha })),
246
+ htmlUrl: compared.html_url
247
+ };
248
+ }
249
+ function normalizeContentType(type) {
250
+ return type;
251
+ }
252
+ function encodeContentPath(path) {
253
+ return path.split("/").map((part) => encodeURIComponent(part)).join("/");
254
+ }
255
+ function extractIndexHints(metadata) {
256
+ const hints = metadata?.indexHints;
257
+ return Array.isArray(hints) && hints.every((hint) => typeof hint === "string") ? hints : [];
258
+ }
@@ -0,0 +1,6 @@
1
+ export { actionsBridgeAuth, createGitHubAppJwt, githubAppAuth, githubUserAuth, GitHubAuthError, staticTokenAuth } from "./auth.js";
2
+ export { asyncGithubApp, defineGithubApp } from "./app.js";
3
+ export { createGitHubClient, formatGitHubRepo, GitHubApiError, parseGitHubRepo } from "./github.js";
4
+ export { assertSafeChangeFilePath, UnsafeChangePathError, validateChangeFiles } from "./safety.js";
5
+ export type { ActionsBridgeAuthOptions, AsyncGithubAppMetadata, ChangeFile, ChangeFileAction, ChangeFileReceipt, ChangeSet, ChangeSetMode, CommitChangeSetOptions, CommitReceipt, CompareBranchOptions, CompareBranchReceipt, DefineGithubAppOptions, EnsureBranchOptions, EnsureBranchReceipt, GitAuthor, GitHubAppAuthOptions, GitHubAuthProvider, GitHubAuthScope, GitHubBaseUrl, GitHubClient, GithubAppDefinition, GitHubRepo, GitHubRepoInput, OpenOrUpdatePullRequestOptions, PathSafetyOptions, PullRequestReceipt, TokenAuthOptions, TreeSnapshot, TreeSnapshotEntry, TreeSnapshotOptions } from "./types.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,eAAe,EACf,eAAe,EAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,eAAe,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,wBAAwB,EACxB,sBAAsB,EACtB,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,SAAS,EACT,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,8BAA8B,EAC9B,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { actionsBridgeAuth, createGitHubAppJwt, githubAppAuth, githubUserAuth, GitHubAuthError, staticTokenAuth } from "./auth.js";
2
+ export { asyncGithubApp, defineGithubApp } from "./app.js";
3
+ export { createGitHubClient, formatGitHubRepo, GitHubApiError, parseGitHubRepo } from "./github.js";
4
+ export { assertSafeChangeFilePath, UnsafeChangePathError, validateChangeFiles } from "./safety.js";
@@ -0,0 +1,7 @@
1
+ import type { ChangeFile, PathSafetyOptions } from "./types.js";
2
+ export declare class UnsafeChangePathError extends Error {
3
+ constructor(path: string, reason: string);
4
+ }
5
+ export declare function assertSafeChangeFilePath(path: string, options?: PathSafetyOptions): void;
6
+ export declare function validateChangeFiles(files: readonly ChangeFile[], options?: PathSafetyOptions): void;
7
+ //# sourceMappingURL=safety.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../src/safety.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEhE,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIzC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAqB5F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAiBvG"}
package/dist/safety.js ADDED
@@ -0,0 +1,57 @@
1
+ export class UnsafeChangePathError extends Error {
2
+ constructor(path, reason) {
3
+ super(`Unsafe GitHub change path "${path}": ${reason}`);
4
+ this.name = "UnsafeChangePathError";
5
+ }
6
+ }
7
+ export function assertSafeChangeFilePath(path, options = {}) {
8
+ if (!path || path.trim() !== path) {
9
+ throw new UnsafeChangePathError(path, "paths must be non-empty and cannot include leading or trailing whitespace");
10
+ }
11
+ if (path.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(path)) {
12
+ throw new UnsafeChangePathError(path, "absolute paths are not allowed");
13
+ }
14
+ const parts = path.split("/");
15
+ if (parts.some((part) => part === ".." || part === "")) {
16
+ throw new UnsafeChangePathError(path, "paths cannot contain empty segments or ..");
17
+ }
18
+ if (!options.allowWorkflowPaths && path.startsWith(".github/workflows/")) {
19
+ throw new UnsafeChangePathError(path, ".github/workflows writes require allowWorkflowPaths");
20
+ }
21
+ if (options.allowedPathGlobs?.length && !options.allowedPathGlobs.some((glob) => matchesSimpleGlob(path, glob))) {
22
+ throw new UnsafeChangePathError(path, `path is outside allowed globs: ${options.allowedPathGlobs.join(", ")}`);
23
+ }
24
+ }
25
+ export function validateChangeFiles(files, options = {}) {
26
+ if (!files.length) {
27
+ throw new UnsafeChangePathError("(empty)", "change sets must contain at least one file");
28
+ }
29
+ const seen = new Set();
30
+ for (const file of files) {
31
+ assertSafeChangeFilePath(file.path, options);
32
+ if (seen.has(file.path)) {
33
+ throw new UnsafeChangePathError(file.path, "a change set cannot include the same path more than once");
34
+ }
35
+ seen.add(file.path);
36
+ if (file.action === "upsert" && file.content === undefined) {
37
+ throw new UnsafeChangePathError(file.path, "upsert files require content");
38
+ }
39
+ }
40
+ }
41
+ function matchesSimpleGlob(path, glob) {
42
+ if (glob.endsWith("/**")) {
43
+ return path.startsWith(glob.slice(0, -2));
44
+ }
45
+ if (glob.endsWith("/*")) {
46
+ const prefix = glob.slice(0, -1);
47
+ const rest = path.slice(prefix.length);
48
+ return path.startsWith(prefix) && rest.length > 0 && !rest.includes("/");
49
+ }
50
+ if (glob.includes("*")) {
51
+ const escaped = glob
52
+ .replace(/[.+?^${}()|[\]\\]/gu, "\\$&")
53
+ .replaceAll("\\*", "[^/]*");
54
+ return new RegExp(`^${escaped}$`, "u").test(path);
55
+ }
56
+ return path === glob || path.startsWith(`${glob}/`);
57
+ }
@@ -0,0 +1,34 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export interface WebhookVerifyInput {
3
+ readonly signature: string | null;
4
+ readonly body: string;
5
+ readonly event: string;
6
+ readonly deliveryId: string;
7
+ }
8
+ export type WebhookVerifier = string | {
9
+ readonly secret: string | (() => string | Promise<string>);
10
+ } | ((input: WebhookVerifyInput) => boolean | Promise<boolean>);
11
+ export interface GithubWebhookEvent {
12
+ readonly event: string;
13
+ readonly deliveryId: string;
14
+ readonly payload: unknown;
15
+ readonly rawBody: string;
16
+ readonly headers: Headers;
17
+ }
18
+ export type GithubWebhookEventHandler = (event: GithubWebhookEvent) => void | Response | Promise<void | Response>;
19
+ export interface CreateGithubWebhookHandlerOptions {
20
+ readonly verify: WebhookVerifier;
21
+ readonly route?: Record<string, GithubWebhookEventHandler>;
22
+ readonly onEvent?: GithubWebhookEventHandler;
23
+ readonly maxBodyBytes?: number;
24
+ readonly seenDeliveries?: Set<string>;
25
+ }
26
+ export declare function verifyWebhookSignature(options: {
27
+ readonly secret: string;
28
+ readonly body: string | Uint8Array;
29
+ readonly signature: string | null | undefined;
30
+ }): boolean;
31
+ export declare function createGithubWebhookHandler(options: CreateGithubWebhookHandlerOptions): (request: Request) => Promise<Response>;
32
+ export declare function createNodeWebhookHandler(handler: (request: Request) => Promise<Response>): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
33
+ export declare function createDenoWebhookHandler(handler: (request: Request) => Promise<Response>): (request: Request) => Promise<Response>;
34
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;CAC5D,GAAG,CAAC,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AAEhE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;AAElH,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvC;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC/C,GAAG,OAAO,CAUV;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2D9H;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,IAC9C,SAAS,eAAe,EAAE,UAAU,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CA2B5G;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAElI"}
package/dist/server.js ADDED
@@ -0,0 +1,111 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export function verifyWebhookSignature(options) {
3
+ const signature = options.signature;
4
+ if (!signature?.startsWith("sha256=")) {
5
+ return false;
6
+ }
7
+ const expected = `sha256=${createHmac("sha256", options.secret).update(options.body).digest("hex")}`;
8
+ const expectedBuffer = Buffer.from(expected);
9
+ const actualBuffer = Buffer.from(signature);
10
+ return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
11
+ }
12
+ export function createGithubWebhookHandler(options) {
13
+ const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
14
+ const seenDeliveries = options.seenDeliveries ?? new Set();
15
+ return async function githubWebhookHandler(request) {
16
+ if (request.method !== "POST") {
17
+ return json({ ok: false, error: "method_not_allowed" }, 405);
18
+ }
19
+ const event = request.headers.get("x-github-event") ?? "";
20
+ const deliveryId = request.headers.get("x-github-delivery") ?? "";
21
+ const signature = request.headers.get("x-hub-signature-256");
22
+ const rawBody = await request.text();
23
+ if (Buffer.byteLength(rawBody, "utf8") > maxBodyBytes) {
24
+ return json({ ok: false, error: "body_too_large" }, 413);
25
+ }
26
+ if (!event || !deliveryId) {
27
+ return json({ ok: false, error: "missing_github_headers" }, 400);
28
+ }
29
+ const verified = await verifyRequest(options.verify, {
30
+ signature,
31
+ body: rawBody,
32
+ event,
33
+ deliveryId
34
+ });
35
+ if (!verified) {
36
+ return json({ ok: false, error: "invalid_signature" }, 401);
37
+ }
38
+ if (seenDeliveries.has(deliveryId)) {
39
+ return json({ ok: true, duplicate: true });
40
+ }
41
+ seenDeliveries.add(deliveryId);
42
+ let payload;
43
+ try {
44
+ payload = JSON.parse(rawBody);
45
+ }
46
+ catch {
47
+ return json({ ok: false, error: "invalid_json" }, 400);
48
+ }
49
+ const handler = options.route?.[event] ?? options.route?.["*"] ?? options.onEvent;
50
+ if (!handler) {
51
+ return json({ ok: true, ignored: true });
52
+ }
53
+ const response = await handler({
54
+ event,
55
+ deliveryId,
56
+ payload,
57
+ rawBody,
58
+ headers: request.headers
59
+ });
60
+ return response ?? json({ ok: true });
61
+ };
62
+ }
63
+ export function createNodeWebhookHandler(handler) {
64
+ return async function nodeWebhookHandler(request, response) {
65
+ const chunks = [];
66
+ for await (const chunk of request) {
67
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
68
+ }
69
+ const headers = new Headers();
70
+ for (const [key, value] of Object.entries(request.headers)) {
71
+ if (Array.isArray(value)) {
72
+ for (const item of value) {
73
+ headers.append(key, item);
74
+ }
75
+ }
76
+ else if (value !== undefined) {
77
+ headers.set(key, value);
78
+ }
79
+ }
80
+ const fetchRequest = new Request(`http://localhost${request.url ?? "/"}`, {
81
+ method: request.method ?? "POST",
82
+ headers,
83
+ body: Buffer.concat(chunks)
84
+ });
85
+ const fetchResponse = await handler(fetchRequest);
86
+ response.statusCode = fetchResponse.status;
87
+ fetchResponse.headers.forEach((value, key) => response.setHeader(key, value));
88
+ response.end(Buffer.from(await fetchResponse.arrayBuffer()));
89
+ };
90
+ }
91
+ export function createDenoWebhookHandler(handler) {
92
+ return handler;
93
+ }
94
+ async function verifyRequest(verifier, input) {
95
+ if (typeof verifier === "string") {
96
+ return verifyWebhookSignature({ secret: verifier, body: input.body, signature: input.signature });
97
+ }
98
+ if (typeof verifier === "function") {
99
+ return verifier(input);
100
+ }
101
+ const secret = typeof verifier.secret === "function" ? await verifier.secret() : verifier.secret;
102
+ return verifyWebhookSignature({ secret, body: input.body, signature: input.signature });
103
+ }
104
+ function json(body, status = 200) {
105
+ return new Response(JSON.stringify(body), {
106
+ status,
107
+ headers: {
108
+ "content-type": "application/json"
109
+ }
110
+ });
111
+ }
@@ -0,0 +1,177 @@
1
+ export type GitHubBaseUrl = `https://${string}` | `http://${string}`;
2
+ export interface GitHubAuthProvider {
3
+ readonly kind: string;
4
+ readonly baseUrl: GitHubBaseUrl;
5
+ getToken(scope?: GitHubAuthScope): Promise<string>;
6
+ }
7
+ export interface GitHubAuthScope {
8
+ readonly repo?: GitHubRepoInput;
9
+ readonly permissions?: Record<string, "read" | "write">;
10
+ }
11
+ export interface GitHubAppAuthOptions {
12
+ readonly appId: string | number;
13
+ readonly privateKey: string;
14
+ readonly installationId: string | number;
15
+ readonly baseUrl?: GitHubBaseUrl | undefined;
16
+ readonly fetch?: typeof fetch | undefined;
17
+ readonly now?: (() => Date) | undefined;
18
+ }
19
+ export interface TokenAuthOptions {
20
+ readonly token: string;
21
+ readonly baseUrl?: GitHubBaseUrl | undefined;
22
+ }
23
+ export interface ActionsBridgeAuthOptions {
24
+ readonly tokenEnv?: string | undefined;
25
+ readonly env?: Record<string, string | undefined> | undefined;
26
+ readonly baseUrl?: GitHubBaseUrl | undefined;
27
+ }
28
+ export interface AsyncGithubAppMetadata {
29
+ readonly slug: string;
30
+ readonly installUrl: string;
31
+ readonly callbackUrl: string;
32
+ readonly webhookEvents: readonly string[];
33
+ readonly permissions: Readonly<Record<string, "read" | "write">>;
34
+ }
35
+ export interface GithubAppDefinition {
36
+ readonly metadata: AsyncGithubAppMetadata;
37
+ readonly auth?: GitHubAuthProvider | undefined;
38
+ readonly permissions: Readonly<Record<string, "read" | "write">>;
39
+ readonly endpoints: Readonly<Record<string, string>>;
40
+ }
41
+ export interface DefineGithubAppOptions {
42
+ readonly metadata?: Partial<AsyncGithubAppMetadata> | undefined;
43
+ readonly auth?: GitHubAuthProvider | undefined;
44
+ readonly permissions?: Record<string, "read" | "write"> | undefined;
45
+ readonly endpoints?: Record<string, string> | undefined;
46
+ }
47
+ export interface GitHubRepo {
48
+ readonly owner: string;
49
+ readonly repo: string;
50
+ }
51
+ export type GitHubRepoInput = GitHubRepo | `${string}/${string}`;
52
+ export type ChangeFileAction = "upsert" | "delete";
53
+ export interface ChangeFile {
54
+ readonly path: string;
55
+ readonly action: ChangeFileAction;
56
+ readonly content?: string | undefined;
57
+ readonly encoding?: "utf8" | "base64" | undefined;
58
+ readonly previousSha?: string | undefined;
59
+ }
60
+ export type ChangeSetMode = "app" | "actions-pull" | "actions-dispatch" | "token" | "branch" | "pull_request" | "direct";
61
+ export interface ChangeSet {
62
+ readonly id: string;
63
+ readonly repo: GitHubRepoInput;
64
+ readonly baseBranch: string;
65
+ readonly targetBranch: string;
66
+ readonly mode: ChangeSetMode;
67
+ readonly files: readonly ChangeFile[];
68
+ readonly message?: string | undefined;
69
+ readonly title?: string | undefined;
70
+ readonly body?: string | undefined;
71
+ readonly metadata?: Record<string, unknown> | undefined;
72
+ }
73
+ export interface GitAuthor {
74
+ readonly name: string;
75
+ readonly email: string;
76
+ readonly date?: string | undefined;
77
+ }
78
+ export interface CommitChangeSetOptions {
79
+ readonly repo: GitHubRepoInput;
80
+ readonly branch: string;
81
+ readonly message: string;
82
+ readonly files: readonly ChangeFile[];
83
+ readonly baseBranch?: string | undefined;
84
+ readonly changeSetId?: string | undefined;
85
+ readonly author?: GitAuthor | undefined;
86
+ readonly committer?: GitAuthor | undefined;
87
+ readonly allowWorkflowPaths?: boolean | undefined;
88
+ readonly allowedPathGlobs?: readonly string[] | undefined;
89
+ readonly metadata?: Record<string, unknown> | undefined;
90
+ }
91
+ export interface ChangeFileReceipt {
92
+ readonly path: string;
93
+ readonly action: ChangeFileAction;
94
+ readonly commitSha?: string | undefined;
95
+ readonly contentSha?: string | undefined;
96
+ }
97
+ export interface CommitReceipt {
98
+ readonly id?: string | undefined;
99
+ readonly repo: string;
100
+ readonly branch: string;
101
+ readonly baseBranch?: string | undefined;
102
+ readonly commitSha?: string | undefined;
103
+ readonly commitShas: readonly string[];
104
+ readonly pullRequestUrl?: string | undefined;
105
+ readonly files: readonly ChangeFileReceipt[];
106
+ readonly indexHints: readonly string[];
107
+ readonly metadata?: Record<string, unknown> | undefined;
108
+ }
109
+ export interface EnsureBranchOptions {
110
+ readonly repo: GitHubRepoInput;
111
+ readonly from: string;
112
+ readonly branch: string;
113
+ }
114
+ export interface EnsureBranchReceipt {
115
+ readonly repo: string;
116
+ readonly branch: string;
117
+ readonly sha: string;
118
+ readonly created: boolean;
119
+ }
120
+ export interface OpenOrUpdatePullRequestOptions {
121
+ readonly repo: GitHubRepoInput;
122
+ readonly head: string;
123
+ readonly base: string;
124
+ readonly title: string;
125
+ readonly body?: string | undefined;
126
+ readonly draft?: boolean | undefined;
127
+ }
128
+ export interface PullRequestReceipt {
129
+ readonly number: number;
130
+ readonly url: string;
131
+ readonly head: string;
132
+ readonly base: string;
133
+ readonly created: boolean;
134
+ }
135
+ export interface TreeSnapshotOptions {
136
+ readonly repo: GitHubRepoInput;
137
+ readonly ref: string;
138
+ readonly paths?: readonly string[];
139
+ }
140
+ export interface TreeSnapshotEntry {
141
+ readonly path: string;
142
+ readonly sha: string;
143
+ readonly type: "file" | "dir" | "symlink" | "submodule" | "tree" | "blob";
144
+ readonly size?: number | undefined;
145
+ }
146
+ export interface TreeSnapshot {
147
+ readonly repo: string;
148
+ readonly ref: string;
149
+ readonly entries: readonly TreeSnapshotEntry[];
150
+ }
151
+ export interface CompareBranchOptions {
152
+ readonly repo: GitHubRepoInput;
153
+ readonly base: string;
154
+ readonly head: string;
155
+ }
156
+ export interface CompareBranchReceipt {
157
+ readonly status: string;
158
+ readonly aheadBy: number;
159
+ readonly behindBy: number;
160
+ readonly commits: readonly {
161
+ readonly sha: string;
162
+ }[];
163
+ readonly htmlUrl?: string | undefined;
164
+ }
165
+ export interface GitHubClient {
166
+ request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
167
+ ensureBranch(options: EnsureBranchOptions): Promise<EnsureBranchReceipt>;
168
+ commitChangeSet(options: CommitChangeSetOptions): Promise<CommitReceipt>;
169
+ openOrUpdatePullRequest(options: OpenOrUpdatePullRequestOptions): Promise<PullRequestReceipt>;
170
+ getTreeSnapshot(options: TreeSnapshotOptions): Promise<TreeSnapshot>;
171
+ compareBranch(options: CompareBranchOptions): Promise<CompareBranchReceipt>;
172
+ }
173
+ export interface PathSafetyOptions {
174
+ readonly allowWorkflowPaths?: boolean | undefined;
175
+ readonly allowedPathGlobs?: readonly string[] | undefined;
176
+ }
177
+ //# sourceMappingURL=types.d.ts.map