@shipfox/api-integration-github 5.0.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +48 -0
- package/dist/core/webhook-processor.d.ts +18 -0
- package/dist/core/webhook-processor.d.ts.map +1 -0
- package/dist/core/webhook-processor.js +83 -0
- package/dist/core/webhook-processor.js.map +1 -0
- package/dist/core/webhook.d.ts +7 -6
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +12 -28
- package/dist/core/webhook.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -6
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/install.d.ts +6 -1
- package/dist/presentation/routes/install.d.ts.map +1 -1
- package/dist/presentation/routes/install.js +5 -3
- package/dist/presentation/routes/install.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +2 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +73 -56
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -7
- package/src/core/webhook-processor.test.ts +166 -0
- package/src/core/webhook-processor.ts +97 -0
- package/src/core/webhook.test.ts +16 -16
- package/src/core/webhook.ts +29 -44
- package/src/index.test.ts +50 -0
- package/src/index.ts +24 -8
- package/src/presentation/routes/install.test.ts +2 -6
- package/src/presentation/routes/install.ts +16 -2
- package/src/presentation/routes/webhooks.ts +81 -47
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { defineRoute, rawBodyPlugin, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
|
|
4
|
-
import {
|
|
5
|
-
import { config } from '#config.js';
|
|
6
|
-
import { handleGithubEvent } from '#core/webhook.js';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { ClientError, defineRoute, rawBodyPlugin, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
|
|
4
|
+
import { createGithubWebhookProcessor } from '#core/webhook-processor.js';
|
|
7
5
|
const SIGNATURE_HEADER = 'x-hub-signature-256';
|
|
8
6
|
const EVENT_HEADER = 'x-github-event';
|
|
9
7
|
const DELIVERY_HEADER = 'x-github-delivery';
|
|
10
8
|
export function createGithubWebhookRoutes(options) {
|
|
11
|
-
const
|
|
12
|
-
secret: config.GITHUB_APP_WEBHOOK_SECRET
|
|
13
|
-
});
|
|
9
|
+
const processor = options.processor ?? createGithubWebhookProcessor(options);
|
|
14
10
|
const pushRoute = defineRoute({
|
|
15
11
|
method: 'POST',
|
|
16
12
|
path: '/',
|
|
@@ -41,58 +37,18 @@ export function createGithubWebhookRoutes(options) {
|
|
|
41
37
|
error: 'missing X-GitHub-Event header'
|
|
42
38
|
};
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
if (!Buffer.isBuffer(body)) {
|
|
40
|
+
if (!(request.body instanceof Uint8Array)) {
|
|
46
41
|
reply.code(400);
|
|
47
42
|
return {
|
|
48
43
|
error: 'expected raw JSON body'
|
|
49
44
|
};
|
|
50
45
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
deliveryId,
|
|
58
|
-
err: error
|
|
59
|
-
}, 'github webhook signature verification threw');
|
|
60
|
-
verified = false;
|
|
61
|
-
}
|
|
62
|
-
if (!verified) {
|
|
63
|
-
reply.code(401);
|
|
64
|
-
return {
|
|
65
|
-
error: 'invalid signature'
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
let parsedJson;
|
|
69
|
-
try {
|
|
70
|
-
parsedJson = JSON.parse(rawBody);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
logger().warn({
|
|
73
|
-
deliveryId,
|
|
74
|
-
err: error
|
|
75
|
-
}, 'github webhook payload JSON parse failed');
|
|
76
|
-
reply.code(400);
|
|
77
|
-
return {
|
|
78
|
-
error: 'malformed JSON'
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
await options.coreDb().transaction(async (tx)=>{
|
|
82
|
-
await handleGithubEvent({
|
|
83
|
-
tx,
|
|
84
|
-
deliveryId,
|
|
85
|
-
event,
|
|
86
|
-
payload: parsedJson,
|
|
87
|
-
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
88
|
-
publishSourcePush: options.publishSourcePush,
|
|
89
|
-
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
90
|
-
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
91
|
-
deleteInstallationTokenSecret: options.deleteInstallationTokenSecret
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
reply.code(204);
|
|
95
|
-
return null;
|
|
46
|
+
const result = await processor.process(createGithubStoredWebhookRequest({
|
|
47
|
+
body: request.body,
|
|
48
|
+
headers: request.headers,
|
|
49
|
+
rawQueryString: request.raw.url?.split('?')[1] ?? ''
|
|
50
|
+
}));
|
|
51
|
+
return sendGithubWebhookResponse(reply, result);
|
|
96
52
|
}
|
|
97
53
|
});
|
|
98
54
|
return {
|
|
@@ -106,5 +62,66 @@ export function createGithubWebhookRoutes(options) {
|
|
|
106
62
|
]
|
|
107
63
|
};
|
|
108
64
|
}
|
|
65
|
+
function createGithubStoredWebhookRequest(input) {
|
|
66
|
+
if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {
|
|
67
|
+
throw new ClientError('Webhook request body is too large', 'body-too-large', {
|
|
68
|
+
status: 413
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return createStoredWebhookRequest({
|
|
73
|
+
requestId: randomUUID(),
|
|
74
|
+
routeId: 'github',
|
|
75
|
+
receivedAt: new Date().toISOString(),
|
|
76
|
+
rawQueryString: input.rawQueryString,
|
|
77
|
+
headers: githubWebhookHeaders(input.headers),
|
|
78
|
+
body: input.body
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {
|
|
82
|
+
cause: error
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function githubWebhookHeaders(headers) {
|
|
87
|
+
return Object.fromEntries([
|
|
88
|
+
'content-type',
|
|
89
|
+
DELIVERY_HEADER,
|
|
90
|
+
EVENT_HEADER,
|
|
91
|
+
SIGNATURE_HEADER
|
|
92
|
+
].flatMap((name)=>{
|
|
93
|
+
const value = headers[name];
|
|
94
|
+
return typeof value === 'string' ? [
|
|
95
|
+
[
|
|
96
|
+
name,
|
|
97
|
+
value
|
|
98
|
+
]
|
|
99
|
+
] : [];
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function sendGithubWebhookResponse(reply, result) {
|
|
103
|
+
if (result.outcome !== 'discarded') {
|
|
104
|
+
reply.code(204);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
switch(result.reason){
|
|
108
|
+
case 'invalid_signature':
|
|
109
|
+
reply.code(401);
|
|
110
|
+
return {
|
|
111
|
+
error: 'invalid signature'
|
|
112
|
+
};
|
|
113
|
+
case 'malformed_payload':
|
|
114
|
+
reply.code(400);
|
|
115
|
+
return {
|
|
116
|
+
error: 'malformed JSON'
|
|
117
|
+
};
|
|
118
|
+
case 'connection_unavailable':
|
|
119
|
+
case 'missing_required_input':
|
|
120
|
+
case 'stale_at_receipt':
|
|
121
|
+
case 'unsupported_event':
|
|
122
|
+
reply.code(204);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
109
126
|
|
|
110
127
|
//# sourceMappingURL=webhooks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n PublishSourcePushFn,\n RecordDeliveryOnlyFn,\n StoredWebhookRequest,\n WebhookProcessingResult,\n} from '@shipfox/api-integration-core-dto';\nimport {\n createStoredWebhookRequest,\n WEBHOOK_MAX_RAW_BODY_BYTES,\n} from '@shipfox/api-integration-core-dto';\nimport {\n ClientError,\n defineRoute,\n type RouteGroup,\n rawBodyPlugin,\n WEBHOOK_BODY_LIMIT,\n} from '@shipfox/node-fastify';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {\n createGithubWebhookProcessor,\n type GithubWebhookProcessor,\n} from '#core/webhook-processor.js';\n\nconst SIGNATURE_HEADER = 'x-hub-signature-256';\nconst EVENT_HEADER = 'x-github-event';\nconst DELIVERY_HEADER = 'x-github-delivery';\n\nexport interface CreateGithubWebhookRoutesOptions {\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n publishSourcePush: PublishSourcePushFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n deleteInstallationTokenSecret?:\n | ((params: {workspaceId: string; installationId: number}) => Promise<unknown>)\n | undefined;\n processor?: GithubWebhookProcessor | undefined;\n}\n\nexport function createGithubWebhookRoutes(options: CreateGithubWebhookRoutesOptions): RouteGroup {\n const processor = options.processor ?? createGithubWebhookProcessor(options);\n\n const pushRoute = defineRoute({\n method: 'POST',\n path: '/',\n auth: [],\n description: 'GitHub App webhook receiver.',\n options: {bodyLimit: WEBHOOK_BODY_LIMIT},\n handler: async (request, reply) => {\n const deliveryId = request.headers[DELIVERY_HEADER];\n const signature = request.headers[SIGNATURE_HEADER];\n const event = request.headers[EVENT_HEADER];\n\n if (typeof deliveryId !== 'string' || !deliveryId) {\n reply.code(400);\n return {error: 'missing X-GitHub-Delivery header'};\n }\n if (typeof signature !== 'string' || !signature) {\n reply.code(401);\n return {error: 'missing X-Hub-Signature-256 header'};\n }\n if (typeof event !== 'string' || !event) {\n reply.code(400);\n return {error: 'missing X-GitHub-Event header'};\n }\n\n if (!(request.body instanceof Uint8Array)) {\n reply.code(400);\n return {error: 'expected raw JSON body'};\n }\n const result = await processor.process(\n createGithubStoredWebhookRequest({\n body: request.body,\n headers: request.headers,\n rawQueryString: request.raw.url?.split('?')[1] ?? '',\n }),\n );\n return sendGithubWebhookResponse(reply, result);\n },\n });\n\n return {\n prefix: '/webhooks/integrations/github',\n auth: [],\n plugins: [rawBodyPlugin],\n routes: [pushRoute],\n };\n}\n\nfunction createGithubStoredWebhookRequest(input: {\n body: Uint8Array;\n headers: Record<string, string | string[] | undefined>;\n rawQueryString: string;\n}): StoredWebhookRequest {\n if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {\n throw new ClientError('Webhook request body is too large', 'body-too-large', {status: 413});\n }\n try {\n return createStoredWebhookRequest({\n requestId: randomUUID(),\n routeId: 'github',\n receivedAt: new Date().toISOString(),\n rawQueryString: input.rawQueryString,\n headers: githubWebhookHeaders(input.headers),\n body: input.body,\n });\n } catch (error) {\n throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {\n cause: error,\n });\n }\n}\n\nfunction githubWebhookHeaders(headers: Record<string, string | string[] | undefined>) {\n return Object.fromEntries(\n ['content-type', DELIVERY_HEADER, EVENT_HEADER, SIGNATURE_HEADER].flatMap((name) => {\n const value = headers[name];\n return typeof value === 'string' ? [[name, value]] : [];\n }),\n );\n}\n\nfunction sendGithubWebhookResponse(\n reply: {code(statusCode: number): void},\n result: WebhookProcessingResult,\n) {\n if (result.outcome !== 'discarded') {\n reply.code(204);\n return null;\n }\n\n switch (result.reason) {\n case 'invalid_signature':\n reply.code(401);\n return {error: 'invalid signature'};\n case 'malformed_payload':\n reply.code(400);\n return {error: 'malformed JSON'};\n case 'connection_unavailable':\n case 'missing_required_input':\n case 'stale_at_receipt':\n case 'unsupported_event':\n reply.code(204);\n return null;\n }\n}\n"],"names":["randomUUID","createStoredWebhookRequest","WEBHOOK_MAX_RAW_BODY_BYTES","ClientError","defineRoute","rawBodyPlugin","WEBHOOK_BODY_LIMIT","createGithubWebhookProcessor","SIGNATURE_HEADER","EVENT_HEADER","DELIVERY_HEADER","createGithubWebhookRoutes","options","processor","pushRoute","method","path","auth","description","bodyLimit","handler","request","reply","deliveryId","headers","signature","event","code","error","body","Uint8Array","result","process","createGithubStoredWebhookRequest","rawQueryString","raw","url","split","sendGithubWebhookResponse","prefix","plugins","routes","input","byteLength","status","requestId","routeId","receivedAt","Date","toISOString","githubWebhookHeaders","cause","Object","fromEntries","flatMap","name","value","outcome","reason"],"mappings":"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"}
|