@inkeep/agents-work-apps 0.0.0-dev-20260203033642

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,278 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import runDbClient_default from "../../db/runDbClient.js";
3
+ import { getWebhookSecret, isWebhookConfigured } from "../config.js";
4
+ import { addRepositories, deleteInstallation, getInstallationByGitHubId, removeRepositories, updateInstallationStatusByGitHubId } from "@inkeep/agents-core";
5
+ import { Hono } from "hono";
6
+ import { createHmac, timingSafeEqual } from "node:crypto";
7
+
8
+ //#region src/github/routes/webhooks.ts
9
+ const logger = getLogger("github-webhooks");
10
+ function verifyWebhookSignature(payload, signature, secret) {
11
+ if (!signature) return {
12
+ success: false,
13
+ error: "Missing X-Hub-Signature-256 header"
14
+ };
15
+ if (!signature.startsWith("sha256=")) return {
16
+ success: false,
17
+ error: "Invalid signature format"
18
+ };
19
+ const providedSignature = signature.slice(7);
20
+ const hmac = createHmac("sha256", secret);
21
+ hmac.update(payload);
22
+ const expectedSignature = hmac.digest("hex");
23
+ try {
24
+ const providedBuffer = Buffer.from(providedSignature, "hex");
25
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
26
+ if (providedBuffer.length !== expectedBuffer.length) return {
27
+ success: false,
28
+ error: "Invalid signature"
29
+ };
30
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) return {
31
+ success: false,
32
+ error: "Invalid signature"
33
+ };
34
+ return { success: true };
35
+ } catch {
36
+ return {
37
+ success: false,
38
+ error: "Invalid signature format"
39
+ };
40
+ }
41
+ }
42
+ const app = new Hono();
43
+ app.post("/", async (c) => {
44
+ if (!isWebhookConfigured()) {
45
+ logger.error({}, "GitHub webhook secret not configured");
46
+ return c.json({ error: "GitHub webhook secret not configured" }, 500);
47
+ }
48
+ const rawBody = await c.req.text();
49
+ const signature = c.req.header("X-Hub-Signature-256");
50
+ const eventType = c.req.header("X-GitHub-Event");
51
+ const deliveryId = c.req.header("X-GitHub-Delivery");
52
+ logger.info({
53
+ eventType,
54
+ deliveryId
55
+ }, "Received GitHub webhook");
56
+ const verificationResult = verifyWebhookSignature(rawBody, signature, getWebhookSecret());
57
+ if (!verificationResult.success) {
58
+ logger.warn({
59
+ eventType,
60
+ deliveryId,
61
+ error: verificationResult.error
62
+ }, "Webhook signature verification failed");
63
+ return c.json({ error: verificationResult.error }, 401);
64
+ }
65
+ logger.info({
66
+ eventType,
67
+ deliveryId
68
+ }, "Webhook signature verified");
69
+ if (!eventType) {
70
+ logger.warn({ deliveryId }, "Missing X-GitHub-Event header");
71
+ return c.json({ error: "Missing X-GitHub-Event header" }, 400);
72
+ }
73
+ const payload = JSON.parse(rawBody);
74
+ if (eventType === "installation") return handleInstallationEvent(c, payload, deliveryId);
75
+ if (eventType === "installation_repositories") return handleInstallationRepositoriesEvent(c, payload, deliveryId);
76
+ logger.info({
77
+ eventType,
78
+ deliveryId
79
+ }, "Received unhandled event type, acknowledging");
80
+ return c.json({ received: true }, 200);
81
+ });
82
+ async function handleInstallationEvent(c, payload, deliveryId) {
83
+ const { action, installation } = payload;
84
+ const installationId = String(installation.id);
85
+ logger.info({
86
+ action,
87
+ deliveryId,
88
+ installationId,
89
+ accountLogin: installation.account.login,
90
+ accountType: installation.account.type
91
+ }, "Processing installation webhook event");
92
+ try {
93
+ switch (action) {
94
+ case "created": {
95
+ const existing = await getInstallationByGitHubId(runDbClient_default)(installationId);
96
+ if (existing) if (existing.status === "pending") {
97
+ logger.info({
98
+ installationId,
99
+ existingId: existing.id
100
+ }, "Activating pending installation");
101
+ await updateInstallationStatusByGitHubId(runDbClient_default)({
102
+ gitHubInstallationId: installationId,
103
+ status: "active"
104
+ });
105
+ } else logger.info({
106
+ installationId,
107
+ existingId: existing.id,
108
+ currentStatus: existing.status
109
+ }, "Installation already exists, no action needed");
110
+ else logger.warn({
111
+ installationId,
112
+ accountLogin: installation.account.login
113
+ }, "Received created event for unknown installation - cannot create without tenant association");
114
+ if (existing && payload.repositories && payload.repositories.length > 0) {
115
+ const repos = payload.repositories.map((repo) => ({
116
+ repositoryId: String(repo.id),
117
+ repositoryName: repo.name,
118
+ repositoryFullName: repo.full_name,
119
+ private: repo.private
120
+ }));
121
+ await addRepositories(runDbClient_default)({
122
+ installationId: existing.id,
123
+ repositories: repos
124
+ });
125
+ logger.info({
126
+ installationId,
127
+ repositoryCount: repos.length
128
+ }, "Added initial repositories from installation created event");
129
+ }
130
+ break;
131
+ }
132
+ case "deleted": {
133
+ const existing = await getInstallationByGitHubId(runDbClient_default)(installationId);
134
+ if (existing) {
135
+ logger.info({
136
+ installationId,
137
+ existingId: existing.id
138
+ }, "Deleting installation");
139
+ await deleteInstallation(runDbClient_default)({
140
+ tenantId: existing.tenantId,
141
+ id: existing.id
142
+ });
143
+ } else logger.warn({ installationId }, "Received deleted event for unknown installation");
144
+ break;
145
+ }
146
+ case "suspend": {
147
+ const existing = await getInstallationByGitHubId(runDbClient_default)(installationId);
148
+ if (existing) {
149
+ logger.info({
150
+ installationId,
151
+ existingId: existing.id
152
+ }, "Suspending installation");
153
+ await updateInstallationStatusByGitHubId(runDbClient_default)({
154
+ gitHubInstallationId: installationId,
155
+ status: "suspended"
156
+ });
157
+ } else logger.warn({ installationId }, "Received suspend event for unknown installation");
158
+ break;
159
+ }
160
+ case "unsuspend": {
161
+ const existing = await getInstallationByGitHubId(runDbClient_default)(installationId);
162
+ if (existing) {
163
+ logger.info({
164
+ installationId,
165
+ existingId: existing.id
166
+ }, "Unsuspending installation");
167
+ await updateInstallationStatusByGitHubId(runDbClient_default)({
168
+ gitHubInstallationId: installationId,
169
+ status: "active"
170
+ });
171
+ } else logger.warn({ installationId }, "Received unsuspend event for unknown installation");
172
+ break;
173
+ }
174
+ case "new_permissions_accepted":
175
+ logger.info({ installationId }, "New permissions accepted for installation");
176
+ break;
177
+ default: logger.info({
178
+ action,
179
+ deliveryId
180
+ }, "Received unhandled installation action");
181
+ }
182
+ return c.json({
183
+ received: true,
184
+ action
185
+ }, 200);
186
+ } catch (error) {
187
+ logger.error({
188
+ error,
189
+ action,
190
+ installationId,
191
+ deliveryId
192
+ }, "Failed to process installation event");
193
+ return c.json({
194
+ received: true,
195
+ error: "Processing failed"
196
+ }, 200);
197
+ }
198
+ }
199
+ async function handleInstallationRepositoriesEvent(c, payload, deliveryId) {
200
+ const { action, installation, repositories_added, repositories_removed } = payload;
201
+ const installationId = String(installation.id);
202
+ logger.info({
203
+ action,
204
+ deliveryId,
205
+ installationId,
206
+ accountLogin: installation.account.login,
207
+ addedCount: repositories_added?.length ?? 0,
208
+ removedCount: repositories_removed?.length ?? 0
209
+ }, "Processing installation_repositories webhook event");
210
+ try {
211
+ const existing = await getInstallationByGitHubId(runDbClient_default)(installationId);
212
+ if (!existing) {
213
+ logger.warn({
214
+ installationId,
215
+ accountLogin: installation.account.login
216
+ }, "Received repository event for unknown installation");
217
+ return c.json({
218
+ received: true,
219
+ warning: "Unknown installation"
220
+ }, 200);
221
+ }
222
+ if (existing.status === "deleted") {
223
+ logger.info({ installationId }, "Ignoring repository event for deleted installation");
224
+ return c.json({
225
+ received: true,
226
+ skipped: "Installation deleted"
227
+ }, 200);
228
+ }
229
+ if (action === "added" && repositories_added && repositories_added.length > 0) {
230
+ const repos = repositories_added.map((repo) => ({
231
+ repositoryId: String(repo.id),
232
+ repositoryName: repo.name,
233
+ repositoryFullName: repo.full_name,
234
+ private: repo.private
235
+ }));
236
+ const added = await addRepositories(runDbClient_default)({
237
+ installationId: existing.id,
238
+ repositories: repos
239
+ });
240
+ logger.info({
241
+ installationId,
242
+ addedCount: added.length,
243
+ requestedCount: repos.length
244
+ }, "Added repositories to installation");
245
+ }
246
+ if (action === "removed" && repositories_removed && repositories_removed.length > 0) {
247
+ const repoIds = repositories_removed.map((repo) => String(repo.id));
248
+ const removedCount = await removeRepositories(runDbClient_default)({
249
+ installationId: existing.id,
250
+ repositoryIds: repoIds
251
+ });
252
+ logger.info({
253
+ installationId,
254
+ removedCount,
255
+ requestedCount: repoIds.length
256
+ }, "Removed repositories from installation");
257
+ }
258
+ return c.json({
259
+ received: true,
260
+ action
261
+ }, 200);
262
+ } catch (error) {
263
+ logger.error({
264
+ error,
265
+ action,
266
+ installationId,
267
+ deliveryId
268
+ }, "Failed to process installation_repositories event");
269
+ return c.json({
270
+ received: true,
271
+ error: "Processing failed"
272
+ }, 200);
273
+ }
274
+ }
275
+ var webhooks_default = app;
276
+
277
+ //#endregion
278
+ export { webhooks_default as default, verifyWebhookSignature };
@@ -0,0 +1,2 @@
1
+ import { getLogger } from "@inkeep/agents-core";
2
+ export { getLogger };
package/dist/logger.js ADDED
@@ -0,0 +1,3 @@
1
+ import { getLogger } from "@inkeep/agents-core";
2
+
3
+ export { getLogger };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@inkeep/agents-work-apps",
3
+ "version": "0.0.0-dev-20260203033642",
4
+ "description": "First party integrations for Inkeep Agents",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE.md",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./github": {
14
+ "types": "./dist/github/index.d.ts",
15
+ "import": "./dist/github/index.js"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.25.3",
20
+ "@octokit/auth-app": "^8.1.2",
21
+ "@octokit/rest": "^22.0.1",
22
+ "drizzle-orm": "^0.44.7",
23
+ "fetch-to-node": "^2.1.0",
24
+ "hono": "^4.10.4",
25
+ "jose": "^6.1.0",
26
+ "minimatch": "^10.1.1",
27
+ "@inkeep/agents-core": "0.0.0-dev-20260203033642"
28
+ },
29
+ "peerDependencies": {
30
+ "@hono/zod-openapi": "^1.1.5",
31
+ "zod": "^4.1.11"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.11.24",
35
+ "typescript": "^5.9.2",
36
+ "vitest": "^3.2.4"
37
+ },
38
+ "engines": {
39
+ "node": ">=22.18.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "registry": "https://registry.npmjs.org/"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE.md"
49
+ ],
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/inkeep/agents.git",
53
+ "directory": "packages/agents-workapps"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "dev": "pnpm build --watch",
58
+ "test": "vitest --run",
59
+ "lint": "biome lint --error-on-warnings",
60
+ "lint:fix": "biome check --write src",
61
+ "format": "biome format --write src",
62
+ "format:check": "biome format src",
63
+ "typecheck": "tsc --noEmit"
64
+ }
65
+ }