@shipfox/api-integration-core 4.0.0 → 5.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 +35 -0
- 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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/index.js +1 -1
- 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/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +110 -3
- package/dist/providers/jira.js.map +1 -1
- package/dist/providers/linear.d.ts.map +1 -1
- package/dist/providers/linear.js +15 -1
- package/dist/providers/linear.js.map +1 -1
- package/dist/providers/slack.d.ts.map +1 -1
- package/dist/providers/slack.js +17 -2
- package/dist/providers/slack.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +28 -38
- package/src/db/webhook-deliveries.test.ts +11 -0
- package/src/db/webhook-deliveries.ts +12 -3
- package/src/index.ts +2 -1
- package/src/presentation/routes/index.ts +1 -1
- package/src/presentation/routes/manage-connections.test.ts +113 -0
- package/src/presentation/routes/manage-connections.ts +26 -2
- package/src/providers/jira.ts +152 -7
- package/src/providers/linear.test.ts +136 -0
- package/src/providers/linear.ts +14 -0
- package/src/providers/slack.test.ts +139 -0
- package/src/providers/slack.ts +20 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/providers/linear.ts
CHANGED
|
@@ -32,9 +32,11 @@ async function loadLinearModuleParts(
|
|
|
32
32
|
createLinearE2eRoutes,
|
|
33
33
|
createLinearIntegrationProvider,
|
|
34
34
|
config: linearConfig,
|
|
35
|
+
deleteLinearInstallationByConnectionId,
|
|
35
36
|
disconnectLinearInstallation: disconnectLinearInstallationRecords,
|
|
36
37
|
getLinearInstallationByOrganizationId,
|
|
37
38
|
db: linearDb,
|
|
39
|
+
linearSecretsNamespace,
|
|
38
40
|
migrationsPath: linearMigrationsPath,
|
|
39
41
|
upsertLinearInstallation,
|
|
40
42
|
} = await import('@shipfox/api-integration-linear');
|
|
@@ -137,6 +139,18 @@ async function loadLinearModuleParts(
|
|
|
137
139
|
return {
|
|
138
140
|
provider: createLinearIntegrationProvider({
|
|
139
141
|
agentTools: {tokenStore, endpoint: linearConfig.LINEAR_MCP_ENDPOINT},
|
|
142
|
+
cleanup: {
|
|
143
|
+
deleteConnectionRecords: async (connection, {tx}) => {
|
|
144
|
+
await deleteLinearInstallationByConnectionId(connection.id, {tx});
|
|
145
|
+
},
|
|
146
|
+
deleteConnectionSecrets: async (connection) => {
|
|
147
|
+
// Scoped secrets accept the provider-local suffix, after this helper validates its prefix.
|
|
148
|
+
await (options.secrets?.linear?.deleteSecrets({
|
|
149
|
+
workspaceId: connection.workspaceId,
|
|
150
|
+
namespace: linearNamespaceSuffix(linearSecretsNamespace(connection.id)),
|
|
151
|
+
}) ?? Promise.resolve());
|
|
152
|
+
},
|
|
153
|
+
},
|
|
140
154
|
routes: {
|
|
141
155
|
tokenStore,
|
|
142
156
|
getExistingLinearConnection,
|
|
@@ -4,8 +4,12 @@ import {
|
|
|
4
4
|
} from '@shipfox/api-integration-slack';
|
|
5
5
|
import {runMigrations} from '@shipfox/node-drizzle';
|
|
6
6
|
import {getIntegrationConnectionById, upsertIntegrationConnection} from '#db/connections.js';
|
|
7
|
+
import {db} from '#db/db.js';
|
|
8
|
+
import {createTestApp, useIntegrationRouteTest} from '#test/route-utils.js';
|
|
7
9
|
|
|
8
10
|
describe('slackProviderModule', () => {
|
|
11
|
+
const context = useIntegrationRouteTest();
|
|
12
|
+
|
|
9
13
|
afterEach(() => {
|
|
10
14
|
vi.unstubAllEnvs();
|
|
11
15
|
vi.resetModules();
|
|
@@ -54,4 +58,139 @@ describe('slackProviderModule', () => {
|
|
|
54
58
|
teamId,
|
|
55
59
|
});
|
|
56
60
|
});
|
|
61
|
+
|
|
62
|
+
it('deletes the installation and token through the generic route before allowing a reinstall', async () => {
|
|
63
|
+
vi.stubEnv('INTEGRATIONS_ENABLE_SLACK_PROVIDER', 'true');
|
|
64
|
+
vi.resetModules();
|
|
65
|
+
const deleteSecrets = vi.fn(() => Promise.resolve(1));
|
|
66
|
+
const scopedSecrets = {
|
|
67
|
+
getSecret: vi.fn(() => Promise.resolve(null)),
|
|
68
|
+
setSecrets: vi.fn(() => Promise.resolve()),
|
|
69
|
+
deleteSecrets,
|
|
70
|
+
};
|
|
71
|
+
const {createPostgresClient} = await import('@shipfox/node-postgres');
|
|
72
|
+
createPostgresClient();
|
|
73
|
+
const {loadEnabledProviderModules} = await import('#providers/modules.js');
|
|
74
|
+
const parts = await loadEnabledProviderModules({
|
|
75
|
+
secrets: {slack: scopedSecrets, deleteSecrets},
|
|
76
|
+
});
|
|
77
|
+
const slackPart = parts.find((part) => part.provider.provider === 'slack');
|
|
78
|
+
if (!slackPart?.database) throw new Error('Slack provider database is not configured');
|
|
79
|
+
const teamId = `T${crypto.randomUUID()}`;
|
|
80
|
+
|
|
81
|
+
await runMigrations(
|
|
82
|
+
slackPart.database.db(),
|
|
83
|
+
slackPart.database.migrationsPath,
|
|
84
|
+
slackPart.database.migrationsTableName,
|
|
85
|
+
);
|
|
86
|
+
const app = await createTestApp([slackPart.provider]);
|
|
87
|
+
const connection = await upsertIntegrationConnection({
|
|
88
|
+
workspaceId: context.workspaceId,
|
|
89
|
+
provider: 'slack',
|
|
90
|
+
externalAccountId: teamId,
|
|
91
|
+
slug: `slack_${teamId}`,
|
|
92
|
+
displayName: 'Slack Acme',
|
|
93
|
+
});
|
|
94
|
+
await upsertSlackInstallation({
|
|
95
|
+
connectionId: connection.id,
|
|
96
|
+
teamId,
|
|
97
|
+
teamName: 'Acme',
|
|
98
|
+
appId: 'A123',
|
|
99
|
+
botUserId: 'U123',
|
|
100
|
+
scopes: ['app_mentions:read'],
|
|
101
|
+
status: 'installed',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const res = await app.inject({
|
|
105
|
+
method: 'DELETE',
|
|
106
|
+
url: `/integration-connections/${connection.id}`,
|
|
107
|
+
headers: {authorization: 'Bearer user'},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(res.statusCode).toBe(204);
|
|
111
|
+
await expect(getIntegrationConnectionById(connection.id)).resolves.toBeUndefined();
|
|
112
|
+
await expect(getSlackInstallationByConnectionId(connection.id)).resolves.toBeUndefined();
|
|
113
|
+
expect(deleteSecrets).toHaveBeenCalledWith({
|
|
114
|
+
workspaceId: context.workspaceId,
|
|
115
|
+
namespace: connection.id,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const replacement = await upsertIntegrationConnection({
|
|
119
|
+
workspaceId: context.workspaceId,
|
|
120
|
+
provider: 'slack',
|
|
121
|
+
externalAccountId: teamId,
|
|
122
|
+
slug: `slack_${teamId}_again`,
|
|
123
|
+
displayName: 'Slack Acme',
|
|
124
|
+
});
|
|
125
|
+
await upsertSlackInstallation({
|
|
126
|
+
connectionId: replacement.id,
|
|
127
|
+
teamId,
|
|
128
|
+
teamName: 'Acme',
|
|
129
|
+
appId: 'A123',
|
|
130
|
+
botUserId: 'U123',
|
|
131
|
+
scopes: ['app_mentions:read'],
|
|
132
|
+
status: 'installed',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await expect(getSlackInstallationByConnectionId(replacement.id)).resolves.toMatchObject({
|
|
136
|
+
teamId,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('rolls back provider record cleanup when its transaction fails', async () => {
|
|
141
|
+
vi.stubEnv('INTEGRATIONS_ENABLE_SLACK_PROVIDER', 'true');
|
|
142
|
+
vi.resetModules();
|
|
143
|
+
const deleteSecrets = vi.fn(() => Promise.resolve(1));
|
|
144
|
+
const scopedSecrets = {
|
|
145
|
+
getSecret: vi.fn(() => Promise.resolve(null)),
|
|
146
|
+
setSecrets: vi.fn(() => Promise.resolve()),
|
|
147
|
+
deleteSecrets,
|
|
148
|
+
};
|
|
149
|
+
const {createPostgresClient} = await import('@shipfox/node-postgres');
|
|
150
|
+
createPostgresClient();
|
|
151
|
+
const {loadEnabledProviderModules} = await import('#providers/modules.js');
|
|
152
|
+
const parts = await loadEnabledProviderModules({
|
|
153
|
+
secrets: {slack: scopedSecrets, deleteSecrets},
|
|
154
|
+
});
|
|
155
|
+
const slackPart = parts.find((part) => part.provider.provider === 'slack');
|
|
156
|
+
if (!slackPart?.database) throw new Error('Slack provider database is not configured');
|
|
157
|
+
const teamId = `T${crypto.randomUUID()}`;
|
|
158
|
+
|
|
159
|
+
await runMigrations(
|
|
160
|
+
slackPart.database.db(),
|
|
161
|
+
slackPart.database.migrationsPath,
|
|
162
|
+
slackPart.database.migrationsTableName,
|
|
163
|
+
);
|
|
164
|
+
const connection = await upsertIntegrationConnection({
|
|
165
|
+
workspaceId: context.workspaceId,
|
|
166
|
+
provider: 'slack',
|
|
167
|
+
externalAccountId: teamId,
|
|
168
|
+
slug: `slack_${teamId}`,
|
|
169
|
+
displayName: 'Slack Acme',
|
|
170
|
+
});
|
|
171
|
+
await upsertSlackInstallation({
|
|
172
|
+
connectionId: connection.id,
|
|
173
|
+
teamId,
|
|
174
|
+
teamName: 'Acme',
|
|
175
|
+
appId: 'A123',
|
|
176
|
+
botUserId: 'U123',
|
|
177
|
+
scopes: ['app_mentions:read'],
|
|
178
|
+
status: 'installed',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
db().transaction(async (tx) => {
|
|
183
|
+
await slackPart.provider.deleteConnectionRecords?.(connection, {tx});
|
|
184
|
+
throw new Error('transaction failed');
|
|
185
|
+
}),
|
|
186
|
+
).rejects.toThrow('transaction failed');
|
|
187
|
+
|
|
188
|
+
await expect(getIntegrationConnectionById(connection.id)).resolves.toMatchObject({
|
|
189
|
+
id: connection.id,
|
|
190
|
+
});
|
|
191
|
+
await expect(getSlackInstallationByConnectionId(connection.id)).resolves.toMatchObject({
|
|
192
|
+
connectionId: connection.id,
|
|
193
|
+
});
|
|
194
|
+
expect(deleteSecrets).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
57
196
|
});
|
package/src/providers/slack.ts
CHANGED
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
upsertIntegrationConnection,
|
|
15
15
|
} from '#db/connections.js';
|
|
16
16
|
import {db} from '#db/db.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
claimWebhookDelivery,
|
|
19
|
+
publishIntegrationEventReceived,
|
|
20
|
+
recordDeliveryOnly,
|
|
21
|
+
} from '#db/webhook-deliveries.js';
|
|
18
22
|
import {retryConnectionSlugCollision} from '#providers/connection-slug.js';
|
|
19
23
|
import type {IntegrationModuleParts, IntegrationProviderModule} from '#providers/types.js';
|
|
20
24
|
|
|
@@ -32,9 +36,11 @@ async function loadSlackModuleParts(
|
|
|
32
36
|
createSlackIntegrationProvider,
|
|
33
37
|
createSlackTokenStore,
|
|
34
38
|
db: slackDb,
|
|
39
|
+
deleteSlackInstallationByConnectionId,
|
|
35
40
|
disconnectSlackInstallation: disconnectSlackInstallationRecords,
|
|
36
41
|
getSlackInstallationByTeamId,
|
|
37
42
|
migrationsPath: slackMigrationsPath,
|
|
43
|
+
slackSecretsNamespace,
|
|
38
44
|
upsertSlackInstallation,
|
|
39
45
|
} = await import('@shipfox/api-integration-slack');
|
|
40
46
|
|
|
@@ -135,12 +141,25 @@ async function loadSlackModuleParts(
|
|
|
135
141
|
return {
|
|
136
142
|
provider: createSlackIntegrationProvider({
|
|
137
143
|
agentTools: {tokenStore},
|
|
144
|
+
cleanup: {
|
|
145
|
+
deleteConnectionRecords: async (connection, {tx}) => {
|
|
146
|
+
await deleteSlackInstallationByConnectionId(connection.id, {tx});
|
|
147
|
+
},
|
|
148
|
+
deleteConnectionSecrets: async (connection) => {
|
|
149
|
+
// Scoped secrets accept the provider-local suffix, after this helper validates its prefix.
|
|
150
|
+
await (options.secrets?.slack?.deleteSecrets({
|
|
151
|
+
workspaceId: connection.workspaceId,
|
|
152
|
+
namespace: slackNamespaceSuffix(slackSecretsNamespace(connection.id)),
|
|
153
|
+
}) ?? Promise.resolve());
|
|
154
|
+
},
|
|
155
|
+
},
|
|
138
156
|
routes: {
|
|
139
157
|
tokenStore,
|
|
140
158
|
getExistingSlackConnection,
|
|
141
159
|
connectSlackInstallation,
|
|
142
160
|
disconnectSlackInstallation,
|
|
143
161
|
coreDb: db,
|
|
162
|
+
claimWebhookDelivery,
|
|
144
163
|
publishIntegrationEventReceived,
|
|
145
164
|
recordDeliveryOnly,
|
|
146
165
|
getIntegrationConnectionById,
|