@shipfox/api-integration-core 5.0.0 → 7.0.1

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 (73) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +83 -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/index.d.ts +17 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +50 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/presentation/inter-module.d.ts +9 -0
  15. package/dist/presentation/inter-module.d.ts.map +1 -0
  16. package/dist/presentation/inter-module.js +121 -0
  17. package/dist/presentation/inter-module.js.map +1 -0
  18. package/dist/presentation/routes/agent-tools-gateway/index.d.ts +1 -0
  19. package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -1
  20. package/dist/presentation/routes/agent-tools-gateway/index.js +1 -0
  21. package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -1
  22. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +5 -2
  23. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -1
  24. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +53 -17
  25. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -1
  26. package/dist/presentation/routes/index.d.ts +2 -3
  27. package/dist/presentation/routes/index.d.ts.map +1 -1
  28. package/dist/presentation/routes/index.js +2 -2
  29. package/dist/presentation/routes/index.js.map +1 -1
  30. package/dist/providers/gitea.d.ts.map +1 -1
  31. package/dist/providers/gitea.js +10 -8
  32. package/dist/providers/gitea.js.map +1 -1
  33. package/dist/providers/github.d.ts.map +1 -1
  34. package/dist/providers/github.js +18 -13
  35. package/dist/providers/github.js.map +1 -1
  36. package/dist/providers/jira.d.ts.map +1 -1
  37. package/dist/providers/jira.js +4 -1
  38. package/dist/providers/jira.js.map +1 -1
  39. package/dist/providers/linear.d.ts.map +1 -1
  40. package/dist/providers/linear.js +34 -29
  41. package/dist/providers/linear.js.map +1 -1
  42. package/dist/providers/sentry.d.ts.map +1 -1
  43. package/dist/providers/sentry.js +13 -11
  44. package/dist/providers/sentry.js.map +1 -1
  45. package/dist/providers/slack.d.ts.map +1 -1
  46. package/dist/providers/slack.js +34 -29
  47. package/dist/providers/slack.js.map +1 -1
  48. package/dist/providers/types.d.ts +12 -0
  49. package/dist/providers/types.d.ts.map +1 -1
  50. package/dist/providers/types.js.map +1 -1
  51. package/dist/providers/webhook.d.ts.map +1 -1
  52. package/dist/providers/webhook.js +11 -9
  53. package/dist/providers/webhook.js.map +1 -1
  54. package/dist/tsconfig.test.tsbuildinfo +1 -1
  55. package/package.json +22 -17
  56. package/src/core/entities/provider.ts +7 -1
  57. package/src/core/errors.ts +12 -2
  58. package/src/index.test.ts +112 -0
  59. package/src/index.ts +102 -9
  60. package/src/presentation/inter-module.ts +155 -0
  61. package/src/presentation/routes/agent-tools-gateway/index.ts +1 -0
  62. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +36 -0
  63. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +64 -22
  64. package/src/presentation/routes/index.ts +4 -5
  65. package/src/providers/gitea.ts +11 -8
  66. package/src/providers/github.ts +17 -11
  67. package/src/providers/jira.ts +3 -0
  68. package/src/providers/linear.ts +30 -24
  69. package/src/providers/sentry.ts +15 -12
  70. package/src/providers/slack.ts +31 -25
  71. package/src/providers/types.ts +15 -0
  72. package/src/providers/webhook.ts +11 -9
  73. 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": "5.0.0",
4
+ "version": "7.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,26 +27,29 @@
27
27
  "ajv": "^8.20.0",
28
28
  "drizzle-orm": "^0.45.2",
29
29
  "zod": "^4.4.3",
30
- "@shipfox/api-agent-dto": "5.0.0",
31
- "@shipfox/api-auth-context": "5.0.0",
32
- "@shipfox/api-integration-core-dto": "5.0.0",
33
- "@shipfox/api-integration-gitea": "5.0.0",
34
- "@shipfox/api-integration-github": "5.0.0",
35
- "@shipfox/api-integration-jira": "5.0.0",
36
- "@shipfox/api-integration-linear": "5.0.0",
37
- "@shipfox/api-integration-sentry": "5.0.0",
38
- "@shipfox/api-integration-slack": "5.0.0",
39
- "@shipfox/api-integration-webhook": "5.0.0",
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": "7.0.1",
37
+ "@shipfox/api-integration-jira": "7.0.1",
38
+ "@shipfox/api-integration-linear": "7.0.1",
39
+ "@shipfox/api-integration-sentry": "6.0.0",
40
+ "@shipfox/api-integration-slack": "6.0.0",
41
+ "@shipfox/api-integration-webhook": "6.0.0",
40
42
  "@shipfox/config": "1.2.2",
41
- "@shipfox/node-drizzle": "0.3.1",
42
- "@shipfox/node-fastify": "0.2.3",
43
- "@shipfox/node-module": "0.3.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",
44
47
  "@shipfox/node-opentelemetry": "0.5.2",
45
- "@shipfox/node-outbox": "0.2.3",
48
+ "@shipfox/node-outbox": "0.2.4",
46
49
  "@shipfox/node-postgres": "0.4.2",
47
- "@shipfox/node-temporal": "0.3.1",
50
+ "@shipfox/node-temporal": "0.3.2",
48
51
  "@shipfox/regex": "0.2.2",
49
- "@shipfox/redact": "0.2.2"
52
+ "@shipfox/redact": "0.2.3"
50
53
  },
51
54
  "devDependencies": {
52
55
  "@temporalio/activity": "1.18.1",
@@ -60,6 +63,7 @@
60
63
  "fastify-type-provider-zod": "^6.0.0",
61
64
  "fishery": "^2.4.0",
62
65
  "@shipfox/biome": "1.8.2",
66
+ "@shipfox/depcruise": "1.0.2",
63
67
  "@shipfox/swc": "1.2.6",
64
68
  "@shipfox/ts-config": "1.3.8",
65
69
  "@shipfox/typescript": "1.1.7",
@@ -69,6 +73,7 @@
69
73
  "build": "shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js",
70
74
  "check": "shipfox-biome-check",
71
75
  "check:fix": "shipfox-biome-check --write",
76
+ "depcruise": "shipfox-depcruise",
72
77
  "test": "shipfox-vitest-run",
73
78
  "test:watch": "shipfox-vitest-watch",
74
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};
@@ -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,
@@ -119,11 +140,21 @@ export interface CreateIntegrationsModuleOptions {
119
140
  */
120
141
  parts?: IntegrationModuleParts[] | undefined;
121
142
  secrets?: IntegrationProviderSecrets | undefined;
143
+ workspaces?: WorkspacesInterModuleClient | undefined;
122
144
  agentTools?:
123
145
  | {
124
- loadLeasedAgentStep: LeasedAgentStepLoader;
146
+ workflows: WorkflowsModuleClient;
125
147
  }
126
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;
127
158
  }
128
159
 
129
160
  export interface IntegrationsContext {
@@ -133,6 +164,7 @@ export interface IntegrationsContext {
133
164
  sourceControl: IntegrationSourceControlService;
134
165
  };
135
166
  sourceControl: IntegrationSourceControlService;
167
+ webhookProcessor: WebhookRequestProcessor;
136
168
  /**
137
169
  * Runs every enabled provider's one-shot boot-time tasks, after modules are initialized
138
170
  * (migrations done). Failures are isolated and logged, never rethrown, so a provider task
@@ -150,17 +182,41 @@ export async function createIntegrationsModule(
150
182
  export async function createIntegrationsContext(
151
183
  options: CreateIntegrationsModuleOptions = {},
152
184
  ): Promise<IntegrationsContext> {
185
+ const workspaces = options.workspaces;
153
186
  const parts: IntegrationModuleParts[] =
154
187
  options.parts ??
155
188
  (options.providers
156
- ? options.providers.map((provider) => ({provider}))
157
- : 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
+ }));
158
211
 
159
212
  const registry = createIntegrationProviderRegistry(parts.map((part) => part.provider));
160
213
  const sourceControl = createSourceControlIntegrationService({
161
214
  registry,
162
215
  getIntegrationConnectionById,
163
216
  });
217
+ const webhookProcessor = createComposedWebhookProcessor(
218
+ parts.flatMap((part) => part.webhookProcessors ?? []),
219
+ );
164
220
 
165
221
  async function runStartupTasks(): Promise<void> {
166
222
  for (const task of parts.flatMap((part) => part.startupTasks ?? [])) {
@@ -175,6 +231,9 @@ export async function createIntegrationsContext(
175
231
 
176
232
  const module: ShipfoxModule = {
177
233
  name: 'integrations',
234
+ interModulePresentations: [
235
+ createIntegrationsInterModulePresentation({registry, sourceControl}),
236
+ ],
178
237
  startupTasks: runStartupTasks,
179
238
  database: [
180
239
  {db, migrationsPath},
@@ -183,7 +242,7 @@ export async function createIntegrationsContext(
183
242
  routes: createIntegrationRoutes(registry, sourceControl, {
184
243
  agentTools: options.agentTools
185
244
  ? {
186
- loadLeasedAgentStep: options.agentTools.loadLeasedAgentStep,
245
+ workflows: options.agentTools.workflows,
187
246
  getIntegrationConnectionById,
188
247
  }
189
248
  : undefined,
@@ -207,7 +266,41 @@ export async function createIntegrationsContext(
207
266
  },
208
267
  ...parts.flatMap((part) => part.workers ?? []),
209
268
  ],
269
+ ...(options.webhookDeliverySource
270
+ ? {services: [options.webhookDeliverySource.createService(webhookProcessor)]}
271
+ : {}),
210
272
  };
211
273
 
212
- 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
+ };
213
306
  }
@@ -0,0 +1,155 @@
1
+ import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto';
2
+ import {
3
+ createInterModuleKnownError,
4
+ defineInterModulePresentation,
5
+ type InterModuleMethodContract,
6
+ type InterModulePresentation,
7
+ } from '@shipfox/inter-module';
8
+ import {
9
+ buildAgentToolCatalogs,
10
+ buildAgentToolSelectionCatalogs,
11
+ createWorkspaceConnectionSnapshotLoader,
12
+ } from '#core/agent-tool-selection.js';
13
+ import {
14
+ IntegrationCapabilityUnavailableError,
15
+ IntegrationCheckoutUnsupportedError,
16
+ IntegrationConnectionInactiveError,
17
+ IntegrationConnectionNotFoundError,
18
+ IntegrationConnectionWorkspaceMismatchError,
19
+ IntegrationProviderError,
20
+ IntegrationProviderUnavailableError,
21
+ } from '#core/errors.js';
22
+ import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
23
+ import type {IntegrationSourceControlService} from '#core/source-control-service.js';
24
+ import {getIntegrationConnectionById} from '#db/connections.js';
25
+
26
+ export function createIntegrationsInterModulePresentation(params: {
27
+ registry: IntegrationProviderRegistry;
28
+ sourceControl: IntegrationSourceControlService;
29
+ }): InterModulePresentation<typeof integrationsInterModuleContract> {
30
+ const contract = integrationsInterModuleContract;
31
+ return defineInterModulePresentation(contract, {
32
+ resolveSourceRepository: async (input) =>
33
+ await known(contract.methods.resolveSourceRepository, input, async () => {
34
+ const resolved = await params.sourceControl.resolveRepository(input);
35
+ return {
36
+ connection: {
37
+ id: resolved.connection.id,
38
+ provider: resolved.connection.provider,
39
+ slug: resolved.connection.slug,
40
+ },
41
+ repository: resolved.repository,
42
+ };
43
+ }),
44
+ listSourceFiles: async (input) =>
45
+ await known(
46
+ contract.methods.listSourceFiles,
47
+ input,
48
+ async () => await params.sourceControl.listFiles(input),
49
+ ),
50
+ fetchSourceFile: async (input) =>
51
+ await known(
52
+ contract.methods.fetchSourceFile,
53
+ input,
54
+ async () => await params.sourceControl.fetchFile(input),
55
+ ),
56
+ createCheckoutSpec: async (input) =>
57
+ await known(contract.methods.createCheckoutSpec, input, async () => {
58
+ const spec = await params.sourceControl.createCheckoutSpec(input);
59
+ return {
60
+ repositoryUrl: spec.repositoryUrl,
61
+ ref: spec.ref,
62
+ ...(spec.credentials
63
+ ? {
64
+ credentials: {
65
+ ...spec.credentials,
66
+ expiresAt: spec.credentials.expiresAt.toISOString(),
67
+ },
68
+ }
69
+ : {}),
70
+ ...(spec.gitAuthor === undefined ? {} : {gitAuthor: spec.gitAuthor}),
71
+ };
72
+ }),
73
+ getAgentToolsContext: async (input) =>
74
+ await known(contract.methods.getAgentToolsContext, input, async () => {
75
+ const [selectionCatalogs, catalogs, snapshot, defaultConnection] = await Promise.all([
76
+ buildAgentToolSelectionCatalogs(params.registry),
77
+ buildAgentToolCatalogs(params.registry),
78
+ createWorkspaceConnectionSnapshotLoader(params.registry)(input.workspaceId),
79
+ getIntegrationConnectionById(input.defaultConnectionId),
80
+ ]);
81
+ return {
82
+ selectionCatalogs: [...selectionCatalogs].map(([provider, value]) => ({
83
+ provider,
84
+ selectors: value.selectors.map((selector) => ({...selector})),
85
+ })),
86
+ catalogs: [...catalogs].map(([provider, tools]) => ({
87
+ provider,
88
+ tools: tools.map(({methods, ...tool}) => ({
89
+ ...tool,
90
+ ...(methods === undefined ? {} : {methods: methods.map((method) => ({...method}))}),
91
+ })),
92
+ })),
93
+ workspaceConnections: [...snapshot].map(([slug, value]) => ({
94
+ slug,
95
+ ...value,
96
+ capabilities: [...value.capabilities],
97
+ })),
98
+ defaultConnection: defaultConnection
99
+ ? {
100
+ id: defaultConnection.id,
101
+ slug: defaultConnection.slug,
102
+ provider: defaultConnection.provider,
103
+ }
104
+ : null,
105
+ };
106
+ }),
107
+ });
108
+ }
109
+
110
+ async function known<Output>(
111
+ method: InterModuleMethodContract,
112
+ input: {connectionId?: string; defaultConnectionId?: string},
113
+ operation: () => Promise<Output>,
114
+ ): Promise<Output> {
115
+ try {
116
+ return await operation();
117
+ } catch (error) {
118
+ throw mapError(method, input, error);
119
+ }
120
+ }
121
+ function mapError(
122
+ method: InterModuleMethodContract,
123
+ input: {connectionId?: string; defaultConnectionId?: string},
124
+ error: unknown,
125
+ ): unknown {
126
+ if (error instanceof IntegrationConnectionNotFoundError)
127
+ return createInterModuleKnownError(method, 'connection-not-found', {
128
+ connectionId: input.connectionId ?? input.defaultConnectionId,
129
+ });
130
+ if (error instanceof IntegrationConnectionInactiveError)
131
+ return createInterModuleKnownError(method, 'connection-inactive', {
132
+ connectionId: input.connectionId,
133
+ });
134
+ if (error instanceof IntegrationConnectionWorkspaceMismatchError)
135
+ return createInterModuleKnownError(method, 'connection-workspace-mismatch', {
136
+ connectionId: input.connectionId,
137
+ });
138
+ if (error instanceof IntegrationProviderUnavailableError)
139
+ return createInterModuleKnownError(method, 'provider-unavailable', {provider: error.provider});
140
+ if (error instanceof IntegrationCapabilityUnavailableError)
141
+ return createInterModuleKnownError(method, 'capability-unavailable', {
142
+ provider: error.provider,
143
+ capability: error.capability,
144
+ });
145
+ if (error instanceof IntegrationCheckoutUnsupportedError)
146
+ return createInterModuleKnownError(method, 'checkout-unsupported', {provider: error.provider});
147
+ if (error instanceof IntegrationProviderError)
148
+ return createInterModuleKnownError(method, 'provider-failure', {
149
+ reason: error.reason,
150
+ ...(error.retryAfterSeconds === undefined
151
+ ? {}
152
+ : {retryAfterSeconds: error.retryAfterSeconds}),
153
+ });
154
+ return error;
155
+ }
@@ -13,6 +13,7 @@ import {
13
13
  } from './resolve-authorized-tools.js';
14
14
 
15
15
  export type {LeasedAgentStepLoader} from './resolve-authorized-tools.js';
16
+ export {createWorkflowsLeasedAgentStepLoader} from './resolve-authorized-tools.js';
16
17
 
17
18
  export interface CreateAgentToolsGatewayRoutesParams {
18
19
  loadLeasedAgentStep: LeasedAgentStepLoader;
@@ -1,4 +1,6 @@
1
1
  import {setLeasedJobContext} from '@shipfox/api-auth-context';
2
+ import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
3
+ import {createInterModuleKnownError} from '@shipfox/inter-module';
2
4
  import {createIntegrationProviderRegistry} from '#core/providers/registry.js';
3
5
  import {
4
6
  agentStepConfig,
@@ -9,6 +11,7 @@ import {
9
11
  registryWithAgentTools,
10
12
  } from '#test/agent-tools-gateway-helpers.js';
11
13
  import {
14
+ createWorkflowsLeasedAgentStepLoader,
12
15
  mcpToolName,
13
16
  narrowMethodEnum,
14
17
  resolveAuthorizedIntegrationTools,
@@ -16,6 +19,39 @@ import {
16
19
  } from './resolve-authorized-tools.js';
17
20
 
18
21
  describe('resolveAuthorizedIntegrationTools', () => {
22
+ it.each([
23
+ ['lease-not-active', 404],
24
+ ['step-not-found', 404],
25
+ ['job-not-found', 404],
26
+ ['step-attempt-mismatch', 409],
27
+ ['step-not-running', 409],
28
+ ['leased-step-not-agent', 409],
29
+ ['agent-step-config-invalid', 409],
30
+ ] as const)('maps %s from Workflows to HTTP %i', async (code, status) => {
31
+ const request = {};
32
+ const lease = leaseContext();
33
+ setLeasedJobContext(request, lease);
34
+ const workflows = {
35
+ getLeasedAgentToolContext: () =>
36
+ Promise.reject(
37
+ createInterModuleKnownError(
38
+ workflowsInterModuleContract.methods.getLeasedAgentToolContext,
39
+ code,
40
+ {},
41
+ ),
42
+ ),
43
+ };
44
+ const loadLeasedAgentStep = createWorkflowsLeasedAgentStepLoader(workflows as never);
45
+
46
+ const error = await loadLeasedAgentStep({
47
+ request,
48
+ stepId: lease.currentStepId as string,
49
+ attempt: lease.currentStepAttempt as number,
50
+ }).catch((error: unknown) => error);
51
+
52
+ expect(error).toMatchObject({code, status});
53
+ });
54
+
19
55
  it('resolves namespaced tools with live descriptions and Anthropic-compatible method schemas', async () => {
20
56
  const request = {};
21
57
  const lease = leaseContext();