@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,150 @@
1
+ import {
2
+ type IntegrationConnection as CoreIntegrationConnection,
3
+ slugifyConnectionSlug,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import type {ConnectGithubInstallationInput} from '@shipfox/api-integration-github';
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 {
14
+ publishIntegrationEventReceived,
15
+ publishSourcePush,
16
+ recordDeliveryOnly,
17
+ } from '#db/webhook-deliveries.js';
18
+ import {retryConnectionSlugCollision} from '#providers/connection-slug.js';
19
+ import type {
20
+ IntegrationModuleParts,
21
+ IntegrationProviderModule,
22
+ IntegrationProviderModuleLoadOptions,
23
+ } from '#providers/types.js';
24
+
25
+ // Stable migration-tracking table name for the GitHub provider database. This
26
+ // must NOT depend on the provider's position in the module `database` array. A
27
+ // positional name would shift if a provider is flag-disabled and silently
28
+ // re-run migrations against existing tables.
29
+ const GITHUB_MIGRATIONS_TABLE = '__drizzle_migrations_integrations_github';
30
+
31
+ async function loadGithubModuleParts(
32
+ options: IntegrationProviderModuleLoadOptions = {},
33
+ ): Promise<IntegrationModuleParts> {
34
+ const {
35
+ createGithubInstallationTokenProvider,
36
+ createGithubE2eRoutes,
37
+ encodeInstallationTokenEnvelope,
38
+ createGithubIntegrationProvider,
39
+ getGithubInstallationByInstallationId,
40
+ githubInstallationTokenNamespace,
41
+ db: githubDb,
42
+ migrationsPath: githubMigrationsPath,
43
+ upsertGithubInstallation,
44
+ } = await import('@shipfox/api-integration-github');
45
+
46
+ const tokenProvider = createGithubInstallationTokenProvider({
47
+ getIntegrationConnectionById,
48
+ secretStore: options.secrets?.github
49
+ ? {
50
+ read: async (workspaceId, installationId) =>
51
+ (await options.secrets?.github?.getSecret({
52
+ workspaceId,
53
+ namespace: githubInstallationTokenNamespace(installationId),
54
+ key: 'envelope',
55
+ })) ?? null,
56
+ write: async (workspaceId, installationId, envelope) => {
57
+ await options.secrets?.github?.setSecrets({
58
+ workspaceId,
59
+ namespace: githubInstallationTokenNamespace(installationId),
60
+ values: {envelope: encodeInstallationTokenEnvelope(envelope)},
61
+ });
62
+ },
63
+ }
64
+ : undefined,
65
+ });
66
+
67
+ async function getExistingGithubConnection(input: {
68
+ installationId: string;
69
+ }): Promise<CoreIntegrationConnection<'github'> | undefined> {
70
+ const installation = await getGithubInstallationByInstallationId(input.installationId);
71
+ if (!installation) return undefined;
72
+ const connection = await getIntegrationConnectionById(installation.connectionId);
73
+ if (!connection) return undefined;
74
+ return connection as CoreIntegrationConnection<'github'>;
75
+ }
76
+
77
+ async function connectGithubInstallation(
78
+ input: ConnectGithubInstallationInput,
79
+ ): Promise<CoreIntegrationConnection<'github'>> {
80
+ return await retryConnectionSlugCollision(() =>
81
+ db().transaction(async (tx) => {
82
+ const baseSlug = slugifyConnectionSlug(`github_${input.installation.accountLogin}`, {
83
+ fallback: 'github',
84
+ });
85
+ const slug = await resolveUniqueConnectionSlug(
86
+ {
87
+ workspaceId: input.workspaceId,
88
+ provider: 'github',
89
+ externalAccountId: input.installationId,
90
+ baseSlug,
91
+ },
92
+ {tx},
93
+ );
94
+ const connection = await upsertIntegrationConnection(
95
+ {
96
+ workspaceId: input.workspaceId,
97
+ provider: 'github',
98
+ externalAccountId: input.installationId,
99
+ slug,
100
+ displayName: input.displayName,
101
+ lifecycleStatus: 'active',
102
+ },
103
+ {tx},
104
+ );
105
+
106
+ await upsertGithubInstallation(
107
+ {
108
+ connectionId: connection.id,
109
+ ...input.installation,
110
+ },
111
+ {tx},
112
+ );
113
+
114
+ return connection as CoreIntegrationConnection<'github'>;
115
+ }),
116
+ );
117
+ }
118
+
119
+ return {
120
+ provider: createGithubIntegrationProvider({
121
+ getExistingGithubConnection,
122
+ connectGithubInstallation,
123
+ publishIntegrationEventReceived,
124
+ publishSourcePush,
125
+ recordDeliveryOnly,
126
+ getIntegrationConnectionById,
127
+ coreDb: db,
128
+ deleteSecrets: options.secrets?.deleteSecrets,
129
+ agentTools: {tokenProvider},
130
+ }),
131
+ e2eRoutes: [
132
+ createGithubE2eRoutes({
133
+ getExistingGithubConnection,
134
+ connectGithubInstallation,
135
+ connectionCapabilities: ['source_control', 'agent_tools'],
136
+ }),
137
+ ],
138
+ database: {
139
+ db: githubDb,
140
+ migrationsPath: githubMigrationsPath,
141
+ migrationsTableName: GITHUB_MIGRATIONS_TABLE,
142
+ },
143
+ };
144
+ }
145
+
146
+ export const githubProviderModule: IntegrationProviderModule = {
147
+ id: 'github',
148
+ enabled: config.INTEGRATIONS_ENABLE_GITHUB_PROVIDER,
149
+ load: loadGithubModuleParts,
150
+ };
@@ -0,0 +1,179 @@
1
+ import {
2
+ type IntegrationConnection as CoreIntegrationConnection,
3
+ slugifyConnectionSlug,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import type {
6
+ ConnectLinearInstallationInput,
7
+ LinearSecretsStore,
8
+ } from '@shipfox/api-integration-linear';
9
+ import {config} from '#config.js';
10
+ import {
11
+ deleteIntegrationConnection,
12
+ getIntegrationConnectionById,
13
+ resolveUniqueConnectionSlug,
14
+ upsertIntegrationConnection,
15
+ } from '#db/connections.js';
16
+ import {db} from '#db/db.js';
17
+ import {publishIntegrationEventReceived, recordDeliveryOnly} from '#db/webhook-deliveries.js';
18
+ import {retryConnectionSlugCollision} from '#providers/connection-slug.js';
19
+ import type {IntegrationModuleParts, IntegrationProviderModule} from '#providers/types.js';
20
+
21
+ const LINEAR_MIGRATIONS_TABLE = '__drizzle_migrations_integrations_linear';
22
+ const LINEAR_SECRETS_NAMESPACE_PREFIX = 'system/integrations/linear/';
23
+
24
+ type IntegrationDb = ReturnType<typeof db>;
25
+ type IntegrationTx = Parameters<Parameters<IntegrationDb['transaction']>[0]>[0];
26
+
27
+ async function loadLinearModuleParts(
28
+ options: Parameters<IntegrationProviderModule['load']>[0] = {},
29
+ ): Promise<IntegrationModuleParts> {
30
+ const {
31
+ createLinearTokenStore,
32
+ createLinearE2eRoutes,
33
+ createLinearIntegrationProvider,
34
+ config: linearConfig,
35
+ disconnectLinearInstallation: disconnectLinearInstallationRecords,
36
+ getLinearInstallationByOrganizationId,
37
+ db: linearDb,
38
+ migrationsPath: linearMigrationsPath,
39
+ upsertLinearInstallation,
40
+ } = await import('@shipfox/api-integration-linear');
41
+
42
+ async function getExistingLinearConnection(input: {
43
+ organizationId: string;
44
+ }): Promise<CoreIntegrationConnection<'linear'> | undefined> {
45
+ const installation = await getLinearInstallationByOrganizationId(input.organizationId);
46
+ if (!installation) return undefined;
47
+ const connection = await getIntegrationConnectionById(installation.connectionId);
48
+ if (!connection) return undefined;
49
+ return connection as CoreIntegrationConnection<'linear'>;
50
+ }
51
+
52
+ async function connectLinearInstallation(
53
+ input: ConnectLinearInstallationInput,
54
+ ): Promise<CoreIntegrationConnection<'linear'>> {
55
+ return await retryConnectionSlugCollision(() =>
56
+ db().transaction(async (tx) => {
57
+ const baseSlug = slugifyConnectionSlug(`linear_${input.organizationUrlKey}`, {
58
+ fallback: 'linear',
59
+ });
60
+ const slug = await resolveUniqueConnectionSlug(
61
+ {
62
+ workspaceId: input.workspaceId,
63
+ provider: 'linear',
64
+ externalAccountId: input.organizationId,
65
+ baseSlug,
66
+ },
67
+ {tx},
68
+ );
69
+ const connection = await upsertIntegrationConnection(
70
+ {
71
+ workspaceId: input.workspaceId,
72
+ provider: 'linear',
73
+ externalAccountId: input.organizationId,
74
+ slug,
75
+ displayName: input.displayName,
76
+ lifecycleStatus: 'active',
77
+ },
78
+ {tx},
79
+ );
80
+
81
+ await upsertLinearInstallation(
82
+ {
83
+ connectionId: connection.id,
84
+ organizationId: input.organizationId,
85
+ organizationUrlKey: input.organizationUrlKey,
86
+ appUserId: input.appUserId,
87
+ scopes: input.scopes,
88
+ tokenExpiresAt: input.tokenExpiresAt,
89
+ status: 'installed',
90
+ },
91
+ {tx},
92
+ );
93
+
94
+ return connection as CoreIntegrationConnection<'linear'>;
95
+ }),
96
+ );
97
+ }
98
+
99
+ async function disconnectLinearInstallation(input: {connectionId: string}): Promise<void> {
100
+ await disconnectLinearInstallationRecords<IntegrationTx>({
101
+ connectionId: input.connectionId,
102
+ getConnection: getIntegrationConnectionById,
103
+ deleteSecrets: (params) =>
104
+ options.secrets?.linear?.deleteSecrets({
105
+ ...params,
106
+ namespace: linearNamespaceSuffix(params.namespace),
107
+ }) ?? Promise.resolve(0),
108
+ transaction: (fn) => db().transaction((tx) => fn(tx)),
109
+ deleteConnection: (params, options) =>
110
+ deleteIntegrationConnection({id: params.connectionId}, options),
111
+ });
112
+ }
113
+
114
+ const fallbackSecrets: LinearSecretsStore = {
115
+ getSecret: () => Promise.resolve(null),
116
+ setSecrets: () => Promise.reject(new Error('Linear token storage is not configured')),
117
+ };
118
+ const secrets: LinearSecretsStore = options.secrets?.linear
119
+ ? {
120
+ getSecret: (params: Parameters<LinearSecretsStore['getSecret']>[0]) =>
121
+ options.secrets?.linear?.getSecret({
122
+ ...params,
123
+ namespace: linearNamespaceSuffix(params.namespace),
124
+ }) ?? Promise.resolve(null),
125
+ setSecrets: (params: Parameters<LinearSecretsStore['setSecrets']>[0]) =>
126
+ options.secrets?.linear?.setSecrets({
127
+ ...params,
128
+ namespace: linearNamespaceSuffix(params.namespace),
129
+ }) ?? Promise.resolve(),
130
+ }
131
+ : fallbackSecrets;
132
+ const tokenStore = createLinearTokenStore({
133
+ resolveConnection: async (connectionId) => getIntegrationConnectionById(connectionId),
134
+ secrets,
135
+ });
136
+
137
+ return {
138
+ provider: createLinearIntegrationProvider({
139
+ agentTools: {tokenStore, endpoint: linearConfig.LINEAR_MCP_ENDPOINT},
140
+ routes: {
141
+ tokenStore,
142
+ getExistingLinearConnection,
143
+ connectLinearInstallation,
144
+ disconnectLinearInstallation,
145
+ publishIntegrationEventReceived,
146
+ recordDeliveryOnly,
147
+ getIntegrationConnectionById,
148
+ coreDb: db,
149
+ },
150
+ }),
151
+ e2eRoutes: [
152
+ createLinearE2eRoutes({
153
+ tokenStore,
154
+ getExistingLinearConnection,
155
+ connectLinearInstallation,
156
+ disconnectLinearInstallation,
157
+ connectionCapabilities: ['agent_tools'],
158
+ }),
159
+ ],
160
+ database: {
161
+ db: linearDb,
162
+ migrationsPath: linearMigrationsPath,
163
+ migrationsTableName: LINEAR_MIGRATIONS_TABLE,
164
+ },
165
+ };
166
+ }
167
+
168
+ export const linearProviderModule: IntegrationProviderModule = {
169
+ id: 'linear',
170
+ enabled: config.INTEGRATIONS_ENABLE_LINEAR_PROVIDER,
171
+ load: loadLinearModuleParts,
172
+ };
173
+
174
+ function linearNamespaceSuffix(namespace: string): string {
175
+ if (!namespace.startsWith(LINEAR_SECRETS_NAMESPACE_PREFIX)) {
176
+ throw new Error('Linear provider attempted to access an unscoped secret namespace');
177
+ }
178
+ return namespace.slice(LINEAR_SECRETS_NAMESPACE_PREFIX.length);
179
+ }
@@ -0,0 +1,40 @@
1
+ describe('loadEnabledProviderModules', () => {
2
+ afterEach(() => {
3
+ vi.unstubAllEnvs();
4
+ vi.resetModules();
5
+ });
6
+
7
+ it('loads cron alongside webhook by default', async () => {
8
+ vi.resetModules();
9
+
10
+ const {loadEnabledProviderModules} = await import('#providers/modules.js');
11
+ const parts = await loadEnabledProviderModules();
12
+
13
+ expect(parts.map((part) => part.provider.provider)).toEqual(['cron', 'webhook']);
14
+ });
15
+
16
+ it('does not load cron when the provider is disabled', async () => {
17
+ vi.stubEnv('INTEGRATIONS_ENABLE_CRON_PROVIDER', 'false');
18
+ vi.resetModules();
19
+
20
+ const {loadEnabledProviderModules} = await import('#providers/modules.js');
21
+ const parts = await loadEnabledProviderModules();
22
+
23
+ expect(parts.map((part) => part.provider.provider)).toEqual(['webhook']);
24
+ });
25
+
26
+ it('loads Linear when the provider is enabled', async () => {
27
+ vi.stubEnv('INTEGRATIONS_ENABLE_LINEAR_PROVIDER', 'true');
28
+ vi.resetModules();
29
+
30
+ const {loadEnabledProviderModules} = await import('#providers/modules.js');
31
+ const parts = await loadEnabledProviderModules();
32
+
33
+ expect(parts.map((part) => part.provider.provider)).toEqual(['linear', 'cron', 'webhook']);
34
+ expect(parts[0]?.provider).toMatchObject({
35
+ provider: 'linear',
36
+ displayName: 'Linear',
37
+ adapters: {},
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,32 @@
1
+ import {cronProviderModule} from '#providers/cron.js';
2
+ import {giteaProviderModule} from '#providers/gitea.js';
3
+ import {githubProviderModule} from '#providers/github.js';
4
+ import {linearProviderModule} from '#providers/linear.js';
5
+ import {sentryProviderModule} from '#providers/sentry.js';
6
+ import type {
7
+ IntegrationModuleParts,
8
+ IntegrationProviderModuleLoadOptions,
9
+ } from '#providers/types.js';
10
+ import {webhookProviderModule} from '#providers/webhook.js';
11
+
12
+ // Order is significant: databases are migrated in this order, so list a provider
13
+ // before any that depend on its tables.
14
+ const providerModules = [
15
+ githubProviderModule,
16
+ linearProviderModule,
17
+ sentryProviderModule,
18
+ giteaProviderModule,
19
+ cronProviderModule,
20
+ webhookProviderModule,
21
+ ];
22
+
23
+ export async function loadEnabledProviderModules(
24
+ options: IntegrationProviderModuleLoadOptions = {},
25
+ ): Promise<IntegrationModuleParts[]> {
26
+ const parts: IntegrationModuleParts[] = [];
27
+ for (const module of providerModules) {
28
+ if (!module.enabled) continue;
29
+ parts.push(await module.load(options));
30
+ }
31
+ return parts;
32
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ type IntegrationConnection as CoreIntegrationConnection,
3
+ slugifyConnectionSlug,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import type {ConnectSentryInstallationInput} from '@shipfox/api-integration-sentry';
6
+ import {config} from '#config.js';
7
+ import {
8
+ getIntegrationConnectionById,
9
+ resolveUniqueConnectionSlug,
10
+ updateIntegrationConnectionLifecycleStatus,
11
+ upsertIntegrationConnection,
12
+ } from '#db/connections.js';
13
+ import {db} from '#db/db.js';
14
+ import {publishIntegrationEventReceived, recordDeliveryOnly} from '#db/webhook-deliveries.js';
15
+ import {retryConnectionSlugCollision} from '#providers/connection-slug.js';
16
+ import type {IntegrationModuleParts, IntegrationProviderModule} from '#providers/types.js';
17
+
18
+ // Stable migration-tracking table name for the Sentry provider database. This
19
+ // must NOT depend on the provider's position in the module `database` array. A
20
+ // positional name would shift if a provider is flag-disabled and silently
21
+ // re-run migrations against existing tables.
22
+ const SENTRY_MIGRATIONS_TABLE = '__drizzle_migrations_integrations_sentry';
23
+
24
+ async function loadSentryModuleParts(): Promise<IntegrationModuleParts> {
25
+ const {
26
+ createSentryIntegrationProvider,
27
+ createSentryMaintenanceWorker,
28
+ getSentryInstallationByInstallationUuid,
29
+ persistVerifiedUnclaimedInstallation,
30
+ upsertSentryInstallation,
31
+ db: sentryDb,
32
+ migrationsPath: sentryMigrationsPath,
33
+ } = await import('@shipfox/api-integration-sentry');
34
+
35
+ async function getConnectionById(
36
+ id: string,
37
+ ): Promise<CoreIntegrationConnection<'sentry'> | undefined> {
38
+ const connection = await getIntegrationConnectionById(id);
39
+ if (!connection) return undefined;
40
+ return connection as CoreIntegrationConnection<'sentry'>;
41
+ }
42
+
43
+ async function connectSentryInstallation(
44
+ input: ConnectSentryInstallationInput,
45
+ ): Promise<CoreIntegrationConnection<'sentry'>> {
46
+ return await retryConnectionSlugCollision(() =>
47
+ db().transaction(async (tx) => {
48
+ const baseSlug = slugifyConnectionSlug(`sentry_${input.orgSlug}`, {fallback: 'sentry'});
49
+ const slug = await resolveUniqueConnectionSlug(
50
+ {
51
+ workspaceId: input.workspaceId,
52
+ provider: 'sentry',
53
+ externalAccountId: input.installationUuid,
54
+ baseSlug,
55
+ },
56
+ {tx},
57
+ );
58
+ const connection = await upsertIntegrationConnection(
59
+ {
60
+ workspaceId: input.workspaceId,
61
+ provider: 'sentry',
62
+ externalAccountId: input.installationUuid,
63
+ slug,
64
+ displayName: input.displayName,
65
+ lifecycleStatus: 'active',
66
+ },
67
+ {tx},
68
+ );
69
+
70
+ await upsertSentryInstallation(
71
+ {
72
+ connectionId: connection.id,
73
+ installationUuid: input.installationUuid,
74
+ orgSlug: input.orgSlug,
75
+ status: 'installed',
76
+ codeHash: input.codeHash,
77
+ installerUserId: input.installerUserId,
78
+ },
79
+ {tx},
80
+ );
81
+
82
+ return connection as CoreIntegrationConnection<'sentry'>;
83
+ }),
84
+ );
85
+ }
86
+
87
+ return {
88
+ provider: createSentryIntegrationProvider({
89
+ getSentryInstallation: ({installationUuid}) =>
90
+ getSentryInstallationByInstallationUuid(installationUuid),
91
+ getConnectionById,
92
+ connectSentryInstallation,
93
+ persistVerifiedUnclaimedInstallation,
94
+ coreDb: db,
95
+ publishIntegrationEventReceived,
96
+ recordDeliveryOnly,
97
+ getIntegrationConnectionById,
98
+ updateConnectionLifecycleStatus: updateIntegrationConnectionLifecycleStatus,
99
+ }),
100
+ database: {
101
+ db: sentryDb,
102
+ migrationsPath: sentryMigrationsPath,
103
+ migrationsTableName: SENTRY_MIGRATIONS_TABLE,
104
+ },
105
+ workers: [createSentryMaintenanceWorker()],
106
+ };
107
+ }
108
+
109
+ export const sentryProviderModule: IntegrationProviderModule = {
110
+ id: 'sentry',
111
+ enabled: config.INTEGRATIONS_ENABLE_SENTRY_PROVIDER,
112
+ load: loadSentryModuleParts,
113
+ };
@@ -0,0 +1,52 @@
1
+ import type {RouteExport} from '@shipfox/node-fastify';
2
+ import type {ModuleDatabase, ModuleWorker} from '@shipfox/node-module';
3
+ import type {IntegrationProvider} from '#core/entities/provider.js';
4
+
5
+ /**
6
+ * Everything one integration contributes to the composed integrations module:
7
+ * a registry provider, plus an optional dedicated database, background workers,
8
+ * and one-shot boot-time tasks. Providers that own none of these simply omit them.
9
+ *
10
+ * A startup task is run once after modules are initialized (migrations done). The
11
+ * provider owns its own wiring — core runs each task generically and isolates
12
+ * failures so a task can never gate API boot.
13
+ */
14
+ export interface IntegrationModuleParts {
15
+ provider: IntegrationProvider;
16
+ database?: ModuleDatabase | undefined;
17
+ e2eRoutes?: RouteExport[] | undefined;
18
+ workers?: ModuleWorker[] | undefined;
19
+ startupTasks?: Array<() => Promise<void>> | undefined;
20
+ }
21
+
22
+ /**
23
+ * A config-gated integration, registered once in `providerModules`. `load` is
24
+ * called lazily and only when `enabled`, so a disabled provider never imports
25
+ * its (potentially heavy) implementation package.
26
+ */
27
+ export interface IntegrationProviderModule {
28
+ id: string;
29
+ enabled: boolean;
30
+ load(options?: IntegrationProviderModuleLoadOptions): Promise<IntegrationModuleParts>;
31
+ }
32
+
33
+ export interface IntegrationProviderModuleLoadOptions {
34
+ secrets?: IntegrationProviderSecrets | undefined;
35
+ }
36
+
37
+ export interface IntegrationProviderSecrets {
38
+ github?: IntegrationProviderScopedSecrets | undefined;
39
+ linear?: IntegrationProviderScopedSecrets | undefined;
40
+ deleteSecrets(params: {workspaceId: string; namespace: string}): Promise<number>;
41
+ }
42
+
43
+ export interface IntegrationProviderScopedSecrets {
44
+ getSecret(params: {workspaceId: string; namespace: string; key: string}): Promise<string | null>;
45
+ setSecrets(params: {
46
+ workspaceId: string;
47
+ namespace: string;
48
+ values: Record<string, string>;
49
+ editedBy?: string | null | undefined;
50
+ }): Promise<void>;
51
+ deleteSecrets(params: {workspaceId: string; namespace: string}): Promise<number>;
52
+ }
@@ -0,0 +1,30 @@
1
+ import {config} from '#config.js';
2
+ import {
3
+ createIntegrationConnection,
4
+ deleteIntegrationConnection,
5
+ getIntegrationConnectionById,
6
+ listIntegrationConnections,
7
+ updateIntegrationConnectionLifecycleStatus,
8
+ } from '#db/connections.js';
9
+ import {db} from '#db/db.js';
10
+ import {publishIntegrationEventReceived} from '#db/webhook-deliveries.js';
11
+ import type {IntegrationProviderModule} from '#providers/types.js';
12
+
13
+ export const webhookProviderModule: IntegrationProviderModule = {
14
+ id: 'webhook',
15
+ enabled: config.INTEGRATIONS_ENABLE_WEBHOOK_PROVIDER,
16
+ load: async () => {
17
+ const {createWebhookIntegrationProvider} = await import('@shipfox/api-integration-webhook');
18
+ return {
19
+ provider: createWebhookIntegrationProvider({
20
+ coreDb: db,
21
+ createIntegrationConnection,
22
+ listIntegrationConnections,
23
+ getIntegrationConnectionById,
24
+ updateIntegrationConnectionLifecycleStatus,
25
+ deleteIntegrationConnection,
26
+ publishIntegrationEventReceived,
27
+ }),
28
+ };
29
+ },
30
+ };
@@ -0,0 +1,7 @@
1
+ import {pruneWebhookDeliveriesActivity} from './prune-webhook-deliveries.js';
2
+
3
+ export function createIntegrationsMaintenanceActivities() {
4
+ return {
5
+ pruneWebhookDeliveriesActivity,
6
+ };
7
+ }
@@ -0,0 +1,39 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {inArray} from 'drizzle-orm';
3
+ import {db} from '#db/db.js';
4
+ import {integrationsWebhookDeliveries} from '#db/schema/webhook-deliveries.js';
5
+ import {pruneWebhookDeliveriesActivity} from './prune-webhook-deliveries.js';
6
+
7
+ async function insertDelivery(deliveryId: string, receivedAt: Date): Promise<void> {
8
+ await db()
9
+ .insert(integrationsWebhookDeliveries)
10
+ .values({provider: 'github', dedupScope: 'provider:github', deliveryId, receivedAt});
11
+ }
12
+
13
+ describe('pruneWebhookDeliveriesActivity', () => {
14
+ it('deletes rows older than the retention window and keeps recent ones', async () => {
15
+ const oldId = randomUUID();
16
+ const recentId = randomUUID();
17
+ const longAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
18
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
19
+ await insertDelivery(oldId, longAgo);
20
+ await insertDelivery(recentId, yesterday);
21
+
22
+ const result = await pruneWebhookDeliveriesActivity();
23
+
24
+ expect(result.deleted).toBe(1);
25
+ const remaining = await db()
26
+ .select()
27
+ .from(integrationsWebhookDeliveries)
28
+ .where(inArray(integrationsWebhookDeliveries.deliveryId, [oldId, recentId]));
29
+ expect(remaining.map((row) => row.deliveryId)).toEqual([recentId]);
30
+ });
31
+
32
+ it('returns zero deleted when nothing is past the retention window', async () => {
33
+ await insertDelivery(randomUUID(), new Date());
34
+
35
+ const result = await pruneWebhookDeliveriesActivity();
36
+
37
+ expect(result.deleted).toBe(0);
38
+ });
39
+ });
@@ -0,0 +1,9 @@
1
+ import {pruneWebhookDeliveries} from '#db/webhook-deliveries.js';
2
+ import {WEBHOOK_DELIVERY_RETENTION_DAYS} from '#temporal/constants.js';
3
+
4
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
5
+
6
+ export async function pruneWebhookDeliveriesActivity(): Promise<{deleted: number}> {
7
+ const olderThan = new Date(Date.now() - WEBHOOK_DELIVERY_RETENTION_DAYS * MS_PER_DAY);
8
+ return await pruneWebhookDeliveries({olderThan});
9
+ }
@@ -0,0 +1,3 @@
1
+ export const INTEGRATIONS_MAINTENANCE_TASK_QUEUE = 'integrations-maintenance';
2
+
3
+ export const WEBHOOK_DELIVERY_RETENTION_DAYS = 30;
@@ -0,0 +1 @@
1
+ export {pruneWebhookDeliveriesCron} from './prune-webhook-deliveries-cron.js';
@@ -0,0 +1,15 @@
1
+ import {log, proxyActivities} from '@temporalio/workflow';
2
+ import type {createIntegrationsMaintenanceActivities} from '../activities/index.js';
3
+
4
+ const {pruneWebhookDeliveriesActivity} = proxyActivities<
5
+ ReturnType<typeof createIntegrationsMaintenanceActivities>
6
+ >({
7
+ startToCloseTimeout: '5 minutes',
8
+ });
9
+
10
+ export async function pruneWebhookDeliveriesCron(): Promise<void> {
11
+ const {deleted} = await pruneWebhookDeliveriesActivity();
12
+ if (deleted > 0) {
13
+ log.info('Pruned integrations webhook deliveries', {deleted});
14
+ }
15
+ }