@ouija-dev/plugin-github 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.
@@ -0,0 +1,75 @@
1
+ import type { OpenPRParams, StandardPR } from '@ouija-dev/types';
2
+ /**
3
+ * Encode a GitHub PR as a branded PrId.
4
+ * Format: "owner/repo#number"
5
+ * This is the canonical parse boundary — all external callers use this format.
6
+ */
7
+ export declare function encodePrId(owner: string, repo: string, prNumber: number): import("@ouija-dev/types").PrId;
8
+ /**
9
+ * Decode a PrId back to its GitHub coordinates.
10
+ * Throws if the format is unexpected — a broken PrId means something upstream
11
+ * gave us bad data.
12
+ */
13
+ export declare function decodePrId(id: string): {
14
+ owner: string;
15
+ repo: string;
16
+ prNumber: number;
17
+ };
18
+ /**
19
+ * Parse a GitHub repo URL into owner/repo components.
20
+ * Accepts:
21
+ * https://github.com/owner/repo
22
+ * https://github.com/owner/repo.git
23
+ * git@github.com:owner/repo.git
24
+ */
25
+ export declare function parseRepoUrl(repoUrl: string): {
26
+ owner: string;
27
+ repo: string;
28
+ };
29
+ export declare class GitHubApiClient {
30
+ private readonly octokit;
31
+ constructor(token: string);
32
+ /**
33
+ * Create a new branch from an existing ref.
34
+ * `fromRef` may be a branch name, tag, or full SHA.
35
+ */
36
+ createBranch(owner: string, repo: string, branch: string, fromRef: string): Promise<void>;
37
+ /**
38
+ * Open a pull request and return its standard representation.
39
+ * `repoUrl` is parsed to extract owner/repo.
40
+ */
41
+ openPR(repoUrl: string, params: OpenPRParams): Promise<StandardPR>;
42
+ /**
43
+ * Merge a pull request by its PrId.
44
+ * Uses the squash merge strategy by default.
45
+ */
46
+ mergePR(prIdValue: string): Promise<void>;
47
+ /**
48
+ * Add a comment to a pull request (as an issue comment — visible on the PR timeline).
49
+ */
50
+ addPRComment(prIdValue: string, body: string): Promise<void>;
51
+ /**
52
+ * Get a pull request by its PrId.
53
+ */
54
+ getPR(prIdValue: string): Promise<StandardPR>;
55
+ /**
56
+ * Check whether a branch exists on the repo.
57
+ * Returns null if not found, the branch ref object if found.
58
+ */
59
+ getBranch(owner: string, repo: string, branch: string): Promise<{
60
+ sha: string;
61
+ name: string;
62
+ } | null>;
63
+ /**
64
+ * Verify that the token has sufficient access by listing repos.
65
+ * Throws if the request fails (bad token, revoked, etc.).
66
+ */
67
+ verifyToken(): Promise<void>;
68
+ /**
69
+ * Lightweight health check — call GET /user and return whether it succeeds.
70
+ */
71
+ getAuthenticatedUser(): Promise<{
72
+ login: string;
73
+ }>;
74
+ }
75
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKjE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mCAEvE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAUxF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAc7E;AAID,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,KAAK,EAAE,MAAM;IAIzB;;;OAGG;IACG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB/F;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;IA2BxE;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/C;;OAEG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlE;;OAEG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiCnD;;;OAGG;IACG,SAAS,CACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAiBhD;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAIzD"}
@@ -0,0 +1,209 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { prId } from '@ouija-dev/types';
3
+ // ---- Helpers ----
4
+ /**
5
+ * Encode a GitHub PR as a branded PrId.
6
+ * Format: "owner/repo#number"
7
+ * This is the canonical parse boundary — all external callers use this format.
8
+ */
9
+ export function encodePrId(owner, repo, prNumber) {
10
+ return prId(`${owner}/${repo}#${prNumber}`);
11
+ }
12
+ /**
13
+ * Decode a PrId back to its GitHub coordinates.
14
+ * Throws if the format is unexpected — a broken PrId means something upstream
15
+ * gave us bad data.
16
+ */
17
+ export function decodePrId(id) {
18
+ const match = /^([^/]+)\/([^#]+)#(\d+)$/.exec(id);
19
+ if (!match) {
20
+ throw new Error(`GitHubApiClient: cannot decode PrId "${id}" — expected "owner/repo#number"`);
21
+ }
22
+ return {
23
+ owner: match[1],
24
+ repo: match[2],
25
+ prNumber: parseInt(match[3], 10),
26
+ };
27
+ }
28
+ /**
29
+ * Parse a GitHub repo URL into owner/repo components.
30
+ * Accepts:
31
+ * https://github.com/owner/repo
32
+ * https://github.com/owner/repo.git
33
+ * git@github.com:owner/repo.git
34
+ */
35
+ export function parseRepoUrl(repoUrl) {
36
+ // SSH format: git@github.com:owner/repo.git
37
+ const sshMatch = /git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/.exec(repoUrl);
38
+ if (sshMatch) {
39
+ return { owner: sshMatch[1], repo: sshMatch[2] };
40
+ }
41
+ // HTTPS format: https://github.com/owner/repo[.git]
42
+ const httpsMatch = /github\.com\/([^/]+)\/(.+?)(?:\.git)?$/.exec(repoUrl);
43
+ if (httpsMatch) {
44
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
45
+ }
46
+ throw new Error(`GitHubApiClient: cannot parse repo URL "${repoUrl}"`);
47
+ }
48
+ // ---- GitHub API client ----
49
+ export class GitHubApiClient {
50
+ octokit;
51
+ constructor(token) {
52
+ this.octokit = new Octokit({ auth: token });
53
+ }
54
+ /**
55
+ * Create a new branch from an existing ref.
56
+ * `fromRef` may be a branch name, tag, or full SHA.
57
+ */
58
+ async createBranch(owner, repo, branch, fromRef) {
59
+ // Resolve fromRef to a SHA. If it's already a SHA (40 hex chars) we skip this.
60
+ let sha;
61
+ if (/^[0-9a-f]{40}$/i.test(fromRef)) {
62
+ sha = fromRef;
63
+ }
64
+ else {
65
+ const { data: refData } = await this.octokit.git.getRef({
66
+ owner,
67
+ repo,
68
+ ref: `heads/${fromRef}`,
69
+ });
70
+ sha = refData.object.sha;
71
+ }
72
+ await this.octokit.git.createRef({
73
+ owner,
74
+ repo,
75
+ ref: `refs/heads/${branch}`,
76
+ sha,
77
+ });
78
+ }
79
+ /**
80
+ * Open a pull request and return its standard representation.
81
+ * `repoUrl` is parsed to extract owner/repo.
82
+ */
83
+ async openPR(repoUrl, params) {
84
+ const { owner, repo } = parseRepoUrl(repoUrl);
85
+ const { data } = await this.octokit.pulls.create({
86
+ owner,
87
+ repo,
88
+ title: params.title,
89
+ body: params.body,
90
+ head: params.branch,
91
+ base: params.baseBranch,
92
+ draft: params.draft ?? false,
93
+ });
94
+ return {
95
+ id: encodePrId(owner, repo, data.number),
96
+ url: data.html_url,
97
+ title: data.title,
98
+ body: data.body ?? '',
99
+ branch: data.head.ref,
100
+ baseBranch: data.base.ref,
101
+ state: 'open',
102
+ draft: data.draft ?? false,
103
+ createdAt: data.created_at,
104
+ updatedAt: data.updated_at,
105
+ };
106
+ }
107
+ /**
108
+ * Merge a pull request by its PrId.
109
+ * Uses the squash merge strategy by default.
110
+ */
111
+ async mergePR(prIdValue) {
112
+ const { owner, repo, prNumber } = decodePrId(prIdValue);
113
+ await this.octokit.pulls.merge({
114
+ owner,
115
+ repo,
116
+ pull_number: prNumber,
117
+ merge_method: 'squash',
118
+ });
119
+ }
120
+ /**
121
+ * Add a comment to a pull request (as an issue comment — visible on the PR timeline).
122
+ */
123
+ async addPRComment(prIdValue, body) {
124
+ const { owner, repo, prNumber } = decodePrId(prIdValue);
125
+ await this.octokit.issues.createComment({
126
+ owner,
127
+ repo,
128
+ issue_number: prNumber,
129
+ body,
130
+ });
131
+ }
132
+ /**
133
+ * Get a pull request by its PrId.
134
+ */
135
+ async getPR(prIdValue) {
136
+ const { owner, repo, prNumber } = decodePrId(prIdValue);
137
+ const { data } = await this.octokit.pulls.get({
138
+ owner,
139
+ repo,
140
+ pull_number: prNumber,
141
+ });
142
+ let state;
143
+ if (data.merged) {
144
+ state = 'merged';
145
+ }
146
+ else if (data.state === 'closed') {
147
+ state = 'closed';
148
+ }
149
+ else {
150
+ state = 'open';
151
+ }
152
+ return {
153
+ id: encodePrId(owner, repo, data.number),
154
+ url: data.html_url,
155
+ title: data.title,
156
+ body: data.body ?? '',
157
+ branch: data.head.ref,
158
+ baseBranch: data.base.ref,
159
+ state,
160
+ draft: data.draft ?? false,
161
+ createdAt: data.created_at,
162
+ updatedAt: data.updated_at,
163
+ ...(data.merged_at ? { mergedAt: data.merged_at } : {}),
164
+ };
165
+ }
166
+ /**
167
+ * Check whether a branch exists on the repo.
168
+ * Returns null if not found, the branch ref object if found.
169
+ */
170
+ async getBranch(owner, repo, branch) {
171
+ try {
172
+ const { data } = await this.octokit.git.getRef({
173
+ owner,
174
+ repo,
175
+ ref: `heads/${branch}`,
176
+ });
177
+ return { sha: data.object.sha, name: branch };
178
+ }
179
+ catch (err) {
180
+ // Octokit throws a RequestError with status 404 when the ref doesn't exist.
181
+ if (isOctokitNotFound(err)) {
182
+ return null;
183
+ }
184
+ throw err;
185
+ }
186
+ }
187
+ /**
188
+ * Verify that the token has sufficient access by listing repos.
189
+ * Throws if the request fails (bad token, revoked, etc.).
190
+ */
191
+ async verifyToken() {
192
+ await this.octokit.repos.listForAuthenticatedUser({ per_page: 1 });
193
+ }
194
+ /**
195
+ * Lightweight health check — call GET /user and return whether it succeeds.
196
+ */
197
+ async getAuthenticatedUser() {
198
+ const { data } = await this.octokit.users.getAuthenticated();
199
+ return { login: data.login };
200
+ }
201
+ }
202
+ // ---- Error helpers ----
203
+ function isOctokitNotFound(err) {
204
+ return (typeof err === 'object' &&
205
+ err !== null &&
206
+ 'status' in err &&
207
+ err.status === 404);
208
+ }
209
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ export declare const githubConfigSchema: {
2
+ readonly type: "object";
3
+ readonly properties: {
4
+ readonly personalAccessToken: {
5
+ readonly type: "string";
6
+ readonly minLength: 1;
7
+ };
8
+ readonly defaultOrg: {
9
+ readonly type: "string";
10
+ };
11
+ readonly webhookSecret: {
12
+ readonly type: "string";
13
+ readonly minLength: 1;
14
+ };
15
+ };
16
+ readonly required: readonly ["personalAccessToken", "webhookSecret"];
17
+ readonly additionalProperties: false;
18
+ };
19
+ export interface GitHubConfig {
20
+ personalAccessToken: string;
21
+ defaultOrg?: string;
22
+ webhookSecret: string;
23
+ }
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;CASrB,CAAC;AAIX,MAAM,WAAW,YAAY;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB"}
package/dist/config.js ADDED
@@ -0,0 +1,12 @@
1
+ // ---- GitHub plugin config schema ----
2
+ export const githubConfigSchema = {
3
+ type: 'object',
4
+ properties: {
5
+ personalAccessToken: { type: 'string', minLength: 1 },
6
+ defaultOrg: { type: 'string' },
7
+ webhookSecret: { type: 'string', minLength: 1 },
8
+ },
9
+ required: ['personalAccessToken', 'webhookSecret'],
10
+ additionalProperties: false,
11
+ };
12
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAExC,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,mBAAmB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;QACrD,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC9B,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;KAChD;IACD,QAAQ,EAAE,CAAC,qBAAqB,EAAE,eAAe,CAAC;IAClD,oBAAoB,EAAE,KAAK;CACnB,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { GitPlugin, PluginManifest, PluginContext, PluginHealth, StandardPR, OpenPRParams } from '@ouija-dev/types';
3
+ import type { PrId } from '@ouija-dev/types';
4
+ import type { PluginFactory } from '@ouija-dev/plugin-sdk';
5
+ import { type GitHubConfig } from './config.js';
6
+ export declare class GitHubPlugin implements GitPlugin<GitHubConfig> {
7
+ readonly manifest: PluginManifest;
8
+ private client;
9
+ private context;
10
+ init(context: PluginContext<GitHubConfig>): Promise<void>;
11
+ /**
12
+ * Verify that the personal access token has list-repos scope.
13
+ * A 401/403 at this point surfaces immediately rather than silently at PR time.
14
+ */
15
+ start(): Promise<void>;
16
+ stop(): Promise<void>;
17
+ healthCheck(): Promise<PluginHealth>;
18
+ /**
19
+ * Register the GitHub webhook ingress route.
20
+ *
21
+ * Route: POST /hooks/github/:secret
22
+ *
23
+ * The :secret path segment is compared against `config.webhookSecret` to
24
+ * allow routing multiple GitHub app installations to different endpoints
25
+ * without embedding secrets in GitHub's per-repo webhook URL. The
26
+ * X-Hub-Signature-256 header is additionally verified with HMAC-SHA256
27
+ * so that even a leaked URL cannot be abused.
28
+ */
29
+ registerRoutes(fastify: FastifyInstance): Promise<void>;
30
+ /**
31
+ * Create a branch from `fromBranch` on the repo identified by `repoUrl`.
32
+ */
33
+ createBranch(repoUrl: string, branchName: string, fromBranch: string): Promise<void>;
34
+ /**
35
+ * Open a pull request on the repo identified by `repoUrl`.
36
+ */
37
+ openPR(repoUrl: string, params: OpenPRParams): Promise<StandardPR>;
38
+ /**
39
+ * Merge the pull request identified by `prId`.
40
+ */
41
+ mergePR(id: PrId): Promise<void>;
42
+ /**
43
+ * Add a comment to the pull request identified by `prId`.
44
+ */
45
+ addPRComment(id: PrId, body: string): Promise<void>;
46
+ /**
47
+ * Get the current state of a pull request by its `prId`.
48
+ */
49
+ getPR(id: PrId): Promise<StandardPR>;
50
+ }
51
+ declare const PluginFactoryExport: PluginFactory<GitHubConfig>;
52
+ export { PluginFactoryExport as PluginFactory };
53
+ export default PluginFactoryExport;
54
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,EACV,YAAY,EACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAqBpE,qBAAa,YAAa,YAAW,SAAS,CAAC,YAAY,CAAC;IAC1D,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAY;IAE7C,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,OAAO,CAA+B;IAIxC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/D;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IAkB1C;;;;;;;;;;OAUG;IACG,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAmE7D;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1F;;OAEG;IACG,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;IAIxE;;OAEG;IACG,OAAO,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC;;OAEG;IACG,YAAY,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD;;OAEG;IACG,KAAK,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC;CAG3C;AAID,QAAA,MAAM,mBAAmB,EAAE,aAAa,CAAC,YAAY,CAKpD,CAAC;AAEF,OAAO,EAAE,mBAAmB,IAAI,aAAa,EAAE,CAAC;AAChD,eAAe,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ import { githubConfigSchema } from './config.js';
2
+ import { GitHubApiClient, parseRepoUrl } from './api-client.js';
3
+ import { normalizeWebhook, verifySignature } from './webhook-handler.js';
4
+ // ---- Plugin manifest ----
5
+ const manifest = {
6
+ name: '@ouija-dev/plugin-github',
7
+ version: '0.1.0',
8
+ type: 'git',
9
+ coreApiVersion: '>=1.0.0 <2.0.0',
10
+ configSchema: githubConfigSchema,
11
+ dependencies: [],
12
+ events: {
13
+ produces: ['git.pr.opened', 'git.pr.merged'],
14
+ consumes: [],
15
+ },
16
+ };
17
+ // ---- Plugin implementation ----
18
+ export class GitHubPlugin {
19
+ manifest = manifest;
20
+ client;
21
+ context;
22
+ // ---- Lifecycle ----
23
+ async init(context) {
24
+ this.context = context;
25
+ this.client = new GitHubApiClient(context.config.personalAccessToken);
26
+ context.logger.info('GitHubPlugin initialised');
27
+ }
28
+ /**
29
+ * Verify that the personal access token has list-repos scope.
30
+ * A 401/403 at this point surfaces immediately rather than silently at PR time.
31
+ */
32
+ async start() {
33
+ try {
34
+ await this.client.verifyToken();
35
+ this.context.logger.info('GitHubPlugin started — token verified');
36
+ }
37
+ catch (err) {
38
+ this.context.logger.error('GitHubPlugin start failed — token verification error', {
39
+ error: String(err),
40
+ });
41
+ throw err;
42
+ }
43
+ }
44
+ async stop() {
45
+ this.context.logger.info('GitHubPlugin stopped');
46
+ }
47
+ async healthCheck() {
48
+ try {
49
+ const user = await this.client.getAuthenticatedUser();
50
+ return {
51
+ healthy: true,
52
+ message: `Authenticated as ${user.login}`,
53
+ details: { login: user.login },
54
+ };
55
+ }
56
+ catch (err) {
57
+ return {
58
+ healthy: false,
59
+ message: `GitHub API unreachable: ${String(err)}`,
60
+ };
61
+ }
62
+ }
63
+ // ---- Route registration ----
64
+ /**
65
+ * Register the GitHub webhook ingress route.
66
+ *
67
+ * Route: POST /hooks/github/:secret
68
+ *
69
+ * The :secret path segment is compared against `config.webhookSecret` to
70
+ * allow routing multiple GitHub app installations to different endpoints
71
+ * without embedding secrets in GitHub's per-repo webhook URL. The
72
+ * X-Hub-Signature-256 header is additionally verified with HMAC-SHA256
73
+ * so that even a leaked URL cannot be abused.
74
+ */
75
+ async registerRoutes(fastify) {
76
+ const webhookSecret = this.context.config.webhookSecret;
77
+ fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (_req, body, done) => {
78
+ done(null, body);
79
+ });
80
+ fastify.post('/hooks/github/:secret', async (request, reply) => {
81
+ const params = request.params;
82
+ const headers = request.headers;
83
+ // 1. Constant-time secret comparison is handled inside verifySignature.
84
+ // We additionally check the path secret matches to ensure correct routing.
85
+ if (params.secret !== webhookSecret) {
86
+ return reply.status(403).send({ error: 'Forbidden' });
87
+ }
88
+ const signatureHeader = headers['x-hub-signature-256'] ?? '';
89
+ const rawBody = request.body;
90
+ if (!verifySignature(webhookSecret, rawBody, signatureHeader)) {
91
+ return reply.status(403).send({ error: 'Invalid signature' });
92
+ }
93
+ // 2. Parse and normalize the webhook.
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(rawBody.toString('utf8'));
97
+ }
98
+ catch {
99
+ return reply.status(400).send({ error: 'Invalid JSON body' });
100
+ }
101
+ const githubEvent = headers['x-github-event'] ?? '';
102
+ const event = normalizeWebhook(githubEvent, parsed);
103
+ if (event !== null) {
104
+ try {
105
+ await this.context.publishEvent(event.topic, event.payload);
106
+ }
107
+ catch (err) {
108
+ this.context.logger.error('Failed to publish webhook event', {
109
+ topic: event.topic,
110
+ error: String(err),
111
+ });
112
+ // Still return 200 — GitHub will retry on non-2xx responses, which
113
+ // would cause duplicate events. We log and move on.
114
+ }
115
+ }
116
+ return reply.status(200).send({ ok: true });
117
+ });
118
+ }
119
+ // ---- GitPlugin interface ----
120
+ /**
121
+ * Create a branch from `fromBranch` on the repo identified by `repoUrl`.
122
+ */
123
+ async createBranch(repoUrl, branchName, fromBranch) {
124
+ const { owner, repo } = parseRepoUrl(repoUrl);
125
+ await this.client.createBranch(owner, repo, branchName, fromBranch);
126
+ }
127
+ /**
128
+ * Open a pull request on the repo identified by `repoUrl`.
129
+ */
130
+ async openPR(repoUrl, params) {
131
+ return this.client.openPR(repoUrl, params);
132
+ }
133
+ /**
134
+ * Merge the pull request identified by `prId`.
135
+ */
136
+ async mergePR(id) {
137
+ await this.client.mergePR(id);
138
+ }
139
+ /**
140
+ * Add a comment to the pull request identified by `prId`.
141
+ */
142
+ async addPRComment(id, body) {
143
+ await this.client.addPRComment(id, body);
144
+ }
145
+ /**
146
+ * Get the current state of a pull request by its `prId`.
147
+ */
148
+ async getPR(id) {
149
+ return this.client.getPR(id);
150
+ }
151
+ }
152
+ // ---- PluginFactory (required by PluginLoader) ----
153
+ const PluginFactoryExport = {
154
+ manifest,
155
+ create() {
156
+ return new GitHubPlugin();
157
+ },
158
+ };
159
+ export { PluginFactoryExport as PluginFactory };
160
+ export default PluginFactoryExport;
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { OuijaEvent, StandardPR } from '@ouija-dev/types';
2
+ interface GitHubPR {
3
+ number: number;
4
+ html_url: string;
5
+ title: string;
6
+ body: string | null;
7
+ state: string;
8
+ draft: boolean;
9
+ merged: boolean;
10
+ merged_at: string | null;
11
+ created_at: string;
12
+ updated_at: string;
13
+ head: {
14
+ ref: string;
15
+ };
16
+ base: {
17
+ ref: string;
18
+ };
19
+ }
20
+ /**
21
+ * Compute the expected HMAC-SHA256 signature for a raw webhook body.
22
+ * Returns the value in the format GitHub sends: "sha256=<hex>".
23
+ */
24
+ export declare function computeSignature(secret: string, rawBody: string | Buffer): string;
25
+ /**
26
+ * Verify the X-Hub-Signature-256 header against the raw request body.
27
+ * Uses timingSafeEqual to prevent timing attacks.
28
+ *
29
+ * Returns true if the signature matches, false otherwise.
30
+ */
31
+ export declare function verifySignature(secret: string, rawBody: string | Buffer, signatureHeader: string): boolean;
32
+ /**
33
+ * Normalize a raw GitHub webhook into an OuijaEvent.
34
+ *
35
+ * Supported mappings:
36
+ * pull_request / opened → git.pr.opened
37
+ * pull_request / closed (merged: true) → git.pr.merged
38
+ *
39
+ * All other events/actions return null (caller should 200 OK and discard).
40
+ */
41
+ export declare function normalizeWebhook(githubEvent: string, payload: unknown): OuijaEvent<'git.pr.opened'> | OuijaEvent<'git.pr.merged'> | null;
42
+ /**
43
+ * Build a StandardPR from a GitHub pull_request payload object.
44
+ * Exported so the main plugin can use it when returning from openPR.
45
+ */
46
+ export declare function normalizePR(pr: GitHubPR, owner: string, repoName: string): StandardPR;
47
+ export {};
48
+ //# sourceMappingURL=webhook-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-handler.d.ts","sourceRoot":"","sources":["../src/webhook-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAA0C,MAAM,kBAAkB,CAAC;AAavG,UAAU,QAAQ;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACtB,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACvB;AAWD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAIjF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,eAAe,EAAE,MAAM,GACtB,OAAO,CAeT;AAID;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,GACf,UAAU,CAAC,eAAe,CAAC,GAAG,UAAU,CAAC,eAAe,CAAC,GAAG,IAAI,CAwClE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAuBrF"}
@@ -0,0 +1,117 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { prId, instanceId } from '@ouija-dev/types';
3
+ import { encodePrId } from './api-client.js';
4
+ // ---- Signature verification ----
5
+ /**
6
+ * Compute the expected HMAC-SHA256 signature for a raw webhook body.
7
+ * Returns the value in the format GitHub sends: "sha256=<hex>".
8
+ */
9
+ export function computeSignature(secret, rawBody) {
10
+ const hmac = createHmac('sha256', secret);
11
+ hmac.update(typeof rawBody === 'string' ? rawBody : rawBody);
12
+ return `sha256=${hmac.digest('hex')}`;
13
+ }
14
+ /**
15
+ * Verify the X-Hub-Signature-256 header against the raw request body.
16
+ * Uses timingSafeEqual to prevent timing attacks.
17
+ *
18
+ * Returns true if the signature matches, false otherwise.
19
+ */
20
+ export function verifySignature(secret, rawBody, signatureHeader) {
21
+ const expected = computeSignature(secret, rawBody);
22
+ try {
23
+ const expectedBuf = Buffer.from(expected, 'utf8');
24
+ const actualBuf = Buffer.from(signatureHeader, 'utf8');
25
+ if (expectedBuf.length !== actualBuf.length) {
26
+ return false;
27
+ }
28
+ return timingSafeEqual(expectedBuf, actualBuf);
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ // ---- Payload normalizer ----
35
+ /**
36
+ * Normalize a raw GitHub webhook into an OuijaEvent.
37
+ *
38
+ * Supported mappings:
39
+ * pull_request / opened → git.pr.opened
40
+ * pull_request / closed (merged: true) → git.pr.merged
41
+ *
42
+ * All other events/actions return null (caller should 200 OK and discard).
43
+ */
44
+ export function normalizeWebhook(githubEvent, payload) {
45
+ if (githubEvent !== 'pull_request') {
46
+ return null;
47
+ }
48
+ const typed = payload;
49
+ const { action, pull_request: pr, repository: repo } = typed;
50
+ const owner = repo.owner.login;
51
+ const repoName = repo.name;
52
+ const encodedPrId = encodePrId(owner, repoName, pr.number);
53
+ if (action === 'opened') {
54
+ const eventPayload = {
55
+ prId: encodedPrId,
56
+ url: pr.html_url,
57
+ // instanceId is not available from a bare webhook — callers that
58
+ // need it will correlate by prId. We generate a placeholder here.
59
+ instanceId: instanceId(`github-pr-${String(pr.number)}`),
60
+ branch: pr.head.ref,
61
+ targetBranch: pr.base.ref,
62
+ };
63
+ return buildEvent('git.pr.opened', eventPayload);
64
+ }
65
+ if (action === 'closed' && pr.merged === true) {
66
+ const mergedAt = pr.merged_at ?? new Date().toISOString();
67
+ const eventPayload = {
68
+ prId: encodedPrId,
69
+ instanceId: instanceId(`github-pr-${String(pr.number)}`),
70
+ mergedAt,
71
+ };
72
+ return buildEvent('git.pr.merged', eventPayload);
73
+ }
74
+ // PR closed without merge, or any other action (synchronize, review_requested, etc.)
75
+ return null;
76
+ }
77
+ /**
78
+ * Build a StandardPR from a GitHub pull_request payload object.
79
+ * Exported so the main plugin can use it when returning from openPR.
80
+ */
81
+ export function normalizePR(pr, owner, repoName) {
82
+ let state;
83
+ if (pr.merged) {
84
+ state = 'merged';
85
+ }
86
+ else if (pr.state === 'closed') {
87
+ state = 'closed';
88
+ }
89
+ else {
90
+ state = 'open';
91
+ }
92
+ return {
93
+ id: prId(encodePrId(owner, repoName, pr.number)),
94
+ url: pr.html_url,
95
+ title: pr.title,
96
+ body: pr.body ?? '',
97
+ branch: pr.head.ref,
98
+ baseBranch: pr.base.ref,
99
+ state,
100
+ draft: pr.draft,
101
+ createdAt: pr.created_at,
102
+ updatedAt: pr.updated_at,
103
+ ...(pr.merged_at ? { mergedAt: pr.merged_at } : {}),
104
+ };
105
+ }
106
+ // ---- Internal helpers ----
107
+ function buildEvent(topic, payload) {
108
+ return {
109
+ id: crypto.randomUUID(),
110
+ topic,
111
+ payload,
112
+ timestamp: new Date().toISOString(),
113
+ sourcePlugin: '@ouija-dev/plugin-github',
114
+ correlationId: crypto.randomUUID(),
115
+ };
116
+ }
117
+ //# sourceMappingURL=webhook-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-handler.js","sourceRoot":"","sources":["../src/webhook-handler.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@ouija-dev/plugin-github",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist/", "README.md"],
8
+ "publishConfig": { "access": "public" },
9
+ "repository": { "type": "git", "url": "https://github.com/muhammadkh4n/ouija.git" },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "vitest run"
14
+ },
15
+ "dependencies": {
16
+ "@ouija-dev/types": "*",
17
+ "@ouija-dev/plugin-sdk": "*",
18
+ "@octokit/rest": "^21.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "fastify": "^5.8.4",
23
+ "typescript": "^5.5.0",
24
+ "vitest": "^3.0.0"
25
+ }
26
+ }