@shipfox/api-integration-core 13.0.0 → 14.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 +44 -0
- package/dist/core/event-catalogs.d.ts +18 -0
- package/dist/core/event-catalogs.d.ts.map +1 -0
- package/dist/core/event-catalogs.js +27 -0
- package/dist/core/event-catalogs.js.map +1 -0
- package/dist/core/providers/registry.d.ts +1 -0
- package/dist/core/providers/registry.d.ts.map +1 -1
- package/dist/core/providers/registry.js +4 -1
- package/dist/core/providers/registry.js.map +1 -1
- package/dist/core/providers/source-control.d.ts +1 -1
- package/dist/core/providers/source-control.d.ts.map +1 -1
- package/dist/core/providers/source-control.js.map +1 -1
- package/dist/core/source-control-service.d.ts +5 -1
- package/dist/core/source-control-service.d.ts.map +1 -1
- package/dist/core/source-control-service.js +12 -0
- package/dist/core/source-control-service.js.map +1 -1
- package/dist/db/connections.d.ts +4 -1
- package/dist/db/connections.d.ts.map +1 -1
- package/dist/db/connections.js +15 -6
- package/dist/db/connections.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +2 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +27 -5
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/errors.d.ts.map +1 -1
- package/dist/presentation/routes/errors.js +1 -1
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
- package/dist/presentation/routes/manage-connections.js +7 -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 +5 -1
- package/dist/providers/gitea.js.map +1 -1
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +6 -5
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +7 -2
- package/dist/providers/jira.js.map +1 -1
- package/dist/providers/linear.d.ts.map +1 -1
- package/dist/providers/linear.js +6 -4
- package/dist/providers/linear.js.map +1 -1
- package/dist/providers/sentry.d.ts.map +1 -1
- package/dist/providers/sentry.js +3 -1
- package/dist/providers/sentry.js.map +1 -1
- package/dist/providers/slack.d.ts.map +1 -1
- package/dist/providers/slack.js +6 -4
- package/dist/providers/slack.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/core/agent-tool-selection.test.ts +4 -0
- package/src/core/event-catalogs.test.ts +16 -0
- package/src/core/event-catalogs.ts +34 -0
- package/src/core/providers/registry.test.ts +4 -0
- package/src/core/providers/registry.ts +9 -3
- package/src/core/providers/source-control.ts +2 -0
- package/src/core/secret-cleanup.test.ts +1 -0
- package/src/core/source-control-service.test.ts +48 -0
- package/src/core/source-control-service.ts +16 -0
- package/src/db/connections.test.ts +167 -0
- package/src/db/connections.ts +21 -4
- package/src/db/secret-cleanups.test.ts +1 -0
- package/src/index.ts +2 -0
- package/src/metrics/instance.ts +2 -0
- package/src/presentation/inter-module.test.ts +219 -0
- package/src/presentation/inter-module.ts +28 -3
- package/src/presentation/routes/errors.ts +3 -0
- package/src/presentation/routes/list-connections.test.ts +13 -1
- package/src/presentation/routes/list-repositories.test.ts +9 -0
- package/src/presentation/routes/manage-connections.test.ts +52 -0
- package/src/presentation/routes/manage-connections.ts +6 -2
- package/src/providers/gitea.ts +5 -0
- package/src/providers/github.ts +6 -1
- package/src/providers/jira.test.ts +3 -0
- package/src/providers/jira.ts +6 -0
- package/src/providers/linear.test.ts +3 -0
- package/src/providers/linear.ts +6 -1
- package/src/providers/modules.test.ts +64 -1
- package/src/providers/sentry.ts +3 -0
- package/src/providers/slack.test.ts +4 -0
- package/src/providers/slack.ts +6 -1
- package/test/route-utils.ts +4 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/db/connections.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
CONNECTION_SLUG_MAX_LENGTH,
|
|
3
3
|
INTEGRATION_CONNECTION_AVAILABLE,
|
|
4
4
|
type IntegrationsEventMap,
|
|
5
|
+
RESERVED_CONNECTION_SLUGS,
|
|
5
6
|
} from '@shipfox/api-integration-core-dto';
|
|
6
7
|
import {ConnectionSlugConflictError} from '@shipfox/api-integration-spi';
|
|
7
8
|
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
@@ -10,7 +11,7 @@ import type {
|
|
|
10
11
|
IntegrationConnection,
|
|
11
12
|
IntegrationConnectionLifecycleStatus,
|
|
12
13
|
} from '#core/entities/connection.js';
|
|
13
|
-
import type {IntegrationProviderKind} from '#core/entities/provider.js';
|
|
14
|
+
import type {IntegrationCapability, IntegrationProviderKind} from '#core/entities/provider.js';
|
|
14
15
|
import {IntegrationConnectionAlreadyExistsError} from '#core/errors.js';
|
|
15
16
|
import {db} from './db.js';
|
|
16
17
|
import {integrationConnections, toIntegrationConnection} from './schema/connections.js';
|
|
@@ -26,12 +27,14 @@ export interface UpsertIntegrationConnectionParams {
|
|
|
26
27
|
slug: string;
|
|
27
28
|
displayName: string;
|
|
28
29
|
lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;
|
|
30
|
+
capabilities?: IntegrationCapability[] | undefined;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function upsertIntegrationConnection(
|
|
32
34
|
params: UpsertIntegrationConnectionParams,
|
|
33
35
|
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
34
36
|
): Promise<IntegrationConnection> {
|
|
37
|
+
assertConnectionSlugIsNotReserved(params.slug);
|
|
35
38
|
if (options.tx === undefined) {
|
|
36
39
|
return await db().transaction((tx) => upsertIntegrationConnection(params, {tx}));
|
|
37
40
|
}
|
|
@@ -91,7 +94,7 @@ export async function upsertIntegrationConnection(
|
|
|
91
94
|
if (!row) throw new Error('Integration connection upsert returned no rows');
|
|
92
95
|
const connection = toIntegrationConnection(row);
|
|
93
96
|
if (becameAvailable) {
|
|
94
|
-
await writeConnectionAvailableEvent(executor, connection);
|
|
97
|
+
await writeConnectionAvailableEvent(executor, connection, params.capabilities);
|
|
95
98
|
}
|
|
96
99
|
return connection;
|
|
97
100
|
}
|
|
@@ -103,6 +106,7 @@ export interface CreateIntegrationConnectionParams {
|
|
|
103
106
|
slug: string;
|
|
104
107
|
displayName: string;
|
|
105
108
|
lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;
|
|
109
|
+
capabilities?: IntegrationCapability[] | undefined;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
const INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT =
|
|
@@ -140,6 +144,7 @@ export async function createIntegrationConnection(
|
|
|
140
144
|
params: CreateIntegrationConnectionParams,
|
|
141
145
|
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
142
146
|
): Promise<IntegrationConnection> {
|
|
147
|
+
assertConnectionSlugIsNotReserved(params.slug);
|
|
143
148
|
if (options.tx === undefined) {
|
|
144
149
|
return await db().transaction((tx) => createIntegrationConnection(params, {tx}));
|
|
145
150
|
}
|
|
@@ -176,7 +181,7 @@ export async function createIntegrationConnection(
|
|
|
176
181
|
if (!row) throw new Error('Integration connection insert returned no rows');
|
|
177
182
|
const connection = toIntegrationConnection(row);
|
|
178
183
|
if (connection.lifecycleStatus === 'active') {
|
|
179
|
-
await writeConnectionAvailableEvent(executor, connection);
|
|
184
|
+
await writeConnectionAvailableEvent(executor, connection, params.capabilities);
|
|
180
185
|
}
|
|
181
186
|
return connection;
|
|
182
187
|
}
|
|
@@ -192,6 +197,7 @@ export async function resolveUniqueConnectionSlug(
|
|
|
192
197
|
params: ResolveUniqueConnectionSlugParams,
|
|
193
198
|
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
194
199
|
): Promise<string> {
|
|
200
|
+
assertConnectionSlugIsNotReserved(params.baseSlug);
|
|
195
201
|
const executor = options.tx ?? db();
|
|
196
202
|
const [existing] = await executor
|
|
197
203
|
.select({slug: integrationConnections.slug})
|
|
@@ -220,6 +226,14 @@ export async function resolveUniqueConnectionSlug(
|
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
function assertConnectionSlugIsNotReserved(slug: string): void {
|
|
230
|
+
const normalizedSlug = slug.toLowerCase().replaceAll(/[_-]+$/g, '');
|
|
231
|
+
if (!(RESERVED_CONNECTION_SLUGS as readonly string[]).includes(normalizedSlug)) return;
|
|
232
|
+
throw new ConnectionSlugConflictError(
|
|
233
|
+
new Error(`Slug "${slug}" is reserved for a built-in trigger source`),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
223
237
|
export type CreateIntegrationConnectionFn = typeof createIntegrationConnection;
|
|
224
238
|
|
|
225
239
|
export async function getIntegrationConnectionById(
|
|
@@ -267,6 +281,7 @@ export type GetIntegrationConnectionBySlugFn = typeof getIntegrationConnectionBy
|
|
|
267
281
|
export interface UpdateIntegrationConnectionLifecycleStatusParams {
|
|
268
282
|
id: string;
|
|
269
283
|
lifecycleStatus: IntegrationConnectionLifecycleStatus;
|
|
284
|
+
capabilities?: IntegrationCapability[] | undefined;
|
|
270
285
|
}
|
|
271
286
|
|
|
272
287
|
export async function updateIntegrationConnectionLifecycleStatus(
|
|
@@ -294,7 +309,7 @@ export async function updateIntegrationConnectionLifecycleStatus(
|
|
|
294
309
|
if (!row) return undefined;
|
|
295
310
|
const connection = toIntegrationConnection(row);
|
|
296
311
|
if (existing.lifecycleStatus !== 'active' && connection.lifecycleStatus === 'active') {
|
|
297
|
-
await writeConnectionAvailableEvent(executor, connection);
|
|
312
|
+
await writeConnectionAvailableEvent(executor, connection, params.capabilities);
|
|
298
313
|
}
|
|
299
314
|
return connection;
|
|
300
315
|
}
|
|
@@ -305,6 +320,7 @@ export type UpdateIntegrationConnectionLifecycleStatusFn =
|
|
|
305
320
|
async function writeConnectionAvailableEvent(
|
|
306
321
|
executor: IntegrationDb | IntegrationTx,
|
|
307
322
|
connection: IntegrationConnection,
|
|
323
|
+
capabilities: IntegrationCapability[] | undefined,
|
|
308
324
|
): Promise<void> {
|
|
309
325
|
await writeOutboxEvent<IntegrationsEventMap>(executor, integrationsOutbox, {
|
|
310
326
|
type: INTEGRATION_CONNECTION_AVAILABLE,
|
|
@@ -313,6 +329,7 @@ async function writeConnectionAvailableEvent(
|
|
|
313
329
|
workspaceId: connection.workspaceId,
|
|
314
330
|
connectionId: connection.id,
|
|
315
331
|
slug: connection.slug,
|
|
332
|
+
capabilities: capabilities ?? [],
|
|
316
333
|
},
|
|
317
334
|
});
|
|
318
335
|
}
|
|
@@ -107,6 +107,7 @@ async function createCleanupConnection(overrides: {externalAccountId?: string} =
|
|
|
107
107
|
externalAccountId: overrides.externalAccountId ?? crypto.randomUUID(),
|
|
108
108
|
slug: `slack_${crypto.randomUUID()}`,
|
|
109
109
|
displayName: 'Slack',
|
|
110
|
+
capabilities: ['agent_tools'],
|
|
110
111
|
});
|
|
111
112
|
await enqueueIntegrationSecretCleanup({connection});
|
|
112
113
|
return connection;
|
package/src/index.ts
CHANGED
package/src/metrics/instance.ts
CHANGED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto/inter-module';
|
|
2
|
+
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
3
|
+
import {createInMemoryInterModuleTransport} from '@shipfox/node-module/inter-module';
|
|
4
|
+
import {IntegrationProviderError} from '#core/errors.js';
|
|
5
|
+
import {createIntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
6
|
+
import type {SourceControlProvider} from '#core/providers/source-control.js';
|
|
7
|
+
import {createSourceControlIntegrationService} from '#core/source-control-service.js';
|
|
8
|
+
import {createIntegrationsInterModulePresentation} from './inter-module.js';
|
|
9
|
+
|
|
10
|
+
const workspaceId = crypto.randomUUID();
|
|
11
|
+
const connectionId = crypto.randomUUID();
|
|
12
|
+
|
|
13
|
+
function createClient(
|
|
14
|
+
resolveRef: (input: {ref: string}) => Promise<{ref: string; commit: string}>,
|
|
15
|
+
resolveRepository: SourceControlProvider['resolveRepository'] = () => {
|
|
16
|
+
throw new Error('not used');
|
|
17
|
+
},
|
|
18
|
+
) {
|
|
19
|
+
const transport = createInMemoryInterModuleTransport();
|
|
20
|
+
const client = transport.createClient(integrationsInterModuleContract);
|
|
21
|
+
|
|
22
|
+
const sourceControl = createSourceControlIntegrationService({
|
|
23
|
+
registry: createIntegrationProviderRegistry([
|
|
24
|
+
{
|
|
25
|
+
provider: 'gitea',
|
|
26
|
+
displayName: 'Gitea',
|
|
27
|
+
adapters: {
|
|
28
|
+
source_control: {
|
|
29
|
+
listRepositories: vi.fn(),
|
|
30
|
+
resolveRepository: vi.fn(resolveRepository),
|
|
31
|
+
listFiles: vi.fn(),
|
|
32
|
+
fetchFile: vi.fn(),
|
|
33
|
+
resolveTriggerReference: () => null,
|
|
34
|
+
resolveRef: async (input) => await resolveRef(input),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
]),
|
|
39
|
+
getIntegrationConnectionById: async (id) => {
|
|
40
|
+
await Promise.resolve();
|
|
41
|
+
return id === connectionId
|
|
42
|
+
? {
|
|
43
|
+
id: connectionId,
|
|
44
|
+
workspaceId,
|
|
45
|
+
provider: 'gitea',
|
|
46
|
+
externalAccountId: 'gitea-owner',
|
|
47
|
+
slug: 'gitea_owner',
|
|
48
|
+
displayName: 'Gitea',
|
|
49
|
+
lifecycleStatus: 'active' as const,
|
|
50
|
+
createdAt: new Date(),
|
|
51
|
+
updatedAt: new Date(),
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
transport.register(
|
|
58
|
+
createIntegrationsInterModulePresentation({
|
|
59
|
+
registry: createIntegrationProviderRegistry([]),
|
|
60
|
+
sourceControl,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
transport.seal();
|
|
64
|
+
return client;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('integrations inter-module presentation', () => {
|
|
68
|
+
const input = {
|
|
69
|
+
workspaceId,
|
|
70
|
+
connectionId,
|
|
71
|
+
externalRepositoryId: 'gitea:gitea-owner/platform',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
it('resolves a source ref through the transport', async () => {
|
|
75
|
+
const client = createClient(async (input) => ({
|
|
76
|
+
ref: input.ref,
|
|
77
|
+
commit: 'a'.repeat(40),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const result = await client.resolveSourceRef({...input, ref: 'refs/heads/main'});
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({ref: 'refs/heads/main', commit: 'a'.repeat(40)});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('maps a missing ref to the ref-not-found known error', async () => {
|
|
86
|
+
const client = createClient(() => {
|
|
87
|
+
throw new IntegrationProviderError('ref-not-found', 'Ref not found');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const error = await client
|
|
91
|
+
.resolveSourceRef({...input, ref: 'refs/heads/missing'})
|
|
92
|
+
.catch((caught: unknown) => caught);
|
|
93
|
+
|
|
94
|
+
expect(
|
|
95
|
+
isInterModuleKnownError(integrationsInterModuleContract.methods.resolveSourceRef, error),
|
|
96
|
+
).toBe(true);
|
|
97
|
+
if (isInterModuleKnownError(integrationsInterModuleContract.methods.resolveSourceRef, error)) {
|
|
98
|
+
expect(error.code).toBe('ref-not-found');
|
|
99
|
+
expect(error.details).toEqual({ref: 'refs/heads/missing'});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('maps an invalid ref to the ref-invalid known error', async () => {
|
|
104
|
+
const client = createClient(() => {
|
|
105
|
+
throw new IntegrationProviderError('ref-invalid', 'Ref is not a branch or tag name');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const error = await client
|
|
109
|
+
.resolveSourceRef({...input, ref: 'a'.repeat(40)})
|
|
110
|
+
.catch((caught: unknown) => caught);
|
|
111
|
+
|
|
112
|
+
if (isInterModuleKnownError(integrationsInterModuleContract.methods.resolveSourceRef, error)) {
|
|
113
|
+
expect(error.code).toBe('ref-invalid');
|
|
114
|
+
expect(error.details).toEqual({ref: 'a'.repeat(40)});
|
|
115
|
+
} else {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('keeps other provider failures as provider-failure known errors', async () => {
|
|
121
|
+
const client = createClient(() => {
|
|
122
|
+
throw new IntegrationProviderError('rate-limited', 'Rate limited', 60);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const error = await client
|
|
126
|
+
.resolveSourceRef({...input, ref: 'refs/heads/main'})
|
|
127
|
+
.catch((caught: unknown) => caught);
|
|
128
|
+
|
|
129
|
+
if (isInterModuleKnownError(integrationsInterModuleContract.methods.resolveSourceRef, error)) {
|
|
130
|
+
expect(error.code).toBe('provider-failure');
|
|
131
|
+
expect(error.details).toEqual({reason: 'rate-limited', retryAfterSeconds: 60});
|
|
132
|
+
} else {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('keeps ref reasons generic for methods that do not declare ref errors', async () => {
|
|
138
|
+
const client = createClient(
|
|
139
|
+
async (input) => ({ref: input.ref, commit: 'a'.repeat(40)}),
|
|
140
|
+
() => {
|
|
141
|
+
throw new IntegrationProviderError('ref-not-found', 'Ref not found');
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const error = await client.resolveSourceRepository(input).catch((caught: unknown) => caught);
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
isInterModuleKnownError(
|
|
149
|
+
integrationsInterModuleContract.methods.resolveSourceRepository,
|
|
150
|
+
error,
|
|
151
|
+
)
|
|
152
|
+
) {
|
|
153
|
+
expect(error.code).toBe('provider-failure');
|
|
154
|
+
expect(error.details).toEqual({reason: 'ref-not-found'});
|
|
155
|
+
} else {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('serves provider event catalogs and fixed event providers on the validation context', async () => {
|
|
161
|
+
const transport = createInMemoryInterModuleTransport();
|
|
162
|
+
const client = transport.createClient(integrationsInterModuleContract);
|
|
163
|
+
|
|
164
|
+
const sourceControl = createSourceControlIntegrationService({
|
|
165
|
+
registry: createIntegrationProviderRegistry([]),
|
|
166
|
+
getIntegrationConnectionById: async () => undefined,
|
|
167
|
+
});
|
|
168
|
+
transport.register(
|
|
169
|
+
createIntegrationsInterModulePresentation({
|
|
170
|
+
registry: createIntegrationProviderRegistry([
|
|
171
|
+
{
|
|
172
|
+
provider: 'github',
|
|
173
|
+
displayName: 'GitHub',
|
|
174
|
+
eventCatalog: {
|
|
175
|
+
provider: 'GitHub',
|
|
176
|
+
events: [
|
|
177
|
+
{
|
|
178
|
+
name: 'push',
|
|
179
|
+
summary: 'A push.',
|
|
180
|
+
emittedWhen: 'GitHub sends a push webhook.',
|
|
181
|
+
payloadKind: 'raw-provider',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
provider: 'webhook',
|
|
188
|
+
displayName: 'Webhook',
|
|
189
|
+
eventCatalog: {
|
|
190
|
+
provider: 'Custom webhook',
|
|
191
|
+
events: [
|
|
192
|
+
{
|
|
193
|
+
name: 'received',
|
|
194
|
+
summary: 'A webhook request is accepted.',
|
|
195
|
+
emittedWhen: 'Shipfox accepts a request.',
|
|
196
|
+
payloadKind: 'shipfox-normalized',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{provider: 'gitea', displayName: 'Gitea'},
|
|
202
|
+
]),
|
|
203
|
+
sourceControl,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
transport.seal();
|
|
207
|
+
|
|
208
|
+
const context = await client.getAgentToolsContext({
|
|
209
|
+
workspaceId,
|
|
210
|
+
defaultConnectionId: connectionId,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(context.eventCatalogs).toEqual([
|
|
214
|
+
{provider: 'github', events: ['push']},
|
|
215
|
+
{provider: 'webhook', events: ['received']},
|
|
216
|
+
]);
|
|
217
|
+
expect(context.fixedEventProviders).toEqual(['webhook']);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
IntegrationProviderError,
|
|
20
20
|
IntegrationProviderUnavailableError,
|
|
21
21
|
} from '#core/errors.js';
|
|
22
|
+
import {buildFixedEventProviders, buildProviderEventCatalogs} from '#core/event-catalogs.js';
|
|
22
23
|
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
23
24
|
import type {IntegrationSourceControlService} from '#core/source-control-service.js';
|
|
24
25
|
import {getIntegrationConnectionById, getIntegrationConnectionBySlug} from '#db/connections.js';
|
|
@@ -51,6 +52,12 @@ export function createIntegrationsInterModulePresentation(params: {
|
|
|
51
52
|
input,
|
|
52
53
|
async () => await params.sourceControl.resolveTriggerReference(input),
|
|
53
54
|
),
|
|
55
|
+
resolveSourceRef: async (input) =>
|
|
56
|
+
await known(
|
|
57
|
+
contract.methods.resolveSourceRef,
|
|
58
|
+
input,
|
|
59
|
+
async () => await params.sourceControl.resolveSourceRef(input),
|
|
60
|
+
),
|
|
54
61
|
listSourceFiles: async (input) =>
|
|
55
62
|
await known(
|
|
56
63
|
contract.methods.listSourceFiles,
|
|
@@ -105,6 +112,8 @@ export function createIntegrationsInterModulePresentation(params: {
|
|
|
105
112
|
...value,
|
|
106
113
|
capabilities: [...value.capabilities],
|
|
107
114
|
})),
|
|
115
|
+
eventCatalogs: buildProviderEventCatalogs(params.registry),
|
|
116
|
+
fixedEventProviders: buildFixedEventProviders(params.registry),
|
|
108
117
|
defaultConnection: defaultConnection
|
|
109
118
|
? {
|
|
110
119
|
id: defaultConnection.id,
|
|
@@ -119,7 +128,7 @@ export function createIntegrationsInterModulePresentation(params: {
|
|
|
119
128
|
|
|
120
129
|
async function known<Output>(
|
|
121
130
|
method: InterModuleMethodContract,
|
|
122
|
-
input: {connectionId?: string; defaultConnectionId?: string},
|
|
131
|
+
input: {connectionId?: string; defaultConnectionId?: string; ref?: string | undefined},
|
|
123
132
|
operation: () => Promise<Output>,
|
|
124
133
|
): Promise<Output> {
|
|
125
134
|
try {
|
|
@@ -130,7 +139,7 @@ async function known<Output>(
|
|
|
130
139
|
}
|
|
131
140
|
function mapError(
|
|
132
141
|
method: InterModuleMethodContract,
|
|
133
|
-
input: {connectionId?: string; defaultConnectionId?: string},
|
|
142
|
+
input: {connectionId?: string; defaultConnectionId?: string; ref?: string | undefined},
|
|
134
143
|
error: unknown,
|
|
135
144
|
): unknown {
|
|
136
145
|
if (error instanceof IntegrationConnectionNotFoundError)
|
|
@@ -154,12 +163,28 @@ function mapError(
|
|
|
154
163
|
});
|
|
155
164
|
if (error instanceof IntegrationCheckoutUnsupportedError)
|
|
156
165
|
return createInterModuleKnownError(method, 'checkout-unsupported', {provider: error.provider});
|
|
157
|
-
if (error instanceof IntegrationProviderError)
|
|
166
|
+
if (error instanceof IntegrationProviderError) {
|
|
167
|
+
// Only methods that resolve refs declare these codes; other methods keep
|
|
168
|
+
// seeing the failure as a generic provider failure.
|
|
169
|
+
if (error.reason === 'ref-not-found' && 'ref-not-found' in method.errors) {
|
|
170
|
+
return createInterModuleKnownError(method, 'ref-not-found', refDetails(input));
|
|
171
|
+
}
|
|
172
|
+
if (error.reason === 'ref-invalid' && 'ref-invalid' in method.errors) {
|
|
173
|
+
return createInterModuleKnownError(method, 'ref-invalid', refDetails(input));
|
|
174
|
+
}
|
|
158
175
|
return createInterModuleKnownError(method, 'provider-failure', {
|
|
159
176
|
reason: error.reason,
|
|
160
177
|
...(error.retryAfterSeconds === undefined
|
|
161
178
|
? {}
|
|
162
179
|
: {retryAfterSeconds: error.retryAfterSeconds}),
|
|
163
180
|
});
|
|
181
|
+
}
|
|
164
182
|
return error;
|
|
165
183
|
}
|
|
184
|
+
|
|
185
|
+
function refDetails(input: {ref?: string | undefined}): {ref: string} {
|
|
186
|
+
if (input.ref === undefined) {
|
|
187
|
+
throw new Error('Ref error mapping requires a ref input');
|
|
188
|
+
}
|
|
189
|
+
return {ref: input.ref};
|
|
190
|
+
}
|
|
@@ -23,7 +23,10 @@ function isProviderError(error: unknown): error is IntegrationProviderError {
|
|
|
23
23
|
'reason' in error &&
|
|
24
24
|
typeof error.reason === 'string' &&
|
|
25
25
|
(error.reason === 'repository-not-found' ||
|
|
26
|
+
error.reason === 'installation-not-found' ||
|
|
26
27
|
error.reason === 'file-not-found' ||
|
|
28
|
+
error.reason === 'ref-not-found' ||
|
|
29
|
+
error.reason === 'ref-invalid' ||
|
|
27
30
|
error.reason === 'access-denied' ||
|
|
28
31
|
error.reason === 'rate-limited' ||
|
|
29
32
|
error.reason === 'timeout' ||
|
|
@@ -13,6 +13,7 @@ describe('GET /integration-connections', () => {
|
|
|
13
13
|
externalAccountId: 'debug-active',
|
|
14
14
|
slug: 'debug_active',
|
|
15
15
|
displayName: 'Gitea',
|
|
16
|
+
capabilities: ['source_control'],
|
|
16
17
|
});
|
|
17
18
|
await upsertIntegrationConnection({
|
|
18
19
|
workspaceId: context.workspaceId,
|
|
@@ -20,6 +21,7 @@ describe('GET /integration-connections', () => {
|
|
|
20
21
|
externalAccountId: 'debug-error',
|
|
21
22
|
slug: 'debug_error',
|
|
22
23
|
displayName: 'Gitea',
|
|
24
|
+
capabilities: ['source_control'],
|
|
23
25
|
lifecycleStatus: 'error',
|
|
24
26
|
});
|
|
25
27
|
|
|
@@ -38,13 +40,17 @@ describe('GET /integration-connections', () => {
|
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
it('drops connections whose provider misses the capability filter', async () => {
|
|
41
|
-
const app = await createTestApp([
|
|
43
|
+
const app = await createTestApp([
|
|
44
|
+
sourceProvider(),
|
|
45
|
+
{provider: 'github', displayName: 'GitHub', adapters: {}},
|
|
46
|
+
]);
|
|
42
47
|
await upsertIntegrationConnection({
|
|
43
48
|
workspaceId: context.workspaceId,
|
|
44
49
|
provider: 'gitea',
|
|
45
50
|
externalAccountId: 'gitea-owner',
|
|
46
51
|
slug: 'gitea_owner',
|
|
47
52
|
displayName: 'Gitea',
|
|
53
|
+
capabilities: ['source_control'],
|
|
48
54
|
});
|
|
49
55
|
await upsertIntegrationConnection({
|
|
50
56
|
workspaceId: context.workspaceId,
|
|
@@ -52,6 +58,7 @@ describe('GET /integration-connections', () => {
|
|
|
52
58
|
externalAccountId: 'team-1',
|
|
53
59
|
slug: 'github_team_1',
|
|
54
60
|
displayName: 'GitHub',
|
|
61
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
const res = await app.inject({
|
|
@@ -94,6 +101,7 @@ describe('GET /integration-connections', () => {
|
|
|
94
101
|
externalAccountId: 'team-1',
|
|
95
102
|
slug: 'github_team_1',
|
|
96
103
|
displayName: 'GitHub',
|
|
104
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
97
105
|
});
|
|
98
106
|
|
|
99
107
|
const res = await app.inject({
|
|
@@ -119,6 +127,7 @@ describe('GET /integration-connections', () => {
|
|
|
119
127
|
externalAccountId: 'org-1',
|
|
120
128
|
slug: 'linear_org_1',
|
|
121
129
|
displayName: 'Linear Org',
|
|
130
|
+
capabilities: ['agent_tools'],
|
|
122
131
|
});
|
|
123
132
|
|
|
124
133
|
const res = await app.inject({
|
|
@@ -145,6 +154,7 @@ describe('GET /integration-connections', () => {
|
|
|
145
154
|
externalAccountId: 'org-1',
|
|
146
155
|
slug: 'linear_org_1',
|
|
147
156
|
displayName: 'Linear Org',
|
|
157
|
+
capabilities: ['agent_tools'],
|
|
148
158
|
});
|
|
149
159
|
|
|
150
160
|
const res = await app.inject({
|
|
@@ -174,6 +184,7 @@ describe('GET /integration-connections', () => {
|
|
|
174
184
|
externalAccountId: 'gitea-owner',
|
|
175
185
|
slug: 'gitea_owner',
|
|
176
186
|
displayName: 'Gitea',
|
|
187
|
+
capabilities: ['source_control'],
|
|
177
188
|
});
|
|
178
189
|
|
|
179
190
|
const res = await app.inject({
|
|
@@ -198,6 +209,7 @@ describe('GET /integration-connections', () => {
|
|
|
198
209
|
externalAccountId: 'gitea-owner',
|
|
199
210
|
slug: 'gitea_owner',
|
|
200
211
|
displayName: 'Gitea',
|
|
212
|
+
capabilities: ['source_control'],
|
|
201
213
|
});
|
|
202
214
|
|
|
203
215
|
const res = await app.inject({
|
|
@@ -13,6 +13,7 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
|
|
|
13
13
|
externalAccountId: 'gitea-owner',
|
|
14
14
|
slug: 'gitea_owner',
|
|
15
15
|
displayName: 'Gitea',
|
|
16
|
+
capabilities: ['source_control'],
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
const res = await app.inject({
|
|
@@ -46,6 +47,7 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
|
|
|
46
47
|
externalAccountId: 'gitea-owner',
|
|
47
48
|
slug: 'gitea_owner',
|
|
48
49
|
displayName: 'Gitea',
|
|
50
|
+
capabilities: ['source_control'],
|
|
49
51
|
lifecycleStatus: 'disabled',
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -73,6 +75,7 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
|
|
|
73
75
|
externalAccountId: 'team-1',
|
|
74
76
|
slug: 'github_team_1',
|
|
75
77
|
displayName: 'GitHub',
|
|
78
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
const res = await app.inject({
|
|
@@ -93,6 +96,7 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
|
|
|
93
96
|
externalAccountId: 'installation-1',
|
|
94
97
|
slug: 'github_installation_1',
|
|
95
98
|
displayName: 'GitHub',
|
|
99
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
96
100
|
});
|
|
97
101
|
|
|
98
102
|
const res = await app.inject({
|
|
@@ -127,6 +131,10 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
|
|
|
127
131
|
throw new Error('not used');
|
|
128
132
|
},
|
|
129
133
|
resolveTriggerReference: () => null,
|
|
134
|
+
resolveRef: async () => {
|
|
135
|
+
await Promise.resolve();
|
|
136
|
+
throw new Error('not used');
|
|
137
|
+
},
|
|
130
138
|
},
|
|
131
139
|
},
|
|
132
140
|
}),
|
|
@@ -137,6 +145,7 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
|
|
|
137
145
|
externalAccountId: 'gitea-owner',
|
|
138
146
|
slug: 'gitea_owner',
|
|
139
147
|
displayName: 'Gitea',
|
|
148
|
+
capabilities: ['source_control'],
|
|
140
149
|
});
|
|
141
150
|
|
|
142
151
|
const res = await app.inject({
|