@shipfox/api-integration-core 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 (275) 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 +264 -0
  5. package/LICENSE +21 -0
  6. package/dist/config.d.ts +9 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +29 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/agent-tool-selection.d.ts +15 -0
  11. package/dist/core/agent-tool-selection.d.ts.map +1 -0
  12. package/dist/core/agent-tool-selection.js +48 -0
  13. package/dist/core/agent-tool-selection.js.map +1 -0
  14. package/dist/core/entities/connection.d.ts +2 -0
  15. package/dist/core/entities/connection.d.ts.map +1 -0
  16. package/dist/core/entities/connection.js +3 -0
  17. package/dist/core/entities/connection.js.map +1 -0
  18. package/dist/core/entities/provider.d.ts +6 -0
  19. package/dist/core/entities/provider.d.ts.map +1 -0
  20. package/dist/core/entities/provider.js +3 -0
  21. package/dist/core/entities/provider.js.map +1 -0
  22. package/dist/core/errors.d.ts +29 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +40 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/providers/agent-tools.d.ts +2 -0
  27. package/dist/core/providers/agent-tools.d.ts.map +1 -0
  28. package/dist/core/providers/agent-tools.js +3 -0
  29. package/dist/core/providers/agent-tools.js.map +1 -0
  30. package/dist/core/providers/redact-checkout-spec.d.ts +9 -0
  31. package/dist/core/providers/redact-checkout-spec.d.ts.map +1 -0
  32. package/dist/core/providers/redact-checkout-spec.js +25 -0
  33. package/dist/core/providers/redact-checkout-spec.js.map +1 -0
  34. package/dist/core/providers/registry.d.ts +9 -0
  35. package/dist/core/providers/registry.d.ts.map +1 -0
  36. package/dist/core/providers/registry.js +52 -0
  37. package/dist/core/providers/registry.js.map +1 -0
  38. package/dist/core/providers/source-control.d.ts +2 -0
  39. package/dist/core/providers/source-control.d.ts.map +1 -0
  40. package/dist/core/providers/source-control.js +3 -0
  41. package/dist/core/providers/source-control.js.map +1 -0
  42. package/dist/core/source-control-service.d.ts +46 -0
  43. package/dist/core/source-control-service.d.ts.map +1 -0
  44. package/dist/core/source-control-service.js +84 -0
  45. package/dist/core/source-control-service.js.map +1 -0
  46. package/dist/db/connections.d.ts +67 -0
  47. package/dist/db/connections.d.ts.map +1 -0
  48. package/dist/db/connections.js +130 -0
  49. package/dist/db/connections.js.map +1 -0
  50. package/dist/db/db.d.ts +865 -0
  51. package/dist/db/db.d.ts.map +1 -0
  52. package/dist/db/db.js +22 -0
  53. package/dist/db/db.js.map +1 -0
  54. package/dist/db/migrations.d.ts +2 -0
  55. package/dist/db/migrations.d.ts.map +1 -0
  56. package/dist/db/migrations.js +5 -0
  57. package/dist/db/migrations.js.map +1 -0
  58. package/dist/db/schema/common.d.ts +2 -0
  59. package/dist/db/schema/common.d.ts.map +1 -0
  60. package/dist/db/schema/common.js +4 -0
  61. package/dist/db/schema/common.js.map +1 -0
  62. package/dist/db/schema/connections.d.ts +165 -0
  63. package/dist/db/schema/connections.d.ts.map +1 -0
  64. package/dist/db/schema/connections.js +37 -0
  65. package/dist/db/schema/connections.js.map +1 -0
  66. package/dist/db/schema/outbox.d.ts +195 -0
  67. package/dist/db/schema/outbox.d.ts.map +1 -0
  68. package/dist/db/schema/outbox.js +5 -0
  69. package/dist/db/schema/outbox.js.map +1 -0
  70. package/dist/db/schema/webhook-deliveries.d.ts +76 -0
  71. package/dist/db/schema/webhook-deliveries.d.ts.map +1 -0
  72. package/dist/db/schema/webhook-deliveries.js +22 -0
  73. package/dist/db/schema/webhook-deliveries.js.map +1 -0
  74. package/dist/db/webhook-deliveries.d.ts +55 -0
  75. package/dist/db/webhook-deliveries.d.ts.map +1 -0
  76. package/dist/db/webhook-deliveries.js +132 -0
  77. package/dist/db/webhook-deliveries.js.map +1 -0
  78. package/dist/index.d.ts +54 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +105 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/metrics/index.d.ts +2 -0
  83. package/dist/metrics/index.d.ts.map +1 -0
  84. package/dist/metrics/index.js +3 -0
  85. package/dist/metrics/index.js.map +1 -0
  86. package/dist/metrics/instance.d.ts +8 -0
  87. package/dist/metrics/instance.d.ts.map +1 -0
  88. package/dist/metrics/instance.js +17 -0
  89. package/dist/metrics/instance.js.map +1 -0
  90. package/dist/presentation/dto/integrations.d.ts +36 -0
  91. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  92. package/dist/presentation/dto/integrations.js +26 -0
  93. package/dist/presentation/dto/integrations.js.map +1 -0
  94. package/dist/presentation/routes/agent-tools-gateway/audit.d.ts +24 -0
  95. package/dist/presentation/routes/agent-tools-gateway/audit.d.ts.map +1 -0
  96. package/dist/presentation/routes/agent-tools-gateway/audit.js +52 -0
  97. package/dist/presentation/routes/agent-tools-gateway/audit.js.map +1 -0
  98. package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts +7 -0
  99. package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts.map +1 -0
  100. package/dist/presentation/routes/agent-tools-gateway/dispatch.js +108 -0
  101. package/dist/presentation/routes/agent-tools-gateway/dispatch.js.map +1 -0
  102. package/dist/presentation/routes/agent-tools-gateway/index.d.ts +12 -0
  103. package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -0
  104. package/dist/presentation/routes/agent-tools-gateway/index.js +46 -0
  105. package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -0
  106. package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts +17 -0
  107. package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts.map +1 -0
  108. package/dist/presentation/routes/agent-tools-gateway/mcp-server.js +126 -0
  109. package/dist/presentation/routes/agent-tools-gateway/mcp-server.js.map +1 -0
  110. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +37 -0
  111. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -0
  112. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +145 -0
  113. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -0
  114. package/dist/presentation/routes/errors.d.ts +2 -0
  115. package/dist/presentation/routes/errors.d.ts.map +1 -0
  116. package/dist/presentation/routes/errors.js +53 -0
  117. package/dist/presentation/routes/errors.js.map +1 -0
  118. package/dist/presentation/routes/index.d.ts +14 -0
  119. package/dist/presentation/routes/index.d.ts.map +1 -0
  120. package/dist/presentation/routes/index.js +26 -0
  121. package/dist/presentation/routes/index.js.map +1 -0
  122. package/dist/presentation/routes/list-connections.d.ts +3 -0
  123. package/dist/presentation/routes/list-connections.d.ts.map +1 -0
  124. package/dist/presentation/routes/list-connections.js +58 -0
  125. package/dist/presentation/routes/list-connections.js.map +1 -0
  126. package/dist/presentation/routes/list-providers.d.ts +3 -0
  127. package/dist/presentation/routes/list-providers.d.ts.map +1 -0
  128. package/dist/presentation/routes/list-providers.js +27 -0
  129. package/dist/presentation/routes/list-providers.js.map +1 -0
  130. package/dist/presentation/routes/list-repositories.d.ts +3 -0
  131. package/dist/presentation/routes/list-repositories.d.ts.map +1 -0
  132. package/dist/presentation/routes/list-repositories.js +41 -0
  133. package/dist/presentation/routes/list-repositories.js.map +1 -0
  134. package/dist/presentation/routes/manage-connections.d.ts +4 -0
  135. package/dist/presentation/routes/manage-connections.d.ts.map +1 -0
  136. package/dist/presentation/routes/manage-connections.js +81 -0
  137. package/dist/presentation/routes/manage-connections.js.map +1 -0
  138. package/dist/providers/connection-slug.d.ts +2 -0
  139. package/dist/providers/connection-slug.d.ts.map +1 -0
  140. package/dist/providers/connection-slug.js +19 -0
  141. package/dist/providers/connection-slug.js.map +1 -0
  142. package/dist/providers/cron.d.ts +3 -0
  143. package/dist/providers/cron.d.ts.map +1 -0
  144. package/dist/providers/cron.js +13 -0
  145. package/dist/providers/cron.js.map +1 -0
  146. package/dist/providers/gitea.d.ts +3 -0
  147. package/dist/providers/gitea.d.ts.map +1 -0
  148. package/dist/providers/gitea.js +75 -0
  149. package/dist/providers/gitea.js.map +1 -0
  150. package/dist/providers/github.d.ts +3 -0
  151. package/dist/providers/github.d.ts.map +1 -0
  152. package/dist/providers/github.js +109 -0
  153. package/dist/providers/github.js.map +1 -0
  154. package/dist/providers/linear.d.ts +3 -0
  155. package/dist/providers/linear.d.ts.map +1 -0
  156. package/dist/providers/linear.js +134 -0
  157. package/dist/providers/linear.js.map +1 -0
  158. package/dist/providers/modules.d.ts +3 -0
  159. package/dist/providers/modules.d.ts.map +1 -0
  160. package/dist/providers/modules.js +26 -0
  161. package/dist/providers/modules.js.map +1 -0
  162. package/dist/providers/sentry.d.ts +3 -0
  163. package/dist/providers/sentry.d.ts.map +1 -0
  164. package/dist/providers/sentry.js +83 -0
  165. package/dist/providers/sentry.js.map +1 -0
  166. package/dist/providers/types.d.ts +58 -0
  167. package/dist/providers/types.d.ts.map +1 -0
  168. package/dist/providers/types.js +3 -0
  169. package/dist/providers/types.js.map +1 -0
  170. package/dist/providers/webhook.d.ts +3 -0
  171. package/dist/providers/webhook.d.ts.map +1 -0
  172. package/dist/providers/webhook.js +24 -0
  173. package/dist/providers/webhook.js.map +1 -0
  174. package/dist/temporal/activities/index.d.ts +5 -0
  175. package/dist/temporal/activities/index.d.ts.map +1 -0
  176. package/dist/temporal/activities/index.js +8 -0
  177. package/dist/temporal/activities/index.js.map +1 -0
  178. package/dist/temporal/activities/prune-webhook-deliveries.d.ts +4 -0
  179. package/dist/temporal/activities/prune-webhook-deliveries.d.ts.map +1 -0
  180. package/dist/temporal/activities/prune-webhook-deliveries.js +11 -0
  181. package/dist/temporal/activities/prune-webhook-deliveries.js.map +1 -0
  182. package/dist/temporal/constants.d.ts +3 -0
  183. package/dist/temporal/constants.d.ts.map +1 -0
  184. package/dist/temporal/constants.js +4 -0
  185. package/dist/temporal/constants.js.map +1 -0
  186. package/dist/temporal/workflows/index.d.ts +2 -0
  187. package/dist/temporal/workflows/index.d.ts.map +1 -0
  188. package/dist/temporal/workflows/index.js +3 -0
  189. package/dist/temporal/workflows/index.js.map +1 -0
  190. package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts +2 -0
  191. package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts.map +1 -0
  192. package/dist/temporal/workflows/prune-webhook-deliveries-cron.js +14 -0
  193. package/dist/temporal/workflows/prune-webhook-deliveries-cron.js.map +1 -0
  194. package/dist/tsconfig.test.tsbuildinfo +1 -0
  195. package/drizzle/0000_initial.sql +41 -0
  196. package/drizzle/meta/0001_snapshot.json +341 -0
  197. package/drizzle/meta/_journal.json +13 -0
  198. package/drizzle.config.ts +7 -0
  199. package/package.json +85 -0
  200. package/src/config.test.ts +32 -0
  201. package/src/config.ts +28 -0
  202. package/src/core/agent-tool-selection.test.ts +137 -0
  203. package/src/core/agent-tool-selection.ts +80 -0
  204. package/src/core/entities/connection.ts +4 -0
  205. package/src/core/entities/provider.ts +21 -0
  206. package/src/core/errors.ts +55 -0
  207. package/src/core/providers/agent-tools.ts +10 -0
  208. package/src/core/providers/redact-checkout-spec.test.ts +78 -0
  209. package/src/core/providers/redact-checkout-spec.ts +20 -0
  210. package/src/core/providers/registry.test.ts +138 -0
  211. package/src/core/providers/registry.ts +90 -0
  212. package/src/core/providers/source-control.ts +17 -0
  213. package/src/core/source-control-service.test.ts +230 -0
  214. package/src/core/source-control-service.ts +161 -0
  215. package/src/db/connections.test.ts +296 -0
  216. package/src/db/connections.ts +262 -0
  217. package/src/db/db.ts +22 -0
  218. package/src/db/migrations.ts +4 -0
  219. package/src/db/schema/common.ts +3 -0
  220. package/src/db/schema/connections.ts +49 -0
  221. package/src/db/schema/outbox.ts +4 -0
  222. package/src/db/schema/webhook-deliveries.ts +19 -0
  223. package/src/db/webhook-deliveries.test.ts +267 -0
  224. package/src/db/webhook-deliveries.ts +207 -0
  225. package/src/index.ts +212 -0
  226. package/src/metrics/index.ts +1 -0
  227. package/src/metrics/instance.ts +35 -0
  228. package/src/presentation/dto/integrations.ts +39 -0
  229. package/src/presentation/routes/agent-tools-gateway/agent-tools-gateway.route.test.ts +371 -0
  230. package/src/presentation/routes/agent-tools-gateway/audit.test.ts +86 -0
  231. package/src/presentation/routes/agent-tools-gateway/audit.ts +90 -0
  232. package/src/presentation/routes/agent-tools-gateway/dispatch.ts +139 -0
  233. package/src/presentation/routes/agent-tools-gateway/index.ts +62 -0
  234. package/src/presentation/routes/agent-tools-gateway/mcp-server.test.ts +308 -0
  235. package/src/presentation/routes/agent-tools-gateway/mcp-server.ts +155 -0
  236. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +181 -0
  237. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +245 -0
  238. package/src/presentation/routes/errors.ts +63 -0
  239. package/src/presentation/routes/index.ts +53 -0
  240. package/src/presentation/routes/list-connections.test.ts +227 -0
  241. package/src/presentation/routes/list-connections.ts +56 -0
  242. package/src/presentation/routes/list-providers.test.ts +113 -0
  243. package/src/presentation/routes/list-providers.ts +28 -0
  244. package/src/presentation/routes/list-repositories.test.ts +150 -0
  245. package/src/presentation/routes/list-repositories.ts +47 -0
  246. package/src/presentation/routes/manage-connections.test.ts +125 -0
  247. package/src/presentation/routes/manage-connections.ts +77 -0
  248. package/src/providers/connection-slug.ts +19 -0
  249. package/src/providers/cron.ts +13 -0
  250. package/src/providers/gitea.ts +103 -0
  251. package/src/providers/github.ts +150 -0
  252. package/src/providers/linear.ts +179 -0
  253. package/src/providers/modules.test.ts +40 -0
  254. package/src/providers/modules.ts +32 -0
  255. package/src/providers/sentry.ts +113 -0
  256. package/src/providers/types.ts +52 -0
  257. package/src/providers/webhook.ts +30 -0
  258. package/src/temporal/activities/index.ts +7 -0
  259. package/src/temporal/activities/prune-webhook-deliveries.test.ts +39 -0
  260. package/src/temporal/activities/prune-webhook-deliveries.ts +9 -0
  261. package/src/temporal/constants.ts +3 -0
  262. package/src/temporal/workflows/index.ts +1 -0
  263. package/src/temporal/workflows/prune-webhook-deliveries-cron.ts +15 -0
  264. package/test/agent-tools-gateway-helpers.ts +210 -0
  265. package/test/env.ts +23 -0
  266. package/test/factories/connection.ts +21 -0
  267. package/test/globalSetup.ts +22 -0
  268. package/test/index.ts +1 -0
  269. package/test/route-utils.ts +104 -0
  270. package/test/setup.ts +19 -0
  271. package/tsconfig.build.json +9 -0
  272. package/tsconfig.build.tsbuildinfo +1 -0
  273. package/tsconfig.json +3 -0
  274. package/tsconfig.test.json +8 -0
  275. package/vitest.config.ts +13 -0
@@ -0,0 +1,113 @@
1
+ import {giteaProviderModule} from '#providers/gitea.js';
2
+ import {linearProviderModule} from '#providers/linear.js';
3
+ import {createTestApp, sourceProvider, useIntegrationRouteTest} from '#test/route-utils.js';
4
+
5
+ describe('GET /integration-providers', () => {
6
+ useIntegrationRouteTest();
7
+
8
+ it('requires user auth', async () => {
9
+ const app = await createTestApp([sourceProvider()]);
10
+
11
+ const res = await app.inject({
12
+ method: 'GET',
13
+ url: '/integration-providers',
14
+ });
15
+
16
+ expect(res.statusCode).toBe(401);
17
+ });
18
+
19
+ it('lists providers by capability', async () => {
20
+ const app = await createTestApp([
21
+ sourceProvider(),
22
+ {
23
+ provider: 'github',
24
+ displayName: 'GitHub',
25
+ },
26
+ ]);
27
+
28
+ const res = await app.inject({
29
+ method: 'GET',
30
+ url: '/integration-providers?capability=source_control',
31
+ headers: {authorization: 'Bearer user'},
32
+ });
33
+
34
+ expect(res.statusCode).toBe(200);
35
+ expect(res.json().providers.map((provider: {provider: string}) => provider.provider)).toEqual([
36
+ 'gitea',
37
+ ]);
38
+ });
39
+
40
+ it('lists providers with agent tools capability', async () => {
41
+ const app = await createTestApp([
42
+ sourceProvider(),
43
+ {
44
+ provider: 'github',
45
+ displayName: 'GitHub',
46
+ adapters: {
47
+ agent_tools: {
48
+ catalog: () => [],
49
+ selectionCatalog: () => ({selectors: []}),
50
+ openSession: async () => {
51
+ await Promise.resolve();
52
+ return {
53
+ call: async () => {
54
+ await Promise.resolve();
55
+ return {};
56
+ },
57
+ };
58
+ },
59
+ },
60
+ },
61
+ },
62
+ ]);
63
+
64
+ const res = await app.inject({
65
+ method: 'GET',
66
+ url: '/integration-providers?capability=agent_tools',
67
+ headers: {authorization: 'Bearer user'},
68
+ });
69
+
70
+ expect(res.statusCode).toBe(200);
71
+ expect(res.json().providers).toEqual([
72
+ {
73
+ provider: 'github',
74
+ display_name: 'GitHub',
75
+ capabilities: ['agent_tools'],
76
+ },
77
+ ]);
78
+ });
79
+
80
+ it('surfaces the gitea provider once its module is registered', async () => {
81
+ const {provider} = await giteaProviderModule.load();
82
+ const app = await createTestApp([provider]);
83
+
84
+ const res = await app.inject({
85
+ method: 'GET',
86
+ url: '/integration-providers',
87
+ headers: {authorization: 'Bearer user'},
88
+ });
89
+
90
+ expect(res.statusCode).toBe(200);
91
+ expect(res.json().providers.map((entry: {provider: string}) => entry.provider)).toContain(
92
+ 'gitea',
93
+ );
94
+ });
95
+
96
+ it('surfaces the Linear provider with agent tools once its module is registered', async () => {
97
+ const {provider} = await linearProviderModule.load();
98
+ const app = await createTestApp([provider]);
99
+
100
+ const res = await app.inject({
101
+ method: 'GET',
102
+ url: '/integration-providers',
103
+ headers: {authorization: 'Bearer user'},
104
+ });
105
+
106
+ expect(res.statusCode).toBe(200);
107
+ expect(res.json().providers).toContainEqual({
108
+ provider: 'linear',
109
+ display_name: 'Linear',
110
+ capabilities: ['agent_tools'],
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,28 @@
1
+ import {AUTH_USER} from '@shipfox/api-auth-context';
2
+ import {
3
+ listIntegrationProvidersQuerySchema,
4
+ listIntegrationProvidersResponseSchema,
5
+ } from '@shipfox/api-integration-core-dto';
6
+ import {defineRoute} from '@shipfox/node-fastify';
7
+ import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
8
+ import {toIntegrationProviderDto} from '#presentation/dto/integrations.js';
9
+
10
+ export function createListIntegrationProvidersRoute(registry: IntegrationProviderRegistry) {
11
+ return defineRoute({
12
+ method: 'GET',
13
+ path: '/integration-providers',
14
+ auth: AUTH_USER,
15
+ description: 'List integration providers available to the API.',
16
+ schema: {
17
+ querystring: listIntegrationProvidersQuerySchema,
18
+ response: {
19
+ 200: listIntegrationProvidersResponseSchema,
20
+ },
21
+ },
22
+ handler: async (request) => {
23
+ await Promise.resolve();
24
+ const providers = registry.list(request.query.capability);
25
+ return {providers: providers.map(toIntegrationProviderDto)};
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,150 @@
1
+ import {IntegrationProviderError} from '#core/errors.js';
2
+ import {upsertIntegrationConnection} from '#db/connections.js';
3
+ import {createTestApp, sourceProvider, useIntegrationRouteTest} from '#test/route-utils.js';
4
+
5
+ describe('GET /integration-connections/:connectionId/repositories', () => {
6
+ const context = useIntegrationRouteTest();
7
+
8
+ it('loads a connection before authorizing repository listing', async () => {
9
+ const app = await createTestApp([sourceProvider()]);
10
+ const connection = await upsertIntegrationConnection({
11
+ workspaceId: context.workspaceId,
12
+ provider: 'gitea',
13
+ externalAccountId: 'gitea-owner',
14
+ slug: 'gitea_owner',
15
+ displayName: 'Gitea',
16
+ });
17
+
18
+ const res = await app.inject({
19
+ method: 'GET',
20
+ url: `/integration-connections/${connection.id}/repositories`,
21
+ headers: {authorization: 'Bearer user'},
22
+ });
23
+
24
+ expect(res.statusCode).toBe(200);
25
+ expect(res.json().repositories[0].full_name).toBe('gitea-owner/platform');
26
+ });
27
+
28
+ it('returns 404 when connection is missing', async () => {
29
+ const app = await createTestApp([sourceProvider()]);
30
+
31
+ const res = await app.inject({
32
+ method: 'GET',
33
+ url: `/integration-connections/${crypto.randomUUID()}/repositories`,
34
+ headers: {authorization: 'Bearer user'},
35
+ });
36
+
37
+ expect(res.statusCode).toBe(404);
38
+ expect(res.json().code).toBe('integration-connection-not-found');
39
+ });
40
+
41
+ it('rejects inactive connections', async () => {
42
+ const app = await createTestApp([sourceProvider()]);
43
+ const connection = await upsertIntegrationConnection({
44
+ workspaceId: context.workspaceId,
45
+ provider: 'gitea',
46
+ externalAccountId: 'gitea-owner',
47
+ slug: 'gitea_owner',
48
+ displayName: 'Gitea',
49
+ lifecycleStatus: 'disabled',
50
+ });
51
+
52
+ const res = await app.inject({
53
+ method: 'GET',
54
+ url: `/integration-connections/${connection.id}/repositories`,
55
+ headers: {authorization: 'Bearer user'},
56
+ });
57
+
58
+ expect(res.statusCode).toBe(422);
59
+ expect(res.json().code).toBe('integration-connection-inactive');
60
+ });
61
+
62
+ it('rejects connections without source-control capability', async () => {
63
+ const app = await createTestApp([
64
+ sourceProvider({
65
+ provider: 'github',
66
+ displayName: 'GitHub',
67
+ adapters: {},
68
+ }),
69
+ ]);
70
+ const connection = await upsertIntegrationConnection({
71
+ workspaceId: context.workspaceId,
72
+ provider: 'github',
73
+ externalAccountId: 'team-1',
74
+ slug: 'github_team_1',
75
+ displayName: 'GitHub',
76
+ });
77
+
78
+ const res = await app.inject({
79
+ method: 'GET',
80
+ url: `/integration-connections/${connection.id}/repositories`,
81
+ headers: {authorization: 'Bearer user'},
82
+ });
83
+
84
+ expect(res.statusCode).toBe(422);
85
+ expect(res.json().code).toBe('integration-capability-unavailable');
86
+ });
87
+
88
+ it('returns a stable error when the connection provider is not registered', async () => {
89
+ const app = await createTestApp([]);
90
+ const connection = await upsertIntegrationConnection({
91
+ workspaceId: context.workspaceId,
92
+ provider: 'github',
93
+ externalAccountId: 'installation-1',
94
+ slug: 'github_installation_1',
95
+ displayName: 'GitHub',
96
+ });
97
+
98
+ const res = await app.inject({
99
+ method: 'GET',
100
+ url: `/integration-connections/${connection.id}/repositories`,
101
+ headers: {authorization: 'Bearer user'},
102
+ });
103
+
104
+ expect(res.statusCode).toBe(422);
105
+ expect(res.json().code).toBe('integration-provider-unavailable');
106
+ });
107
+
108
+ it('maps provider repository listing errors', async () => {
109
+ const app = await createTestApp([
110
+ sourceProvider({
111
+ adapters: {
112
+ source_control: {
113
+ listRepositories: async () => {
114
+ await Promise.resolve();
115
+ throw new IntegrationProviderError('rate-limited', 'Provider rate limited', 60);
116
+ },
117
+ resolveRepository: async () => {
118
+ await Promise.resolve();
119
+ throw new Error('not used');
120
+ },
121
+ listFiles: async () => {
122
+ await Promise.resolve();
123
+ return {files: [], nextCursor: null};
124
+ },
125
+ fetchFile: async () => {
126
+ await Promise.resolve();
127
+ throw new Error('not used');
128
+ },
129
+ },
130
+ },
131
+ }),
132
+ ]);
133
+ const connection = await upsertIntegrationConnection({
134
+ workspaceId: context.workspaceId,
135
+ provider: 'gitea',
136
+ externalAccountId: 'gitea-owner',
137
+ slug: 'gitea_owner',
138
+ displayName: 'Gitea',
139
+ });
140
+
141
+ const res = await app.inject({
142
+ method: 'GET',
143
+ url: `/integration-connections/${connection.id}/repositories`,
144
+ headers: {authorization: 'Bearer user'},
145
+ });
146
+
147
+ expect(res.statusCode).toBe(429);
148
+ expect(res.json().code).toBe('rate-limited');
149
+ });
150
+ });
@@ -0,0 +1,47 @@
1
+ import {AUTH_USER, requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
+ import {
3
+ listRepositoriesParamsSchema,
4
+ listRepositoriesQuerySchema,
5
+ listRepositoriesResponseSchema,
6
+ } from '@shipfox/api-integration-core-dto';
7
+ import {defineRoute} from '@shipfox/node-fastify';
8
+ import type {IntegrationSourceControlService} from '#core/source-control-service.js';
9
+ import {toRepositoryDto} from '#presentation/dto/integrations.js';
10
+ import {integrationRouteErrorHandler} from './errors.js';
11
+
12
+ export function createListRepositoriesRoute(sourceControl: IntegrationSourceControlService) {
13
+ return defineRoute({
14
+ method: 'GET',
15
+ path: '/integration-connections/:connectionId/repositories',
16
+ auth: AUTH_USER,
17
+ description: 'List repositories visible to a source-control integration connection.',
18
+ schema: {
19
+ params: listRepositoriesParamsSchema,
20
+ querystring: listRepositoriesQuerySchema,
21
+ response: {
22
+ 200: listRepositoriesResponseSchema,
23
+ },
24
+ },
25
+ errorHandler: integrationRouteErrorHandler,
26
+ handler: async (request) => {
27
+ const {connectionId} = request.params;
28
+ const connection = await sourceControl.getConnection(connectionId);
29
+
30
+ requireWorkspaceAccess({request, workspaceId: connection.workspaceId});
31
+
32
+ const page = await sourceControl.listRepositories({
33
+ connection,
34
+ limit: request.query.limit,
35
+ cursor: request.query.cursor,
36
+ search: request.query.search,
37
+ });
38
+
39
+ return {
40
+ repositories: page.repositories.map((repository) =>
41
+ toRepositoryDto(connection.id, repository),
42
+ ),
43
+ next_cursor: page.nextCursor,
44
+ };
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,125 @@
1
+ import {getIntegrationConnectionById, upsertIntegrationConnection} from '#db/connections.js';
2
+ import {createTestApp, sourceProvider, useIntegrationRouteTest} from '#test/route-utils.js';
3
+
4
+ describe('PATCH /integration-connections/:connectionId', () => {
5
+ const context = useIntegrationRouteTest();
6
+
7
+ it('updates a connection lifecycle status', async () => {
8
+ const app = await createTestApp([sourceProvider()]);
9
+ const connection = await upsertIntegrationConnection({
10
+ workspaceId: context.workspaceId,
11
+ provider: 'gitea',
12
+ externalAccountId: 'gitea-owner',
13
+ slug: 'gitea_owner',
14
+ displayName: 'Gitea',
15
+ });
16
+
17
+ const res = await app.inject({
18
+ method: 'PATCH',
19
+ url: `/integration-connections/${connection.id}`,
20
+ headers: {authorization: 'Bearer user'},
21
+ payload: {lifecycle_status: 'disabled'},
22
+ });
23
+
24
+ const reloaded = await getIntegrationConnectionById(connection.id);
25
+ expect(res.statusCode).toBe(200);
26
+ expect(res.json().lifecycle_status).toBe('disabled');
27
+ expect(res.json().capabilities).toEqual(['source_control']);
28
+ expect(reloaded?.lifecycleStatus).toBe('disabled');
29
+ });
30
+
31
+ it('returns not-found for a missing connection', async () => {
32
+ const app = await createTestApp([sourceProvider()]);
33
+
34
+ const res = await app.inject({
35
+ method: 'PATCH',
36
+ url: `/integration-connections/${crypto.randomUUID()}`,
37
+ headers: {authorization: 'Bearer user'},
38
+ payload: {lifecycle_status: 'disabled'},
39
+ });
40
+
41
+ expect(res.statusCode).toBe(404);
42
+ expect(res.json().code).toBe('not-found');
43
+ });
44
+
45
+ it('rejects system-owned lifecycle statuses', async () => {
46
+ const app = await createTestApp([sourceProvider()]);
47
+ const connection = await upsertIntegrationConnection({
48
+ workspaceId: context.workspaceId,
49
+ provider: 'gitea',
50
+ externalAccountId: 'gitea-owner',
51
+ slug: 'gitea_owner',
52
+ displayName: 'Gitea',
53
+ });
54
+
55
+ const res = await app.inject({
56
+ method: 'PATCH',
57
+ url: `/integration-connections/${connection.id}`,
58
+ headers: {authorization: 'Bearer user'},
59
+ payload: {lifecycle_status: 'error'},
60
+ });
61
+
62
+ const reloaded = await getIntegrationConnectionById(connection.id);
63
+ expect(res.statusCode).toBe(400);
64
+ expect(reloaded?.lifecycleStatus).toBe('active');
65
+ });
66
+
67
+ it('returns membership errors', async () => {
68
+ const app = await createTestApp([sourceProvider()]);
69
+ const connection = await upsertIntegrationConnection({
70
+ workspaceId: crypto.randomUUID(),
71
+ provider: 'gitea',
72
+ externalAccountId: 'gitea-owner',
73
+ slug: 'gitea_owner',
74
+ displayName: 'Gitea',
75
+ });
76
+
77
+ const res = await app.inject({
78
+ method: 'PATCH',
79
+ url: `/integration-connections/${connection.id}`,
80
+ headers: {authorization: 'Bearer user'},
81
+ payload: {lifecycle_status: 'disabled'},
82
+ });
83
+
84
+ expect(res.statusCode).toBe(403);
85
+ expect(res.json().code).toBe('forbidden');
86
+ });
87
+ });
88
+
89
+ describe('DELETE /integration-connections/:connectionId', () => {
90
+ const context = useIntegrationRouteTest();
91
+
92
+ it('deletes a connection', async () => {
93
+ const app = await createTestApp([sourceProvider()]);
94
+ const connection = await upsertIntegrationConnection({
95
+ workspaceId: context.workspaceId,
96
+ provider: 'gitea',
97
+ externalAccountId: 'gitea-owner',
98
+ slug: 'gitea_owner',
99
+ displayName: 'Gitea',
100
+ });
101
+
102
+ const res = await app.inject({
103
+ method: 'DELETE',
104
+ url: `/integration-connections/${connection.id}`,
105
+ headers: {authorization: 'Bearer user'},
106
+ });
107
+
108
+ const reloaded = await getIntegrationConnectionById(connection.id);
109
+ expect(res.statusCode).toBe(204);
110
+ expect(reloaded).toBeUndefined();
111
+ });
112
+
113
+ it('returns not-found for a missing connection', async () => {
114
+ const app = await createTestApp([sourceProvider()]);
115
+
116
+ const res = await app.inject({
117
+ method: 'DELETE',
118
+ url: `/integration-connections/${crypto.randomUUID()}`,
119
+ headers: {authorization: 'Bearer user'},
120
+ });
121
+
122
+ expect(res.statusCode).toBe(404);
123
+ expect(res.json().code).toBe('not-found');
124
+ });
125
+ });
@@ -0,0 +1,77 @@
1
+ import {AUTH_USER, requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
+ import {
3
+ integrationConnectionDtoSchema,
4
+ updateIntegrationConnectionBodySchema,
5
+ } from '@shipfox/api-integration-core-dto';
6
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
7
+ import {z} from 'zod';
8
+ import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
9
+ import {
10
+ deleteIntegrationConnection,
11
+ getIntegrationConnectionById,
12
+ updateIntegrationConnectionLifecycleStatus,
13
+ } from '#db/connections.js';
14
+ import {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';
15
+
16
+ const connectionParamsSchema = z.object({
17
+ connectionId: z.string().uuid(),
18
+ });
19
+
20
+ export function createUpdateIntegrationConnectionRoute(registry: IntegrationProviderRegistry) {
21
+ return defineRoute({
22
+ method: 'PATCH',
23
+ path: '/integration-connections/:connectionId',
24
+ auth: AUTH_USER,
25
+ description: 'Update an integration connection.',
26
+ schema: {
27
+ params: connectionParamsSchema,
28
+ body: updateIntegrationConnectionBodySchema,
29
+ response: {
30
+ 200: integrationConnectionDtoSchema,
31
+ },
32
+ },
33
+ handler: async (request) => {
34
+ const connection = await getIntegrationConnectionById(request.params.connectionId);
35
+ if (!connection) {
36
+ throw new ClientError('Integration connection not found', 'not-found', {status: 404});
37
+ }
38
+
39
+ requireWorkspaceAccess({request, workspaceId: connection.workspaceId});
40
+ const updated = await updateIntegrationConnectionLifecycleStatus({
41
+ id: connection.id,
42
+ lifecycleStatus: request.body.lifecycle_status,
43
+ });
44
+ if (!updated) {
45
+ throw new ClientError('Integration connection not found', 'not-found', {status: 404});
46
+ }
47
+
48
+ const provider = registry.list().find((candidate) => candidate.provider === updated.provider);
49
+ return toIntegrationConnectionDto(updated, {capabilities: provider?.capabilities ?? []});
50
+ },
51
+ });
52
+ }
53
+
54
+ export function createDeleteIntegrationConnectionRoute() {
55
+ return defineRoute({
56
+ method: 'DELETE',
57
+ path: '/integration-connections/:connectionId',
58
+ auth: AUTH_USER,
59
+ description: 'Delete an integration connection.',
60
+ schema: {
61
+ params: connectionParamsSchema,
62
+ response: {
63
+ 204: z.void(),
64
+ },
65
+ },
66
+ handler: async (request, reply) => {
67
+ const connection = await getIntegrationConnectionById(request.params.connectionId);
68
+ if (!connection) {
69
+ throw new ClientError('Integration connection not found', 'not-found', {status: 404});
70
+ }
71
+
72
+ requireWorkspaceAccess({request, workspaceId: connection.workspaceId});
73
+ await deleteIntegrationConnection({id: connection.id});
74
+ reply.status(204);
75
+ },
76
+ });
77
+ }
@@ -0,0 +1,19 @@
1
+ import {ConnectionSlugConflictError} from '@shipfox/api-integration-core-dto';
2
+ import {isIntegrationConnectionSlugUniqueViolation} from '#db/connections.js';
3
+
4
+ const MAX_CONNECTION_SLUG_ATTEMPTS = 3;
5
+
6
+ export async function retryConnectionSlugCollision<T>(operation: () => Promise<T>): Promise<T> {
7
+ for (let attempt = 1; ; attempt += 1) {
8
+ try {
9
+ return await operation();
10
+ } catch (error) {
11
+ if (!isIntegrationConnectionSlugUniqueViolation(error)) {
12
+ throw error;
13
+ }
14
+ if (attempt >= MAX_CONNECTION_SLUG_ATTEMPTS) {
15
+ throw new ConnectionSlugConflictError(error);
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ import {config} from '#config.js';
2
+ import type {IntegrationProviderModule} from '#providers/types.js';
3
+
4
+ export const cronProviderModule: IntegrationProviderModule = {
5
+ id: 'cron',
6
+ enabled: config.INTEGRATIONS_ENABLE_CRON_PROVIDER,
7
+ load: async () => ({
8
+ provider: {
9
+ provider: 'cron',
10
+ displayName: 'Cron',
11
+ },
12
+ }),
13
+ };
@@ -0,0 +1,103 @@
1
+ import {
2
+ type IntegrationConnection as CoreIntegrationConnection,
3
+ slugifyConnectionSlug,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import type {ConnectGiteaConnectionInput} from '@shipfox/api-integration-gitea';
6
+ import {config} from '#config.js';
7
+ import {
8
+ getIntegrationConnectionById,
9
+ resolveUniqueConnectionSlug,
10
+ upsertIntegrationConnection,
11
+ } from '#db/connections.js';
12
+ import {db} from '#db/db.js';
13
+ import {publishSourcePush, recordDeliveryOnly} from '#db/webhook-deliveries.js';
14
+ import {retryConnectionSlugCollision} from '#providers/connection-slug.js';
15
+ import type {IntegrationModuleParts, IntegrationProviderModule} from '#providers/types.js';
16
+
17
+ // Stable migration-tracking table name for the Gitea provider database. This
18
+ // must NOT depend on the provider's position in the module `database` array. A
19
+ // positional name would shift if a provider is flag-disabled and silently
20
+ // re-run migrations against existing tables.
21
+ const GITEA_MIGRATIONS_TABLE = '__drizzle_migrations_integrations_gitea';
22
+
23
+ async function loadGiteaModuleParts(): Promise<IntegrationModuleParts> {
24
+ const {
25
+ createGiteaIntegrationProvider,
26
+ getGiteaConnectionByOrg,
27
+ upsertGiteaConnection,
28
+ db: giteaDb,
29
+ migrationsPath: giteaMigrationsPath,
30
+ } = await import('@shipfox/api-integration-gitea');
31
+
32
+ async function getExistingGiteaConnection(input: {
33
+ org: string;
34
+ }): Promise<CoreIntegrationConnection<'gitea'> | undefined> {
35
+ const row = await getGiteaConnectionByOrg(input.org);
36
+ if (!row) return undefined;
37
+ const connection = await getIntegrationConnectionById(row.connectionId);
38
+ if (!connection) return undefined;
39
+ return connection as CoreIntegrationConnection<'gitea'>;
40
+ }
41
+
42
+ async function connectGiteaConnection(
43
+ input: ConnectGiteaConnectionInput,
44
+ ): Promise<CoreIntegrationConnection<'gitea'>> {
45
+ return await retryConnectionSlugCollision(() =>
46
+ db().transaction(async (tx) => {
47
+ const baseSlug = slugifyConnectionSlug(`gitea_${input.org}`, {fallback: 'gitea'});
48
+ const slug = await resolveUniqueConnectionSlug(
49
+ {
50
+ workspaceId: input.workspaceId,
51
+ provider: 'gitea',
52
+ externalAccountId: input.org,
53
+ baseSlug,
54
+ },
55
+ {tx},
56
+ );
57
+ const connection = await upsertIntegrationConnection(
58
+ {
59
+ workspaceId: input.workspaceId,
60
+ provider: 'gitea',
61
+ externalAccountId: input.org,
62
+ slug,
63
+ displayName: input.displayName,
64
+ lifecycleStatus: 'active',
65
+ },
66
+ {tx},
67
+ );
68
+
69
+ await upsertGiteaConnection(
70
+ {
71
+ connectionId: connection.id,
72
+ org: input.org,
73
+ },
74
+ {tx},
75
+ );
76
+
77
+ return connection as CoreIntegrationConnection<'gitea'>;
78
+ }),
79
+ );
80
+ }
81
+
82
+ return {
83
+ provider: createGiteaIntegrationProvider({
84
+ getExistingGiteaConnection,
85
+ connectGiteaConnection,
86
+ publishSourcePush,
87
+ recordDeliveryOnly,
88
+ getIntegrationConnectionById,
89
+ coreDb: db,
90
+ }),
91
+ database: {
92
+ db: giteaDb,
93
+ migrationsPath: giteaMigrationsPath,
94
+ migrationsTableName: GITEA_MIGRATIONS_TABLE,
95
+ },
96
+ };
97
+ }
98
+
99
+ export const giteaProviderModule: IntegrationProviderModule = {
100
+ id: 'gitea',
101
+ enabled: config.INTEGRATIONS_ENABLE_GITEA_PROVIDER,
102
+ load: loadGiteaModuleParts,
103
+ };