@shipfox/api-integration-github 2.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.
Files changed (179) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +138 -0
  5. package/LICENSE +21 -0
  6. package/dist/api/client.d.ts +90 -0
  7. package/dist/api/client.d.ts.map +1 -0
  8. package/dist/api/client.js +306 -0
  9. package/dist/api/client.js.map +1 -0
  10. package/dist/api/installation-token-envelope.d.ts +36 -0
  11. package/dist/api/installation-token-envelope.d.ts.map +1 -0
  12. package/dist/api/installation-token-envelope.js +123 -0
  13. package/dist/api/installation-token-envelope.js.map +1 -0
  14. package/dist/api/installation-token-provider.d.ts +23 -0
  15. package/dist/api/installation-token-provider.d.ts.map +1 -0
  16. package/dist/api/installation-token-provider.js +130 -0
  17. package/dist/api/installation-token-provider.js.map +1 -0
  18. package/dist/api/shared-installation-token-cache.d.ts +43 -0
  19. package/dist/api/shared-installation-token-cache.d.ts.map +1 -0
  20. package/dist/api/shared-installation-token-cache.js +224 -0
  21. package/dist/api/shared-installation-token-cache.js.map +1 -0
  22. package/dist/config.d.ts +15 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +44 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/core/actions-logs.d.ts +47 -0
  27. package/dist/core/actions-logs.d.ts.map +1 -0
  28. package/dist/core/actions-logs.js +134 -0
  29. package/dist/core/actions-logs.js.map +1 -0
  30. package/dist/core/agent-tool-selection.d.ts +4 -0
  31. package/dist/core/agent-tool-selection.d.ts.map +1 -0
  32. package/dist/core/agent-tool-selection.js +38 -0
  33. package/dist/core/agent-tool-selection.js.map +1 -0
  34. package/dist/core/agent-tools.d.ts +50 -0
  35. package/dist/core/agent-tools.d.ts.map +1 -0
  36. package/dist/core/agent-tools.js +1122 -0
  37. package/dist/core/agent-tools.js.map +1 -0
  38. package/dist/core/errors.d.ts +15 -0
  39. package/dist/core/errors.d.ts.map +1 -0
  40. package/dist/core/errors.js +22 -0
  41. package/dist/core/errors.js.map +1 -0
  42. package/dist/core/install.d.ts +38 -0
  43. package/dist/core/install.d.ts.map +1 -0
  44. package/dist/core/install.js +64 -0
  45. package/dist/core/install.js.map +1 -0
  46. package/dist/core/source-control.d.ts +15 -0
  47. package/dist/core/source-control.d.ts.map +1 -0
  48. package/dist/core/source-control.js +175 -0
  49. package/dist/core/source-control.js.map +1 -0
  50. package/dist/core/state.d.ts +12 -0
  51. package/dist/core/state.d.ts.map +1 -0
  52. package/dist/core/state.js +54 -0
  53. package/dist/core/state.js.map +1 -0
  54. package/dist/core/webhook.d.ts +20 -0
  55. package/dist/core/webhook.d.ts.map +1 -0
  56. package/dist/core/webhook.js +216 -0
  57. package/dist/core/webhook.js.map +1 -0
  58. package/dist/db/db.d.ts +433 -0
  59. package/dist/db/db.d.ts.map +1 -0
  60. package/dist/db/db.js +18 -0
  61. package/dist/db/db.js.map +1 -0
  62. package/dist/db/installation-token-lock.d.ts +8 -0
  63. package/dist/db/installation-token-lock.d.ts.map +1 -0
  64. package/dist/db/installation-token-lock.js +37 -0
  65. package/dist/db/installation-token-lock.js.map +1 -0
  66. package/dist/db/installations.d.ts +33 -0
  67. package/dist/db/installations.d.ts.map +1 -0
  68. package/dist/db/installations.js +56 -0
  69. package/dist/db/installations.js.map +1 -0
  70. package/dist/db/migrations.d.ts +2 -0
  71. package/dist/db/migrations.d.ts.map +1 -0
  72. package/dist/db/migrations.js +5 -0
  73. package/dist/db/migrations.js.map +1 -0
  74. package/dist/db/schema/common.d.ts +2 -0
  75. package/dist/db/schema/common.d.ts.map +1 -0
  76. package/dist/db/schema/common.js +4 -0
  77. package/dist/db/schema/common.js.map +1 -0
  78. package/dist/db/schema/installations.d.ts +218 -0
  79. package/dist/db/schema/installations.d.ts.map +1 -0
  80. package/dist/db/schema/installations.js +46 -0
  81. package/dist/db/schema/installations.js.map +1 -0
  82. package/dist/index.d.ts +53 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +66 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/metrics/index.d.ts +2 -0
  87. package/dist/metrics/index.d.ts.map +1 -0
  88. package/dist/metrics/index.js +3 -0
  89. package/dist/metrics/index.js.map +1 -0
  90. package/dist/metrics/instance.d.ts +14 -0
  91. package/dist/metrics/instance.d.ts.map +1 -0
  92. package/dist/metrics/instance.js +75 -0
  93. package/dist/metrics/instance.js.map +1 -0
  94. package/dist/presentation/dto/integrations.d.ts +15 -0
  95. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  96. package/dist/presentation/dto/integrations.js +10 -0
  97. package/dist/presentation/dto/integrations.js.map +1 -0
  98. package/dist/presentation/e2eRoutes/create-connection.d.ts +11 -0
  99. package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
  100. package/dist/presentation/e2eRoutes/create-connection.js +57 -0
  101. package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
  102. package/dist/presentation/e2eRoutes/index.d.ts +5 -0
  103. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  104. package/dist/presentation/e2eRoutes/index.js +11 -0
  105. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  106. package/dist/presentation/routes/errors.d.ts +2 -0
  107. package/dist/presentation/routes/errors.d.ts.map +1 -0
  108. package/dist/presentation/routes/errors.js +46 -0
  109. package/dist/presentation/routes/errors.js.map +1 -0
  110. package/dist/presentation/routes/install.d.ts +13 -0
  111. package/dist/presentation/routes/install.d.ts.map +1 -0
  112. package/dist/presentation/routes/install.js +77 -0
  113. package/dist/presentation/routes/install.js.map +1 -0
  114. package/dist/presentation/routes/webhooks.d.ts +16 -0
  115. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  116. package/dist/presentation/routes/webhooks.js +110 -0
  117. package/dist/presentation/routes/webhooks.js.map +1 -0
  118. package/dist/tsconfig.test.tsbuildinfo +1 -0
  119. package/drizzle/0000_initial.sql +17 -0
  120. package/drizzle/meta/_journal.json +13 -0
  121. package/drizzle.config.ts +7 -0
  122. package/package.json +71 -0
  123. package/src/api/client.test.ts +94 -0
  124. package/src/api/client.ts +525 -0
  125. package/src/api/installation-token-envelope.ts +171 -0
  126. package/src/api/installation-token-provider.test.ts +241 -0
  127. package/src/api/installation-token-provider.ts +224 -0
  128. package/src/api/shared-installation-token-cache.test.ts +294 -0
  129. package/src/api/shared-installation-token-cache.ts +346 -0
  130. package/src/config.test.ts +14 -0
  131. package/src/config.ts +47 -0
  132. package/src/connection-external-url.test.ts +78 -0
  133. package/src/core/actions-logs.test.ts +156 -0
  134. package/src/core/actions-logs.ts +186 -0
  135. package/src/core/agent-tool-selection.test.ts +41 -0
  136. package/src/core/agent-tool-selection.ts +42 -0
  137. package/src/core/agent-tools.test.ts +539 -0
  138. package/src/core/agent-tools.ts +1269 -0
  139. package/src/core/errors.ts +23 -0
  140. package/src/core/install.test.ts +238 -0
  141. package/src/core/install.ts +119 -0
  142. package/src/core/source-control.test.ts +327 -0
  143. package/src/core/source-control.ts +223 -0
  144. package/src/core/state.test.ts +43 -0
  145. package/src/core/state.ts +85 -0
  146. package/src/core/webhook.test.ts +522 -0
  147. package/src/core/webhook.ts +296 -0
  148. package/src/db/db.ts +18 -0
  149. package/src/db/installation-token-lock.test.ts +68 -0
  150. package/src/db/installation-token-lock.ts +39 -0
  151. package/src/db/installations.test.ts +67 -0
  152. package/src/db/installations.ts +108 -0
  153. package/src/db/migrations.ts +4 -0
  154. package/src/db/schema/common.ts +3 -0
  155. package/src/db/schema/installations.ts +46 -0
  156. package/src/index.ts +124 -0
  157. package/src/metrics/index.ts +1 -0
  158. package/src/metrics/instance.ts +82 -0
  159. package/src/presentation/dto/integrations.ts +8 -0
  160. package/src/presentation/e2eRoutes/create-connection.ts +70 -0
  161. package/src/presentation/e2eRoutes/index.test.ts +126 -0
  162. package/src/presentation/e2eRoutes/index.ts +14 -0
  163. package/src/presentation/routes/errors.ts +43 -0
  164. package/src/presentation/routes/install.test.ts +279 -0
  165. package/src/presentation/routes/install.ts +93 -0
  166. package/src/presentation/routes/webhooks.test.ts +560 -0
  167. package/src/presentation/routes/webhooks.ts +115 -0
  168. package/test/env.ts +16 -0
  169. package/test/factories/github-installation.ts +35 -0
  170. package/test/factories/index.ts +1 -0
  171. package/test/fixtures/github-webhook.ts +20 -0
  172. package/test/globalSetup.ts +14 -0
  173. package/test/index.ts +2 -0
  174. package/test/setup.ts +17 -0
  175. package/tsconfig.build.json +9 -0
  176. package/tsconfig.build.tsbuildinfo +1 -0
  177. package/tsconfig.json +3 -0
  178. package/tsconfig.test.json +9 -0
  179. package/vitest.config.ts +12 -0
@@ -0,0 +1,279 @@
1
+ import {
2
+ AUTH_USER,
3
+ buildUserContext,
4
+ setUserContext,
5
+ type UserContextMembership,
6
+ } from '@shipfox/api-auth-context';
7
+ import {
8
+ ConnectionSlugConflictError,
9
+ type IntegrationConnection,
10
+ } from '@shipfox/api-integration-core-dto';
11
+ import {type AuthMethod, ClientError, closeApp, createApp} from '@shipfox/node-fastify';
12
+ import type {FastifyInstance, FastifyRequest} from 'fastify';
13
+ import type {GithubApiClient} from '#api/client.js';
14
+ import type {ConnectGithubInstallationInput} from '#core/install.js';
15
+ import {verifyGithubInstallState} from '#core/state.js';
16
+ import {createGithubIntegrationProvider} from '#index.js';
17
+
18
+ vi.mock('@shipfox/api-workspaces', () => ({
19
+ requireWorkspaceMembership: vi.fn(() => Promise.resolve()),
20
+ }));
21
+
22
+ const {requireWorkspaceMembership} = await import('@shipfox/api-workspaces');
23
+ const requireWorkspaceMembershipMock = vi.mocked(requireWorkspaceMembership);
24
+ let authenticatedMemberships: UserContextMembership[] = [];
25
+
26
+ const fakeUserAuth: AuthMethod = {
27
+ name: AUTH_USER,
28
+ authenticate: (request: FastifyRequest) => {
29
+ if (request.headers.authorization !== 'Bearer user') {
30
+ throw new ClientError('Invalid user token', 'unauthorized', {status: 401});
31
+ }
32
+
33
+ setUserContext(
34
+ request,
35
+ buildUserContext({
36
+ userId: 'user-1',
37
+ email: 'user@example.com',
38
+ memberships: authenticatedMemberships,
39
+ }),
40
+ );
41
+ return Promise.resolve();
42
+ },
43
+ };
44
+
45
+ function githubClient(overrides: Partial<GithubApiClient> = {}): GithubApiClient {
46
+ return {
47
+ exchangeOAuthCode: vi.fn(() => Promise.resolve('user-token')),
48
+ listUserInstallations: vi.fn(() => Promise.resolve({installationIds: [123], nextCursor: null})),
49
+ getInstallation: vi.fn(() =>
50
+ Promise.resolve({
51
+ id: 123,
52
+ account: {login: 'shipfox', type: 'Organization'},
53
+ repositorySelection: 'all',
54
+ suspendedAt: null,
55
+ htmlUrl: 'https://github.com/apps/shipfox/installations/123',
56
+ raw: {id: 123},
57
+ }),
58
+ ),
59
+ listInstallationRepositories: vi.fn(() =>
60
+ Promise.resolve({repositories: [], nextCursor: null}),
61
+ ),
62
+ getRepository: vi.fn(() => {
63
+ throw new Error('not used');
64
+ }),
65
+ listRepositoryFiles: vi.fn(() => Promise.resolve({files: [], nextCursor: null})),
66
+ fetchRepositoryFile: vi.fn(() => {
67
+ throw new Error('not used');
68
+ }),
69
+ createInstallationAccessToken: vi.fn(() =>
70
+ Promise.resolve({
71
+ token: 'ghs_installationtoken',
72
+ expiresAt: new Date('2026-06-10T12:00:00.000Z'),
73
+ }),
74
+ ),
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ interface CreateTestAppOptions {
80
+ github?: GithubApiClient;
81
+ existingConnection?: IntegrationConnection<'github'> | undefined;
82
+ connectGithubInstallation?:
83
+ | ((input: ConnectGithubInstallationInput) => Promise<IntegrationConnection<'github'>>)
84
+ | undefined;
85
+ }
86
+
87
+ async function createTestApp(options: CreateTestAppOptions = {}): Promise<FastifyInstance> {
88
+ const provider = createGithubIntegrationProvider({
89
+ github: options.github ?? githubClient(),
90
+ getExistingGithubConnection: vi.fn(() => Promise.resolve(options.existingConnection)),
91
+ connectGithubInstallation:
92
+ options.connectGithubInstallation ??
93
+ vi.fn((input: ConnectGithubInstallationInput) => {
94
+ const connection: IntegrationConnection<'github'> = {
95
+ id: crypto.randomUUID(),
96
+ workspaceId: input.workspaceId,
97
+ provider: 'github',
98
+ externalAccountId: input.installationId,
99
+ slug: 'github_shipfox',
100
+ displayName: input.displayName,
101
+ lifecycleStatus: 'active',
102
+ createdAt: new Date(),
103
+ updatedAt: new Date(),
104
+ };
105
+
106
+ return Promise.resolve(connection);
107
+ }),
108
+ // Webhook receiver dependencies — install/OAuth tests don't exercise them.
109
+ coreDb: vi.fn() as never,
110
+ publishIntegrationEventReceived: vi.fn(() => Promise.resolve({published: false})),
111
+ publishSourcePush: vi.fn(() => Promise.resolve({published: false})),
112
+ recordDeliveryOnly: vi.fn(() => Promise.resolve()),
113
+ getIntegrationConnectionById: vi.fn(() => Promise.resolve(undefined)),
114
+ });
115
+ const app = await createApp({
116
+ auth: [fakeUserAuth],
117
+ routes: provider.routes,
118
+ swagger: false,
119
+ });
120
+ await app.ready();
121
+ return app;
122
+ }
123
+
124
+ describe('GitHub integration routes', () => {
125
+ beforeEach(async () => {
126
+ authenticatedMemberships = [];
127
+ await closeApp();
128
+ });
129
+
130
+ afterEach(async () => {
131
+ await closeApp();
132
+ });
133
+
134
+ it('requires auth for install URL creation', async () => {
135
+ const app = await createTestApp();
136
+
137
+ const res = await app.inject({
138
+ method: 'POST',
139
+ url: '/integrations/github/install',
140
+ payload: {workspace_id: crypto.randomUUID()},
141
+ });
142
+
143
+ expect(res.statusCode).toBe(401);
144
+ });
145
+
146
+ it('returns an install URL with signed workspace state', async () => {
147
+ const app = await createTestApp();
148
+ const workspaceId = crypto.randomUUID();
149
+ authenticatedMemberships = [{workspaceId, role: 'admin'}];
150
+
151
+ const res = await app.inject({
152
+ method: 'POST',
153
+ url: '/integrations/github/install',
154
+ headers: {authorization: 'Bearer user'},
155
+ payload: {workspace_id: workspaceId},
156
+ });
157
+
158
+ const installUrl = new URL(res.json().install_url);
159
+ const state = installUrl.searchParams.get('state');
160
+ const claims = verifyGithubInstallState(state ?? '');
161
+ expect(res.statusCode).toBe(200);
162
+ expect(installUrl.toString()).toContain(
163
+ 'https://github.com/apps/shipfox-test/installations/new',
164
+ );
165
+ expect(claims.workspaceId).toBe(workspaceId);
166
+ expect(claims.userId).toBe('user-1');
167
+ });
168
+
169
+ it('requires auth on the GitHub callback API', async () => {
170
+ const app = await createTestApp();
171
+ const state = await createInstallState(app, crypto.randomUUID());
172
+
173
+ const res = await app.inject({
174
+ method: 'GET',
175
+ url: `/integrations/github/callback/api?code=code&installation_id=123&state=${state}`,
176
+ });
177
+
178
+ expect(res.statusCode).toBe(401);
179
+ });
180
+
181
+ it('handles a verified GitHub callback', async () => {
182
+ const app = await createTestApp();
183
+ const workspaceId = crypto.randomUUID();
184
+ const state = await createInstallState(app, workspaceId);
185
+
186
+ const res = await app.inject({
187
+ method: 'GET',
188
+ url: `/integrations/github/callback/api?code=code&installation_id=123&state=${state}`,
189
+ headers: {authorization: 'Bearer user'},
190
+ });
191
+
192
+ expect(res.statusCode).toBe(200);
193
+ expect(res.json().provider).toBe('github');
194
+ expect(res.json().external_account_id).toBe('123');
195
+ expect(requireWorkspaceMembershipMock).toHaveBeenCalledWith({
196
+ workspaceId,
197
+ userId: 'user-1',
198
+ memberships: [{workspaceId, role: 'admin'}],
199
+ });
200
+ });
201
+
202
+ it('rejects callbacks for inaccessible installations', async () => {
203
+ const app = await createTestApp({
204
+ github: githubClient({
205
+ listUserInstallations: vi.fn(() =>
206
+ Promise.resolve({installationIds: [999], nextCursor: null}),
207
+ ),
208
+ }),
209
+ });
210
+ const state = await createInstallState(app, crypto.randomUUID());
211
+
212
+ const res = await app.inject({
213
+ method: 'GET',
214
+ url: `/integrations/github/callback/api?code=code&installation_id=123&state=${state}`,
215
+ headers: {authorization: 'Bearer user'},
216
+ });
217
+
218
+ expect(res.statusCode).toBe(403);
219
+ expect(res.json().code).toBe('github-installation-not-authorized');
220
+ });
221
+
222
+ it('returns 409 when the installation is already linked to another workspace', async () => {
223
+ const app = await createTestApp({
224
+ existingConnection: {
225
+ id: crypto.randomUUID(),
226
+ workspaceId: crypto.randomUUID(),
227
+ provider: 'github',
228
+ externalAccountId: '123',
229
+ slug: 'github_shipfox',
230
+ displayName: 'GitHub shipfox',
231
+ lifecycleStatus: 'active',
232
+ createdAt: new Date(),
233
+ updatedAt: new Date(),
234
+ },
235
+ });
236
+ const state = await createInstallState(app, crypto.randomUUID());
237
+
238
+ const res = await app.inject({
239
+ method: 'GET',
240
+ url: `/integrations/github/callback/api?code=code&installation_id=123&state=${state}`,
241
+ headers: {authorization: 'Bearer user'},
242
+ });
243
+
244
+ expect(res.statusCode).toBe(409);
245
+ expect(res.json().code).toBe('github-installation-already-linked');
246
+ });
247
+
248
+ it('returns 409 when connection slug allocation conflicts repeatedly', async () => {
249
+ const app = await createTestApp({
250
+ connectGithubInstallation: vi.fn(() =>
251
+ Promise.reject(new ConnectionSlugConflictError(new Error('duplicate slug'))),
252
+ ),
253
+ });
254
+ const state = await createInstallState(app, crypto.randomUUID());
255
+
256
+ const res = await app.inject({
257
+ method: 'GET',
258
+ url: `/integrations/github/callback/api?code=code&installation_id=123&state=${state}`,
259
+ headers: {authorization: 'Bearer user'},
260
+ });
261
+
262
+ expect(res.statusCode).toBe(409);
263
+ expect(res.json().code).toBe('slug-conflict');
264
+ });
265
+ });
266
+
267
+ async function createInstallState(app: FastifyInstance, workspaceId: string): Promise<string> {
268
+ authenticatedMemberships = [{workspaceId, role: 'admin'}];
269
+ const res = await app.inject({
270
+ method: 'POST',
271
+ url: '/integrations/github/install',
272
+ headers: {authorization: 'Bearer user'},
273
+ payload: {workspace_id: workspaceId},
274
+ });
275
+ const installUrl = new URL(res.json().install_url);
276
+ const state = installUrl.searchParams.get('state');
277
+ if (!state) throw new Error('Install URL did not include state');
278
+ return encodeURIComponent(state);
279
+ }
@@ -0,0 +1,93 @@
1
+ import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
+ import type {IntegrationConnection} from '@shipfox/api-integration-core-dto';
3
+ import {
4
+ createGithubInstallBodySchema,
5
+ createGithubInstallResponseSchema,
6
+ githubCallbackQuerySchema,
7
+ githubCallbackResponseSchema,
8
+ } from '@shipfox/api-integration-github-dto';
9
+ import {requireWorkspaceMembership} from '@shipfox/api-workspaces';
10
+ import {defineRoute, type RouteGroup} from '@shipfox/node-fastify';
11
+ import type {GithubApiClient} from '#api/client.js';
12
+ import {config} from '#config.js';
13
+ import {type ConnectGithubInstallationInput, handleGithubCallback} from '#core/install.js';
14
+ import {signGithubInstallState} from '#core/state.js';
15
+ import {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';
16
+ import {githubRouteErrorHandler} from './errors.js';
17
+
18
+ export interface CreateGithubIntegrationRoutesOptions {
19
+ github: GithubApiClient;
20
+ getExistingGithubConnection: (input: {
21
+ installationId: string;
22
+ }) => Promise<IntegrationConnection<'github'> | undefined>;
23
+ connectGithubInstallation: (
24
+ input: ConnectGithubInstallationInput,
25
+ ) => Promise<IntegrationConnection<'github'>>;
26
+ }
27
+
28
+ export function createGithubIntegrationRoutes({
29
+ github,
30
+ getExistingGithubConnection,
31
+ connectGithubInstallation,
32
+ }: CreateGithubIntegrationRoutesOptions): RouteGroup {
33
+ const createInstallRoute = defineRoute({
34
+ method: 'POST',
35
+ path: '/install',
36
+ auth: AUTH_USER,
37
+ description: 'Create a GitHub App installation URL for a workspace.',
38
+ schema: {
39
+ body: createGithubInstallBodySchema,
40
+ response: {
41
+ 200: createGithubInstallResponseSchema,
42
+ },
43
+ },
44
+ handler: (request) => {
45
+ const {workspace_id: workspaceId} = request.body;
46
+ const actor = requireUserContext(request);
47
+
48
+ requireWorkspaceAccess({request, workspaceId});
49
+ const state = signGithubInstallState({workspaceId, userId: actor.userId});
50
+ const installUrl = new URL(
51
+ `https://github.com/apps/${config.GITHUB_APP_SLUG}/installations/new`,
52
+ );
53
+ installUrl.searchParams.set('state', state);
54
+
55
+ return {install_url: installUrl.toString()};
56
+ },
57
+ });
58
+
59
+ const callbackApiRoute = defineRoute({
60
+ method: 'GET',
61
+ path: '/callback/api',
62
+ auth: AUTH_USER,
63
+ description: 'Handle the GitHub App installation callback.',
64
+ schema: {
65
+ querystring: githubCallbackQuerySchema,
66
+ response: {
67
+ 200: githubCallbackResponseSchema,
68
+ },
69
+ },
70
+ errorHandler: githubRouteErrorHandler,
71
+ handler: async (request) => {
72
+ const actor = requireUserContext(request);
73
+ const connection = await handleGithubCallback({
74
+ github,
75
+ code: request.query.code,
76
+ installationId: request.query.installation_id,
77
+ state: request.query.state,
78
+ sessionUserId: actor.userId,
79
+ sessionMemberships: actor.memberships,
80
+ requireWorkspaceMembership,
81
+ getExistingGithubConnection,
82
+ connectGithubInstallation,
83
+ });
84
+
85
+ return toIntegrationConnectionDto(connection);
86
+ },
87
+ });
88
+
89
+ return {
90
+ prefix: '/integrations/github',
91
+ routes: [createInstallRoute, callbackApiRoute],
92
+ };
93
+ }