@shipfox/api-integration-core 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +100 -0
  3. package/dist/core/entities/provider.d.ts +7 -2
  4. package/dist/core/entities/provider.d.ts.map +1 -1
  5. package/dist/core/entities/provider.js.map +1 -1
  6. package/dist/core/errors.d.ts +7 -0
  7. package/dist/core/errors.d.ts.map +1 -1
  8. package/dist/core/errors.js +8 -2
  9. package/dist/core/errors.js.map +1 -1
  10. package/dist/db/webhook-deliveries.d.ts +4 -0
  11. package/dist/db/webhook-deliveries.d.ts.map +1 -1
  12. package/dist/db/webhook-deliveries.js +10 -2
  13. package/dist/db/webhook-deliveries.js.map +1 -1
  14. package/dist/index.d.ts +19 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +51 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/presentation/inter-module.d.ts +9 -0
  19. package/dist/presentation/inter-module.d.ts.map +1 -0
  20. package/dist/presentation/inter-module.js +121 -0
  21. package/dist/presentation/inter-module.js.map +1 -0
  22. package/dist/presentation/routes/agent-tools-gateway/index.d.ts +1 -0
  23. package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -1
  24. package/dist/presentation/routes/agent-tools-gateway/index.js +1 -0
  25. package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -1
  26. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +5 -2
  27. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -1
  28. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +53 -17
  29. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -1
  30. package/dist/presentation/routes/index.d.ts +2 -3
  31. package/dist/presentation/routes/index.d.ts.map +1 -1
  32. package/dist/presentation/routes/index.js +3 -3
  33. package/dist/presentation/routes/index.js.map +1 -1
  34. package/dist/presentation/routes/manage-connections.d.ts +1 -1
  35. package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
  36. package/dist/presentation/routes/manage-connections.js +28 -3
  37. package/dist/presentation/routes/manage-connections.js.map +1 -1
  38. package/dist/providers/gitea.d.ts.map +1 -1
  39. package/dist/providers/gitea.js +10 -8
  40. package/dist/providers/gitea.js.map +1 -1
  41. package/dist/providers/github.d.ts.map +1 -1
  42. package/dist/providers/github.js +18 -13
  43. package/dist/providers/github.js.map +1 -1
  44. package/dist/providers/jira.d.ts.map +1 -1
  45. package/dist/providers/jira.js +113 -3
  46. package/dist/providers/jira.js.map +1 -1
  47. package/dist/providers/linear.d.ts.map +1 -1
  48. package/dist/providers/linear.js +35 -16
  49. package/dist/providers/linear.js.map +1 -1
  50. package/dist/providers/sentry.d.ts.map +1 -1
  51. package/dist/providers/sentry.js +13 -11
  52. package/dist/providers/sentry.js.map +1 -1
  53. package/dist/providers/slack.d.ts.map +1 -1
  54. package/dist/providers/slack.js +36 -16
  55. package/dist/providers/slack.js.map +1 -1
  56. package/dist/providers/types.d.ts +12 -0
  57. package/dist/providers/types.d.ts.map +1 -1
  58. package/dist/providers/types.js.map +1 -1
  59. package/dist/providers/webhook.d.ts.map +1 -1
  60. package/dist/providers/webhook.js +11 -9
  61. package/dist/providers/webhook.js.map +1 -1
  62. package/dist/tsconfig.test.tsbuildinfo +1 -1
  63. package/package.json +33 -38
  64. package/src/core/entities/provider.ts +7 -1
  65. package/src/core/errors.ts +12 -2
  66. package/src/db/webhook-deliveries.test.ts +11 -0
  67. package/src/db/webhook-deliveries.ts +12 -3
  68. package/src/index.test.ts +112 -0
  69. package/src/index.ts +104 -10
  70. package/src/presentation/inter-module.ts +155 -0
  71. package/src/presentation/routes/agent-tools-gateway/index.ts +1 -0
  72. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +36 -0
  73. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +64 -22
  74. package/src/presentation/routes/index.ts +5 -6
  75. package/src/presentation/routes/manage-connections.test.ts +113 -0
  76. package/src/presentation/routes/manage-connections.ts +26 -2
  77. package/src/providers/gitea.ts +11 -8
  78. package/src/providers/github.ts +17 -11
  79. package/src/providers/jira.ts +155 -7
  80. package/src/providers/linear.test.ts +136 -0
  81. package/src/providers/linear.ts +33 -13
  82. package/src/providers/sentry.ts +15 -12
  83. package/src/providers/slack.test.ts +139 -0
  84. package/src/providers/slack.ts +39 -14
  85. package/src/providers/types.ts +15 -0
  86. package/src/providers/webhook.ts +11 -9
  87. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-integration-core",
3
3
  "license": "MIT",
4
- "version": "4.0.0",
4
+ "version": "6.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -13,22 +13,12 @@
13
13
  "types": "dist/index.d.ts",
14
14
  "imports": {
15
15
  "#test/*": "./test/*",
16
- "#*": {
17
- "workspace-source": "./src/*",
18
- "development": "./src/*",
19
- "default": "./dist/*"
20
- }
16
+ "#*": "./dist/*"
21
17
  },
22
18
  "exports": {
23
19
  ".": {
24
- "development": {
25
- "types": "./src/index.ts",
26
- "default": "./src/index.ts"
27
- },
28
- "default": {
29
- "types": "./dist/index.d.ts",
30
- "default": "./dist/index.js"
31
- }
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
32
22
  }
33
23
  },
34
24
  "dependencies": {
@@ -37,26 +27,29 @@
37
27
  "ajv": "^8.20.0",
38
28
  "drizzle-orm": "^0.45.2",
39
29
  "zod": "^4.4.3",
40
- "@shipfox/api-agent-dto": "3.0.0",
41
- "@shipfox/api-auth-context": "3.0.0",
42
- "@shipfox/api-integration-core-dto": "3.0.0",
43
- "@shipfox/api-integration-gitea": "4.0.0",
44
- "@shipfox/api-integration-github": "4.0.0",
45
- "@shipfox/api-integration-jira": "4.0.0",
46
- "@shipfox/api-integration-linear": "4.0.0",
47
- "@shipfox/api-integration-sentry": "4.0.0",
48
- "@shipfox/api-integration-slack": "4.0.0",
49
- "@shipfox/api-integration-webhook": "3.0.0",
50
- "@shipfox/config": "1.2.1",
51
- "@shipfox/node-drizzle": "0.3.0",
52
- "@shipfox/node-fastify": "0.2.2",
53
- "@shipfox/node-module": "0.3.1",
54
- "@shipfox/node-opentelemetry": "0.5.1",
55
- "@shipfox/node-outbox": "0.2.2",
56
- "@shipfox/node-postgres": "0.4.1",
57
- "@shipfox/node-temporal": "0.3.0",
58
- "@shipfox/regex": "0.2.1",
59
- "@shipfox/redact": "0.2.1"
30
+ "@shipfox/api-agent-dto": "6.0.0",
31
+ "@shipfox/api-auth-context": "6.0.0",
32
+ "@shipfox/api-workflows-dto": "6.0.0",
33
+ "@shipfox/api-integration-core-dto": "6.0.0",
34
+ "@shipfox/api-workspaces-dto": "6.0.0",
35
+ "@shipfox/api-integration-gitea": "6.0.0",
36
+ "@shipfox/api-integration-github": "6.0.0",
37
+ "@shipfox/api-integration-jira": "6.0.0",
38
+ "@shipfox/api-integration-linear": "6.0.0",
39
+ "@shipfox/api-integration-sentry": "6.0.0",
40
+ "@shipfox/api-integration-slack": "6.0.0",
41
+ "@shipfox/api-integration-webhook": "6.0.0",
42
+ "@shipfox/config": "1.2.2",
43
+ "@shipfox/inter-module": "0.2.0",
44
+ "@shipfox/node-drizzle": "0.3.2",
45
+ "@shipfox/node-fastify": "0.2.4",
46
+ "@shipfox/node-module": "0.4.0",
47
+ "@shipfox/node-opentelemetry": "0.5.2",
48
+ "@shipfox/node-outbox": "0.2.4",
49
+ "@shipfox/node-postgres": "0.4.2",
50
+ "@shipfox/node-temporal": "0.3.2",
51
+ "@shipfox/regex": "0.2.2",
52
+ "@shipfox/redact": "0.2.3"
60
53
  },
61
54
  "devDependencies": {
62
55
  "@temporalio/activity": "1.18.1",
@@ -69,16 +62,18 @@
69
62
  "fastify": "^5.3.3",
70
63
  "fastify-type-provider-zod": "^6.0.0",
71
64
  "fishery": "^2.4.0",
72
- "@shipfox/biome": "1.8.1",
73
- "@shipfox/swc": "1.2.5",
65
+ "@shipfox/biome": "1.8.2",
66
+ "@shipfox/depcruise": "1.0.2",
67
+ "@shipfox/swc": "1.2.6",
74
68
  "@shipfox/ts-config": "1.3.8",
75
- "@shipfox/typescript": "1.1.6",
76
- "@shipfox/vitest": "1.2.2"
69
+ "@shipfox/typescript": "1.1.7",
70
+ "@shipfox/vitest": "1.2.3"
77
71
  },
78
72
  "scripts": {
79
73
  "build": "shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js",
80
74
  "check": "shipfox-biome-check",
81
75
  "check:fix": "shipfox-biome-check --write",
76
+ "depcruise": "shipfox-depcruise",
82
77
  "test": "shipfox-vitest-run",
83
78
  "test:watch": "shipfox-vitest-watch",
84
79
  "type": "shipfox-tsc-check",
@@ -1,6 +1,8 @@
1
1
  import type {
2
2
  IntegrationProvider as CoreIntegrationProvider,
3
3
  RegisteredIntegrationProvider as CoreRegisteredIntegrationProvider,
4
+ WebhookRequestProcessor,
5
+ WebhookRouteId,
4
6
  } from '@shipfox/api-integration-core-dto';
5
7
  import type {RouteExport} from '@shipfox/node-fastify';
6
8
 
@@ -17,5 +19,9 @@ export type {
17
19
  OpenAgentToolsSessionInput,
18
20
  } from '@shipfox/api-integration-core-dto';
19
21
 
20
- export type IntegrationProvider = CoreIntegrationProvider<string, RouteExport>;
22
+ export type IntegrationProvider = CoreIntegrationProvider<string, RouteExport> & {
23
+ webhookProcessors?:
24
+ | Array<{routeIds: readonly WebhookRouteId[]; processor: WebhookRequestProcessor}>
25
+ | undefined;
26
+ };
21
27
  export type RegisteredIntegrationProvider = CoreRegisteredIntegrationProvider<string, RouteExport>;
@@ -35,7 +35,10 @@ export class IntegrationConnectionAlreadyExistsError extends Error {
35
35
  }
36
36
 
37
37
  export class IntegrationCapabilityUnavailableError extends Error {
38
- constructor(capability: IntegrationCapability, provider: IntegrationProviderKind) {
38
+ constructor(
39
+ public readonly capability: IntegrationCapability,
40
+ public readonly provider: IntegrationProviderKind,
41
+ ) {
39
42
  super(`Integration provider ${provider} does not expose ${capability}`);
40
43
  }
41
44
  }
@@ -47,9 +50,16 @@ export class IntegrationCheckoutUnsupportedError extends Error {
47
50
  }
48
51
 
49
52
  export class IntegrationProviderUnavailableError extends Error {
50
- constructor(provider: IntegrationProviderKind) {
53
+ constructor(public readonly provider: IntegrationProviderKind) {
51
54
  super(`No integration provider registered for ${provider}`);
52
55
  }
53
56
  }
54
57
 
58
+ export class WebhookProcessorNotConfiguredError extends Error {
59
+ constructor(public readonly routeId: string) {
60
+ super(`No webhook processor is configured for ${routeId}`);
61
+ this.name = 'WebhookProcessorNotConfiguredError';
62
+ }
63
+ }
64
+
55
65
  export {ConnectionSlugConflictError, IntegrationProviderError, type IntegrationProviderErrorReason};
@@ -9,6 +9,7 @@ import {db} from './db.js';
9
9
  import {integrationsOutbox} from './schema/outbox.js';
10
10
  import {integrationsWebhookDeliveries} from './schema/webhook-deliveries.js';
11
11
  import {
12
+ claimWebhookDelivery,
12
13
  publishIntegrationEventReceived,
13
14
  publishSourceCommitPushed,
14
15
  publishSourcePush,
@@ -165,6 +166,16 @@ describe('integration webhook delivery persistence', () => {
165
166
 
166
167
  expect(await deliveriesFor('github', deliveryId)).toHaveLength(1);
167
168
  });
169
+
170
+ it('reports whether it claimed a delivery', async () => {
171
+ const deliveryId = crypto.randomUUID();
172
+
173
+ const first = await claimWebhookDelivery({tx: db(), provider: 'github', deliveryId});
174
+ const duplicate = await claimWebhookDelivery({tx: db(), provider: 'github', deliveryId});
175
+
176
+ expect(first.claimed).toBe(true);
177
+ expect(duplicate.claimed).toBe(false);
178
+ });
168
179
  });
169
180
 
170
181
  describe('publishSourcePush', () => {
@@ -171,8 +171,10 @@ export interface RecordDeliveryOnlyParams {
171
171
  deliveryId: string;
172
172
  }
173
173
 
174
- export async function recordDeliveryOnly(params: RecordDeliveryOnlyParams): Promise<void> {
175
- await params.tx
174
+ export async function claimWebhookDelivery(
175
+ params: RecordDeliveryOnlyParams,
176
+ ): Promise<{claimed: boolean}> {
177
+ const inserted = await params.tx
176
178
  .insert(integrationsWebhookDeliveries)
177
179
  .values({
178
180
  provider: params.provider,
@@ -185,7 +187,13 @@ export async function recordDeliveryOnly(params: RecordDeliveryOnlyParams): Prom
185
187
  integrationsWebhookDeliveries.dedupScope,
186
188
  integrationsWebhookDeliveries.deliveryId,
187
189
  ],
188
- });
190
+ })
191
+ .returning({deliveryId: integrationsWebhookDeliveries.deliveryId});
192
+ return {claimed: inserted.length > 0};
193
+ }
194
+
195
+ export async function recordDeliveryOnly(params: RecordDeliveryOnlyParams): Promise<void> {
196
+ await claimWebhookDelivery(params);
189
197
  }
190
198
 
191
199
  export interface PruneWebhookDeliveriesParams {
@@ -204,4 +212,5 @@ export async function pruneWebhookDeliveries(
204
212
  export type PublishIntegrationEventReceivedFn = typeof publishIntegrationEventReceived;
205
213
  export type PublishSourcePushFn = typeof publishSourcePush;
206
214
  export type PublishSourceCommitPushedFn = typeof publishSourceCommitPushed;
215
+ export type ClaimWebhookDeliveryFn = typeof claimWebhookDelivery;
207
216
  export type RecordDeliveryOnlyFn = typeof recordDeliveryOnly;
@@ -0,0 +1,112 @@
1
+ import type {
2
+ StoredWebhookRequest,
3
+ WebhookRequestProcessor,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import {createOutboxRegistry, type ModuleService, startModuleServices} from '@shipfox/node-module';
6
+ import {createIntegrationsContext, WebhookProcessorNotConfiguredError} from './index.js';
7
+
8
+ const request = {
9
+ schema_version: 1,
10
+ request_id: 'a8a44e12-f4bd-4bd1-82d4-ccdba70a9f3e',
11
+ route_id: 'github',
12
+ received_at: '2026-07-20T12:00:00.000Z',
13
+ method: 'POST',
14
+ path_parameters: {},
15
+ raw_query_string: '',
16
+ headers: {},
17
+ body: {encoding: 'base64', data: ''},
18
+ } as const satisfies StoredWebhookRequest;
19
+
20
+ describe('createIntegrationsContext', () => {
21
+ it('binds the composed direct-route processor to an optional delivery source', async () => {
22
+ const directProcessor: WebhookRequestProcessor = {
23
+ process: vi.fn().mockResolvedValue({outcome: 'processed', deliveryId: 'delivery-1'}),
24
+ };
25
+ const stop = vi.fn().mockResolvedValue(undefined);
26
+ const service: ModuleService = {
27
+ name: 'queued-webhook-deliveries',
28
+ shutdownTimeoutMs: 1_000,
29
+ start: vi.fn().mockResolvedValue({finished: Promise.resolve(), stop}),
30
+ };
31
+ const deliverySource = {createService: vi.fn().mockReturnValue(service)};
32
+
33
+ const context = await createIntegrationsContext({
34
+ parts: [
35
+ {
36
+ provider: {provider: 'github', displayName: 'GitHub', adapters: {}},
37
+ webhookProcessors: [{routeIds: ['github'], processor: directProcessor}],
38
+ },
39
+ ],
40
+ webhookDeliverySource: deliverySource,
41
+ });
42
+
43
+ expect(context.module.services).toEqual([service]);
44
+ expect(deliverySource.createService).toHaveBeenCalledWith(context.webhookProcessor);
45
+
46
+ const result = await context.webhookProcessor.process(request);
47
+ const services = await startModuleServices({
48
+ services: context.module.services ?? [],
49
+ context: {outboxRegistry: createOutboxRegistry()},
50
+ });
51
+ await services.stop();
52
+
53
+ expect(result).toEqual({outcome: 'processed', deliveryId: 'delivery-1'});
54
+ expect(directProcessor.process).toHaveBeenCalledWith(request);
55
+ expect(stop).toHaveBeenCalledOnce();
56
+ });
57
+
58
+ it('does not register a service without a delivery source', async () => {
59
+ const context = await createIntegrationsContext({
60
+ parts: [{provider: {provider: 'github', displayName: 'GitHub', adapters: {}}}],
61
+ });
62
+
63
+ expect(context.module.services).toBeUndefined();
64
+ });
65
+
66
+ it('rejects a queued request with no registered processor', async () => {
67
+ const context = await createIntegrationsContext({
68
+ parts: [{provider: {provider: 'github', displayName: 'GitHub', adapters: {}}}],
69
+ });
70
+
71
+ const result = context.webhookProcessor.process(request);
72
+
73
+ await expect(result).rejects.toEqual(new WebhookProcessorNotConfiguredError('github'));
74
+ });
75
+
76
+ it('fails composition when more than one processor handles a route', async () => {
77
+ const processor: WebhookRequestProcessor = {
78
+ process: vi.fn().mockResolvedValue({outcome: 'processed', deliveryId: 'delivery-1'}),
79
+ };
80
+
81
+ const result = createIntegrationsContext({
82
+ parts: [
83
+ {
84
+ provider: {provider: 'github', displayName: 'GitHub', adapters: {}},
85
+ webhookProcessors: [
86
+ {routeIds: ['github'], processor},
87
+ {routeIds: ['github'], processor},
88
+ ],
89
+ },
90
+ ],
91
+ });
92
+
93
+ await expect(result).rejects.toThrow(
94
+ 'Webhook processor is registered more than once for github',
95
+ );
96
+ });
97
+
98
+ it('fails composition when a configured delivery source is invalid', async () => {
99
+ const sourceError = new Error('WEBHOOK_QUEUE_URL is required');
100
+
101
+ const result = createIntegrationsContext({
102
+ parts: [{provider: {provider: 'github', displayName: 'GitHub', adapters: {}}}],
103
+ webhookDeliverySource: {
104
+ createService: () => {
105
+ throw sourceError;
106
+ },
107
+ },
108
+ });
109
+
110
+ await expect(result).rejects.toThrow(sourceError);
111
+ });
112
+ });
package/src/index.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import {dirname, resolve} from 'node:path';
2
2
  import {fileURLToPath} from 'node:url';
3
- import {integrationsEventSchemas} from '@shipfox/api-integration-core-dto';
4
- import type {ShipfoxModule} from '@shipfox/node-module';
3
+ import {
4
+ integrationsEventSchemas,
5
+ type StoredWebhookRequest,
6
+ type WebhookProcessingResult,
7
+ type WebhookRequestProcessor,
8
+ type WebhookRouteId,
9
+ } from '@shipfox/api-integration-core-dto';
10
+ import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
11
+ import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
12
+ import type {ModuleService, ShipfoxModule} from '@shipfox/node-module';
5
13
  import {logger} from '@shipfox/node-opentelemetry';
6
14
  import type {IntegrationProvider} from '#core/entities/provider.js';
15
+ import {WebhookProcessorNotConfiguredError} from '#core/errors.js';
7
16
  import {
8
17
  createIntegrationProviderRegistry,
9
18
  type IntegrationProviderRegistry,
@@ -16,15 +25,26 @@ import {getIntegrationConnectionById} from '#db/connections.js';
16
25
  import {db} from '#db/db.js';
17
26
  import {migrationsPath} from '#db/migrations.js';
18
27
  import {integrationsOutbox} from '#db/schema/outbox.js';
19
- import {createIntegrationRoutes, type LeasedAgentStepLoader} from '#presentation/routes/index.js';
28
+ import {createIntegrationsInterModulePresentation} from '#presentation/inter-module.js';
29
+ import {createIntegrationRoutes} from '#presentation/routes/index.js';
20
30
  import {loadEnabledProviderModules} from '#providers/modules.js';
21
- import type {IntegrationModuleParts, IntegrationProviderSecrets} from '#providers/types.js';
31
+ import type {
32
+ IntegrationModuleParts,
33
+ IntegrationProviderSecrets,
34
+ WebhookProcessorRegistration,
35
+ } from '#providers/types.js';
22
36
  import {createIntegrationsMaintenanceActivities} from '#temporal/activities/index.js';
23
37
  import {INTEGRATIONS_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';
24
38
 
25
39
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
40
  const maintenanceWorkflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
27
41
 
42
+ export type {
43
+ StoredWebhookRequest,
44
+ WebhookProcessingResult,
45
+ WebhookRequestProcessor,
46
+ WebhookRouteId,
47
+ } from '@shipfox/api-integration-core-dto';
28
48
  export {
29
49
  buildProviderRepositoryId,
30
50
  MAX_REPOSITORY_FILE_BYTES,
@@ -63,6 +83,7 @@ export {
63
83
  IntegrationConnectionWorkspaceMismatchError,
64
84
  IntegrationProviderError,
65
85
  IntegrationProviderUnavailableError,
86
+ WebhookProcessorNotConfiguredError,
66
87
  } from '#core/errors.js';
67
88
  export type {
68
89
  AgentToolCallInput,
@@ -98,6 +119,7 @@ export {createSourceControlIntegrationService} from '#core/source-control-servic
98
119
  export type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
99
120
  export {getIntegrationConnectionById} from '#db/connections.js';
100
121
  export type {
122
+ ClaimWebhookDeliveryFn,
101
123
  PublishIntegrationEventReceivedFn,
102
124
  PublishIntegrationEventReceivedParams,
103
125
  PublishIntegrationEventReceivedResult,
@@ -106,7 +128,7 @@ export type {
106
128
  RecordDeliveryOnlyFn,
107
129
  RecordDeliveryOnlyParams,
108
130
  } from '#db/webhook-deliveries.js';
109
- export {pruneWebhookDeliveries} from '#db/webhook-deliveries.js';
131
+ export {claimWebhookDelivery, pruneWebhookDeliveries} from '#db/webhook-deliveries.js';
110
132
  export {integrationRouteErrorHandler} from '#presentation/routes/errors.js';
111
133
 
112
134
  export interface CreateIntegrationsModuleOptions {
@@ -118,11 +140,21 @@ export interface CreateIntegrationsModuleOptions {
118
140
  */
119
141
  parts?: IntegrationModuleParts[] | undefined;
120
142
  secrets?: IntegrationProviderSecrets | undefined;
143
+ workspaces?: WorkspacesInterModuleClient | undefined;
121
144
  agentTools?:
122
145
  | {
123
- loadLeasedAgentStep: LeasedAgentStepLoader;
146
+ workflows: WorkflowsModuleClient;
124
147
  }
125
148
  | undefined;
149
+ webhookDeliverySource?: WebhookDeliverySource | undefined;
150
+ }
151
+
152
+ /**
153
+ * Hosted runtimes implement this port to receive stored webhook requests. The
154
+ * integration module starts its returned service after migrations complete.
155
+ */
156
+ export interface WebhookDeliverySource {
157
+ createService(processor: WebhookRequestProcessor): ModuleService;
126
158
  }
127
159
 
128
160
  export interface IntegrationsContext {
@@ -132,6 +164,7 @@ export interface IntegrationsContext {
132
164
  sourceControl: IntegrationSourceControlService;
133
165
  };
134
166
  sourceControl: IntegrationSourceControlService;
167
+ webhookProcessor: WebhookRequestProcessor;
135
168
  /**
136
169
  * Runs every enabled provider's one-shot boot-time tasks, after modules are initialized
137
170
  * (migrations done). Failures are isolated and logged, never rethrown, so a provider task
@@ -149,17 +182,41 @@ export async function createIntegrationsModule(
149
182
  export async function createIntegrationsContext(
150
183
  options: CreateIntegrationsModuleOptions = {},
151
184
  ): Promise<IntegrationsContext> {
185
+ const workspaces = options.workspaces;
152
186
  const parts: IntegrationModuleParts[] =
153
187
  options.parts ??
154
188
  (options.providers
155
- ? options.providers.map((provider) => ({provider}))
156
- : await loadEnabledProviderModules({secrets: options.secrets}));
189
+ ? options.providers.map((provider) => ({
190
+ provider,
191
+ webhookProcessors: provider.webhookProcessors,
192
+ }))
193
+ : await loadEnabledProviderModules({
194
+ secrets: options.secrets,
195
+ ...(workspaces
196
+ ? {
197
+ requireActiveWorkspaceMembership: (input: {
198
+ workspaceId: string;
199
+ userId: string;
200
+ memberships: ReadonlyArray<
201
+ import('@shipfox/api-auth-context').UserContextMembership
202
+ >;
203
+ }) =>
204
+ workspaces.requireActiveMembership({
205
+ ...input,
206
+ memberships: [...input.memberships],
207
+ }),
208
+ }
209
+ : {}),
210
+ }));
157
211
 
158
212
  const registry = createIntegrationProviderRegistry(parts.map((part) => part.provider));
159
213
  const sourceControl = createSourceControlIntegrationService({
160
214
  registry,
161
215
  getIntegrationConnectionById,
162
216
  });
217
+ const webhookProcessor = createComposedWebhookProcessor(
218
+ parts.flatMap((part) => part.webhookProcessors ?? []),
219
+ );
163
220
 
164
221
  async function runStartupTasks(): Promise<void> {
165
222
  for (const task of parts.flatMap((part) => part.startupTasks ?? [])) {
@@ -174,6 +231,9 @@ export async function createIntegrationsContext(
174
231
 
175
232
  const module: ShipfoxModule = {
176
233
  name: 'integrations',
234
+ interModulePresentations: [
235
+ createIntegrationsInterModulePresentation({registry, sourceControl}),
236
+ ],
177
237
  startupTasks: runStartupTasks,
178
238
  database: [
179
239
  {db, migrationsPath},
@@ -182,7 +242,7 @@ export async function createIntegrationsContext(
182
242
  routes: createIntegrationRoutes(registry, sourceControl, {
183
243
  agentTools: options.agentTools
184
244
  ? {
185
- loadLeasedAgentStep: options.agentTools.loadLeasedAgentStep,
245
+ workflows: options.agentTools.workflows,
186
246
  getIntegrationConnectionById,
187
247
  }
188
248
  : undefined,
@@ -206,7 +266,41 @@ export async function createIntegrationsContext(
206
266
  },
207
267
  ...parts.flatMap((part) => part.workers ?? []),
208
268
  ],
269
+ ...(options.webhookDeliverySource
270
+ ? {services: [options.webhookDeliverySource.createService(webhookProcessor)]}
271
+ : {}),
209
272
  };
210
273
 
211
- return {module, registry, capabilities: {sourceControl}, sourceControl, runStartupTasks};
274
+ return {
275
+ module,
276
+ registry,
277
+ capabilities: {sourceControl},
278
+ sourceControl,
279
+ webhookProcessor,
280
+ runStartupTasks,
281
+ };
282
+ }
283
+
284
+ function createComposedWebhookProcessor(
285
+ registrations: WebhookProcessorRegistration[],
286
+ ): WebhookRequestProcessor {
287
+ const processors = new Map<WebhookRouteId, WebhookRequestProcessor>();
288
+ for (const registration of registrations) {
289
+ for (const routeId of registration.routeIds) {
290
+ if (processors.has(routeId)) {
291
+ throw new Error(`Webhook processor is registered more than once for ${routeId}`);
292
+ }
293
+ processors.set(routeId, registration.processor);
294
+ }
295
+ }
296
+
297
+ return {
298
+ async process(request: StoredWebhookRequest): Promise<WebhookProcessingResult> {
299
+ const processor = processors.get(request.route_id);
300
+ if (!processor) {
301
+ throw new WebhookProcessorNotConfiguredError(request.route_id);
302
+ }
303
+ return await processor.process(request);
304
+ },
305
+ };
212
306
  }