@shipfox/api-integration-core 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +264 -0
- package/LICENSE +21 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent-tool-selection.d.ts +15 -0
- package/dist/core/agent-tool-selection.d.ts.map +1 -0
- package/dist/core/agent-tool-selection.js +48 -0
- package/dist/core/agent-tool-selection.js.map +1 -0
- package/dist/core/entities/connection.d.ts +2 -0
- package/dist/core/entities/connection.d.ts.map +1 -0
- package/dist/core/entities/connection.js +3 -0
- package/dist/core/entities/connection.js.map +1 -0
- package/dist/core/entities/provider.d.ts +6 -0
- package/dist/core/entities/provider.d.ts.map +1 -0
- package/dist/core/entities/provider.js +3 -0
- package/dist/core/entities/provider.js.map +1 -0
- package/dist/core/errors.d.ts +29 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +40 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/providers/agent-tools.d.ts +2 -0
- package/dist/core/providers/agent-tools.d.ts.map +1 -0
- package/dist/core/providers/agent-tools.js +3 -0
- package/dist/core/providers/agent-tools.js.map +1 -0
- package/dist/core/providers/redact-checkout-spec.d.ts +9 -0
- package/dist/core/providers/redact-checkout-spec.d.ts.map +1 -0
- package/dist/core/providers/redact-checkout-spec.js +25 -0
- package/dist/core/providers/redact-checkout-spec.js.map +1 -0
- package/dist/core/providers/registry.d.ts +9 -0
- package/dist/core/providers/registry.d.ts.map +1 -0
- package/dist/core/providers/registry.js +52 -0
- package/dist/core/providers/registry.js.map +1 -0
- package/dist/core/providers/source-control.d.ts +2 -0
- package/dist/core/providers/source-control.d.ts.map +1 -0
- package/dist/core/providers/source-control.js +3 -0
- package/dist/core/providers/source-control.js.map +1 -0
- package/dist/core/source-control-service.d.ts +46 -0
- package/dist/core/source-control-service.d.ts.map +1 -0
- package/dist/core/source-control-service.js +84 -0
- package/dist/core/source-control-service.js.map +1 -0
- package/dist/db/connections.d.ts +67 -0
- package/dist/db/connections.d.ts.map +1 -0
- package/dist/db/connections.js +130 -0
- package/dist/db/connections.js.map +1 -0
- package/dist/db/db.d.ts +865 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +22 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/connections.d.ts +165 -0
- package/dist/db/schema/connections.d.ts.map +1 -0
- package/dist/db/schema/connections.js +37 -0
- package/dist/db/schema/connections.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/webhook-deliveries.d.ts +76 -0
- package/dist/db/schema/webhook-deliveries.d.ts.map +1 -0
- package/dist/db/schema/webhook-deliveries.js +22 -0
- package/dist/db/schema/webhook-deliveries.js.map +1 -0
- package/dist/db/webhook-deliveries.d.ts +55 -0
- package/dist/db/webhook-deliveries.d.ts.map +1 -0
- package/dist/db/webhook-deliveries.js +132 -0
- package/dist/db/webhook-deliveries.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +8 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +17 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +36 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +26 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.d.ts +24 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.js +52 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts +7 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js +108 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts +12 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js +46 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts +17 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.js +126 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +37 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +145 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +53 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +14 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +26 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-connections.d.ts +3 -0
- package/dist/presentation/routes/list-connections.d.ts.map +1 -0
- package/dist/presentation/routes/list-connections.js +58 -0
- package/dist/presentation/routes/list-connections.js.map +1 -0
- package/dist/presentation/routes/list-providers.d.ts +3 -0
- package/dist/presentation/routes/list-providers.d.ts.map +1 -0
- package/dist/presentation/routes/list-providers.js +27 -0
- package/dist/presentation/routes/list-providers.js.map +1 -0
- package/dist/presentation/routes/list-repositories.d.ts +3 -0
- package/dist/presentation/routes/list-repositories.d.ts.map +1 -0
- package/dist/presentation/routes/list-repositories.js +41 -0
- package/dist/presentation/routes/list-repositories.js.map +1 -0
- package/dist/presentation/routes/manage-connections.d.ts +4 -0
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -0
- package/dist/presentation/routes/manage-connections.js +81 -0
- package/dist/presentation/routes/manage-connections.js.map +1 -0
- package/dist/providers/connection-slug.d.ts +2 -0
- package/dist/providers/connection-slug.d.ts.map +1 -0
- package/dist/providers/connection-slug.js +19 -0
- package/dist/providers/connection-slug.js.map +1 -0
- package/dist/providers/cron.d.ts +3 -0
- package/dist/providers/cron.d.ts.map +1 -0
- package/dist/providers/cron.js +13 -0
- package/dist/providers/cron.js.map +1 -0
- package/dist/providers/gitea.d.ts +3 -0
- package/dist/providers/gitea.d.ts.map +1 -0
- package/dist/providers/gitea.js +75 -0
- package/dist/providers/gitea.js.map +1 -0
- package/dist/providers/github.d.ts +3 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/linear.d.ts +3 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +134 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/modules.d.ts +3 -0
- package/dist/providers/modules.d.ts.map +1 -0
- package/dist/providers/modules.js +26 -0
- package/dist/providers/modules.js.map +1 -0
- package/dist/providers/sentry.d.ts +3 -0
- package/dist/providers/sentry.d.ts.map +1 -0
- package/dist/providers/sentry.js +83 -0
- package/dist/providers/sentry.js.map +1 -0
- package/dist/providers/types.d.ts +58 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/webhook.d.ts +3 -0
- package/dist/providers/webhook.d.ts.map +1 -0
- package/dist/providers/webhook.js +24 -0
- package/dist/providers/webhook.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +5 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +8 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-webhook-deliveries.d.ts +4 -0
- package/dist/temporal/activities/prune-webhook-deliveries.d.ts.map +1 -0
- package/dist/temporal/activities/prune-webhook-deliveries.js +11 -0
- package/dist/temporal/activities/prune-webhook-deliveries.js.map +1 -0
- package/dist/temporal/constants.d.ts +3 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +4 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.js +14 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +41 -0
- package/drizzle/meta/0001_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +85 -0
- package/src/config.test.ts +32 -0
- package/src/config.ts +28 -0
- package/src/core/agent-tool-selection.test.ts +137 -0
- package/src/core/agent-tool-selection.ts +80 -0
- package/src/core/entities/connection.ts +4 -0
- package/src/core/entities/provider.ts +21 -0
- package/src/core/errors.ts +55 -0
- package/src/core/providers/agent-tools.ts +10 -0
- package/src/core/providers/redact-checkout-spec.test.ts +78 -0
- package/src/core/providers/redact-checkout-spec.ts +20 -0
- package/src/core/providers/registry.test.ts +138 -0
- package/src/core/providers/registry.ts +90 -0
- package/src/core/providers/source-control.ts +17 -0
- package/src/core/source-control-service.test.ts +230 -0
- package/src/core/source-control-service.ts +161 -0
- package/src/db/connections.test.ts +296 -0
- package/src/db/connections.ts +262 -0
- package/src/db/db.ts +22 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/connections.ts +49 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/webhook-deliveries.ts +19 -0
- package/src/db/webhook-deliveries.test.ts +267 -0
- package/src/db/webhook-deliveries.ts +207 -0
- package/src/index.ts +212 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +35 -0
- package/src/presentation/dto/integrations.ts +39 -0
- package/src/presentation/routes/agent-tools-gateway/agent-tools-gateway.route.test.ts +371 -0
- package/src/presentation/routes/agent-tools-gateway/audit.test.ts +86 -0
- package/src/presentation/routes/agent-tools-gateway/audit.ts +90 -0
- package/src/presentation/routes/agent-tools-gateway/dispatch.ts +139 -0
- package/src/presentation/routes/agent-tools-gateway/index.ts +62 -0
- package/src/presentation/routes/agent-tools-gateway/mcp-server.test.ts +308 -0
- package/src/presentation/routes/agent-tools-gateway/mcp-server.ts +155 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +181 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +245 -0
- package/src/presentation/routes/errors.ts +63 -0
- package/src/presentation/routes/index.ts +53 -0
- package/src/presentation/routes/list-connections.test.ts +227 -0
- package/src/presentation/routes/list-connections.ts +56 -0
- package/src/presentation/routes/list-providers.test.ts +113 -0
- package/src/presentation/routes/list-providers.ts +28 -0
- package/src/presentation/routes/list-repositories.test.ts +150 -0
- package/src/presentation/routes/list-repositories.ts +47 -0
- package/src/presentation/routes/manage-connections.test.ts +125 -0
- package/src/presentation/routes/manage-connections.ts +77 -0
- package/src/providers/connection-slug.ts +19 -0
- package/src/providers/cron.ts +13 -0
- package/src/providers/gitea.ts +103 -0
- package/src/providers/github.ts +150 -0
- package/src/providers/linear.ts +179 -0
- package/src/providers/modules.test.ts +40 -0
- package/src/providers/modules.ts +32 -0
- package/src/providers/sentry.ts +113 -0
- package/src/providers/types.ts +52 -0
- package/src/providers/webhook.ts +30 -0
- package/src/temporal/activities/index.ts +7 -0
- package/src/temporal/activities/prune-webhook-deliveries.test.ts +39 -0
- package/src/temporal/activities/prune-webhook-deliveries.ts +9 -0
- package/src/temporal/constants.ts +3 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/prune-webhook-deliveries-cron.ts +15 -0
- package/test/agent-tools-gateway-helpers.ts +210 -0
- package/test/env.ts +23 -0
- package/test/factories/connection.ts +21 -0
- package/test/globalSetup.ts +22 -0
- package/test/index.ts +1 -0
- package/test/route-utils.ts +104 -0
- package/test/setup.ts +19 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MaterializedAgentIntegrationConfigDto,
|
|
3
|
+
type MaterializedAgentIntegrationToolConfigDto,
|
|
4
|
+
materializedAgentStepConfigSchema,
|
|
5
|
+
} from '@shipfox/api-agent-dto';
|
|
6
|
+
import {requireLeasedJobContext} from '@shipfox/api-auth-context';
|
|
7
|
+
import {ClientError} from '@shipfox/node-fastify';
|
|
8
|
+
import type {IntegrationConnection} from '#core/entities/connection.js';
|
|
9
|
+
import type {IntegrationProviderKind} from '#core/entities/provider.js';
|
|
10
|
+
import type {AgentToolJsonSchema} from '#core/providers/agent-tools.js';
|
|
11
|
+
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
12
|
+
import type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
|
|
13
|
+
|
|
14
|
+
export type LeasedAgentStepLoader = (params: {
|
|
15
|
+
request: object;
|
|
16
|
+
stepId: string;
|
|
17
|
+
attempt: number;
|
|
18
|
+
}) => Promise<{
|
|
19
|
+
step: {type: string; config: Record<string, unknown>};
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export interface AuthorizedIntegrationTool {
|
|
24
|
+
mcpName: string;
|
|
25
|
+
integration: MaterializedAgentIntegrationConfigDto;
|
|
26
|
+
tool: MaterializedAgentIntegrationToolConfigDto;
|
|
27
|
+
connection: IntegrationConnection;
|
|
28
|
+
description: string;
|
|
29
|
+
inputSchema: AgentToolJsonSchema;
|
|
30
|
+
outputSchema?: AgentToolJsonSchema | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AuthorizedIntegrationToolMap = Map<string, AuthorizedIntegrationTool>;
|
|
34
|
+
|
|
35
|
+
export interface ResolveAuthorizedToolsParams {
|
|
36
|
+
request: object;
|
|
37
|
+
loadLeasedAgentStep: LeasedAgentStepLoader;
|
|
38
|
+
registry: IntegrationProviderRegistry;
|
|
39
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function resolveAuthorizedIntegrationTools(
|
|
43
|
+
params: ResolveAuthorizedToolsParams,
|
|
44
|
+
): Promise<AuthorizedIntegrationToolMap> {
|
|
45
|
+
const leasedJob = requireLeasedJobContext(params.request);
|
|
46
|
+
if (!leasedJob.currentStepId || leasedJob.currentStepAttempt === undefined) {
|
|
47
|
+
throw new ClientError('Lease does not identify a current step', 'lease-missing-step', {
|
|
48
|
+
status: 409,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const {step, workspaceId} = await params.loadLeasedAgentStep({
|
|
53
|
+
request: params.request,
|
|
54
|
+
stepId: leasedJob.currentStepId,
|
|
55
|
+
attempt: leasedJob.currentStepAttempt,
|
|
56
|
+
});
|
|
57
|
+
if (step.type !== 'agent') {
|
|
58
|
+
throw new ClientError('Current leased step is not an agent step', 'leased-step-not-agent', {
|
|
59
|
+
status: 409,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const integrations = parseAgentIntegrations(step.config);
|
|
64
|
+
const authorizedTools: AuthorizedIntegrationToolMap = new Map();
|
|
65
|
+
|
|
66
|
+
for (const integration of integrations) {
|
|
67
|
+
const connection = await loadAuthorizedConnection({
|
|
68
|
+
integration,
|
|
69
|
+
workspaceId,
|
|
70
|
+
registry: params.registry,
|
|
71
|
+
getIntegrationConnectionById: params.getIntegrationConnectionById,
|
|
72
|
+
});
|
|
73
|
+
const catalog = await params.registry.getAdapter(integration.provider, 'agent_tools').catalog();
|
|
74
|
+
const catalogByToolId = new Map(catalog.map((entry) => [entry.id, entry]));
|
|
75
|
+
|
|
76
|
+
for (const tool of integration.tools) {
|
|
77
|
+
const catalogTool = catalogByToolId.get(tool.id);
|
|
78
|
+
const mcpName = mcpToolName(integration.connectionSlug, tool.id);
|
|
79
|
+
if (authorizedTools.has(mcpName)) {
|
|
80
|
+
throw new ClientError(
|
|
81
|
+
'Integration tool names collide after MCP namespacing',
|
|
82
|
+
'integration-tool-name-collision',
|
|
83
|
+
{
|
|
84
|
+
status: 409,
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
authorizedTools.set(mcpName, {
|
|
90
|
+
mcpName,
|
|
91
|
+
integration,
|
|
92
|
+
tool,
|
|
93
|
+
connection,
|
|
94
|
+
// The live catalog enriches display metadata only. Frozen step config remains the allowlist.
|
|
95
|
+
description: catalogTool?.description ?? tool.id,
|
|
96
|
+
inputSchema: tool.methods ? toolInputSchema(tool) : tool.inputSchema,
|
|
97
|
+
outputSchema: tool.outputSchema ?? catalogTool?.outputSchema,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return authorizedTools;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function sanitizeSlug(slug: string): string {
|
|
106
|
+
return slug.replaceAll('-', '_');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function mcpToolName(connectionSlug: string, toolId: string): string {
|
|
110
|
+
return `${sanitizeSlug(connectionSlug)}__${toolId}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function narrowMethodEnum(
|
|
114
|
+
inputSchema: AgentToolJsonSchema,
|
|
115
|
+
authorizedMethods: readonly string[],
|
|
116
|
+
): AgentToolJsonSchema {
|
|
117
|
+
const schema = cloneSchema(inputSchema);
|
|
118
|
+
const methodSchema = getObjectProperty(schema, 'method');
|
|
119
|
+
if (methodSchema) {
|
|
120
|
+
narrowEnumOrConst(methodSchema, authorizedMethods);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Claude rejects MCP tools with a top-level oneOf. The narrowed method enum remains the
|
|
124
|
+
// authority boundary, while provider validation enforces method-specific argument shapes.
|
|
125
|
+
delete schema.oneOf;
|
|
126
|
+
|
|
127
|
+
return schema;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toolInputSchema(tool: MaterializedAgentIntegrationToolConfigDto): AgentToolJsonSchema {
|
|
131
|
+
const authorizedMethods = tool.methods?.map((method) => method.id) ?? [];
|
|
132
|
+
return narrowMethodEnum(tool.inputSchema, authorizedMethods);
|
|
133
|
+
}
|
|
134
|
+
|
|
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
|
+
async function loadAuthorizedConnection(params: {
|
|
149
|
+
integration: MaterializedAgentIntegrationConfigDto;
|
|
150
|
+
workspaceId: string;
|
|
151
|
+
registry: IntegrationProviderRegistry;
|
|
152
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
153
|
+
}): Promise<IntegrationConnection> {
|
|
154
|
+
const connection = await params.getIntegrationConnectionById(params.integration.connectionId);
|
|
155
|
+
if (!connection) {
|
|
156
|
+
throw new ClientError(
|
|
157
|
+
'Integration connection is no longer available',
|
|
158
|
+
'integration-tool-connection-unavailable',
|
|
159
|
+
{
|
|
160
|
+
status: 409,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (connection.workspaceId !== params.workspaceId) {
|
|
165
|
+
throw new ClientError(
|
|
166
|
+
'Integration connection does not belong to the leased workspace',
|
|
167
|
+
'integration-tool-connection-unavailable',
|
|
168
|
+
{
|
|
169
|
+
status: 409,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (connection.lifecycleStatus !== 'active') {
|
|
174
|
+
throw new ClientError(
|
|
175
|
+
'Integration connection is not active',
|
|
176
|
+
'integration-tool-connection-unavailable',
|
|
177
|
+
{
|
|
178
|
+
status: 409,
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (connection.provider !== params.integration.provider) {
|
|
183
|
+
throw new ClientError(
|
|
184
|
+
'Integration connection provider changed',
|
|
185
|
+
'integration-tool-connection-unavailable',
|
|
186
|
+
{
|
|
187
|
+
status: 409,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (!providerSupportsAgentTools(params.registry, params.integration.provider)) {
|
|
192
|
+
throw new ClientError(
|
|
193
|
+
'Integration provider no longer exposes agent tools',
|
|
194
|
+
'integration-tool-connection-unavailable',
|
|
195
|
+
{
|
|
196
|
+
status: 409,
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return connection;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function providerSupportsAgentTools(
|
|
205
|
+
registry: IntegrationProviderRegistry,
|
|
206
|
+
provider: IntegrationProviderKind,
|
|
207
|
+
): boolean {
|
|
208
|
+
try {
|
|
209
|
+
return registry.get(provider).capabilities.includes('agent_tools');
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cloneSchema(schema: AgentToolJsonSchema): AgentToolJsonSchema {
|
|
216
|
+
return structuredClone(schema);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getObjectProperty(
|
|
220
|
+
schema: AgentToolJsonSchema,
|
|
221
|
+
property: string,
|
|
222
|
+
): AgentToolJsonSchema | null {
|
|
223
|
+
const properties = schema.properties;
|
|
224
|
+
if (!isRecord(properties)) return null;
|
|
225
|
+
const value = properties[property];
|
|
226
|
+
return isRecord(value) ? value : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function narrowEnumOrConst(
|
|
230
|
+
schema: AgentToolJsonSchema,
|
|
231
|
+
authorizedMethods: readonly string[],
|
|
232
|
+
): void {
|
|
233
|
+
if (Array.isArray(schema.enum)) {
|
|
234
|
+
schema.enum = schema.enum.filter(
|
|
235
|
+
(value): value is string => typeof value === 'string' && authorizedMethods.includes(value),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (typeof schema.const === 'string' && !authorizedMethods.includes(schema.const)) {
|
|
239
|
+
delete schema.const;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isRecord(value: unknown): value is AgentToolJsonSchema {
|
|
244
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
245
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {ClientError} from '@shipfox/node-fastify';
|
|
2
|
+
import {
|
|
3
|
+
IntegrationCapabilityUnavailableError,
|
|
4
|
+
IntegrationCheckoutUnsupportedError,
|
|
5
|
+
IntegrationConnectionInactiveError,
|
|
6
|
+
IntegrationConnectionNotFoundError,
|
|
7
|
+
IntegrationConnectionWorkspaceMismatchError,
|
|
8
|
+
IntegrationProviderError,
|
|
9
|
+
type IntegrationProviderErrorReason,
|
|
10
|
+
IntegrationProviderUnavailableError,
|
|
11
|
+
} from '#core/errors.js';
|
|
12
|
+
|
|
13
|
+
function providerStatus(reason: IntegrationProviderErrorReason): number {
|
|
14
|
+
if (reason === 'rate-limited') return 429;
|
|
15
|
+
if (reason === 'timeout' || reason === 'provider-unavailable') return 503;
|
|
16
|
+
return 422;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isProviderError(error: unknown): error is IntegrationProviderError {
|
|
20
|
+
return (
|
|
21
|
+
error instanceof IntegrationProviderError ||
|
|
22
|
+
(error instanceof Error &&
|
|
23
|
+
'reason' in error &&
|
|
24
|
+
typeof error.reason === 'string' &&
|
|
25
|
+
(error.reason === 'repository-not-found' ||
|
|
26
|
+
error.reason === 'file-not-found' ||
|
|
27
|
+
error.reason === 'access-denied' ||
|
|
28
|
+
error.reason === 'rate-limited' ||
|
|
29
|
+
error.reason === 'timeout' ||
|
|
30
|
+
error.reason === 'provider-unavailable' ||
|
|
31
|
+
error.reason === 'malformed-provider-response' ||
|
|
32
|
+
error.reason === 'content-too-large' ||
|
|
33
|
+
error.reason === 'too-many-files'))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function integrationRouteErrorHandler(error: unknown): never {
|
|
38
|
+
if (error instanceof IntegrationConnectionNotFoundError) {
|
|
39
|
+
throw new ClientError(error.message, 'integration-connection-not-found', {status: 404});
|
|
40
|
+
}
|
|
41
|
+
if (error instanceof IntegrationConnectionInactiveError) {
|
|
42
|
+
throw new ClientError(error.message, 'integration-connection-inactive', {status: 422});
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof IntegrationConnectionWorkspaceMismatchError) {
|
|
45
|
+
throw new ClientError(error.message, 'forbidden', {status: 403});
|
|
46
|
+
}
|
|
47
|
+
if (error instanceof IntegrationProviderUnavailableError) {
|
|
48
|
+
throw new ClientError(error.message, 'integration-provider-unavailable', {status: 422});
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof IntegrationCapabilityUnavailableError) {
|
|
51
|
+
throw new ClientError(error.message, 'integration-capability-unavailable', {status: 422});
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof IntegrationCheckoutUnsupportedError) {
|
|
54
|
+
throw new ClientError(error.message, 'integration-checkout-unsupported', {status: 422});
|
|
55
|
+
}
|
|
56
|
+
if (isProviderError(error)) {
|
|
57
|
+
throw new ClientError(error.message, error.reason, {
|
|
58
|
+
details: {retry_after_seconds: error.retryAfterSeconds},
|
|
59
|
+
status: providerStatus(error.reason),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {RouteExport} from '@shipfox/node-fastify';
|
|
2
|
+
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
3
|
+
import type {IntegrationSourceControlService} from '#core/source-control-service.js';
|
|
4
|
+
import type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
|
|
5
|
+
import {
|
|
6
|
+
createAgentToolsGatewayRoutes,
|
|
7
|
+
type LeasedAgentStepLoader,
|
|
8
|
+
} from './agent-tools-gateway/index.js';
|
|
9
|
+
import {createListIntegrationConnectionsRoute} from './list-connections.js';
|
|
10
|
+
import {createListIntegrationProvidersRoute} from './list-providers.js';
|
|
11
|
+
import {createListRepositoriesRoute} from './list-repositories.js';
|
|
12
|
+
import {
|
|
13
|
+
createDeleteIntegrationConnectionRoute,
|
|
14
|
+
createUpdateIntegrationConnectionRoute,
|
|
15
|
+
} from './manage-connections.js';
|
|
16
|
+
|
|
17
|
+
export type {LeasedAgentStepLoader} from './agent-tools-gateway/index.js';
|
|
18
|
+
|
|
19
|
+
export interface CreateIntegrationRoutesOptions {
|
|
20
|
+
agentTools?:
|
|
21
|
+
| {
|
|
22
|
+
loadLeasedAgentStep: LeasedAgentStepLoader;
|
|
23
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
24
|
+
}
|
|
25
|
+
| undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createIntegrationRoutes(
|
|
29
|
+
registry: IntegrationProviderRegistry,
|
|
30
|
+
sourceControl: IntegrationSourceControlService,
|
|
31
|
+
options: CreateIntegrationRoutesOptions = {},
|
|
32
|
+
): RouteExport[] {
|
|
33
|
+
const providerRoutes = registry.list().flatMap((provider) => provider.routes ?? []);
|
|
34
|
+
const agentToolsRoutes = options.agentTools
|
|
35
|
+
? [
|
|
36
|
+
createAgentToolsGatewayRoutes({
|
|
37
|
+
registry,
|
|
38
|
+
loadLeasedAgentStep: options.agentTools.loadLeasedAgentStep,
|
|
39
|
+
getIntegrationConnectionById: options.agentTools.getIntegrationConnectionById,
|
|
40
|
+
}),
|
|
41
|
+
]
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
createListIntegrationProvidersRoute(registry),
|
|
46
|
+
createListIntegrationConnectionsRoute(registry),
|
|
47
|
+
createUpdateIntegrationConnectionRoute(registry),
|
|
48
|
+
createDeleteIntegrationConnectionRoute(),
|
|
49
|
+
createListRepositoriesRoute(sourceControl),
|
|
50
|
+
...agentToolsRoutes,
|
|
51
|
+
...providerRoutes,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import {upsertIntegrationConnection} from '#db/connections.js';
|
|
2
|
+
import {linearProviderModule} from '#providers/linear.js';
|
|
3
|
+
import {createTestApp, sourceProvider, useIntegrationRouteTest} from '#test/route-utils.js';
|
|
4
|
+
|
|
5
|
+
describe('GET /integration-connections', () => {
|
|
6
|
+
const context = useIntegrationRouteTest();
|
|
7
|
+
|
|
8
|
+
it('lists workspace connections across all lifecycle statuses', async () => {
|
|
9
|
+
const app = await createTestApp([sourceProvider()]);
|
|
10
|
+
await upsertIntegrationConnection({
|
|
11
|
+
workspaceId: context.workspaceId,
|
|
12
|
+
provider: 'gitea',
|
|
13
|
+
externalAccountId: 'debug-active',
|
|
14
|
+
slug: 'debug_active',
|
|
15
|
+
displayName: 'Gitea',
|
|
16
|
+
});
|
|
17
|
+
await upsertIntegrationConnection({
|
|
18
|
+
workspaceId: context.workspaceId,
|
|
19
|
+
provider: 'gitea',
|
|
20
|
+
externalAccountId: 'debug-error',
|
|
21
|
+
slug: 'debug_error',
|
|
22
|
+
displayName: 'Gitea',
|
|
23
|
+
lifecycleStatus: 'error',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const res = await app.inject({
|
|
27
|
+
method: 'GET',
|
|
28
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}`,
|
|
29
|
+
headers: {authorization: 'Bearer user'},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(res.statusCode).toBe(200);
|
|
33
|
+
expect(
|
|
34
|
+
res
|
|
35
|
+
.json()
|
|
36
|
+
.connections.map((connection: {lifecycle_status: string}) => connection.lifecycle_status),
|
|
37
|
+
).toEqual(['active', 'error']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('drops connections whose provider misses the capability filter', async () => {
|
|
41
|
+
const app = await createTestApp([sourceProvider()]);
|
|
42
|
+
await upsertIntegrationConnection({
|
|
43
|
+
workspaceId: context.workspaceId,
|
|
44
|
+
provider: 'gitea',
|
|
45
|
+
externalAccountId: 'gitea-owner',
|
|
46
|
+
slug: 'gitea_owner',
|
|
47
|
+
displayName: 'Gitea',
|
|
48
|
+
});
|
|
49
|
+
await upsertIntegrationConnection({
|
|
50
|
+
workspaceId: context.workspaceId,
|
|
51
|
+
provider: 'github',
|
|
52
|
+
externalAccountId: 'team-1',
|
|
53
|
+
slug: 'github_team_1',
|
|
54
|
+
displayName: 'GitHub',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const res = await app.inject({
|
|
58
|
+
method: 'GET',
|
|
59
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}&capability=source_control`,
|
|
60
|
+
headers: {authorization: 'Bearer user'},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(res.statusCode).toBe(200);
|
|
64
|
+
expect(
|
|
65
|
+
res.json().connections.map((connection: {provider: string}) => connection.provider),
|
|
66
|
+
).toEqual(['gitea']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('advertises agent tools capability on matching connections', async () => {
|
|
70
|
+
const app = await createTestApp([
|
|
71
|
+
{
|
|
72
|
+
provider: 'github',
|
|
73
|
+
displayName: 'GitHub',
|
|
74
|
+
adapters: {
|
|
75
|
+
agent_tools: {
|
|
76
|
+
catalog: () => [],
|
|
77
|
+
selectionCatalog: () => ({selectors: []}),
|
|
78
|
+
openSession: async () => {
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
return {
|
|
81
|
+
call: async () => {
|
|
82
|
+
await Promise.resolve();
|
|
83
|
+
return {};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
await upsertIntegrationConnection({
|
|
92
|
+
workspaceId: context.workspaceId,
|
|
93
|
+
provider: 'github',
|
|
94
|
+
externalAccountId: 'team-1',
|
|
95
|
+
slug: 'github_team_1',
|
|
96
|
+
displayName: 'GitHub',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const res = await app.inject({
|
|
100
|
+
method: 'GET',
|
|
101
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}&capability=agent_tools`,
|
|
102
|
+
headers: {authorization: 'Bearer user'},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(res.statusCode).toBe(200);
|
|
106
|
+
expect(res.json().connections).toMatchObject([
|
|
107
|
+
{
|
|
108
|
+
provider: 'github',
|
|
109
|
+
capabilities: ['agent_tools'],
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('advertises no capabilities on Linear connections before adapters exist', async () => {
|
|
115
|
+
const app = await createTestApp([{provider: 'linear', displayName: 'Linear', adapters: {}}]);
|
|
116
|
+
await upsertIntegrationConnection({
|
|
117
|
+
workspaceId: context.workspaceId,
|
|
118
|
+
provider: 'linear',
|
|
119
|
+
externalAccountId: 'org-1',
|
|
120
|
+
slug: 'linear_org_1',
|
|
121
|
+
displayName: 'Linear Org',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const res = await app.inject({
|
|
125
|
+
method: 'GET',
|
|
126
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}`,
|
|
127
|
+
headers: {authorization: 'Bearer user'},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(res.statusCode).toBe(200);
|
|
131
|
+
expect(res.json().connections).toMatchObject([
|
|
132
|
+
{
|
|
133
|
+
provider: 'linear',
|
|
134
|
+
capabilities: [],
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('advertises agent tools on Linear module connections', async () => {
|
|
140
|
+
const {provider} = await linearProviderModule.load();
|
|
141
|
+
const app = await createTestApp([provider]);
|
|
142
|
+
await upsertIntegrationConnection({
|
|
143
|
+
workspaceId: context.workspaceId,
|
|
144
|
+
provider: 'linear',
|
|
145
|
+
externalAccountId: 'org-1',
|
|
146
|
+
slug: 'linear_org_1',
|
|
147
|
+
displayName: 'Linear Org',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const res = await app.inject({
|
|
151
|
+
method: 'GET',
|
|
152
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}&capability=agent_tools`,
|
|
153
|
+
headers: {authorization: 'Bearer user'},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(res.statusCode).toBe(200);
|
|
157
|
+
expect(res.json().connections).toMatchObject([
|
|
158
|
+
{
|
|
159
|
+
provider: 'linear',
|
|
160
|
+
capabilities: ['agent_tools'],
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('includes external_url when the provider resolves one', async () => {
|
|
166
|
+
const app = await createTestApp([
|
|
167
|
+
sourceProvider({
|
|
168
|
+
connectionExternalUrl: () => Promise.resolve('https://gitea.local/team'),
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
await upsertIntegrationConnection({
|
|
172
|
+
workspaceId: context.workspaceId,
|
|
173
|
+
provider: 'gitea',
|
|
174
|
+
externalAccountId: 'gitea-owner',
|
|
175
|
+
slug: 'gitea_owner',
|
|
176
|
+
displayName: 'Gitea',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const res = await app.inject({
|
|
180
|
+
method: 'GET',
|
|
181
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}`,
|
|
182
|
+
headers: {authorization: 'Bearer user'},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(res.statusCode).toBe(200);
|
|
186
|
+
expect(res.json().connections[0].external_url).toBe('https://gitea.local/team');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('omits external_url and keeps the list alive when the provider lookup throws', async () => {
|
|
190
|
+
const app = await createTestApp([
|
|
191
|
+
sourceProvider({
|
|
192
|
+
connectionExternalUrl: () => Promise.reject(new Error('installation row missing')),
|
|
193
|
+
}),
|
|
194
|
+
]);
|
|
195
|
+
await upsertIntegrationConnection({
|
|
196
|
+
workspaceId: context.workspaceId,
|
|
197
|
+
provider: 'gitea',
|
|
198
|
+
externalAccountId: 'gitea-owner',
|
|
199
|
+
slug: 'gitea_owner',
|
|
200
|
+
displayName: 'Gitea',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const res = await app.inject({
|
|
204
|
+
method: 'GET',
|
|
205
|
+
url: `/integration-connections?workspace_id=${context.workspaceId}`,
|
|
206
|
+
headers: {authorization: 'Bearer user'},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(res.statusCode).toBe(200);
|
|
210
|
+
expect(res.json().connections).toHaveLength(1);
|
|
211
|
+
expect(res.json().connections[0].external_url).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns membership errors', async () => {
|
|
215
|
+
const app = await createTestApp([sourceProvider()]);
|
|
216
|
+
const inaccessibleWorkspaceId = crypto.randomUUID();
|
|
217
|
+
|
|
218
|
+
const res = await app.inject({
|
|
219
|
+
method: 'GET',
|
|
220
|
+
url: `/integration-connections?workspace_id=${inaccessibleWorkspaceId}`,
|
|
221
|
+
headers: {authorization: 'Bearer user'},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(res.statusCode).toBe(403);
|
|
225
|
+
expect(res.json().code).toBe('forbidden');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {AUTH_USER, requireWorkspaceAccess} from '@shipfox/api-auth-context';
|
|
2
|
+
import {
|
|
3
|
+
listIntegrationConnectionsQuerySchema,
|
|
4
|
+
listIntegrationConnectionsResponseSchema,
|
|
5
|
+
} from '@shipfox/api-integration-core-dto';
|
|
6
|
+
import {defineRoute} from '@shipfox/node-fastify';
|
|
7
|
+
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
8
|
+
import {listIntegrationConnections} from '#db/connections.js';
|
|
9
|
+
import {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';
|
|
10
|
+
|
|
11
|
+
export function createListIntegrationConnectionsRoute(registry: IntegrationProviderRegistry) {
|
|
12
|
+
return defineRoute({
|
|
13
|
+
method: 'GET',
|
|
14
|
+
path: '/integration-connections',
|
|
15
|
+
auth: AUTH_USER,
|
|
16
|
+
description: 'List workspace integration connections across all lifecycle statuses.',
|
|
17
|
+
schema: {
|
|
18
|
+
querystring: listIntegrationConnectionsQuerySchema,
|
|
19
|
+
response: {
|
|
20
|
+
200: listIntegrationConnectionsResponseSchema,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
handler: async (request) => {
|
|
24
|
+
const {workspace_id: workspaceId, capability} = request.query;
|
|
25
|
+
|
|
26
|
+
requireWorkspaceAccess({request, workspaceId});
|
|
27
|
+
const connections = await listIntegrationConnections({workspaceId});
|
|
28
|
+
const providers = new Map(
|
|
29
|
+
registry.list(capability).map((provider) => [provider.provider, provider]),
|
|
30
|
+
);
|
|
31
|
+
const connectionDtos = await Promise.all(
|
|
32
|
+
connections.map(async (connection) => {
|
|
33
|
+
const provider = providers.get(connection.provider);
|
|
34
|
+
if (!provider) return undefined;
|
|
35
|
+
// Best-effort: the external link is cosmetic, so a failing or missing
|
|
36
|
+
// provider-side lookup must never fail the whole connections list.
|
|
37
|
+
let externalUrl: string | undefined;
|
|
38
|
+
try {
|
|
39
|
+
externalUrl = await provider.connectionExternalUrl?.(connection);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
request.log.warn(
|
|
42
|
+
{connectionId: connection.id, provider: connection.provider, err: error},
|
|
43
|
+
'Could not resolve integration connection external URL',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return toIntegrationConnectionDto(connection, {
|
|
47
|
+
capabilities: provider.capabilities,
|
|
48
|
+
externalUrl,
|
|
49
|
+
});
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {connections: connectionDtos.filter((connection) => connection !== undefined)};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|