@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +83 -0
- package/dist/core/entities/provider.d.ts +7 -2
- package/dist/core/entities/provider.d.ts.map +1 -1
- package/dist/core/entities/provider.js.map +1 -1
- package/dist/core/errors.d.ts +7 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +8 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -5
- package/dist/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +9 -0
- package/dist/presentation/inter-module.d.ts.map +1 -0
- package/dist/presentation/inter-module.js +121 -0
- package/dist/presentation/inter-module.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -1
- package/dist/presentation/routes/agent-tools-gateway/index.js +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -1
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +5 -2
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -1
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +53 -17
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -3
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +2 -2
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/providers/gitea.d.ts.map +1 -1
- package/dist/providers/gitea.js +10 -8
- package/dist/providers/gitea.js.map +1 -1
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +18 -13
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +4 -1
- package/dist/providers/jira.js.map +1 -1
- package/dist/providers/linear.d.ts.map +1 -1
- package/dist/providers/linear.js +34 -29
- package/dist/providers/linear.js.map +1 -1
- package/dist/providers/sentry.d.ts.map +1 -1
- package/dist/providers/sentry.js +13 -11
- package/dist/providers/sentry.js.map +1 -1
- package/dist/providers/slack.d.ts.map +1 -1
- package/dist/providers/slack.js +34 -29
- package/dist/providers/slack.js.map +1 -1
- package/dist/providers/types.d.ts +12 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/webhook.d.ts.map +1 -1
- package/dist/providers/webhook.js +11 -9
- package/dist/providers/webhook.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +22 -17
- package/src/core/entities/provider.ts +7 -1
- package/src/core/errors.ts +12 -2
- package/src/index.test.ts +112 -0
- package/src/index.ts +102 -9
- package/src/presentation/inter-module.ts +155 -0
- package/src/presentation/routes/agent-tools-gateway/index.ts +1 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +36 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +64 -22
- package/src/presentation/routes/index.ts +4 -5
- package/src/providers/gitea.ts +11 -8
- package/src/providers/github.ts +17 -11
- package/src/providers/jira.ts +3 -0
- package/src/providers/linear.ts +30 -24
- package/src/providers/sentry.ts +15 -12
- package/src/providers/slack.ts +31 -25
- package/src/providers/types.ts +15 -0
- package/src/providers/webhook.ts +11 -9
- 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
|
+
"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": "
|
|
31
|
-
"@shipfox/api-auth-context": "
|
|
32
|
-
"@shipfox/api-
|
|
33
|
-
"@shipfox/api-integration-
|
|
34
|
-
"@shipfox/api-
|
|
35
|
-
"@shipfox/api-integration-
|
|
36
|
-
"@shipfox/api-integration-
|
|
37
|
-
"@shipfox/api-integration-
|
|
38
|
-
"@shipfox/api-integration-
|
|
39
|
-
"@shipfox/api-integration-
|
|
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/
|
|
42
|
-
"@shipfox/node-
|
|
43
|
-
"@shipfox/node-
|
|
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.
|
|
48
|
+
"@shipfox/node-outbox": "0.2.4",
|
|
46
49
|
"@shipfox/node-postgres": "0.4.2",
|
|
47
|
-
"@shipfox/node-temporal": "0.3.
|
|
50
|
+
"@shipfox/node-temporal": "0.3.2",
|
|
48
51
|
"@shipfox/regex": "0.2.2",
|
|
49
|
-
"@shipfox/redact": "0.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>;
|
package/src/core/errors.ts
CHANGED
|
@@ -35,7 +35,10 @@ export class IntegrationConnectionAlreadyExistsError extends Error {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export class IntegrationCapabilityUnavailableError extends Error {
|
|
38
|
-
constructor(
|
|
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 {
|
|
4
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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) => ({
|
|
157
|
-
|
|
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
|
-
|
|
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 {
|
|
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();
|