@happyvertical/repos 0.83.0 → 0.85.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/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHmac, createSign, timingSafeEqual } from "node:crypto";
1
2
  import { GraphQLClient } from "@happyvertical/graphql";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import yaml from "js-yaml";
@@ -127,6 +128,704 @@ async function getRepository(options) {
127
128
  }
128
129
  }
129
130
  //#endregion
131
+ //#region src/forge/errors.ts
132
+ var ForgeError = class extends Error {
133
+ provider;
134
+ code;
135
+ status;
136
+ requestId;
137
+ rateLimit;
138
+ details;
139
+ retryable;
140
+ constructor(message, code, options = {}) {
141
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
142
+ this.name = "ForgeError";
143
+ this.code = code;
144
+ this.provider = options.provider;
145
+ this.status = options.status;
146
+ this.requestId = options.requestId;
147
+ this.rateLimit = options.rateLimit;
148
+ this.details = options.details;
149
+ this.retryable = options.retryable ?? false;
150
+ }
151
+ };
152
+ var ForgeSignatureError = class extends ForgeError {
153
+ constructor(message = "Forge webhook signature is invalid") {
154
+ super(message, "SIGNATURE_INVALID");
155
+ this.name = "ForgeSignatureError";
156
+ }
157
+ };
158
+ //#endregion
159
+ //#region src/github/forge.ts
160
+ function repositoryPath(owner, repo) {
161
+ return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
162
+ }
163
+ function normalizeRepository(data) {
164
+ const owner = data.owner?.login ?? "";
165
+ const name = typeof data.name === "string" ? data.name : "";
166
+ return {
167
+ id: data.node_id === void 0 ? void 0 : String(data.node_id),
168
+ owner,
169
+ name,
170
+ fullName: typeof data.full_name === "string" ? data.full_name : `${owner}/${name}`,
171
+ defaultBranch: typeof data.default_branch === "string" ? data.default_branch : void 0,
172
+ private: typeof data.private === "boolean" ? data.private : void 0,
173
+ url: typeof data.html_url === "string" ? data.html_url : void 0
174
+ };
175
+ }
176
+ function normalizePullRequest(data) {
177
+ const head = data.head ?? {};
178
+ const base = data.base ?? {};
179
+ return {
180
+ id: data.node_id === void 0 ? void 0 : String(data.node_id),
181
+ number: Number(data.number),
182
+ state: data.state === "closed" ? "closed" : "open",
183
+ draft: typeof data.draft === "boolean" ? data.draft : void 0,
184
+ headSha: String(head.sha ?? ""),
185
+ headRef: typeof head.ref === "string" ? head.ref : void 0,
186
+ baseSha: typeof base.sha === "string" ? base.sha : void 0,
187
+ baseRef: typeof base.ref === "string" ? base.ref : void 0,
188
+ merged: typeof data.merged === "boolean" ? data.merged : void 0,
189
+ mergeCommitSha: typeof data.merge_commit_sha === "string" ? data.merge_commit_sha : void 0,
190
+ url: typeof data.html_url === "string" ? data.html_url : void 0
191
+ };
192
+ }
193
+ function normalizeStatus(data) {
194
+ return {
195
+ id: String(data.id ?? data.node_id ?? ""),
196
+ sha: String(data.sha ?? ""),
197
+ state: String(data.state ?? ""),
198
+ context: String(data.context ?? ""),
199
+ description: typeof data.description === "string" ? data.description : void 0,
200
+ targetUrl: typeof data.target_url === "string" ? data.target_url : void 0,
201
+ createdAt: typeof data.created_at === "string" ? new Date(data.created_at) : void 0,
202
+ raw: data
203
+ };
204
+ }
205
+ function normalizeCheck(data) {
206
+ return {
207
+ id: String(data.id ?? data.node_id ?? ""),
208
+ name: String(data.name ?? ""),
209
+ headSha: String(data.head_sha ?? ""),
210
+ status: data.status ?? "queued",
211
+ conclusion: typeof data.conclusion === "string" ? data.conclusion : void 0,
212
+ detailsUrl: typeof data.details_url === "string" ? data.details_url : void 0,
213
+ externalId: typeof data.external_id === "string" ? data.external_id : void 0,
214
+ startedAt: typeof data.started_at === "string" ? new Date(data.started_at) : void 0,
215
+ completedAt: typeof data.completed_at === "string" ? new Date(data.completed_at) : void 0,
216
+ raw: data
217
+ };
218
+ }
219
+ function checkInput(input) {
220
+ return {
221
+ ..."name" in input ? { name: input.name } : {},
222
+ ..."headSha" in input ? { head_sha: input.headSha } : {},
223
+ status: input.status,
224
+ conclusion: input.conclusion,
225
+ details_url: input.detailsUrl,
226
+ external_id: input.externalId,
227
+ started_at: input.startedAt instanceof Date ? input.startedAt.toISOString() : input.startedAt,
228
+ completed_at: input.completedAt instanceof Date ? input.completedAt.toISOString() : input.completedAt,
229
+ output: input.output
230
+ };
231
+ }
232
+ /**
233
+ * Repository-scoped GitHub implementation of provider-neutral forge operations.
234
+ * Every operation may throw `ForgeError` for transport or provider failures.
235
+ */
236
+ var GitHubForgeProvider = class {
237
+ transport;
238
+ path;
239
+ /**
240
+ * Creates a repository-scoped GitHub forge provider.
241
+ * @param options Repository coordinates and authenticated transport.
242
+ */
243
+ constructor(options) {
244
+ this.transport = options.transport;
245
+ this.path = repositoryPath(options.owner, options.repo);
246
+ }
247
+ /** @returns Normalized repository data and request metadata. */
248
+ async getRepository() {
249
+ const response = await this.transport.request({
250
+ method: "GET",
251
+ path: this.path
252
+ });
253
+ return {
254
+ ...response,
255
+ data: normalizeRepository(response.data)
256
+ };
257
+ }
258
+ /**
259
+ * Returns one normalized pull request.
260
+ * @param number Repository-local pull-request number.
261
+ * @returns Pull-request data and request metadata.
262
+ * @throws {ForgeError} When GitHub rejects the request.
263
+ */
264
+ async getPullRequest(number) {
265
+ const response = await this.transport.request({
266
+ method: "GET",
267
+ path: `${this.path}/pulls/${number}`
268
+ });
269
+ return {
270
+ ...response,
271
+ data: normalizePullRequest(response.data)
272
+ };
273
+ }
274
+ /**
275
+ * Lists every raw provider review for one pull request.
276
+ * @param number Repository-local pull-request number.
277
+ * @returns All reviews and final-page request metadata.
278
+ */
279
+ listPullRequestReviews(number) {
280
+ return this.paginate(`${this.path}/pulls/${number}/reviews`, (data) => data);
281
+ }
282
+ /**
283
+ * Returns one raw provider commit payload.
284
+ * @param sha Exact commit SHA.
285
+ * @returns Commit payload and request metadata.
286
+ */
287
+ getCommit(sha) {
288
+ return this.transport.request({
289
+ method: "GET",
290
+ path: `${this.path}/commits/${encodeURIComponent(sha)}`
291
+ });
292
+ }
293
+ /**
294
+ * Returns complete normalized commit-status history.
295
+ * @param sha Exact commit SHA.
296
+ * @returns All status pages and final-page request metadata.
297
+ */
298
+ async listCommitStatuses(sha) {
299
+ return this.paginate(`${this.path}/commits/${encodeURIComponent(sha)}/statuses`, (data) => data.map(normalizeStatus));
300
+ }
301
+ /**
302
+ * Publishes one commit status.
303
+ * @param input Exact SHA and status attributes.
304
+ * @returns Published status and request metadata.
305
+ */
306
+ async createCommitStatus(input) {
307
+ const response = await this.transport.request({
308
+ method: "POST",
309
+ path: `${this.path}/statuses/${encodeURIComponent(input.sha)}`,
310
+ body: {
311
+ state: input.state,
312
+ context: input.context,
313
+ description: input.description,
314
+ target_url: input.targetUrl
315
+ }
316
+ });
317
+ return {
318
+ ...response,
319
+ data: normalizeStatus(response.data)
320
+ };
321
+ }
322
+ /**
323
+ * Returns all normalized check runs, including reruns.
324
+ * @param sha Exact commit SHA.
325
+ * @returns All check pages and final-page request metadata.
326
+ */
327
+ async listCheckRuns(sha) {
328
+ return this.paginate(`${this.path}/commits/${encodeURIComponent(sha)}/check-runs?filter=all`, (data) => (data.check_runs ?? []).map(normalizeCheck), (data) => data.total_count);
329
+ }
330
+ /**
331
+ * Publishes one check run.
332
+ * @param input Check identity, exact head SHA, state, and output.
333
+ * @returns Published check and request metadata.
334
+ */
335
+ async createCheckRun(input) {
336
+ const response = await this.transport.request({
337
+ method: "POST",
338
+ path: `${this.path}/check-runs`,
339
+ body: checkInput(input)
340
+ });
341
+ return {
342
+ ...response,
343
+ data: normalizeCheck(response.data)
344
+ };
345
+ }
346
+ /**
347
+ * Changes one check run by provider ID.
348
+ * @param id GitHub check-run ID.
349
+ * @param input Attributes to change.
350
+ * @returns Updated check and request metadata.
351
+ */
352
+ async updateCheckRun(id, input) {
353
+ const response = await this.transport.request({
354
+ method: "PATCH",
355
+ path: `${this.path}/check-runs/${encodeURIComponent(id)}`,
356
+ body: checkInput(input)
357
+ });
358
+ return {
359
+ ...response,
360
+ data: normalizeCheck(response.data)
361
+ };
362
+ }
363
+ /**
364
+ * Lists raw deployment payloads using narrow filters.
365
+ * @param options Optional SHA, environment, and result limit.
366
+ * @returns Matching deployments and request metadata.
367
+ */
368
+ listDeployments(options = {}) {
369
+ const params = new URLSearchParams();
370
+ if (options.sha) params.set("sha", options.sha);
371
+ if (options.environment) params.set("environment", options.environment);
372
+ params.set("per_page", String(options.limit ?? 30));
373
+ return this.transport.request({
374
+ method: "GET",
375
+ path: `${this.path}/deployments?${params}`
376
+ });
377
+ }
378
+ /**
379
+ * Returns one raw deployment payload.
380
+ * @param id GitHub deployment ID.
381
+ * @returns Deployment payload and request metadata.
382
+ */
383
+ getDeployment(id) {
384
+ return this.transport.request({
385
+ method: "GET",
386
+ path: `${this.path}/deployments/${encodeURIComponent(id)}`
387
+ });
388
+ }
389
+ async paginate(path, items, totalCount) {
390
+ const separator = path.includes("?") ? "&" : "?";
391
+ const collected = [];
392
+ let page = 1;
393
+ let response;
394
+ let providerTotal;
395
+ do {
396
+ response = await this.transport.request({
397
+ method: "GET",
398
+ path: `${path}${separator}per_page=100&page=${page}`
399
+ });
400
+ const pageItems = items(response.data);
401
+ collected.push(...pageItems);
402
+ providerTotal ??= totalCount?.(response.data);
403
+ page += 1;
404
+ if (pageItems.length < 100) break;
405
+ } while (providerTotal === void 0 || collected.length < providerTotal);
406
+ return {
407
+ data: collected,
408
+ metadata: {
409
+ ...response.metadata,
410
+ pagination: {
411
+ pages: page - 1,
412
+ totalCount: providerTotal ?? collected.length
413
+ }
414
+ }
415
+ };
416
+ }
417
+ };
418
+ //#endregion
419
+ //#region src/github/transport.ts
420
+ function optionalInteger(value) {
421
+ if (value === null || value.trim() === "") return void 0;
422
+ const parsed = Number.parseInt(value, 10);
423
+ return Number.isFinite(parsed) ? parsed : void 0;
424
+ }
425
+ function responseRateLimit(headers) {
426
+ const limit = optionalInteger(headers.get("x-ratelimit-limit"));
427
+ const remaining = optionalInteger(headers.get("x-ratelimit-remaining"));
428
+ const used = optionalInteger(headers.get("x-ratelimit-used"));
429
+ const reset = optionalInteger(headers.get("x-ratelimit-reset"));
430
+ const retryAfter = optionalInteger(headers.get("retry-after"));
431
+ if (limit === void 0 && remaining === void 0 && used === void 0 && reset === void 0 && retryAfter === void 0) return;
432
+ return {
433
+ limit,
434
+ remaining,
435
+ used,
436
+ resetAt: reset === void 0 ? void 0 : /* @__PURE__ */ new Date(reset * 1e3),
437
+ retryAfterMs: retryAfter === void 0 ? void 0 : Math.max(0, retryAfter * 1e3)
438
+ };
439
+ }
440
+ function errorCode(status, rateLimit) {
441
+ if (status === 401) return "AUTHENTICATION_FAILED";
442
+ if (status === 404) return "NOT_FOUND";
443
+ if (status === 422) return "INVALID_INPUT";
444
+ if (status === 429 || status === 403 && (rateLimit?.remaining === 0 || rateLimit?.retryAfterMs !== void 0)) return "RATE_LIMITED";
445
+ return "PROVIDER_ERROR";
446
+ }
447
+ var GitHubTransport = class {
448
+ token;
449
+ baseUrl;
450
+ fetchImplementation;
451
+ /**
452
+ * Creates a GitHub REST transport with fixed or lazy credentials.
453
+ * @param options Token source, provider URL, and fetch override.
454
+ */
455
+ constructor(options) {
456
+ this.token = options.token;
457
+ this.baseUrl = (options.baseUrl ?? "https://api.github.com").replace(/\/+$/, "");
458
+ this.fetchImplementation = options.fetch ?? globalThis.fetch;
459
+ }
460
+ /**
461
+ * Executes one GitHub REST request.
462
+ * @param request Method, provider-relative path, body, headers, and signal.
463
+ * @returns Parsed data and metadata for this exact request.
464
+ * @throws {ForgeError} For transport, authentication, rate, or provider failures.
465
+ */
466
+ async request(request) {
467
+ const token = typeof this.token === "function" ? await this.token() : this.token;
468
+ let response;
469
+ try {
470
+ response = await this.fetchImplementation(`${this.baseUrl}${request.path}`, {
471
+ method: request.method,
472
+ headers: {
473
+ Accept: "application/vnd.github+json",
474
+ Authorization: `Bearer ${token}`,
475
+ "Content-Type": "application/json",
476
+ "X-GitHub-Api-Version": "2022-11-28",
477
+ ...request.headers
478
+ },
479
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body),
480
+ signal: request.signal
481
+ });
482
+ } catch (cause) {
483
+ throw new ForgeError("GitHub transport request failed", "TRANSPORT_ERROR", {
484
+ cause,
485
+ provider: "github",
486
+ retryable: true
487
+ });
488
+ }
489
+ const requestId = response.headers.get("x-github-request-id") ?? void 0;
490
+ const rateLimit = responseRateLimit(response.headers);
491
+ const metadata = {
492
+ provider: "github",
493
+ requestId,
494
+ status: response.status,
495
+ rateLimit
496
+ };
497
+ let text;
498
+ try {
499
+ text = response.status === 204 ? "" : await response.text();
500
+ } catch (cause) {
501
+ throw new ForgeError("GitHub transport response body failed", "TRANSPORT_ERROR", {
502
+ cause,
503
+ provider: "github",
504
+ status: response.status,
505
+ requestId,
506
+ rateLimit,
507
+ retryable: true
508
+ });
509
+ }
510
+ let data;
511
+ try {
512
+ data = text === "" ? void 0 : JSON.parse(text);
513
+ } catch {
514
+ data = text;
515
+ }
516
+ if (!response.ok) {
517
+ const code = errorCode(response.status, rateLimit);
518
+ throw new ForgeError(`GitHub API error: ${typeof data === "object" && data !== null && "message" in data && typeof data.message === "string" ? data.message : response.statusText}`, code, {
519
+ provider: "github",
520
+ status: response.status,
521
+ requestId,
522
+ rateLimit,
523
+ details: data,
524
+ retryable: code === "RATE_LIMITED" || response.status >= 500
525
+ });
526
+ }
527
+ return {
528
+ data,
529
+ metadata
530
+ };
531
+ }
532
+ };
533
+ //#endregion
534
+ //#region src/github/app.ts
535
+ function base64UrlJson(value) {
536
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
537
+ }
538
+ /**
539
+ * Creates a short-lived GitHub App JWT without mutating process credentials.
540
+ * @param credentials GitHub App ID and RSA private key.
541
+ * @param now Clock value used for issued-at and expiry claims.
542
+ * @returns Signed RS256 GitHub App JWT.
543
+ * @throws {ForgeError} When credentials are missing or the key cannot sign.
544
+ */
545
+ function createGitHubAppJwt(credentials, now = /* @__PURE__ */ new Date()) {
546
+ if (!credentials.appId || !credentials.privateKey) throw new ForgeError("GitHub App id and private key are required", "CONFIGURATION_ERROR", { provider: "github" });
547
+ const issuedAt = Math.floor(now.getTime() / 1e3) - 60;
548
+ const unsigned = `${base64UrlJson({
549
+ alg: "RS256",
550
+ typ: "JWT"
551
+ })}.${base64UrlJson({
552
+ iat: issuedAt,
553
+ exp: issuedAt + 600,
554
+ iss: String(credentials.appId)
555
+ })}`;
556
+ try {
557
+ const signer = createSign("RSA-SHA256");
558
+ signer.update(unsigned);
559
+ signer.end();
560
+ return `${unsigned}.${signer.sign(credentials.privateKey, "base64url")}`;
561
+ } catch (cause) {
562
+ throw new ForgeError("GitHub App private key could not sign a JWT", "CONFIGURATION_ERROR", {
563
+ cause,
564
+ provider: "github"
565
+ });
566
+ }
567
+ }
568
+ /**
569
+ * Request/job-scoped GitHub App authority.
570
+ *
571
+ * Keep this object inside a single request or job. Its token cache and in-flight
572
+ * acquisitions are instance fields, so installations cannot share credentials.
573
+ */
574
+ var GitHubAppAuth = class {
575
+ credentials;
576
+ baseUrl;
577
+ fetchImplementation;
578
+ now;
579
+ expirySkewMs;
580
+ tokens = /* @__PURE__ */ new Map();
581
+ issuedTokens = /* @__PURE__ */ new Map();
582
+ tokenRequests = /* @__PURE__ */ new Map();
583
+ authorizationRequests = /* @__PURE__ */ new Map();
584
+ revokedInstallations = /* @__PURE__ */ new Set();
585
+ nextTokenGeneration = 1;
586
+ /**
587
+ * Creates isolated GitHub App authority from credentials and dependencies.
588
+ * @param options Credentials, clock, provider URL, and fetch override.
589
+ */
590
+ constructor(options) {
591
+ this.credentials = {
592
+ appId: options.appId,
593
+ privateKey: options.privateKey
594
+ };
595
+ this.baseUrl = options.baseUrl;
596
+ this.fetchImplementation = options.fetch;
597
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
598
+ this.expirySkewMs = options.expirySkewMs ?? 6e4;
599
+ }
600
+ /**
601
+ * Clears locally cached credentials without remote revocation.
602
+ * @param installationId Optional installation to clear; omitted clears all.
603
+ * @returns Nothing.
604
+ */
605
+ clear(installationId) {
606
+ if (installationId === void 0) {
607
+ this.tokens.clear();
608
+ this.issuedTokens.clear();
609
+ this.tokenRequests.clear();
610
+ this.authorizationRequests.clear();
611
+ this.revokedInstallations.clear();
612
+ return;
613
+ }
614
+ const id = String(installationId);
615
+ this.clearCachedInstallation(id);
616
+ this.revokedInstallations.delete(id);
617
+ }
618
+ /**
619
+ * Acquires and verifies one installation/repository boundary.
620
+ * @param scope Installation ID and exact repository coordinates.
621
+ * @returns An isolated forge provider and revocation handle.
622
+ * @throws {ForgeError} When authentication, provider access, or scope fails.
623
+ */
624
+ async createInstallationContext(scope) {
625
+ const installationId = String(scope.installationId);
626
+ const fullName = `${scope.owner}/${scope.repo}`.toLowerCase();
627
+ await this.getAuthorizedToken(installationId, fullName);
628
+ let active = true;
629
+ let revocationInFlight;
630
+ const token = () => {
631
+ if (!active) throw new ForgeError("GitHub installation context has been revoked", "AUTHENTICATION_FAILED", { provider: "github" });
632
+ return this.getAuthorizedToken(installationId, fullName);
633
+ };
634
+ const forge = new GitHubForgeProvider({
635
+ owner: scope.owner,
636
+ repo: scope.repo,
637
+ transport: new GitHubTransport({
638
+ token,
639
+ baseUrl: this.baseUrl,
640
+ fetch: this.fetchImplementation
641
+ })
642
+ });
643
+ return {
644
+ installation: { id: installationId },
645
+ repository: {
646
+ owner: scope.owner,
647
+ name: scope.repo,
648
+ fullName: `${scope.owner}/${scope.repo}`
649
+ },
650
+ forge,
651
+ revoke: async () => {
652
+ active = false;
653
+ if (!revocationInFlight) revocationInFlight = this.revokeInstallationTokens(installationId).finally(() => {
654
+ revocationInFlight = void 0;
655
+ });
656
+ await revocationInFlight;
657
+ }
658
+ };
659
+ }
660
+ async getAuthorizedToken(installationId, fullName) {
661
+ if (this.revokedInstallations.has(installationId)) throw new ForgeError("GitHub App installation authority has been revoked", "AUTHENTICATION_FAILED", { provider: "github" });
662
+ const cached = await this.getInstallationToken(installationId);
663
+ if (this.revokedInstallations.has(installationId)) throw new ForgeError("GitHub App installation authority has been revoked", "AUTHENTICATION_FAILED", { provider: "github" });
664
+ if (cached.authorizedRepositories.has(fullName)) return cached.token;
665
+ const key = `${installationId}:${cached.generation}:${fullName}`;
666
+ let pending = this.authorizationRequests.get(key);
667
+ if (!pending) {
668
+ pending = this.verifyRepositoryScope(cached, fullName).finally(() => {
669
+ this.authorizationRequests.delete(key);
670
+ });
671
+ this.authorizationRequests.set(key, pending);
672
+ }
673
+ await pending;
674
+ if (this.revokedInstallations.has(installationId)) throw new ForgeError("GitHub App installation authority has been revoked", "AUTHENTICATION_FAILED", { provider: "github" });
675
+ return cached.token;
676
+ }
677
+ async getInstallationToken(installationId) {
678
+ const cached = this.tokens.get(installationId);
679
+ if (cached && cached.expiresAt - this.expirySkewMs > this.now().getTime()) return cached;
680
+ let pending = this.tokenRequests.get(installationId);
681
+ if (!pending) {
682
+ pending = this.acquireInstallationToken(installationId).finally(() => {
683
+ this.tokenRequests.delete(installationId);
684
+ });
685
+ this.tokenRequests.set(installationId, pending);
686
+ }
687
+ return pending;
688
+ }
689
+ async acquireInstallationToken(installationId) {
690
+ const response = await new GitHubTransport({
691
+ token: () => createGitHubAppJwt(this.credentials, this.now()),
692
+ baseUrl: this.baseUrl,
693
+ fetch: this.fetchImplementation
694
+ }).request({
695
+ method: "POST",
696
+ path: `/app/installations/${encodeURIComponent(installationId)}/access_tokens`
697
+ });
698
+ const token = response.data.token;
699
+ const expiresAt = Date.parse(response.data.expires_at ?? "");
700
+ if (!token || !Number.isFinite(expiresAt)) throw new ForgeError("GitHub returned an invalid installation token", "AUTHENTICATION_FAILED", {
701
+ provider: "github",
702
+ requestId: response.metadata.requestId
703
+ });
704
+ const cached = {
705
+ token,
706
+ expiresAt,
707
+ generation: this.nextTokenGeneration,
708
+ authorizedRepositories: /* @__PURE__ */ new Set()
709
+ };
710
+ this.nextTokenGeneration += 1;
711
+ const issued = this.issuedTokens.get(installationId) ?? /* @__PURE__ */ new Map();
712
+ for (const [issuedToken, issuedExpiry] of issued) if (issuedExpiry <= this.now().getTime()) issued.delete(issuedToken);
713
+ issued.set(token, expiresAt);
714
+ this.issuedTokens.set(installationId, issued);
715
+ if (this.revokedInstallations.has(installationId)) {
716
+ try {
717
+ await this.revokeToken(token);
718
+ issued.delete(token);
719
+ } catch (cause) {
720
+ const providerError = cause instanceof ForgeError ? cause : void 0;
721
+ throw new ForgeError("A GitHub token acquired during revocation could not be revoked", "PROVIDER_ERROR", {
722
+ cause,
723
+ provider: "github",
724
+ status: providerError?.status,
725
+ requestId: providerError?.requestId,
726
+ rateLimit: providerError?.rateLimit,
727
+ retryable: providerError?.retryable
728
+ });
729
+ }
730
+ throw new ForgeError("GitHub App installation authority was revoked during token acquisition", "AUTHENTICATION_FAILED", { provider: "github" });
731
+ }
732
+ this.tokens.set(installationId, cached);
733
+ return cached;
734
+ }
735
+ async verifyRepositoryScope(cached, fullName) {
736
+ const transport = new GitHubTransport({
737
+ token: cached.token,
738
+ baseUrl: this.baseUrl,
739
+ fetch: this.fetchImplementation
740
+ });
741
+ const separator = fullName.indexOf("/");
742
+ const owner = fullName.slice(0, separator);
743
+ const repo = fullName.slice(separator + 1);
744
+ try {
745
+ await transport.request({
746
+ method: "GET",
747
+ path: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
748
+ });
749
+ } catch (cause) {
750
+ const providerError = cause instanceof ForgeError ? cause : void 0;
751
+ if (providerError?.retryable || providerError?.code === "AUTHENTICATION_FAILED" || providerError?.code === "TRANSPORT_ERROR" || providerError?.status !== 403 && providerError?.status !== 404) throw cause;
752
+ throw new ForgeError(`GitHub App installation is not authorized for ${fullName}`, "AUTHORITY_MISMATCH", {
753
+ cause,
754
+ provider: "github",
755
+ status: providerError?.status,
756
+ requestId: providerError?.requestId,
757
+ rateLimit: providerError?.rateLimit
758
+ });
759
+ }
760
+ cached.authorizedRepositories.add(fullName);
761
+ }
762
+ async revokeInstallationTokens(installationId) {
763
+ this.revokedInstallations.add(installationId);
764
+ const issued = [...this.issuedTokens.get(installationId)?.keys() ?? []];
765
+ this.clearCachedInstallation(installationId, false);
766
+ if (issued.length === 0) return;
767
+ const retained = this.issuedTokens.get(installationId) ?? /* @__PURE__ */ new Map();
768
+ const failures = [];
769
+ for (const token of issued) try {
770
+ await this.revokeToken(token);
771
+ retained.delete(token);
772
+ } catch (error) {
773
+ failures.push(error);
774
+ }
775
+ if (retained.size === 0) this.issuedTokens.delete(installationId);
776
+ else this.issuedTokens.set(installationId, retained);
777
+ if (failures.length > 0) {
778
+ const failure = failures[0];
779
+ const providerError = failure instanceof ForgeError ? failure : void 0;
780
+ throw new ForgeError("One or more GitHub installation tokens could not be revoked", "PROVIDER_ERROR", {
781
+ cause: failure,
782
+ provider: "github",
783
+ status: providerError?.status,
784
+ requestId: providerError?.requestId,
785
+ rateLimit: providerError?.rateLimit,
786
+ retryable: providerError?.retryable
787
+ });
788
+ }
789
+ }
790
+ async revokeToken(token) {
791
+ await new GitHubTransport({
792
+ token,
793
+ baseUrl: this.baseUrl,
794
+ fetch: this.fetchImplementation
795
+ }).request({
796
+ method: "DELETE",
797
+ path: "/installation/token"
798
+ });
799
+ }
800
+ clearCachedInstallation(installationId, clearIssued = true) {
801
+ this.tokens.delete(installationId);
802
+ if (clearIssued) this.issuedTokens.delete(installationId);
803
+ this.tokenRequests.delete(installationId);
804
+ for (const key of this.authorizationRequests.keys()) if (key.startsWith(`${installationId}:`)) this.authorizationRequests.delete(key);
805
+ }
806
+ };
807
+ //#endregion
808
+ //#region src/github/fixtures.ts
809
+ /**
810
+ * Produces exact, repeatable webhook bytes and headers for integration suites.
811
+ * Pass the same delivery id for duplicates, or a new id with the same payload
812
+ * for a provider redelivery.
813
+ * @param options Secret, delivery identity, event name, and payload.
814
+ * @returns Exact bytes and matching GitHub delivery headers.
815
+ */
816
+ function createGitHubWebhookFixture(options) {
817
+ const rawBody = new TextEncoder().encode(JSON.stringify(options.payload));
818
+ const signature = createHmac("sha256", options.secret).update(rawBody).digest("hex");
819
+ return {
820
+ rawBody,
821
+ headers: {
822
+ "x-github-delivery": options.deliveryId,
823
+ "x-github-event": options.event,
824
+ "x-hub-signature-256": `sha256=${signature}`
825
+ }
826
+ };
827
+ }
828
+ //#endregion
130
829
  //#region src/github/rest.ts
131
830
  /**
132
831
  * GitHub REST API client
@@ -488,6 +1187,111 @@ var GitHubRepository = class {
488
1187
  }
489
1188
  }
490
1189
  /**
1190
+ * Publishes one GitHub commit status.
1191
+ * @param input Exact commit SHA and status attributes.
1192
+ * @returns Normalized published status.
1193
+ */
1194
+ async createCommitStatus(input) {
1195
+ const data = await this.rest.post(`/repos/${this.owner}/${this.repo}/statuses/${encodeURIComponent(input.sha)}`, {
1196
+ state: input.state,
1197
+ context: input.context,
1198
+ description: input.description,
1199
+ target_url: input.targetUrl
1200
+ });
1201
+ return this.mapCommitStatus(data);
1202
+ }
1203
+ /**
1204
+ * Returns complete commit-status history across GitHub pages.
1205
+ * @param sha Exact commit SHA.
1206
+ * @returns Every normalized status for the commit.
1207
+ */
1208
+ async listCommitStatuses(sha) {
1209
+ const statuses = [];
1210
+ let page = 1;
1211
+ while (true) {
1212
+ const data = await this.rest.get(`/repos/${this.owner}/${this.repo}/commits/${encodeURIComponent(sha)}/statuses?per_page=100&page=${page}`);
1213
+ statuses.push(...data.map((status) => this.mapCommitStatus(status)));
1214
+ if (data.length < 100) return statuses;
1215
+ page += 1;
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Publishes one GitHub check run.
1220
+ * @param input Check identity, exact head SHA, state, and output.
1221
+ * @returns Normalized published check run.
1222
+ */
1223
+ async createCheckRun(input) {
1224
+ const data = await this.rest.post(`/repos/${this.owner}/${this.repo}/check-runs`, this.mapCheckRunInput(input));
1225
+ return this.mapCheckRun(data);
1226
+ }
1227
+ /**
1228
+ * Changes one GitHub check run.
1229
+ * @param id GitHub check-run ID.
1230
+ * @param input Attributes to change.
1231
+ * @returns Normalized updated check run.
1232
+ */
1233
+ async updateCheckRun(id, input) {
1234
+ const data = await this.rest.patch(`/repos/${this.owner}/${this.repo}/check-runs/${encodeURIComponent(id)}`, this.mapCheckRunInput(input));
1235
+ return this.mapCheckRun(data);
1236
+ }
1237
+ /**
1238
+ * Returns complete GitHub check history, including reruns.
1239
+ * @param sha Exact commit SHA.
1240
+ * @returns Every normalized check run for the commit.
1241
+ */
1242
+ async listCheckRuns(sha) {
1243
+ const checkRuns = [];
1244
+ let page = 1;
1245
+ let totalCount;
1246
+ while (true) {
1247
+ const data = await this.rest.get(`/repos/${this.owner}/${this.repo}/commits/${encodeURIComponent(sha)}/check-runs?filter=all&per_page=100&page=${page}`);
1248
+ const pageRuns = data.check_runs ?? [];
1249
+ totalCount ??= data.total_count;
1250
+ checkRuns.push(...pageRuns.map((checkRun) => this.mapCheckRun(checkRun)));
1251
+ if (pageRuns.length < 100 || totalCount !== void 0 && checkRuns.length >= totalCount) return checkRuns;
1252
+ page += 1;
1253
+ }
1254
+ }
1255
+ mapCommitStatus(data) {
1256
+ return {
1257
+ id: String(data.id ?? data.node_id ?? ""),
1258
+ sha: String(data.sha ?? ""),
1259
+ state: String(data.state ?? ""),
1260
+ context: String(data.context ?? ""),
1261
+ description: typeof data.description === "string" ? data.description : void 0,
1262
+ targetUrl: typeof data.target_url === "string" ? data.target_url : void 0,
1263
+ createdAt: typeof data.created_at === "string" ? new Date(data.created_at) : void 0,
1264
+ raw: data
1265
+ };
1266
+ }
1267
+ mapCheckRunInput(input) {
1268
+ return {
1269
+ ..."name" in input ? { name: input.name } : {},
1270
+ ..."headSha" in input ? { head_sha: input.headSha } : {},
1271
+ status: input.status,
1272
+ conclusion: input.conclusion,
1273
+ details_url: input.detailsUrl,
1274
+ external_id: input.externalId,
1275
+ started_at: input.startedAt instanceof Date ? input.startedAt.toISOString() : input.startedAt,
1276
+ completed_at: input.completedAt instanceof Date ? input.completedAt.toISOString() : input.completedAt,
1277
+ output: input.output
1278
+ };
1279
+ }
1280
+ mapCheckRun(data) {
1281
+ return {
1282
+ id: String(data.id ?? data.node_id ?? ""),
1283
+ name: String(data.name ?? ""),
1284
+ headSha: String(data.head_sha ?? ""),
1285
+ status: data.status ?? "queued",
1286
+ conclusion: typeof data.conclusion === "string" ? data.conclusion : void 0,
1287
+ detailsUrl: typeof data.details_url === "string" ? data.details_url : void 0,
1288
+ externalId: typeof data.external_id === "string" ? data.external_id : void 0,
1289
+ startedAt: typeof data.started_at === "string" ? new Date(data.started_at) : void 0,
1290
+ completedAt: typeof data.completed_at === "string" ? new Date(data.completed_at) : void 0,
1291
+ raw: data
1292
+ };
1293
+ }
1294
+ /**
491
1295
  * Create a new repository from this repository as a template.
492
1296
  *
493
1297
  * Uses the GitHub "Generate" API: POST /repos/{template_owner}/{template_repo}/generate
@@ -512,6 +1316,298 @@ var GitHubRepository = class {
512
1316
  }
513
1317
  };
514
1318
  //#endregion
1319
+ //#region src/github/webhooks.ts
1320
+ function headerValue(headers, requestedName) {
1321
+ if (headers instanceof Headers) return headers.get(requestedName) ?? void 0;
1322
+ const requested = requestedName.toLowerCase();
1323
+ for (const [name, value] of Object.entries(headers)) {
1324
+ if (name.toLowerCase() !== requested) continue;
1325
+ return typeof value === "string" ? value : value?.[0];
1326
+ }
1327
+ }
1328
+ function object(value) {
1329
+ return typeof value === "object" && value !== null ? value : {};
1330
+ }
1331
+ function optionalString(value) {
1332
+ return typeof value === "string" && value !== "" ? value : void 0;
1333
+ }
1334
+ function actor(value) {
1335
+ const data = object(value);
1336
+ const login = optionalString(data.login);
1337
+ if (!login) return void 0;
1338
+ return {
1339
+ id: data.id === void 0 ? void 0 : String(data.id),
1340
+ login,
1341
+ type: optionalString(data.type)
1342
+ };
1343
+ }
1344
+ function repository(value) {
1345
+ const data = object(value);
1346
+ const ownerData = object(data.owner);
1347
+ const fullName = optionalString(data.full_name);
1348
+ const name = optionalString(data.name);
1349
+ const owner = optionalString(ownerData.login);
1350
+ if (!name || !owner && !fullName) return void 0;
1351
+ const resolvedOwner = owner ?? fullName?.split("/")[0] ?? "";
1352
+ return {
1353
+ id: data.node_id === void 0 ? void 0 : String(data.node_id),
1354
+ owner: resolvedOwner,
1355
+ name,
1356
+ fullName: fullName ?? `${resolvedOwner}/${name}`,
1357
+ defaultBranch: optionalString(data.default_branch),
1358
+ private: typeof data.private === "boolean" ? data.private : void 0,
1359
+ url: optionalString(data.html_url)
1360
+ };
1361
+ }
1362
+ function installation(value) {
1363
+ const data = object(value);
1364
+ if (data.id === void 0) return void 0;
1365
+ const accountData = object(data.account);
1366
+ return {
1367
+ id: String(data.id),
1368
+ account: optionalString(accountData.login),
1369
+ repositorySelection: data.repository_selection === "all" || data.repository_selection === "selected" ? data.repository_selection : void 0
1370
+ };
1371
+ }
1372
+ function pullRequest(value) {
1373
+ const data = object(value);
1374
+ const head = object(data.head);
1375
+ const base = object(data.base);
1376
+ return {
1377
+ id: data.node_id === void 0 ? void 0 : String(data.node_id),
1378
+ number: Number(data.number),
1379
+ state: data.state === "closed" ? "closed" : "open",
1380
+ draft: typeof data.draft === "boolean" ? data.draft : void 0,
1381
+ headSha: String(head.sha ?? ""),
1382
+ headRef: optionalString(head.ref),
1383
+ baseSha: optionalString(base.sha),
1384
+ baseRef: optionalString(base.ref),
1385
+ merged: typeof data.merged === "boolean" ? data.merged : void 0,
1386
+ mergeCommitSha: optionalString(data.merge_commit_sha),
1387
+ url: optionalString(data.html_url)
1388
+ };
1389
+ }
1390
+ function check(value) {
1391
+ const data = object(value);
1392
+ const app = object(data.app);
1393
+ return {
1394
+ id: String(data.id ?? data.node_id ?? ""),
1395
+ name: String(data.name ?? app.name ?? "check_suite"),
1396
+ headSha: String(data.head_sha ?? ""),
1397
+ status: data.status ?? "queued",
1398
+ conclusion: typeof data.conclusion === "string" ? data.conclusion : void 0,
1399
+ detailsUrl: optionalString(data.details_url),
1400
+ externalId: optionalString(data.external_id),
1401
+ startedAt: optionalString(data.started_at) ? new Date(String(data.started_at)) : void 0,
1402
+ completedAt: optionalString(data.completed_at) ? new Date(String(data.completed_at)) : void 0,
1403
+ raw: data
1404
+ };
1405
+ }
1406
+ function occurrence(payload) {
1407
+ const candidates = [
1408
+ object(payload.review).submitted_at,
1409
+ object(payload.check_run).completed_at,
1410
+ object(payload.check_run).started_at,
1411
+ payload.created_at,
1412
+ object(payload.status).created_at,
1413
+ object(payload.deployment_status).created_at,
1414
+ object(payload.deployment).created_at,
1415
+ object(payload.pull_request).updated_at,
1416
+ object(payload.head_commit).timestamp
1417
+ ];
1418
+ for (const candidate of candidates) {
1419
+ if (typeof candidate !== "string") continue;
1420
+ const timestamp = new Date(candidate);
1421
+ if (!Number.isNaN(timestamp.getTime())) return timestamp;
1422
+ }
1423
+ }
1424
+ function normalizeObservation(event, payload, action) {
1425
+ if (event === "ping") return {
1426
+ kind: "availability",
1427
+ available: true,
1428
+ message: optionalString(payload.zen)
1429
+ };
1430
+ if (event === "installation" || event === "installation_repositories") {
1431
+ const installationRef = installation(payload.installation);
1432
+ if (!installationRef) return { kind: "unknown" };
1433
+ return {
1434
+ kind: "installation",
1435
+ installation: installationRef,
1436
+ repositories: [
1437
+ ...Array.isArray(payload.repositories) ? payload.repositories : [],
1438
+ ...Array.isArray(payload.repositories_added) ? payload.repositories_added : [],
1439
+ ...Array.isArray(payload.repositories_removed) ? payload.repositories_removed : []
1440
+ ].map(repository).filter((item) => item !== void 0)
1441
+ };
1442
+ }
1443
+ if (event === "repository") {
1444
+ const repositoryRef = repository(payload.repository);
1445
+ return repositoryRef ? {
1446
+ kind: "repository",
1447
+ repository: repositoryRef
1448
+ } : { kind: "unknown" };
1449
+ }
1450
+ if (event === "pull_request") {
1451
+ const pullRequestRef = pullRequest(payload.pull_request);
1452
+ return action === "closed" && pullRequestRef.merged ? {
1453
+ kind: "merge",
1454
+ pullRequest: pullRequestRef,
1455
+ mergeCommitSha: pullRequestRef.mergeCommitSha
1456
+ } : {
1457
+ kind: "pull_request",
1458
+ pullRequest: pullRequestRef
1459
+ };
1460
+ }
1461
+ if (event === "pull_request_review") {
1462
+ const review = object(payload.review);
1463
+ return {
1464
+ kind: "review",
1465
+ pullRequest: pullRequest(payload.pull_request),
1466
+ review: {
1467
+ id: String(review.id ?? review.node_id ?? ""),
1468
+ state: String(review.state ?? ""),
1469
+ body: optionalString(review.body),
1470
+ commitSha: optionalString(review.commit_id),
1471
+ submittedAt: optionalString(review.submitted_at) ? new Date(String(review.submitted_at)) : void 0,
1472
+ author: actor(review.user)
1473
+ }
1474
+ };
1475
+ }
1476
+ if (event === "push") return {
1477
+ kind: "push",
1478
+ ref: String(payload.ref ?? ""),
1479
+ beforeSha: optionalString(payload.before),
1480
+ afterSha: String(payload.after ?? ""),
1481
+ forced: typeof payload.forced === "boolean" ? payload.forced : void 0,
1482
+ created: typeof payload.created === "boolean" ? payload.created : void 0,
1483
+ deleted: typeof payload.deleted === "boolean" ? payload.deleted : void 0
1484
+ };
1485
+ if (event === "status") return {
1486
+ kind: "status",
1487
+ status: {
1488
+ id: String(payload.id ?? ""),
1489
+ sha: String(payload.sha ?? ""),
1490
+ state: String(payload.state ?? ""),
1491
+ context: String(payload.context ?? ""),
1492
+ description: optionalString(payload.description),
1493
+ targetUrl: optionalString(payload.target_url),
1494
+ createdAt: optionalString(payload.created_at) ? new Date(String(payload.created_at)) : void 0,
1495
+ raw: payload
1496
+ }
1497
+ };
1498
+ if (event === "check_run" || event === "check_suite") return {
1499
+ kind: "check",
1500
+ check: check(payload.check_run ?? payload.check_suite)
1501
+ };
1502
+ if (event === "merge_group") {
1503
+ const mergeGroup = object(payload.merge_group);
1504
+ return {
1505
+ kind: "merge_group",
1506
+ headSha: String(mergeGroup.head_sha ?? ""),
1507
+ headRef: optionalString(mergeGroup.head_ref),
1508
+ baseSha: optionalString(mergeGroup.base_sha),
1509
+ baseRef: optionalString(mergeGroup.base_ref)
1510
+ };
1511
+ }
1512
+ if (event === "deployment" || event === "deployment_status") {
1513
+ const deployment = object(payload.deployment);
1514
+ const deploymentStatus = object(payload.deployment_status);
1515
+ return {
1516
+ kind: "deployment",
1517
+ deployment: {
1518
+ id: String(deployment.id ?? ""),
1519
+ sha: optionalString(deployment.sha),
1520
+ ref: optionalString(deployment.ref),
1521
+ environment: optionalString(deploymentStatus.environment) ?? optionalString(deployment.environment),
1522
+ state: optionalString(deploymentStatus.state),
1523
+ url: optionalString(deploymentStatus.environment_url) ?? optionalString(deploymentStatus.target_url)
1524
+ }
1525
+ };
1526
+ }
1527
+ return { kind: "unknown" };
1528
+ }
1529
+ /**
1530
+ * Normalizes one already-verified GitHub payload.
1531
+ * @param deliveryId Stable provider delivery identity.
1532
+ * @param event GitHub event header value.
1533
+ * @param raw Parsed provider payload.
1534
+ * @param receivedAt Local receipt timestamp.
1535
+ * @returns A provider-neutral event envelope preserving the parsed payload.
1536
+ */
1537
+ function normalizeGitHubWebhook(deliveryId, event, raw, receivedAt = /* @__PURE__ */ new Date()) {
1538
+ const payload = object(raw);
1539
+ const action = optionalString(payload.action);
1540
+ return {
1541
+ provider: "github",
1542
+ deliveryId,
1543
+ event,
1544
+ action,
1545
+ occurredAt: occurrence(payload),
1546
+ receivedAt,
1547
+ installation: installation(payload.installation),
1548
+ repository: repository(payload.repository),
1549
+ actor: actor(payload.sender),
1550
+ observation: normalizeObservation(event, payload, action),
1551
+ raw
1552
+ };
1553
+ }
1554
+ var GitHubWebhookVerifier = class {
1555
+ secrets;
1556
+ now;
1557
+ /**
1558
+ * Creates a verifier with a current secret and optional rotation secrets.
1559
+ * @param options Current/rotation secrets and optional clock.
1560
+ * @throws {ForgeError} When no non-empty secret is configured.
1561
+ */
1562
+ constructor(options) {
1563
+ this.secrets = typeof options.secrets === "string" ? [options.secrets] : [...options.secrets];
1564
+ if (this.secrets.length === 0 || this.secrets.some((secret) => !secret)) throw new ForgeError("At least one non-empty GitHub webhook secret is required", "CONFIGURATION_ERROR", { provider: "github" });
1565
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1566
+ }
1567
+ /**
1568
+ * Verifies unchanged provider bytes with constant-time comparisons.
1569
+ * @param rawBody Exact bytes received from the HTTP server.
1570
+ * @param signature GitHub SHA-256 signature header.
1571
+ * @returns Index of the configured secret that matched.
1572
+ * @throws {ForgeSignatureError} When the signature is missing or invalid.
1573
+ */
1574
+ verify(rawBody, signature) {
1575
+ if (!signature?.startsWith("sha256=")) throw new ForgeSignatureError();
1576
+ const suppliedHex = signature.slice(7);
1577
+ if (!/^[a-fA-F0-9]{64}$/.test(suppliedHex)) throw new ForgeSignatureError();
1578
+ const supplied = Buffer.from(suppliedHex, "hex");
1579
+ let matchedIndex = -1;
1580
+ this.secrets.forEach((secret, index) => {
1581
+ if (timingSafeEqual(createHmac("sha256", secret).update(rawBody).digest(), supplied)) matchedIndex = index;
1582
+ });
1583
+ if (matchedIndex === -1) throw new ForgeSignatureError();
1584
+ return matchedIndex;
1585
+ }
1586
+ /**
1587
+ * Verifies raw bytes before decoding and normalizes the delivery.
1588
+ * @param rawBody Exact bytes received from the HTTP server.
1589
+ * @param headers Case-insensitive GitHub delivery headers.
1590
+ * @returns Verified normalized delivery.
1591
+ * @throws {ForgeError} For invalid signatures, headers, UTF-8, or JSON.
1592
+ */
1593
+ verifyAndNormalize(rawBody, headers) {
1594
+ this.verify(rawBody, headerValue(headers, "x-hub-signature-256"));
1595
+ const deliveryId = headerValue(headers, "x-github-delivery");
1596
+ const event = headerValue(headers, "x-github-event");
1597
+ if (!deliveryId || !event) throw new ForgeError("GitHub delivery identity and event headers are required", "INVALID_INPUT", { provider: "github" });
1598
+ let payload;
1599
+ try {
1600
+ payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(rawBody));
1601
+ } catch (cause) {
1602
+ throw new ForgeError("GitHub webhook body is not valid UTF-8 JSON", "INVALID_INPUT", {
1603
+ cause,
1604
+ provider: "github"
1605
+ });
1606
+ }
1607
+ return normalizeGitHubWebhook(deliveryId, event, payload, this.now());
1608
+ }
1609
+ };
1610
+ //#endregion
515
1611
  //#region src/parsing.ts
516
1612
  /**
517
1613
  * Issue body parsing and rendering utilities
@@ -738,6 +1834,6 @@ function detectTemplateFromLabels(labels, templates) {
738
1834
  return bestMatch;
739
1835
  }
740
1836
  //#endregion
741
- export { GitHubRepository, RepositoryError, RepositoryErrorCode, detectTemplateFromLabels, fetchIssueTemplates, getIssueField, getRepository, loadIssueTemplate, parseIssueBody, parseIssueTemplate, renderIssueBody, updateIssueField };
1837
+ export { ForgeError, ForgeSignatureError, GitHubAppAuth, GitHubForgeProvider, GitHubRepository, GitHubTransport, GitHubWebhookVerifier, RepositoryError, RepositoryErrorCode, createGitHubAppJwt, createGitHubWebhookFixture, detectTemplateFromLabels, fetchIssueTemplates, getIssueField, getRepository, loadIssueTemplate, normalizeGitHubWebhook, parseIssueBody, parseIssueTemplate, renderIssueBody, updateIssueField };
742
1838
 
743
1839
  //# sourceMappingURL=index.js.map