@shipfox/api-integration-github 4.0.0 → 6.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +52 -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 +19 -27
- 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
|
@@ -15,12 +15,7 @@ import type {ConnectGithubInstallationInput} from '#core/install.js';
|
|
|
15
15
|
import {verifyGithubInstallState} from '#core/state.js';
|
|
16
16
|
import {createGithubIntegrationProvider} from '#index.js';
|
|
17
17
|
|
|
18
|
-
vi.
|
|
19
|
-
requireWorkspaceMembership: vi.fn(() => Promise.resolve()),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const {requireWorkspaceMembership} = await import('@shipfox/api-workspaces');
|
|
23
|
-
const requireWorkspaceMembershipMock = vi.mocked(requireWorkspaceMembership);
|
|
18
|
+
const requireWorkspaceMembershipMock = vi.fn(() => Promise.resolve());
|
|
24
19
|
let authenticatedMemberships: UserContextMembership[] = [];
|
|
25
20
|
|
|
26
21
|
const fakeUserAuth: AuthMethod = {
|
|
@@ -111,6 +106,7 @@ async function createTestApp(options: CreateTestAppOptions = {}): Promise<Fastif
|
|
|
111
106
|
publishSourcePush: vi.fn(() => Promise.resolve({published: false})),
|
|
112
107
|
recordDeliveryOnly: vi.fn(() => Promise.resolve()),
|
|
113
108
|
getIntegrationConnectionById: vi.fn(() => Promise.resolve(undefined)),
|
|
109
|
+
requireActiveWorkspaceMembership: requireWorkspaceMembershipMock,
|
|
114
110
|
});
|
|
115
111
|
const app = await createApp({
|
|
116
112
|
auth: [fakeUserAuth],
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
githubCallbackQuerySchema,
|
|
7
7
|
githubCallbackResponseSchema,
|
|
8
8
|
} from '@shipfox/api-integration-github-dto';
|
|
9
|
-
import {requireWorkspaceMembership} from '@shipfox/api-workspaces';
|
|
10
9
|
import {defineRoute, type RouteGroup} from '@shipfox/node-fastify';
|
|
11
10
|
import type {GithubApiClient} from '#api/client.js';
|
|
12
11
|
import {config} from '#config.js';
|
|
@@ -23,12 +22,18 @@ export interface CreateGithubIntegrationRoutesOptions {
|
|
|
23
22
|
connectGithubInstallation: (
|
|
24
23
|
input: ConnectGithubInstallationInput,
|
|
25
24
|
) => Promise<IntegrationConnection<'github'>>;
|
|
25
|
+
requireActiveWorkspaceMembership?: (input: {
|
|
26
|
+
workspaceId: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;
|
|
29
|
+
}) => Promise<unknown>;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export function createGithubIntegrationRoutes({
|
|
29
33
|
github,
|
|
30
34
|
getExistingGithubConnection,
|
|
31
35
|
connectGithubInstallation,
|
|
36
|
+
requireActiveWorkspaceMembership,
|
|
32
37
|
}: CreateGithubIntegrationRoutesOptions): RouteGroup {
|
|
33
38
|
const createInstallRoute = defineRoute({
|
|
34
39
|
method: 'POST',
|
|
@@ -77,7 +82,8 @@ export function createGithubIntegrationRoutes({
|
|
|
77
82
|
state: request.query.state,
|
|
78
83
|
sessionUserId: actor.userId,
|
|
79
84
|
sessionMemberships: actor.memberships,
|
|
80
|
-
requireWorkspaceMembership
|
|
85
|
+
requireWorkspaceMembership:
|
|
86
|
+
requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck,
|
|
81
87
|
getExistingGithubConnection,
|
|
82
88
|
connectGithubInstallation,
|
|
83
89
|
});
|
|
@@ -91,3 +97,11 @@ export function createGithubIntegrationRoutes({
|
|
|
91
97
|
routes: [createInstallRoute, callbackApiRoute],
|
|
92
98
|
};
|
|
93
99
|
}
|
|
100
|
+
|
|
101
|
+
function unavailableWorkspaceMembershipCheck(_input: {
|
|
102
|
+
workspaceId: string;
|
|
103
|
+
userId: string;
|
|
104
|
+
memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;
|
|
105
|
+
}): Promise<never> {
|
|
106
|
+
return Promise.reject(new Error('Workspaces inter-module client is not configured'));
|
|
107
|
+
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {Webhooks} from '@octokit/webhooks';
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
3
2
|
import type {
|
|
4
3
|
GetIntegrationConnectionByIdFn,
|
|
5
4
|
PublishIntegrationEventReceivedFn,
|
|
6
5
|
PublishSourcePushFn,
|
|
7
6
|
RecordDeliveryOnlyFn,
|
|
7
|
+
StoredWebhookRequest,
|
|
8
|
+
WebhookProcessingResult,
|
|
8
9
|
} from '@shipfox/api-integration-core-dto';
|
|
9
10
|
import {
|
|
11
|
+
createStoredWebhookRequest,
|
|
12
|
+
WEBHOOK_MAX_RAW_BODY_BYTES,
|
|
13
|
+
} from '@shipfox/api-integration-core-dto';
|
|
14
|
+
import {
|
|
15
|
+
ClientError,
|
|
10
16
|
defineRoute,
|
|
11
17
|
type RouteGroup,
|
|
12
18
|
rawBodyPlugin,
|
|
13
19
|
WEBHOOK_BODY_LIMIT,
|
|
14
20
|
} from '@shipfox/node-fastify';
|
|
15
|
-
import {logger} from '@shipfox/node-opentelemetry';
|
|
16
21
|
import type {NodePgDatabase} from 'drizzle-orm/node-postgres';
|
|
17
|
-
import {
|
|
18
|
-
|
|
22
|
+
import {
|
|
23
|
+
createGithubWebhookProcessor,
|
|
24
|
+
type GithubWebhookProcessor,
|
|
25
|
+
} from '#core/webhook-processor.js';
|
|
19
26
|
|
|
20
27
|
const SIGNATURE_HEADER = 'x-hub-signature-256';
|
|
21
28
|
const EVENT_HEADER = 'x-github-event';
|
|
@@ -30,10 +37,11 @@ export interface CreateGithubWebhookRoutesOptions {
|
|
|
30
37
|
deleteInstallationTokenSecret?:
|
|
31
38
|
| ((params: {workspaceId: string; installationId: number}) => Promise<unknown>)
|
|
32
39
|
| undefined;
|
|
40
|
+
processor?: GithubWebhookProcessor | undefined;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export function createGithubWebhookRoutes(options: CreateGithubWebhookRoutesOptions): RouteGroup {
|
|
36
|
-
const
|
|
44
|
+
const processor = options.processor ?? createGithubWebhookProcessor(options);
|
|
37
45
|
|
|
38
46
|
const pushRoute = defineRoute({
|
|
39
47
|
method: 'POST',
|
|
@@ -59,50 +67,18 @@ export function createGithubWebhookRoutes(options: CreateGithubWebhookRoutesOpti
|
|
|
59
67
|
return {error: 'missing X-GitHub-Event header'};
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
if (!Buffer.isBuffer(body)) {
|
|
70
|
+
if (!(request.body instanceof Uint8Array)) {
|
|
64
71
|
reply.code(400);
|
|
65
72
|
return {error: 'expected raw JSON body'};
|
|
66
73
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
if (!verified) {
|
|
77
|
-
reply.code(401);
|
|
78
|
-
return {error: 'invalid signature'};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let parsedJson: unknown;
|
|
82
|
-
try {
|
|
83
|
-
parsedJson = JSON.parse(rawBody);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
logger().warn({deliveryId, err: error}, 'github webhook payload JSON parse failed');
|
|
86
|
-
reply.code(400);
|
|
87
|
-
return {error: 'malformed JSON'};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await options.coreDb().transaction(async (tx) => {
|
|
91
|
-
await handleGithubEvent({
|
|
92
|
-
tx,
|
|
93
|
-
deliveryId,
|
|
94
|
-
event,
|
|
95
|
-
payload: parsedJson,
|
|
96
|
-
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
97
|
-
publishSourcePush: options.publishSourcePush,
|
|
98
|
-
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
99
|
-
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
100
|
-
deleteInstallationTokenSecret: options.deleteInstallationTokenSecret,
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
reply.code(204);
|
|
105
|
-
return null;
|
|
74
|
+
const result = await processor.process(
|
|
75
|
+
createGithubStoredWebhookRequest({
|
|
76
|
+
body: request.body,
|
|
77
|
+
headers: request.headers,
|
|
78
|
+
rawQueryString: request.raw.url?.split('?')[1] ?? '',
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
return sendGithubWebhookResponse(reply, result);
|
|
106
82
|
},
|
|
107
83
|
});
|
|
108
84
|
|
|
@@ -113,3 +89,61 @@ export function createGithubWebhookRoutes(options: CreateGithubWebhookRoutesOpti
|
|
|
113
89
|
routes: [pushRoute],
|
|
114
90
|
};
|
|
115
91
|
}
|
|
92
|
+
|
|
93
|
+
function createGithubStoredWebhookRequest(input: {
|
|
94
|
+
body: Uint8Array;
|
|
95
|
+
headers: Record<string, string | string[] | undefined>;
|
|
96
|
+
rawQueryString: string;
|
|
97
|
+
}): StoredWebhookRequest {
|
|
98
|
+
if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {
|
|
99
|
+
throw new ClientError('Webhook request body is too large', 'body-too-large', {status: 413});
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return createStoredWebhookRequest({
|
|
103
|
+
requestId: randomUUID(),
|
|
104
|
+
routeId: 'github',
|
|
105
|
+
receivedAt: new Date().toISOString(),
|
|
106
|
+
rawQueryString: input.rawQueryString,
|
|
107
|
+
headers: githubWebhookHeaders(input.headers),
|
|
108
|
+
body: input.body,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {
|
|
112
|
+
cause: error,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function githubWebhookHeaders(headers: Record<string, string | string[] | undefined>) {
|
|
118
|
+
return Object.fromEntries(
|
|
119
|
+
['content-type', DELIVERY_HEADER, EVENT_HEADER, SIGNATURE_HEADER].flatMap((name) => {
|
|
120
|
+
const value = headers[name];
|
|
121
|
+
return typeof value === 'string' ? [[name, value]] : [];
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sendGithubWebhookResponse(
|
|
127
|
+
reply: {code(statusCode: number): void},
|
|
128
|
+
result: WebhookProcessingResult,
|
|
129
|
+
) {
|
|
130
|
+
if (result.outcome !== 'discarded') {
|
|
131
|
+
reply.code(204);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
switch (result.reason) {
|
|
136
|
+
case 'invalid_signature':
|
|
137
|
+
reply.code(401);
|
|
138
|
+
return {error: 'invalid signature'};
|
|
139
|
+
case 'malformed_payload':
|
|
140
|
+
reply.code(400);
|
|
141
|
+
return {error: 'malformed JSON'};
|
|
142
|
+
case 'connection_unavailable':
|
|
143
|
+
case 'missing_required_input':
|
|
144
|
+
case 'stale_at_receipt':
|
|
145
|
+
case 'unsupported_event':
|
|
146
|
+
reply.code(204);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|