pi-ghwebhook 0.0.0-stage → 0.1.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -2
  4. package/README_zh.md +168 -0
  5. package/dist/bus/event-bus.d.ts +14 -0
  6. package/dist/bus/event-bus.d.ts.map +1 -0
  7. package/dist/bus/event-bus.js +53 -0
  8. package/dist/bus/event-bus.js.map +1 -0
  9. package/dist/bus/guards.d.ts +10 -0
  10. package/dist/bus/guards.d.ts.map +1 -0
  11. package/dist/bus/guards.js +88 -0
  12. package/dist/bus/guards.js.map +1 -0
  13. package/dist/bus/normalizer.d.ts +14 -0
  14. package/dist/bus/normalizer.d.ts.map +1 -0
  15. package/dist/bus/normalizer.js +25 -0
  16. package/dist/bus/normalizer.js.map +1 -0
  17. package/dist/config/loader.d.ts +6 -0
  18. package/dist/config/loader.d.ts.map +1 -0
  19. package/dist/config/loader.js +63 -0
  20. package/dist/config/loader.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +81 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/template/renderer.d.ts +4 -0
  26. package/dist/template/renderer.d.ts.map +1 -0
  27. package/dist/template/renderer.js +121 -0
  28. package/dist/template/renderer.js.map +1 -0
  29. package/dist/transport/forwarder.d.ts +18 -0
  30. package/dist/transport/forwarder.d.ts.map +1 -0
  31. package/dist/transport/forwarder.js +73 -0
  32. package/dist/transport/forwarder.js.map +1 -0
  33. package/dist/transport/loopback.d.ts +20 -0
  34. package/dist/transport/loopback.d.ts.map +1 -0
  35. package/dist/transport/loopback.js +96 -0
  36. package/dist/transport/loopback.js.map +1 -0
  37. package/dist/types/enums.d.ts +41 -0
  38. package/dist/types/enums.d.ts.map +1 -0
  39. package/dist/types/enums.js +44 -0
  40. package/dist/types/enums.js.map +1 -0
  41. package/dist/types/payload.d.ts +28 -0
  42. package/dist/types/payload.d.ts.map +1 -0
  43. package/dist/types/payload.js +2 -0
  44. package/dist/types/payload.js.map +1 -0
  45. package/dist/types/schema.d.ts +59 -0
  46. package/dist/types/schema.d.ts.map +1 -0
  47. package/dist/types/schema.js +49 -0
  48. package/dist/types/schema.js.map +1 -0
  49. package/package.json +64 -4
  50. package/src/bus/event-bus.ts +72 -0
  51. package/src/bus/guards.ts +118 -0
  52. package/src/bus/normalizer.ts +32 -0
  53. package/src/config/loader.ts +80 -0
  54. package/src/index.ts +106 -0
  55. package/src/template/renderer.ts +145 -0
  56. package/src/transport/forwarder.ts +92 -0
  57. package/src/transport/loopback.ts +130 -0
  58. package/src/types/enums.ts +42 -0
  59. package/src/types/payload.ts +75 -0
  60. package/src/types/schema.ts +105 -0
@@ -0,0 +1,145 @@
1
+ import type { WebhookPayload } from "../types/payload.js";
2
+
3
+ const VARIABLE_PATTERN = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
4
+
5
+ function extractRepoVars(
6
+ payload: WebhookPayload,
7
+ vars: Record<string, string>,
8
+ ): void {
9
+ if (payload.repository?.full_name) {
10
+ vars.repo = String(payload.repository.full_name);
11
+ vars.repo_url = String(payload.repository.html_url ?? "");
12
+ }
13
+ }
14
+
15
+ function extractBranchVars(
16
+ payload: WebhookPayload,
17
+ vars: Record<string, string>,
18
+ fallbackBranch: string,
19
+ ): void {
20
+ const branch =
21
+ payload.workflow_run?.head_branch ??
22
+ payload.pull_request?.head?.ref ??
23
+ payload.check_suite?.head_branch ??
24
+ payload.workflow_job?.head_branch ??
25
+ fallbackBranch;
26
+ if (branch) vars.branch = String(branch);
27
+
28
+ const headSha =
29
+ payload.workflow_run?.head_sha ??
30
+ payload.pull_request?.head?.sha ??
31
+ payload.check_suite?.head_sha ??
32
+ payload.after ??
33
+ payload.head_commit?.id;
34
+ if (headSha) vars.head_sha = String(headSha);
35
+
36
+ if (payload.head_commit?.message) {
37
+ vars.commit_message = String(payload.head_commit.message);
38
+ }
39
+ }
40
+
41
+ function extractWorkflowVars(
42
+ payload: WebhookPayload,
43
+ vars: Record<string, string>,
44
+ ): void {
45
+ const wf = payload.workflow_run;
46
+ if (!wf) return;
47
+ if (wf.id != null) vars.run_id = String(wf.id);
48
+ if (wf.html_url) vars.run_url = String(wf.html_url);
49
+ if (wf.name) {
50
+ vars.workflow = String(wf.name);
51
+ vars.workflow_name = String(wf.name);
52
+ }
53
+ if (wf.conclusion) vars.conclusion = String(wf.conclusion);
54
+ if (wf.run_number != null) vars.run_number = String(wf.run_number);
55
+ if (wf.run_attempt != null) vars.run_attempt = String(wf.run_attempt);
56
+ }
57
+
58
+ function extractPullRequestVars(
59
+ payload: WebhookPayload,
60
+ vars: Record<string, string>,
61
+ ): void {
62
+ const pr = payload.pull_request;
63
+ if (!pr) return;
64
+ if (pr.number != null) vars.pr_number = String(pr.number);
65
+ if (pr.title) vars.pr_title = String(pr.title);
66
+ if (pr.html_url) vars.pr_url = String(pr.html_url);
67
+ if (pr.state) vars.pr_state = String(pr.state);
68
+ }
69
+
70
+ function extractIssueVars(
71
+ payload: WebhookPayload,
72
+ vars: Record<string, string>,
73
+ ): void {
74
+ const iss = payload.issue;
75
+ if (!iss) return;
76
+ if (iss.number != null) vars.issue_number = String(iss.number);
77
+ if (iss.title) vars.issue_title = String(iss.title);
78
+ if (iss.html_url) vars.issue_url = String(iss.html_url);
79
+ if (iss.body != null) vars.issue_body = String(iss.body);
80
+ }
81
+
82
+ function extractReviewAndCommentVars(
83
+ payload: WebhookPayload,
84
+ vars: Record<string, string>,
85
+ ): void {
86
+ if (payload.review) {
87
+ const rev = payload.review;
88
+ if (rev.state) vars.review_state = String(rev.state);
89
+ if (rev.body != null) vars.review_body = String(rev.body);
90
+ if (rev.html_url) vars.review_url = String(rev.html_url);
91
+ }
92
+
93
+ if (payload.comment) {
94
+ const c = payload.comment;
95
+ if (c.body != null) vars.comment_body = String(c.body);
96
+ if (c.html_url) vars.comment_url = String(c.html_url);
97
+ }
98
+ }
99
+
100
+ function extractSenderVars(
101
+ payload: WebhookPayload,
102
+ vars: Record<string, string>,
103
+ ): void {
104
+ const senderLogin =
105
+ payload.sender?.login ??
106
+ payload.review?.user?.login ??
107
+ payload.comment?.user?.login;
108
+ if (senderLogin) vars.sender = String(senderLogin);
109
+
110
+ const senderType =
111
+ payload.sender?.type ??
112
+ payload.review?.user?.type ??
113
+ payload.comment?.user?.type;
114
+ if (senderType) vars.sender_type = String(senderType);
115
+ }
116
+
117
+ export function extractVariables(
118
+ payload: WebhookPayload,
119
+ fallbackBranch = "",
120
+ ): Record<string, string> {
121
+ const vars: Record<string, string> = {};
122
+
123
+ if (payload.action) {
124
+ vars.action = String(payload.action);
125
+ }
126
+
127
+ extractRepoVars(payload, vars);
128
+ extractBranchVars(payload, vars, fallbackBranch);
129
+ extractWorkflowVars(payload, vars);
130
+ extractPullRequestVars(payload, vars);
131
+ extractIssueVars(payload, vars);
132
+ extractReviewAndCommentVars(payload, vars);
133
+ extractSenderVars(payload, vars);
134
+
135
+ return vars;
136
+ }
137
+
138
+ export function renderTemplate(
139
+ template: string,
140
+ vars: Record<string, string>,
141
+ ): string {
142
+ return template.replace(VARIABLE_PATTERN, (match, key: string) => {
143
+ return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
144
+ });
145
+ }
@@ -0,0 +1,92 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+
3
+ export interface ForwarderOptions {
4
+ repo: string;
5
+ url: string;
6
+ events?: string[];
7
+ secret?: string;
8
+ onLog?: (line: string) => void;
9
+ onError?: (err: Error) => void;
10
+ }
11
+
12
+ export class GhForwarder {
13
+ private child: ChildProcess | null = null;
14
+ private readonly options: ForwarderOptions;
15
+ private isStopping = false;
16
+
17
+ constructor(options: ForwarderOptions) {
18
+ this.options = options;
19
+ }
20
+
21
+ isRunning(): boolean {
22
+ return this.child !== null && !this.child.killed;
23
+ }
24
+
25
+ start(): void {
26
+ if (this.child) return;
27
+
28
+ // Default to '*' to listen to ALL events from the repository
29
+ const eventsArg =
30
+ this.options.events && this.options.events.length > 0
31
+ ? this.options.events.join(",")
32
+ : "*";
33
+
34
+ const args = [
35
+ "webhook",
36
+ "forward",
37
+ `--repo=${this.options.repo}`,
38
+ `--url=${this.options.url}`,
39
+ `--events=${eventsArg}`,
40
+ ];
41
+
42
+ if (this.options.secret) {
43
+ args.push(`--secret=${this.options.secret}`);
44
+ }
45
+
46
+ try {
47
+ this.child = spawn("gh", args, {
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ });
50
+
51
+ this.child.stdout?.on("data", (data: Buffer) => {
52
+ const text = data.toString("utf8").trim();
53
+ if (text && this.options.onLog) {
54
+ this.options.onLog(text);
55
+ }
56
+ });
57
+
58
+ this.child.stderr?.on("data", (data: Buffer) => {
59
+ const text = data.toString("utf8").trim();
60
+ if (text && !this.isStopping && this.options.onLog) {
61
+ this.options.onLog(text);
62
+ }
63
+ });
64
+
65
+ this.child.on("error", (err) => {
66
+ if (!this.isStopping && this.options.onError) {
67
+ this.options.onError(err);
68
+ }
69
+ });
70
+
71
+ this.child.on("exit", () => {
72
+ this.child = null;
73
+ });
74
+ } catch (err) {
75
+ if (this.options.onError && err instanceof Error) {
76
+ this.options.onError(err);
77
+ }
78
+ }
79
+ }
80
+
81
+ stop(): void {
82
+ this.isStopping = true;
83
+ if (this.child) {
84
+ try {
85
+ this.child.kill("SIGTERM");
86
+ } catch {
87
+ // Child process may already be dead
88
+ }
89
+ this.child = null;
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,130 @@
1
+ import {
2
+ createServer,
3
+ type Server,
4
+ type IncomingMessage,
5
+ type ServerResponse,
6
+ } from "node:http";
7
+ import { createHmac, timingSafeEqual } from "node:crypto";
8
+ import type { WebhookPayload } from "../types/payload.js";
9
+
10
+ export type WebhookEventHandler = (
11
+ event: string,
12
+ payload: WebhookPayload,
13
+ ) => void | Promise<void>;
14
+
15
+ export interface LoopbackOptions {
16
+ secret?: string;
17
+ onEvent: WebhookEventHandler;
18
+ }
19
+
20
+ export class LoopbackServer {
21
+ private server: Server | null = null;
22
+ private port = 0;
23
+ private readonly secret?: string;
24
+ private readonly onEvent: WebhookEventHandler;
25
+
26
+ constructor(options: LoopbackOptions) {
27
+ this.secret = options.secret;
28
+ this.onEvent = options.onEvent;
29
+ }
30
+
31
+ getPort(): number {
32
+ return this.port;
33
+ }
34
+
35
+ getUrl(): string {
36
+ return `http://127.0.0.1:${this.port}/webhook`;
37
+ }
38
+
39
+ async start(): Promise<number> {
40
+ this.server = createServer((req, res) => {
41
+ this.handleRequest(req, res);
42
+ });
43
+
44
+ return new Promise((resolve, reject) => {
45
+ this.server!.once("error", reject);
46
+ this.server!.listen(0, "127.0.0.1", () => {
47
+ const addr = this.server!.address();
48
+ if (typeof addr === "object" && addr !== null) {
49
+ this.port = addr.port;
50
+ resolve(this.port);
51
+ } else {
52
+ reject(new Error("Failed to get loopback server port"));
53
+ }
54
+ });
55
+ });
56
+ }
57
+
58
+ async stop(): Promise<void> {
59
+ if (!this.server) return;
60
+ const s = this.server;
61
+ this.server = null;
62
+ return new Promise((resolve) => {
63
+ s.close(() => resolve());
64
+ });
65
+ }
66
+
67
+ private handleRequest(req: IncomingMessage, res: ServerResponse): void {
68
+ if (req.method !== "POST") {
69
+ res.writeHead(405, { "Content-Type": "application/json" });
70
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
71
+ return;
72
+ }
73
+
74
+ const event = req.headers["x-github-event"];
75
+ if (typeof event !== "string") {
76
+ res.writeHead(400, { "Content-Type": "application/json" });
77
+ res.end(JSON.stringify({ error: "Missing X-GitHub-Event header" }));
78
+ return;
79
+ }
80
+
81
+ const signature = req.headers["x-hub-signature-256"];
82
+ const chunks: Buffer[] = [];
83
+
84
+ req.on("data", (chunk: Buffer) => {
85
+ chunks.push(chunk);
86
+ });
87
+
88
+ req.on("end", () => {
89
+ const rawBody = Buffer.concat(chunks);
90
+
91
+ if (this.secret && !this.verifySignature(rawBody, signature)) {
92
+ res.writeHead(401, { "Content-Type": "application/json" });
93
+ res.end(JSON.stringify({ error: "Invalid signature" }));
94
+ return;
95
+ }
96
+
97
+ res.writeHead(200, { "Content-Type": "application/json" });
98
+ res.end(JSON.stringify({ received: true }));
99
+
100
+ try {
101
+ const payload = JSON.parse(rawBody.toString("utf8")) as WebhookPayload;
102
+ Promise.resolve(this.onEvent(event, payload)).catch((err) => {
103
+ console.error("[pi-ghwebhook] Error handling event:", err);
104
+ });
105
+ } catch (err) {
106
+ console.error("[pi-ghwebhook] Failed to parse JSON body:", err);
107
+ }
108
+ });
109
+ }
110
+
111
+ private verifySignature(
112
+ rawBody: Buffer,
113
+ signature: string | string[] | undefined,
114
+ ): boolean {
115
+ if (typeof signature !== "string" || !signature.startsWith("sha256=")) {
116
+ return false;
117
+ }
118
+
119
+ const hmac = createHmac("sha256", this.secret!)
120
+ .update(rawBody)
121
+ .digest("hex");
122
+ const expected = `sha256=${hmac}`;
123
+
124
+ if (signature.length !== expected.length) {
125
+ return false;
126
+ }
127
+
128
+ return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
129
+ }
130
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Standard GitHub Webhook Event names as sent in the `X-GitHub-Event` HTTP header.
3
+ */
4
+ export enum GitHubEvent {
5
+ WorkflowRun = "workflow_run",
6
+ WorkflowJob = "workflow_job",
7
+ PullRequest = "pull_request",
8
+ PullRequestReview = "pull_request_review",
9
+ PullRequestReviewComment = "pull_request_review_comment",
10
+ CheckSuite = "check_suite",
11
+ CheckRun = "check_run",
12
+ IssueComment = "issue_comment",
13
+ Issues = "issues",
14
+ Push = "push",
15
+ Release = "release",
16
+ }
17
+
18
+ /**
19
+ * Standard conclusions for GitHub Actions workflow runs and check suites,
20
+ * aligned with GitHub OpenAPI specification (@octokit/openapi-webhooks-types).
21
+ */
22
+ export enum WorkflowConclusion {
23
+ Success = "success",
24
+ Failure = "failure",
25
+ Cancelled = "cancelled",
26
+ TimedOut = "timed_out",
27
+ Neutral = "neutral",
28
+ ActionRequired = "action_required",
29
+ Skipped = "skipped",
30
+ Stale = "stale",
31
+ StartupFailure = "startup_failure",
32
+ }
33
+
34
+ /**
35
+ * Review state for pull request reviews.
36
+ */
37
+ export enum ReviewState {
38
+ Approved = "approved",
39
+ ChangesRequested = "changes_requested",
40
+ Commented = "commented",
41
+ Dismissed = "dismissed",
42
+ }
@@ -0,0 +1,75 @@
1
+ import type { components } from "@octokit/openapi-webhooks-types";
2
+
3
+ export type WebhookSchemas = components["schemas"];
4
+
5
+ // Workflow Run
6
+ export type WorkflowRunCompletedPayload =
7
+ WebhookSchemas["webhook-workflow-run-completed"];
8
+ export type WorkflowRunInProgressPayload =
9
+ WebhookSchemas["webhook-workflow-run-in-progress"];
10
+ export type WorkflowRunRequestedPayload =
11
+ WebhookSchemas["webhook-workflow-run-requested"];
12
+ export type WorkflowRunPayload =
13
+ | WorkflowRunCompletedPayload
14
+ | WorkflowRunInProgressPayload
15
+ | WorkflowRunRequestedPayload;
16
+
17
+ // Pull Request Review
18
+ export type PullRequestReviewSubmittedPayload =
19
+ WebhookSchemas["webhook-pull-request-review-submitted"];
20
+ export type PullRequestReviewEditedPayload =
21
+ WebhookSchemas["webhook-pull-request-review-edited"];
22
+ export type PullRequestReviewDismissedPayload =
23
+ WebhookSchemas["webhook-pull-request-review-dismissed"];
24
+ export type PullRequestReviewPayload =
25
+ | PullRequestReviewSubmittedPayload
26
+ | PullRequestReviewEditedPayload
27
+ | PullRequestReviewDismissedPayload;
28
+
29
+ // Pull Request Review Comment
30
+ export type PullRequestReviewCommentCreatedPayload =
31
+ WebhookSchemas["webhook-pull-request-review-comment-created"];
32
+ export type PullRequestReviewCommentEditedPayload =
33
+ WebhookSchemas["webhook-pull-request-review-comment-edited"];
34
+ export type PullRequestReviewCommentDeletedPayload =
35
+ WebhookSchemas["webhook-pull-request-review-comment-deleted"];
36
+ export type PullRequestReviewCommentPayload =
37
+ | PullRequestReviewCommentCreatedPayload
38
+ | PullRequestReviewCommentEditedPayload
39
+ | PullRequestReviewCommentDeletedPayload;
40
+
41
+ // Check Suite
42
+ export type CheckSuiteCompletedPayload =
43
+ WebhookSchemas["webhook-check-suite-completed"];
44
+ export type CheckSuiteRequestedPayload =
45
+ WebhookSchemas["webhook-check-suite-requested"];
46
+ export type CheckSuiteRerequestedPayload =
47
+ WebhookSchemas["webhook-check-suite-rerequested"];
48
+ export type CheckSuitePayload =
49
+ | CheckSuiteCompletedPayload
50
+ | CheckSuiteRequestedPayload
51
+ | CheckSuiteRerequestedPayload;
52
+
53
+ // Issue Comment
54
+ export type IssueCommentCreatedPayload =
55
+ WebhookSchemas["webhook-issue-comment-created"];
56
+ export type IssueCommentEditedPayload =
57
+ WebhookSchemas["webhook-issue-comment-edited"];
58
+ export type IssueCommentDeletedPayload =
59
+ WebhookSchemas["webhook-issue-comment-deleted"];
60
+ export type IssueCommentPayload =
61
+ | IssueCommentCreatedPayload
62
+ | IssueCommentEditedPayload
63
+ | IssueCommentDeletedPayload;
64
+
65
+ export type SupportedWebhookPayload =
66
+ | WorkflowRunPayload
67
+ | PullRequestReviewPayload
68
+ | PullRequestReviewCommentPayload
69
+ | CheckSuitePayload
70
+ | IssueCommentPayload;
71
+
72
+ /**
73
+ * Fallback generic webhook payload structure for unknown or untyped events.
74
+ */
75
+ export type WebhookPayload = Record<string, any>;
@@ -0,0 +1,105 @@
1
+ import { Type, type Static } from "typebox";
2
+
3
+ export const FilterSchema = Type.Object(
4
+ {
5
+ branch: Type.Optional(
6
+ Type.String({ description: "Target branch name or '@current'" }),
7
+ ),
8
+ conclusion: Type.Optional(
9
+ Type.String({
10
+ description: "Conclusion filter (e.g. 'failure', 'success')",
11
+ }),
12
+ ),
13
+ workflow: Type.Optional(
14
+ Type.String({ description: "Workflow name filter (e.g. 'CI')" }),
15
+ ),
16
+ state: Type.Optional(
17
+ Type.String({
18
+ description:
19
+ "Review or PR state filter (e.g. 'changes_requested', 'approved')",
20
+ }),
21
+ ),
22
+ senders: Type.Optional(
23
+ Type.Array(Type.String(), { description: "Allowed sender logins" }),
24
+ ),
25
+ isBot: Type.Optional(
26
+ Type.Boolean({
27
+ description: "Filter for bot (true) or human (false) senders",
28
+ }),
29
+ ),
30
+ match: Type.Optional(
31
+ Type.Record(
32
+ Type.String(),
33
+ Type.Union([Type.String(), Type.Number(), Type.Boolean()]),
34
+ {
35
+ description:
36
+ "Arbitrary field matching on payload (dot-separated path)",
37
+ },
38
+ ),
39
+ ),
40
+ },
41
+ { additionalProperties: false },
42
+ );
43
+
44
+ export type FilterConfig = Static<typeof FilterSchema>;
45
+
46
+ export const ActionSchema = Type.Object(
47
+ {
48
+ prompt: Type.String({
49
+ description: "Template string to dispatch to agent",
50
+ }),
51
+ deliverAs: Type.Optional(
52
+ Type.Union([Type.Literal("steer"), Type.Literal("followUp")], {
53
+ description:
54
+ "Delivery urgency: steer interrupts turn, followUp queues next",
55
+ }),
56
+ ),
57
+ },
58
+ { additionalProperties: false },
59
+ );
60
+
61
+ export type ActionConfig = Static<typeof ActionSchema>;
62
+
63
+ export const SubscriptionRuleSchema = Type.Object(
64
+ {
65
+ event: Type.Optional(
66
+ Type.String({
67
+ description:
68
+ "Upstream GitHub Webhook event identifier (e.g. 'workflow_run.completed', 'push', 'issues.opened')",
69
+ }),
70
+ ),
71
+ topic: Type.Optional(
72
+ Type.String({
73
+ description:
74
+ "Alias for 'event' (e.g. 'workflow_run.completed' or 'github.workflow_run.completed')",
75
+ }),
76
+ ),
77
+ filters: Type.Optional(FilterSchema),
78
+ action: ActionSchema,
79
+ },
80
+ { additionalProperties: false },
81
+ );
82
+
83
+ export type SubscriptionRule = Static<typeof SubscriptionRuleSchema>;
84
+
85
+ export const ConfigSchema = Type.Object(
86
+ {
87
+ version: Type.Literal(1, { description: "Schema version" }),
88
+ enabled: Type.Boolean({ default: true }),
89
+ repo: Type.Optional(
90
+ Type.String({
91
+ default: "auto",
92
+ description: "Target GitHub repo (owner/repo or 'auto')",
93
+ }),
94
+ ),
95
+ secret: Type.Optional(
96
+ Type.String({ description: "Optional Webhook HMAC secret" }),
97
+ ),
98
+ subscriptions: Type.Array(SubscriptionRuleSchema, {
99
+ description: "List of declarative webhook event subscriptions",
100
+ }),
101
+ },
102
+ { additionalProperties: false },
103
+ );
104
+
105
+ export type GhWebhookConfig = Static<typeof ConfigSchema>;