@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +100 -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/db/webhook-deliveries.d.ts +4 -0
- package/dist/db/webhook-deliveries.d.ts.map +1 -1
- package/dist/db/webhook-deliveries.js +10 -2
- package/dist/db/webhook-deliveries.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -6
- 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 +3 -3
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/manage-connections.d.ts +1 -1
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
- package/dist/presentation/routes/manage-connections.js +28 -3
- package/dist/presentation/routes/manage-connections.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 +113 -3
- package/dist/providers/jira.js.map +1 -1
- package/dist/providers/linear.d.ts.map +1 -1
- package/dist/providers/linear.js +35 -16
- 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 +36 -16
- 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 +33 -38
- package/src/core/entities/provider.ts +7 -1
- package/src/core/errors.ts +12 -2
- package/src/db/webhook-deliveries.test.ts +11 -0
- package/src/db/webhook-deliveries.ts +12 -3
- package/src/index.test.ts +112 -0
- package/src/index.ts +104 -10
- 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 +5 -6
- package/src/presentation/routes/manage-connections.test.ts +113 -0
- package/src/presentation/routes/manage-connections.ts +26 -2
- package/src/providers/gitea.ts +11 -8
- package/src/providers/github.ts +17 -11
- package/src/providers/jira.ts +155 -7
- package/src/providers/linear.test.ts +136 -0
- package/src/providers/linear.ts +33 -13
- package/src/providers/sentry.ts +15 -12
- package/src/providers/slack.test.ts +139 -0
- package/src/providers/slack.ts +39 -14
- package/src/providers/types.ts +15 -0
- package/src/providers/webhook.ts +11 -9
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -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();
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
materializedAgentStepConfigSchema,
|
|
5
5
|
} from '@shipfox/api-agent-dto';
|
|
6
6
|
import {requireLeasedJobContext} from '@shipfox/api-auth-context';
|
|
7
|
+
import {
|
|
8
|
+
type WorkflowsModuleClient,
|
|
9
|
+
workflowsInterModuleContract,
|
|
10
|
+
} from '@shipfox/api-workflows-dto/inter-module';
|
|
11
|
+
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
7
12
|
import {ClientError} from '@shipfox/node-fastify';
|
|
8
13
|
import type {IntegrationConnection} from '#core/entities/connection.js';
|
|
9
14
|
import type {IntegrationProviderKind} from '#core/entities/provider.js';
|
|
@@ -16,10 +21,49 @@ export type LeasedAgentStepLoader = (params: {
|
|
|
16
21
|
stepId: string;
|
|
17
22
|
attempt: number;
|
|
18
23
|
}) => Promise<{
|
|
19
|
-
step: {type: string; config: Record<string, unknown>};
|
|
20
24
|
workspaceId: string;
|
|
25
|
+
integrations?: MaterializedAgentIntegrationConfigDto[];
|
|
26
|
+
step?: {type: string; config: Record<string, unknown>};
|
|
21
27
|
}>;
|
|
22
28
|
|
|
29
|
+
export function createWorkflowsLeasedAgentStepLoader(
|
|
30
|
+
workflows: WorkflowsModuleClient,
|
|
31
|
+
): LeasedAgentStepLoader {
|
|
32
|
+
return async ({request, stepId, attempt}) => {
|
|
33
|
+
const leasedJob = requireLeasedJobContext(request);
|
|
34
|
+
try {
|
|
35
|
+
const context = await workflows.getLeasedAgentToolContext({
|
|
36
|
+
jobId: leasedJob.jobId,
|
|
37
|
+
jobExecutionId: leasedJob.jobExecutionId,
|
|
38
|
+
runnerSessionId: leasedJob.runnerSessionId,
|
|
39
|
+
stepId,
|
|
40
|
+
attempt,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
workspaceId: context.workspaceId,
|
|
44
|
+
integrations: context.integrations,
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw toLeasedAgentToolClientError(error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toLeasedAgentToolClientError(error: unknown): unknown {
|
|
53
|
+
const method = workflowsInterModuleContract.methods.getLeasedAgentToolContext;
|
|
54
|
+
if (!isInterModuleKnownError(method, error)) return error;
|
|
55
|
+
|
|
56
|
+
const status = [
|
|
57
|
+
'step-attempt-mismatch',
|
|
58
|
+
'step-not-running',
|
|
59
|
+
'leased-step-not-agent',
|
|
60
|
+
'agent-step-config-invalid',
|
|
61
|
+
].includes(error.code)
|
|
62
|
+
? 409
|
|
63
|
+
: 404;
|
|
64
|
+
return new ClientError(error.code.replaceAll('-', ' '), error.code, {status});
|
|
65
|
+
}
|
|
66
|
+
|
|
23
67
|
export interface AuthorizedIntegrationTool {
|
|
24
68
|
mcpName: string;
|
|
25
69
|
integration: MaterializedAgentIntegrationConfigDto;
|
|
@@ -49,18 +93,13 @@ export async function resolveAuthorizedIntegrationTools(
|
|
|
49
93
|
});
|
|
50
94
|
}
|
|
51
95
|
|
|
52
|
-
const
|
|
96
|
+
const loaded = await params.loadLeasedAgentStep({
|
|
53
97
|
request: params.request,
|
|
54
98
|
stepId: leasedJob.currentStepId,
|
|
55
99
|
attempt: leasedJob.currentStepAttempt,
|
|
56
100
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
status: 409,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const integrations = parseAgentIntegrations(step.config);
|
|
101
|
+
const workspaceId = loaded.workspaceId;
|
|
102
|
+
const integrations = loaded.integrations ?? legacyIntegrations(loaded.step);
|
|
64
103
|
const authorizedTools: AuthorizedIntegrationToolMap = new Map();
|
|
65
104
|
|
|
66
105
|
for (const integration of integrations) {
|
|
@@ -102,6 +141,22 @@ export async function resolveAuthorizedIntegrationTools(
|
|
|
102
141
|
return authorizedTools;
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
function legacyIntegrations(step: {type: string; config: Record<string, unknown>} | undefined) {
|
|
145
|
+
if (step?.type !== 'agent') {
|
|
146
|
+
throw new ClientError('Current leased step is not an agent step', 'leased-step-not-agent', {
|
|
147
|
+
status: 409,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return materializedAgentStepConfigSchema.parse(step.config).integrations ?? [];
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new ClientError('Agent step config is invalid', 'agent-step-config-invalid', {
|
|
154
|
+
status: 409,
|
|
155
|
+
cause: error,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
105
160
|
export function sanitizeSlug(slug: string): string {
|
|
106
161
|
return slug.replaceAll('-', '_');
|
|
107
162
|
}
|
|
@@ -132,19 +187,6 @@ function toolInputSchema(tool: MaterializedAgentIntegrationToolConfigDto): Agent
|
|
|
132
187
|
return narrowMethodEnum(tool.inputSchema, authorizedMethods);
|
|
133
188
|
}
|
|
134
189
|
|
|
135
|
-
function parseAgentIntegrations(
|
|
136
|
-
config: Record<string, unknown>,
|
|
137
|
-
): MaterializedAgentIntegrationConfigDto[] {
|
|
138
|
-
try {
|
|
139
|
-
return materializedAgentStepConfigSchema.parse(config).integrations ?? [];
|
|
140
|
-
} catch (error) {
|
|
141
|
-
throw new ClientError('Agent step config is invalid', 'agent-step-config-invalid', {
|
|
142
|
-
status: 409,
|
|
143
|
-
cause: error,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
190
|
async function loadAuthorizedConnection(params: {
|
|
149
191
|
integration: MaterializedAgentIntegrationConfigDto;
|
|
150
192
|
workspaceId: string;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
|
|
1
2
|
import type {RouteExport} from '@shipfox/node-fastify';
|
|
2
3
|
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
3
4
|
import type {IntegrationSourceControlService} from '#core/source-control-service.js';
|
|
4
5
|
import type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
|
|
5
6
|
import {
|
|
6
7
|
createAgentToolsGatewayRoutes,
|
|
7
|
-
|
|
8
|
+
createWorkflowsLeasedAgentStepLoader,
|
|
8
9
|
} from './agent-tools-gateway/index.js';
|
|
9
10
|
import {createListIntegrationConnectionsRoute} from './list-connections.js';
|
|
10
11
|
import {createListIntegrationProvidersRoute} from './list-providers.js';
|
|
@@ -14,12 +15,10 @@ import {
|
|
|
14
15
|
createUpdateIntegrationConnectionRoute,
|
|
15
16
|
} from './manage-connections.js';
|
|
16
17
|
|
|
17
|
-
export type {LeasedAgentStepLoader} from './agent-tools-gateway/index.js';
|
|
18
|
-
|
|
19
18
|
export interface CreateIntegrationRoutesOptions {
|
|
20
19
|
agentTools?:
|
|
21
20
|
| {
|
|
22
|
-
|
|
21
|
+
workflows: WorkflowsModuleClient;
|
|
23
22
|
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
24
23
|
}
|
|
25
24
|
| undefined;
|
|
@@ -35,7 +34,7 @@ export function createIntegrationRoutes(
|
|
|
35
34
|
? [
|
|
36
35
|
createAgentToolsGatewayRoutes({
|
|
37
36
|
registry,
|
|
38
|
-
loadLeasedAgentStep: options.agentTools.
|
|
37
|
+
loadLeasedAgentStep: createWorkflowsLeasedAgentStepLoader(options.agentTools.workflows),
|
|
39
38
|
getIntegrationConnectionById: options.agentTools.getIntegrationConnectionById,
|
|
40
39
|
}),
|
|
41
40
|
]
|
|
@@ -45,7 +44,7 @@ export function createIntegrationRoutes(
|
|
|
45
44
|
createListIntegrationProvidersRoute(registry),
|
|
46
45
|
createListIntegrationConnectionsRoute(registry),
|
|
47
46
|
createUpdateIntegrationConnectionRoute(registry),
|
|
48
|
-
createDeleteIntegrationConnectionRoute(),
|
|
47
|
+
createDeleteIntegrationConnectionRoute(registry),
|
|
49
48
|
createListRepositoriesRoute(sourceControl),
|
|
50
49
|
...agentToolsRoutes,
|
|
51
50
|
...providerRoutes,
|
|
@@ -110,6 +110,119 @@ describe('DELETE /integration-connections/:connectionId', () => {
|
|
|
110
110
|
expect(reloaded).toBeUndefined();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('runs provider cleanup hooks while retaining ownership of the core row', async () => {
|
|
114
|
+
const deleteConnectionRecords = vi.fn(() => Promise.resolve());
|
|
115
|
+
const deleteConnectionSecrets = vi.fn(() => Promise.resolve());
|
|
116
|
+
const app = await createTestApp([
|
|
117
|
+
sourceProvider({
|
|
118
|
+
provider: 'slack',
|
|
119
|
+
displayName: 'Slack',
|
|
120
|
+
adapters: {},
|
|
121
|
+
deleteConnectionRecords,
|
|
122
|
+
deleteConnectionSecrets,
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
const connection = await upsertIntegrationConnection({
|
|
126
|
+
workspaceId: context.workspaceId,
|
|
127
|
+
provider: 'slack',
|
|
128
|
+
externalAccountId: 'T123',
|
|
129
|
+
slug: 'slack_acme',
|
|
130
|
+
displayName: 'Slack Acme',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const res = await app.inject({
|
|
134
|
+
method: 'DELETE',
|
|
135
|
+
url: `/integration-connections/${connection.id}`,
|
|
136
|
+
headers: {authorization: 'Bearer user'},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(res.statusCode).toBe(204);
|
|
140
|
+
expect(deleteConnectionRecords).toHaveBeenCalledWith(connection, {
|
|
141
|
+
tx: expect.anything(),
|
|
142
|
+
});
|
|
143
|
+
expect(deleteConnectionSecrets).toHaveBeenCalledWith(connection);
|
|
144
|
+
await expect(getIntegrationConnectionById(connection.id)).resolves.toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('keeps the connection when provider record cleanup fails', async () => {
|
|
148
|
+
const deleteConnectionSecrets = vi.fn(() => Promise.resolve());
|
|
149
|
+
const app = await createTestApp([
|
|
150
|
+
sourceProvider({
|
|
151
|
+
provider: 'slack',
|
|
152
|
+
displayName: 'Slack',
|
|
153
|
+
adapters: {},
|
|
154
|
+
deleteConnectionRecords: () => Promise.reject(new Error('record cleanup failed')),
|
|
155
|
+
deleteConnectionSecrets,
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
const connection = await upsertIntegrationConnection({
|
|
159
|
+
workspaceId: context.workspaceId,
|
|
160
|
+
provider: 'slack',
|
|
161
|
+
externalAccountId: 'T123',
|
|
162
|
+
slug: 'slack_acme',
|
|
163
|
+
displayName: 'Slack Acme',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const res = await app.inject({
|
|
167
|
+
method: 'DELETE',
|
|
168
|
+
url: `/integration-connections/${connection.id}`,
|
|
169
|
+
headers: {authorization: 'Bearer user'},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(res.statusCode).toBe(500);
|
|
173
|
+
await expect(getIntegrationConnectionById(connection.id)).resolves.toMatchObject({
|
|
174
|
+
id: connection.id,
|
|
175
|
+
});
|
|
176
|
+
expect(deleteConnectionSecrets).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('deletes the connection when provider secret cleanup fails after commit', async () => {
|
|
180
|
+
const app = await createTestApp([
|
|
181
|
+
sourceProvider({
|
|
182
|
+
provider: 'slack',
|
|
183
|
+
displayName: 'Slack',
|
|
184
|
+
adapters: {},
|
|
185
|
+
deleteConnectionSecrets: () => Promise.reject(new Error('secret cleanup failed')),
|
|
186
|
+
}),
|
|
187
|
+
]);
|
|
188
|
+
const connection = await upsertIntegrationConnection({
|
|
189
|
+
workspaceId: context.workspaceId,
|
|
190
|
+
provider: 'slack',
|
|
191
|
+
externalAccountId: 'T123',
|
|
192
|
+
slug: 'slack_acme',
|
|
193
|
+
displayName: 'Slack Acme',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const res = await app.inject({
|
|
197
|
+
method: 'DELETE',
|
|
198
|
+
url: `/integration-connections/${connection.id}`,
|
|
199
|
+
headers: {authorization: 'Bearer user'},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(res.statusCode).toBe(204);
|
|
203
|
+
await expect(getIntegrationConnectionById(connection.id)).resolves.toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('deletes an unregistered provider connection without provider cleanup', async () => {
|
|
207
|
+
const app = await createTestApp([]);
|
|
208
|
+
const connection = await upsertIntegrationConnection({
|
|
209
|
+
workspaceId: context.workspaceId,
|
|
210
|
+
provider: 'slack',
|
|
211
|
+
externalAccountId: 'T123',
|
|
212
|
+
slug: 'slack_acme',
|
|
213
|
+
displayName: 'Slack Acme',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const res = await app.inject({
|
|
217
|
+
method: 'DELETE',
|
|
218
|
+
url: `/integration-connections/${connection.id}`,
|
|
219
|
+
headers: {authorization: 'Bearer user'},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(res.statusCode).toBe(204);
|
|
223
|
+
await expect(getIntegrationConnectionById(connection.id)).resolves.toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
113
226
|
it('returns not-found for a missing connection', async () => {
|
|
114
227
|
const app = await createTestApp([sourceProvider()]);
|
|
115
228
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getIntegrationConnectionById,
|
|
12
12
|
updateIntegrationConnectionLifecycleStatus,
|
|
13
13
|
} from '#db/connections.js';
|
|
14
|
+
import {db} from '#db/db.js';
|
|
14
15
|
import {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';
|
|
15
16
|
|
|
16
17
|
const connectionParamsSchema = z.object({
|
|
@@ -51,7 +52,7 @@ export function createUpdateIntegrationConnectionRoute(registry: IntegrationProv
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
export function createDeleteIntegrationConnectionRoute() {
|
|
55
|
+
export function createDeleteIntegrationConnectionRoute(registry: IntegrationProviderRegistry) {
|
|
55
56
|
return defineRoute({
|
|
56
57
|
method: 'DELETE',
|
|
57
58
|
path: '/integration-connections/:connectionId',
|
|
@@ -70,7 +71,30 @@ export function createDeleteIntegrationConnectionRoute() {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
requireWorkspaceAccess({request, workspaceId: connection.workspaceId});
|
|
73
|
-
|
|
74
|
+
const provider = registry
|
|
75
|
+
.list()
|
|
76
|
+
.find((candidate) => candidate.provider === connection.provider);
|
|
77
|
+
const hasCleanupHooks =
|
|
78
|
+
provider?.deleteConnectionRecords !== undefined ||
|
|
79
|
+
provider?.deleteConnectionSecrets !== undefined;
|
|
80
|
+
if (!hasCleanupHooks) {
|
|
81
|
+
request.log.warn(
|
|
82
|
+
{connectionId: connection.id, provider: connection.provider},
|
|
83
|
+
'Deleting integration connection without provider cleanup',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
await db().transaction(async (tx) => {
|
|
87
|
+
await provider?.deleteConnectionRecords?.(connection, {tx});
|
|
88
|
+
await deleteIntegrationConnection({id: connection.id}, {tx});
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
await provider?.deleteConnectionSecrets?.(connection);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
request.log.error(
|
|
94
|
+
{connectionId: connection.id, provider: connection.provider, err: error},
|
|
95
|
+
'Integration connection secret cleanup failed after connection deletion',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
74
98
|
reply.status(204);
|
|
75
99
|
},
|
|
76
100
|
});
|
package/src/providers/gitea.ts
CHANGED
|
@@ -79,15 +79,18 @@ async function loadGiteaModuleParts(): Promise<IntegrationModuleParts> {
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
const integrationProvider = createGiteaIntegrationProvider({
|
|
83
|
+
getExistingGiteaConnection,
|
|
84
|
+
connectGiteaConnection,
|
|
85
|
+
publishSourcePush,
|
|
86
|
+
recordDeliveryOnly,
|
|
87
|
+
getIntegrationConnectionById,
|
|
88
|
+
coreDb: db,
|
|
89
|
+
});
|
|
90
|
+
|
|
82
91
|
return {
|
|
83
|
-
provider:
|
|
84
|
-
|
|
85
|
-
connectGiteaConnection,
|
|
86
|
-
publishSourcePush,
|
|
87
|
-
recordDeliveryOnly,
|
|
88
|
-
getIntegrationConnectionById,
|
|
89
|
-
coreDb: db,
|
|
90
|
-
}),
|
|
92
|
+
provider: integrationProvider,
|
|
93
|
+
webhookProcessors: integrationProvider.webhookProcessors,
|
|
91
94
|
database: {
|
|
92
95
|
db: giteaDb,
|
|
93
96
|
migrationsPath: giteaMigrationsPath,
|
package/src/providers/github.ts
CHANGED
|
@@ -116,18 +116,24 @@ async function loadGithubModuleParts(
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
const integrationProvider = createGithubIntegrationProvider({
|
|
120
|
+
getExistingGithubConnection,
|
|
121
|
+
connectGithubInstallation,
|
|
122
|
+
publishIntegrationEventReceived,
|
|
123
|
+
publishSourcePush,
|
|
124
|
+
recordDeliveryOnly,
|
|
125
|
+
getIntegrationConnectionById,
|
|
126
|
+
coreDb: db,
|
|
127
|
+
deleteSecrets: options.secrets?.deleteSecrets,
|
|
128
|
+
agentTools: {tokenProvider},
|
|
129
|
+
...(options.requireActiveWorkspaceMembership
|
|
130
|
+
? {requireActiveWorkspaceMembership: options.requireActiveWorkspaceMembership}
|
|
131
|
+
: {}),
|
|
132
|
+
});
|
|
133
|
+
|
|
119
134
|
return {
|
|
120
|
-
provider:
|
|
121
|
-
|
|
122
|
-
connectGithubInstallation,
|
|
123
|
-
publishIntegrationEventReceived,
|
|
124
|
-
publishSourcePush,
|
|
125
|
-
recordDeliveryOnly,
|
|
126
|
-
getIntegrationConnectionById,
|
|
127
|
-
coreDb: db,
|
|
128
|
-
deleteSecrets: options.secrets?.deleteSecrets,
|
|
129
|
-
agentTools: {tokenProvider},
|
|
130
|
-
}),
|
|
135
|
+
provider: integrationProvider,
|
|
136
|
+
webhookProcessors: integrationProvider.webhookProcessors,
|
|
131
137
|
e2eRoutes: [
|
|
132
138
|
createGithubE2eRoutes({
|
|
133
139
|
getExistingGithubConnection,
|