@shipfox/api-integration-core 13.0.0 → 14.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 (89) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +44 -0
  3. package/dist/core/event-catalogs.d.ts +18 -0
  4. package/dist/core/event-catalogs.d.ts.map +1 -0
  5. package/dist/core/event-catalogs.js +27 -0
  6. package/dist/core/event-catalogs.js.map +1 -0
  7. package/dist/core/providers/registry.d.ts +1 -0
  8. package/dist/core/providers/registry.d.ts.map +1 -1
  9. package/dist/core/providers/registry.js +4 -1
  10. package/dist/core/providers/registry.js.map +1 -1
  11. package/dist/core/providers/source-control.d.ts +1 -1
  12. package/dist/core/providers/source-control.d.ts.map +1 -1
  13. package/dist/core/providers/source-control.js.map +1 -1
  14. package/dist/core/source-control-service.d.ts +5 -1
  15. package/dist/core/source-control-service.d.ts.map +1 -1
  16. package/dist/core/source-control-service.js +12 -0
  17. package/dist/core/source-control-service.js.map +1 -1
  18. package/dist/db/connections.d.ts +4 -1
  19. package/dist/db/connections.d.ts.map +1 -1
  20. package/dist/db/connections.js +15 -6
  21. package/dist/db/connections.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/metrics/instance.d.ts.map +1 -1
  26. package/dist/metrics/instance.js +2 -0
  27. package/dist/metrics/instance.js.map +1 -1
  28. package/dist/presentation/inter-module.d.ts.map +1 -1
  29. package/dist/presentation/inter-module.js +27 -5
  30. package/dist/presentation/inter-module.js.map +1 -1
  31. package/dist/presentation/routes/errors.d.ts.map +1 -1
  32. package/dist/presentation/routes/errors.js +1 -1
  33. package/dist/presentation/routes/errors.js.map +1 -1
  34. package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
  35. package/dist/presentation/routes/manage-connections.js +7 -3
  36. package/dist/presentation/routes/manage-connections.js.map +1 -1
  37. package/dist/providers/gitea.d.ts.map +1 -1
  38. package/dist/providers/gitea.js +5 -1
  39. package/dist/providers/gitea.js.map +1 -1
  40. package/dist/providers/github.d.ts.map +1 -1
  41. package/dist/providers/github.js +6 -5
  42. package/dist/providers/github.js.map +1 -1
  43. package/dist/providers/jira.d.ts.map +1 -1
  44. package/dist/providers/jira.js +7 -2
  45. package/dist/providers/jira.js.map +1 -1
  46. package/dist/providers/linear.d.ts.map +1 -1
  47. package/dist/providers/linear.js +6 -4
  48. package/dist/providers/linear.js.map +1 -1
  49. package/dist/providers/sentry.d.ts.map +1 -1
  50. package/dist/providers/sentry.js +3 -1
  51. package/dist/providers/sentry.js.map +1 -1
  52. package/dist/providers/slack.d.ts.map +1 -1
  53. package/dist/providers/slack.js +6 -4
  54. package/dist/providers/slack.js.map +1 -1
  55. package/dist/tsconfig.test.tsbuildinfo +1 -1
  56. package/package.json +12 -12
  57. package/src/core/agent-tool-selection.test.ts +4 -0
  58. package/src/core/event-catalogs.test.ts +16 -0
  59. package/src/core/event-catalogs.ts +34 -0
  60. package/src/core/providers/registry.test.ts +4 -0
  61. package/src/core/providers/registry.ts +9 -3
  62. package/src/core/providers/source-control.ts +2 -0
  63. package/src/core/secret-cleanup.test.ts +1 -0
  64. package/src/core/source-control-service.test.ts +48 -0
  65. package/src/core/source-control-service.ts +16 -0
  66. package/src/db/connections.test.ts +167 -0
  67. package/src/db/connections.ts +21 -4
  68. package/src/db/secret-cleanups.test.ts +1 -0
  69. package/src/index.ts +2 -0
  70. package/src/metrics/instance.ts +2 -0
  71. package/src/presentation/inter-module.test.ts +219 -0
  72. package/src/presentation/inter-module.ts +28 -3
  73. package/src/presentation/routes/errors.ts +3 -0
  74. package/src/presentation/routes/list-connections.test.ts +13 -1
  75. package/src/presentation/routes/list-repositories.test.ts +9 -0
  76. package/src/presentation/routes/manage-connections.test.ts +52 -0
  77. package/src/presentation/routes/manage-connections.ts +6 -2
  78. package/src/providers/gitea.ts +5 -0
  79. package/src/providers/github.ts +6 -1
  80. package/src/providers/jira.test.ts +3 -0
  81. package/src/providers/jira.ts +6 -0
  82. package/src/providers/linear.test.ts +3 -0
  83. package/src/providers/linear.ts +6 -1
  84. package/src/providers/modules.test.ts +64 -1
  85. package/src/providers/sentry.ts +3 -0
  86. package/src/providers/slack.test.ts +4 -0
  87. package/src/providers/slack.ts +6 -1
  88. package/test/route-utils.ts +4 -0
  89. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,6 +1,10 @@
1
+ import {INTEGRATION_CONNECTION_AVAILABLE} from '@shipfox/api-integration-core-dto';
2
+ import {sql} from 'drizzle-orm';
1
3
  import {createIntegrationProviderRegistry} from '#core/providers/registry.js';
2
4
  import {processIntegrationSecretCleanups} from '#core/secret-cleanup.js';
3
5
  import {getIntegrationConnectionById, upsertIntegrationConnection} from '#db/connections.js';
6
+ import {db} from '#db/db.js';
7
+ import {integrationsOutbox} from '#db/schema/outbox.js';
4
8
  import {listIntegrationSecretCleanups} from '#db/secret-cleanups.js';
5
9
  import {createTestApp, sourceProvider, useIntegrationRouteTest} from '#test/route-utils.js';
6
10
 
@@ -15,6 +19,7 @@ describe('PATCH /integration-connections/:connectionId', () => {
15
19
  externalAccountId: 'gitea-owner',
16
20
  slug: 'gitea_owner',
17
21
  displayName: 'Gitea',
22
+ capabilities: ['source_control'],
18
23
  });
19
24
 
20
25
  const res = await app.inject({
@@ -31,6 +36,41 @@ describe('PATCH /integration-connections/:connectionId', () => {
31
36
  expect(reloaded?.lifecycleStatus).toBe('disabled');
32
37
  });
33
38
 
39
+ it('publishes provider capabilities when reactivating a connection', async () => {
40
+ const app = await createTestApp([sourceProvider()]);
41
+ const connection = await upsertIntegrationConnection({
42
+ workspaceId: context.workspaceId,
43
+ provider: 'gitea',
44
+ externalAccountId: 'gitea-owner',
45
+ slug: 'gitea_owner',
46
+ displayName: 'Gitea',
47
+ lifecycleStatus: 'disabled',
48
+ });
49
+
50
+ const res = await app.inject({
51
+ method: 'PATCH',
52
+ url: `/integration-connections/${connection.id}`,
53
+ headers: {authorization: 'Bearer user'},
54
+ payload: {lifecycle_status: 'active'},
55
+ });
56
+
57
+ const [event] = await db()
58
+ .select({payload: integrationsOutbox.payload})
59
+ .from(integrationsOutbox)
60
+ .where(
61
+ sql`${integrationsOutbox.eventType} = ${INTEGRATION_CONNECTION_AVAILABLE} AND ${integrationsOutbox.payload}->>'connectionId' = ${connection.id}`,
62
+ );
63
+
64
+ expect(res.statusCode).toBe(200);
65
+ expect(event?.payload).toEqual({
66
+ provider: 'gitea',
67
+ workspaceId: context.workspaceId,
68
+ connectionId: connection.id,
69
+ slug: 'gitea_owner',
70
+ capabilities: ['source_control'],
71
+ });
72
+ });
73
+
34
74
  it('returns not-found for a missing connection', async () => {
35
75
  const app = await createTestApp([sourceProvider()]);
36
76
 
@@ -53,6 +93,7 @@ describe('PATCH /integration-connections/:connectionId', () => {
53
93
  externalAccountId: 'gitea-owner',
54
94
  slug: 'gitea_owner',
55
95
  displayName: 'Gitea',
96
+ capabilities: ['source_control'],
56
97
  });
57
98
 
58
99
  const res = await app.inject({
@@ -75,6 +116,7 @@ describe('PATCH /integration-connections/:connectionId', () => {
75
116
  externalAccountId: 'gitea-owner',
76
117
  slug: 'gitea_owner',
77
118
  displayName: 'Gitea',
119
+ capabilities: ['source_control'],
78
120
  });
79
121
 
80
122
  const res = await app.inject({
@@ -100,6 +142,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
100
142
  externalAccountId: 'gitea-owner',
101
143
  slug: 'gitea_owner',
102
144
  displayName: 'Gitea',
145
+ capabilities: ['source_control'],
103
146
  });
104
147
 
105
148
  const res = await app.inject({
@@ -133,6 +176,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
133
176
  externalAccountId: 'T123',
134
177
  slug: 'slack_acme',
135
178
  displayName: 'Slack Acme',
179
+ capabilities: [],
136
180
  });
137
181
 
138
182
  const res = await app.inject({
@@ -183,6 +227,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
183
227
  externalAccountId: 'T123',
184
228
  slug: 'slack_acme',
185
229
  displayName: 'Slack Acme',
230
+ capabilities: [],
186
231
  });
187
232
 
188
233
  const res = await app.inject({
@@ -234,6 +279,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
234
279
  externalAccountId: 'T123',
235
280
  slug: 'slack_acme',
236
281
  displayName: 'Slack Acme',
282
+ capabilities: [],
237
283
  });
238
284
 
239
285
  const res = await app.inject({
@@ -264,6 +310,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
264
310
  externalAccountId: 'T123',
265
311
  slug: 'slack_acme',
266
312
  displayName: 'Slack Acme',
313
+ capabilities: [],
267
314
  });
268
315
 
269
316
  const res = await app.inject({
@@ -297,6 +344,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
297
344
  externalAccountId: 'T123',
298
345
  slug: 'slack_acme',
299
346
  displayName: 'Slack Acme',
347
+ capabilities: [],
300
348
  });
301
349
 
302
350
  const res = await app.inject({
@@ -328,6 +376,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
328
376
  externalAccountId: 'T123',
329
377
  slug: 'slack_acme',
330
378
  displayName: 'Slack Acme',
379
+ capabilities: [],
331
380
  });
332
381
 
333
382
  const res = await app.inject({
@@ -356,6 +405,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
356
405
  externalAccountId: 'T123',
357
406
  slug: 'slack_acme',
358
407
  displayName: 'Slack Acme',
408
+ capabilities: [],
359
409
  });
360
410
 
361
411
  const res = await app.inject({
@@ -396,6 +446,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
396
446
  externalAccountId: 'T123',
397
447
  slug: 'slack_acme',
398
448
  displayName: 'Slack Acme',
449
+ capabilities: [],
399
450
  });
400
451
 
401
452
  const res = await app.inject({
@@ -443,6 +494,7 @@ describe('DELETE /integration-connections/:connectionId', () => {
443
494
  externalAccountId: 'T123',
444
495
  slug: 'slack_acme',
445
496
  displayName: 'Slack Acme',
497
+ capabilities: [],
446
498
  });
447
499
 
448
500
  const res = await app.inject({
@@ -40,16 +40,20 @@ export function createUpdateIntegrationConnectionRoute(registry: IntegrationProv
40
40
  }
41
41
 
42
42
  requireWorkspaceAccess({request, workspaceId: connection.workspaceId});
43
+ const provider = registry
44
+ .list()
45
+ .find((candidate) => candidate.provider === connection.provider);
46
+ const capabilities = provider?.capabilities ?? [];
43
47
  const updated = await updateIntegrationConnectionLifecycleStatus({
44
48
  id: connection.id,
45
49
  lifecycleStatus: request.body.lifecycle_status,
50
+ ...(request.body.lifecycle_status === 'active' ? {capabilities} : {}),
46
51
  });
47
52
  if (!updated) {
48
53
  throw new ClientError('Integration connection not found', 'not-found', {status: 404});
49
54
  }
50
55
 
51
- const provider = registry.list().find((candidate) => candidate.provider === updated.provider);
52
- return toIntegrationConnectionDto(updated, {capabilities: provider?.capabilities ?? []});
56
+ return toIntegrationConnectionDto(updated, {capabilities});
53
57
  },
54
58
  });
55
59
  }
@@ -1,6 +1,8 @@
1
1
  import type {ConnectGiteaConnectionInput} from '@shipfox/api-integration-gitea';
2
2
  import type {IntegrationConnection as CoreIntegrationConnection} from '@shipfox/api-integration-spi';
3
3
  import {config} from '#config.js';
4
+ import type {IntegrationCapability} from '#core/entities/provider.js';
5
+ import {getIntegrationProviderCapabilities} from '#core/providers/registry.js';
4
6
  import {
5
7
  getIntegrationConnectionById,
6
8
  resolveUniqueConnectionSlug,
@@ -19,6 +21,7 @@ async function loadGiteaModuleParts(): Promise<IntegrationModuleParts> {
19
21
  db: giteaDb,
20
22
  migrationsPath: giteaMigrationsPath,
21
23
  } = await import('@shipfox/api-integration-gitea');
24
+ let providerCapabilities: IntegrationCapability[] = [];
22
25
 
23
26
  async function getExistingGiteaConnection(input: {
24
27
  org: string;
@@ -53,6 +56,7 @@ async function loadGiteaModuleParts(): Promise<IntegrationModuleParts> {
53
56
  slug,
54
57
  displayName: input.displayName,
55
58
  lifecycleStatus: 'active',
59
+ capabilities: providerCapabilities,
56
60
  },
57
61
  {tx},
58
62
  );
@@ -78,6 +82,7 @@ async function loadGiteaModuleParts(): Promise<IntegrationModuleParts> {
78
82
  getIntegrationConnectionById,
79
83
  coreDb: db,
80
84
  });
85
+ providerCapabilities = getIntegrationProviderCapabilities(integrationProvider.adapters);
81
86
 
82
87
  return {
83
88
  provider: integrationProvider,
@@ -1,6 +1,8 @@
1
1
  import type {ConnectGithubInstallationInput} from '@shipfox/api-integration-github';
2
2
  import type {IntegrationConnection as CoreIntegrationConnection} from '@shipfox/api-integration-spi';
3
3
  import {config} from '#config.js';
4
+ import type {IntegrationCapability} from '#core/entities/provider.js';
5
+ import {getIntegrationProviderCapabilities} from '#core/providers/registry.js';
4
6
  import {
5
7
  getIntegrationConnectionById,
6
8
  resolveUniqueConnectionSlug,
@@ -55,6 +57,7 @@ async function loadGithubModuleParts(
55
57
  }
56
58
  : undefined,
57
59
  });
60
+ let providerCapabilities: IntegrationCapability[] = [];
58
61
 
59
62
  async function getExistingGithubConnection(input: {
60
63
  installationId: string;
@@ -91,6 +94,7 @@ async function loadGithubModuleParts(
91
94
  slug,
92
95
  displayName: input.displayName,
93
96
  lifecycleStatus: 'active',
97
+ capabilities: providerCapabilities,
94
98
  },
95
99
  {tx},
96
100
  );
@@ -123,6 +127,7 @@ async function loadGithubModuleParts(
123
127
  ? {requireActiveWorkspaceMembership: options.requireActiveWorkspaceMembership}
124
128
  : {}),
125
129
  });
130
+ providerCapabilities = getIntegrationProviderCapabilities(integrationProvider.adapters);
126
131
 
127
132
  return {
128
133
  provider: integrationProvider,
@@ -131,7 +136,7 @@ async function loadGithubModuleParts(
131
136
  createGithubE2eRoutes({
132
137
  getExistingGithubConnection,
133
138
  connectGithubInstallation,
134
- connectionCapabilities: ['source_control', 'agent_tools'],
139
+ connectionCapabilities: providerCapabilities,
135
140
  }),
136
141
  ],
137
142
  database: {
@@ -46,6 +46,7 @@ describe('jiraProviderModule', () => {
46
46
  externalAccountId: cloudId,
47
47
  slug: 'jira_acme',
48
48
  displayName: 'Jira Acme',
49
+ capabilities: ['agent_tools'],
49
50
  });
50
51
  await upsertJiraInstallation({
51
52
  connectionId: connection.id,
@@ -88,6 +89,7 @@ describe('jiraProviderModule', () => {
88
89
  externalAccountId: cloudId,
89
90
  slug: `jira_${cloudId}`,
90
91
  displayName: 'Jira Acme',
92
+ capabilities: ['agent_tools'],
91
93
  });
92
94
  await upsertJiraInstallation({
93
95
  connectionId: connection.id,
@@ -159,6 +161,7 @@ describe('jiraProviderModule', () => {
159
161
  externalAccountId: cloudId,
160
162
  slug: 'jira_acme_again',
161
163
  displayName: 'Jira Acme',
164
+ capabilities: ['agent_tools'],
162
165
  });
163
166
  await upsertJiraInstallation({
164
167
  connectionId: replacement.id,
@@ -5,6 +5,8 @@ import type {
5
5
  } from '@shipfox/api-integration-jira';
6
6
  import type {IntegrationConnection as CoreIntegrationConnection} from '@shipfox/api-integration-spi';
7
7
  import {config} from '#config.js';
8
+ import type {IntegrationCapability} from '#core/entities/provider.js';
9
+ import {getIntegrationProviderCapabilities} from '#core/providers/registry.js';
8
10
  import {
9
11
  deleteIntegrationConnection,
10
12
  getIntegrationConnectionById,
@@ -45,6 +47,7 @@ async function loadJiraModuleParts(
45
47
  withJiraWebhookRegistrationLock,
46
48
  } = await import('@shipfox/api-integration-jira');
47
49
  const jira = createJiraApiClient();
50
+ let providerCapabilities: IntegrationCapability[] = [];
48
51
 
49
52
  async function getExistingJiraConnection(input: {
50
53
  cloudId: string;
@@ -80,6 +83,7 @@ async function loadJiraModuleParts(
80
83
  slug,
81
84
  displayName: input.displayName,
82
85
  lifecycleStatus: 'active',
86
+ capabilities: providerCapabilities,
83
87
  },
84
88
  {tx},
85
89
  );
@@ -222,6 +226,7 @@ async function loadJiraModuleParts(
222
226
  {
223
227
  id: connectionId,
224
228
  lifecycleStatus: 'active',
229
+ capabilities: providerCapabilities,
225
230
  },
226
231
  tx === undefined ? {} : {tx: tx as IntegrationTx},
227
232
  );
@@ -240,6 +245,7 @@ async function loadJiraModuleParts(
240
245
  : {}),
241
246
  },
242
247
  });
248
+ providerCapabilities = getIntegrationProviderCapabilities(integrationProvider.adapters);
243
249
 
244
250
  return {
245
251
  provider: integrationProvider,
@@ -36,6 +36,7 @@ describe('linearProviderModule', () => {
36
36
  externalAccountId: organizationId,
37
37
  slug: 'linear_acme',
38
38
  displayName: 'Linear Acme',
39
+ capabilities: ['agent_tools'],
39
40
  });
40
41
  await upsertLinearInstallation({
41
42
  connectionId: connection.id,
@@ -67,6 +68,7 @@ describe('linearProviderModule', () => {
67
68
  externalAccountId: organizationId,
68
69
  slug: 'linear_acme_again',
69
70
  displayName: 'Linear Acme',
71
+ capabilities: ['agent_tools'],
70
72
  });
71
73
  await upsertLinearInstallation({
72
74
  connectionId: replacement.id,
@@ -107,6 +109,7 @@ describe('linearProviderModule', () => {
107
109
  externalAccountId: organizationId,
108
110
  slug: 'linear_acme',
109
111
  displayName: 'Linear Acme',
112
+ capabilities: ['agent_tools'],
110
113
  });
111
114
  await upsertLinearInstallation({
112
115
  connectionId: connection.id,
@@ -4,6 +4,8 @@ import type {
4
4
  } from '@shipfox/api-integration-linear';
5
5
  import type {IntegrationConnection as CoreIntegrationConnection} from '@shipfox/api-integration-spi';
6
6
  import {config} from '#config.js';
7
+ import type {IntegrationCapability} from '#core/entities/provider.js';
8
+ import {getIntegrationProviderCapabilities} from '#core/providers/registry.js';
7
9
  import {
8
10
  deleteIntegrationConnection,
9
11
  getIntegrationConnectionById,
@@ -36,6 +38,7 @@ async function loadLinearModuleParts(
36
38
  migrationsPath: linearMigrationsPath,
37
39
  upsertLinearInstallation,
38
40
  } = await import('@shipfox/api-integration-linear');
41
+ let providerCapabilities: IntegrationCapability[] = [];
39
42
 
40
43
  async function getExistingLinearConnection(input: {
41
44
  organizationId: string;
@@ -72,6 +75,7 @@ async function loadLinearModuleParts(
72
75
  slug,
73
76
  displayName: input.displayName,
74
77
  lifecycleStatus: 'active',
78
+ capabilities: providerCapabilities,
75
79
  },
76
80
  {tx},
77
81
  );
@@ -160,6 +164,7 @@ async function loadLinearModuleParts(
160
164
  : {}),
161
165
  },
162
166
  });
167
+ providerCapabilities = getIntegrationProviderCapabilities(integrationProvider.adapters);
163
168
 
164
169
  return {
165
170
  provider: integrationProvider,
@@ -170,7 +175,7 @@ async function loadLinearModuleParts(
170
175
  getExistingLinearConnection,
171
176
  connectLinearInstallation,
172
177
  disconnectLinearInstallation,
173
- connectionCapabilities: ['agent_tools'],
178
+ connectionCapabilities: providerCapabilities,
174
179
  }),
175
180
  ],
176
181
  database: {
@@ -1,5 +1,13 @@
1
+ import {INTEGRATION_CONNECTION_AVAILABLE} from '@shipfox/api-integration-core-dto';
2
+ import {closeApp, createApp} from '@shipfox/node-fastify';
3
+ import {eq, sql} from 'drizzle-orm';
4
+ import {db} from '#db/db.js';
5
+ import {integrationConnections} from '#db/schema/connections.js';
6
+ import {integrationsOutbox} from '#db/schema/outbox.js';
7
+
1
8
  describe('loadEnabledProviderModules', () => {
2
- afterEach(() => {
9
+ afterEach(async () => {
10
+ await closeApp();
3
11
  vi.unstubAllEnvs();
4
12
  vi.resetModules();
5
13
  });
@@ -63,4 +71,59 @@ describe('loadEnabledProviderModules', () => {
63
71
  expect(parts.map((part) => part.provider.provider)).toEqual(['jira', 'cron', 'webhook']);
64
72
  expect(parts[0]?.provider).toMatchObject({provider: 'jira', displayName: 'Jira'});
65
73
  });
74
+
75
+ it('publishes registry-derived capabilities for a GitHub connect flow', async () => {
76
+ vi.stubEnv('INTEGRATIONS_ENABLE_GITHUB_PROVIDER', 'true');
77
+ vi.resetModules();
78
+
79
+ const {createPostgresClient} = await import('@shipfox/node-postgres');
80
+ createPostgresClient();
81
+ const {loadEnabledProviderModules} = await import('#providers/modules.js');
82
+ const parts = await loadEnabledProviderModules();
83
+ const githubPart = parts.find((part) => part.provider.provider === 'github');
84
+ if (!githubPart?.e2eRoutes) throw new Error('GitHub E2E routes are not configured');
85
+
86
+ const workspaceId = crypto.randomUUID();
87
+ const installationId = Number.parseInt(crypto.randomUUID().replaceAll('-', '').slice(0, 8), 16);
88
+ try {
89
+ const app = await createApp({routes: githubPart.e2eRoutes, swagger: false});
90
+ const response = await app.inject({
91
+ method: 'POST',
92
+ url: '/integrations/github-connections',
93
+ payload: {
94
+ workspace_id: workspaceId,
95
+ installation_id: installationId,
96
+ account_login: 'shipfox-e2e',
97
+ display_name: 'GitHub E2E',
98
+ installer_user_id: crypto.randomUUID(),
99
+ },
100
+ });
101
+ const connection = response.json();
102
+
103
+ expect(response.statusCode).toBe(201);
104
+ expect(connection).toMatchObject({id: expect.any(String)});
105
+
106
+ const [event] = await db()
107
+ .select({payload: integrationsOutbox.payload})
108
+ .from(integrationsOutbox)
109
+ .where(
110
+ sql`${integrationsOutbox.eventType} = ${INTEGRATION_CONNECTION_AVAILABLE} AND ${integrationsOutbox.payload}->>'connectionId' = ${connection.id}`,
111
+ );
112
+
113
+ expect(event?.payload).toEqual({
114
+ provider: 'github',
115
+ workspaceId,
116
+ connectionId: connection.id,
117
+ slug: 'github_shipfox_e2e',
118
+ capabilities: ['source_control', 'agent_tools'],
119
+ });
120
+ } finally {
121
+ await db()
122
+ .delete(integrationsOutbox)
123
+ .where(sql`${integrationsOutbox.payload}->>'workspaceId' = ${workspaceId}`);
124
+ await db()
125
+ .delete(integrationConnections)
126
+ .where(eq(integrationConnections.workspaceId, workspaceId));
127
+ }
128
+ });
66
129
  });
@@ -1,6 +1,7 @@
1
1
  import type {ConnectSentryInstallationInput} from '@shipfox/api-integration-sentry';
2
2
  import type {IntegrationConnection as CoreIntegrationConnection} from '@shipfox/api-integration-spi';
3
3
  import {config} from '#config.js';
4
+ import type {IntegrationCapability} from '#core/entities/provider.js';
4
5
  import {
5
6
  getIntegrationConnectionById,
6
7
  resolveUniqueConnectionSlug,
@@ -22,6 +23,7 @@ async function loadSentryModuleParts(): Promise<IntegrationModuleParts> {
22
23
  db: sentryDb,
23
24
  migrationsPath: sentryMigrationsPath,
24
25
  } = await import('@shipfox/api-integration-sentry');
26
+ const providerCapabilities: IntegrationCapability[] = [];
25
27
 
26
28
  async function getConnectionById(
27
29
  id: string,
@@ -54,6 +56,7 @@ async function loadSentryModuleParts(): Promise<IntegrationModuleParts> {
54
56
  slug,
55
57
  displayName: input.displayName,
56
58
  lifecycleStatus: 'active',
59
+ capabilities: providerCapabilities,
57
60
  },
58
61
  {tx},
59
62
  );
@@ -38,6 +38,7 @@ describe('slackProviderModule', () => {
38
38
  externalAccountId: teamId,
39
39
  slug: `slack_${teamId}`,
40
40
  displayName: 'Slack Acme',
41
+ capabilities: ['agent_tools'],
41
42
  });
42
43
  await upsertSlackInstallation({
43
44
  connectionId: connection.id,
@@ -90,6 +91,7 @@ describe('slackProviderModule', () => {
90
91
  externalAccountId: teamId,
91
92
  slug: `slack_${teamId}`,
92
93
  displayName: 'Slack Acme',
94
+ capabilities: ['agent_tools'],
93
95
  });
94
96
  await upsertSlackInstallation({
95
97
  connectionId: connection.id,
@@ -121,6 +123,7 @@ describe('slackProviderModule', () => {
121
123
  externalAccountId: teamId,
122
124
  slug: `slack_${teamId}_again`,
123
125
  displayName: 'Slack Acme',
126
+ capabilities: ['agent_tools'],
124
127
  });
125
128
  await upsertSlackInstallation({
126
129
  connectionId: replacement.id,
@@ -167,6 +170,7 @@ describe('slackProviderModule', () => {
167
170
  externalAccountId: teamId,
168
171
  slug: `slack_${teamId}`,
169
172
  displayName: 'Slack Acme',
173
+ capabilities: ['agent_tools'],
170
174
  });
171
175
  await upsertSlackInstallation({
172
176
  connectionId: connection.id,
@@ -4,6 +4,8 @@ import type {
4
4
  } from '@shipfox/api-integration-slack';
5
5
  import type {IntegrationConnection as CoreIntegrationConnection} from '@shipfox/api-integration-spi';
6
6
  import {config} from '#config.js';
7
+ import type {IntegrationCapability} from '#core/entities/provider.js';
8
+ import {getIntegrationProviderCapabilities} from '#core/providers/registry.js';
7
9
  import {
8
10
  deleteIntegrationConnection,
9
11
  getIntegrationConnectionById,
@@ -39,6 +41,7 @@ async function loadSlackModuleParts(
39
41
  slackSecretsNamespace,
40
42
  upsertSlackInstallation,
41
43
  } = await import('@shipfox/api-integration-slack');
44
+ let providerCapabilities: IntegrationCapability[] = [];
42
45
 
43
46
  async function getExistingSlackConnection(input: {
44
47
  teamId: string;
@@ -75,6 +78,7 @@ async function loadSlackModuleParts(
75
78
  slug,
76
79
  displayName: input.displayName,
77
80
  lifecycleStatus: 'active',
81
+ capabilities: providerCapabilities,
78
82
  },
79
83
  {tx},
80
84
  );
@@ -163,6 +167,7 @@ async function loadSlackModuleParts(
163
167
  : {}),
164
168
  },
165
169
  });
170
+ providerCapabilities = getIntegrationProviderCapabilities(integrationProvider.adapters);
166
171
 
167
172
  return {
168
173
  provider: integrationProvider,
@@ -173,7 +178,7 @@ async function loadSlackModuleParts(
173
178
  getExistingSlackConnection,
174
179
  connectSlackInstallation,
175
180
  disconnectSlackInstallation,
176
- connectionCapabilities: ['agent_tools'],
181
+ connectionCapabilities: providerCapabilities,
177
182
  }),
178
183
  ],
179
184
  database: {
@@ -69,6 +69,10 @@ export function sourceProvider(overrides: Partial<IntegrationProvider> = {}): In
69
69
  throw new Error('not used');
70
70
  },
71
71
  resolveTriggerReference: () => null,
72
+ resolveRef: async () => {
73
+ await Promise.resolve();
74
+ throw new Error('not used');
75
+ },
72
76
  },
73
77
  },
74
78
  ...overrides,