@shipfox/api-integration-core 12.2.0 → 12.4.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 +11 -8
- package/CHANGELOG.md +25 -0
- package/dist/core/secret-cleanup.d.ts +20 -0
- package/dist/core/secret-cleanup.d.ts.map +1 -0
- package/dist/core/secret-cleanup.js +178 -0
- package/dist/core/secret-cleanup.js.map +1 -0
- package/dist/db/db.d.ts +562 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/secret-cleanups.d.ts +284 -0
- package/dist/db/schema/secret-cleanups.d.ts.map +1 -0
- package/dist/db/schema/secret-cleanups.js +39 -0
- package/dist/db/schema/secret-cleanups.js.map +1 -0
- package/dist/db/secret-cleanups.d.ts +52 -0
- package/dist/db/secret-cleanups.d.ts.map +1 -0
- package/dist/db/secret-cleanups.js +105 -0
- package/dist/db/secret-cleanups.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
- package/dist/presentation/routes/manage-connections.js +57 -18
- package/dist/presentation/routes/manage-connections.js.map +1 -1
- package/dist/providers/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +51 -14
- package/dist/providers/jira.js.map +1 -1
- package/dist/temporal/activities/index.d.ts +5 -1
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +8 -2
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/temporal/constants.d.ts +1 -0
- package/dist/temporal/constants.d.ts.map +1 -1
- package/dist/temporal/constants.js +1 -0
- package/dist/temporal/constants.js.map +1 -1
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.d.ts +2 -0
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.js +21 -0
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.js.map +1 -0
- package/dist/temporal/workflows/index.bundle.js +64 -3
- package/dist/temporal/workflows/index.d.ts +1 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -1
- package/dist/temporal/workflows/index.js +1 -0
- package/dist/temporal/workflows/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_durable_secret_cleanup.sql +22 -0
- package/drizzle/meta/0001_snapshot.json +180 -2
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +11 -10
- package/src/core/secret-cleanup.test.ts +129 -0
- package/src/core/secret-cleanup.ts +229 -0
- package/src/db/db.ts +2 -0
- package/src/db/schema/secret-cleanups.ts +42 -0
- package/src/db/secret-cleanups.test.ts +113 -0
- package/src/db/secret-cleanups.ts +209 -0
- package/src/index.ts +6 -1
- package/src/presentation/routes/manage-connections.test.ts +232 -0
- package/src/presentation/routes/manage-connections.ts +45 -11
- package/src/providers/jira.ts +61 -13
- package/src/temporal/activities/index.ts +11 -1
- package/src/temporal/constants.ts +2 -0
- package/src/temporal/workflows/cleanup-integration-secrets-cron.ts +20 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/test/env.ts +3 -0
- package/test/globalSetup.ts +1 -0
- package/test/route-utils.ts +3 -0
- package/test/setup.ts +12 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/providers/jira.ts
CHANGED
|
@@ -25,19 +25,26 @@ async function loadJiraModuleParts(
|
|
|
25
25
|
options: Parameters<IntegrationProviderModule['load']>[0] = {},
|
|
26
26
|
): Promise<IntegrationModuleParts> {
|
|
27
27
|
const {
|
|
28
|
+
createJiraApiClient,
|
|
28
29
|
createJiraIntegrationProvider,
|
|
29
30
|
createJiraMaintenanceWorker,
|
|
30
31
|
createJiraPendingSelectionStore,
|
|
31
32
|
createJiraTokenStore,
|
|
32
33
|
db: jiraDb,
|
|
34
|
+
deregisterJiraWebhooks,
|
|
33
35
|
deleteJiraInstallationByConnectionId,
|
|
34
36
|
disconnectJiraInstallation: disconnectJiraInstallationRecords,
|
|
35
37
|
getJiraInstallationByCloudId,
|
|
38
|
+
getJiraInstallationByConnectionId,
|
|
36
39
|
jiraSecretsNamespace,
|
|
40
|
+
jiraWebhookUrl,
|
|
37
41
|
migrationsPath,
|
|
42
|
+
prepareJiraWebhookDeregistration,
|
|
38
43
|
upsertJiraInstallation,
|
|
39
44
|
withJiraRefreshLockAndWait,
|
|
45
|
+
withJiraWebhookRegistrationLock,
|
|
40
46
|
} = await import('@shipfox/api-integration-jira');
|
|
47
|
+
const jira = createJiraApiClient();
|
|
41
48
|
|
|
42
49
|
async function getExistingJiraConnection(input: {
|
|
43
50
|
cloudId: string;
|
|
@@ -94,19 +101,40 @@ async function loadJiraModuleParts(
|
|
|
94
101
|
);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
async function disconnectJiraInstallation(input: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
async function disconnectJiraInstallation(input: {
|
|
105
|
+
connectionId: string;
|
|
106
|
+
lockAlreadyHeld?: boolean | undefined;
|
|
107
|
+
}): Promise<void> {
|
|
108
|
+
const disconnect = () =>
|
|
109
|
+
disconnectJiraInstallationRecords<IntegrationTx>({
|
|
110
|
+
connectionId: input.connectionId,
|
|
111
|
+
getConnection: getIntegrationConnectionById,
|
|
112
|
+
deleteSecrets: (params) =>
|
|
113
|
+
options.secrets?.jira?.deleteSecrets({
|
|
114
|
+
...params,
|
|
115
|
+
namespace: jiraNamespaceSuffix(params.namespace),
|
|
116
|
+
}) ?? Promise.resolve(0),
|
|
117
|
+
deregisterWebhooks: () =>
|
|
118
|
+
deregisterJiraWebhooks({
|
|
119
|
+
connectionId: input.connectionId,
|
|
120
|
+
getInstallation: getJiraInstallationByConnectionId,
|
|
121
|
+
tokenStore,
|
|
122
|
+
jira,
|
|
123
|
+
}),
|
|
124
|
+
transaction: (fn) => db().transaction((tx) => fn(tx)),
|
|
125
|
+
deleteConnection: (params, transactionOptions) =>
|
|
126
|
+
deleteIntegrationConnection({id: params.connectionId}, transactionOptions),
|
|
127
|
+
});
|
|
128
|
+
if (input.lockAlreadyHeld) {
|
|
129
|
+
await disconnect();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const installation = await getJiraInstallationByConnectionId(input.connectionId);
|
|
133
|
+
if (!installation) {
|
|
134
|
+
await disconnect();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await withJiraWebhookRegistrationLock(installation.cloudId, disconnect);
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
const fallbackSecrets: JiraSecretsStore & JiraPendingSelectionSecretsStore = {
|
|
@@ -134,6 +162,7 @@ async function loadJiraModuleParts(
|
|
|
134
162
|
}
|
|
135
163
|
: fallbackSecrets;
|
|
136
164
|
const tokenStore = createJiraTokenStore({
|
|
165
|
+
client: jira,
|
|
137
166
|
resolveConnection: getIntegrationConnectionById,
|
|
138
167
|
secrets,
|
|
139
168
|
markConnectionError: async ({connectionId}) => {
|
|
@@ -146,8 +175,25 @@ async function loadJiraModuleParts(
|
|
|
146
175
|
const pendingStore = createJiraPendingSelectionStore({secrets});
|
|
147
176
|
|
|
148
177
|
const integrationProvider = createJiraIntegrationProvider({
|
|
178
|
+
jira,
|
|
149
179
|
agentTools: {tokenStore},
|
|
150
180
|
cleanup: {
|
|
181
|
+
deleteConnectionRemoteResources: async (connection) => {
|
|
182
|
+
return await prepareJiraWebhookDeregistration({
|
|
183
|
+
connectionId: connection.id,
|
|
184
|
+
getInstallation: getJiraInstallationByConnectionId,
|
|
185
|
+
tokenStore,
|
|
186
|
+
jira,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
withConnectionDeletionLock: async (connection, fn) => {
|
|
190
|
+
const installation = await getJiraInstallationByConnectionId(connection.id);
|
|
191
|
+
if (!installation) {
|
|
192
|
+
await fn();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await withJiraWebhookRegistrationLock(installation.cloudId, fn);
|
|
196
|
+
},
|
|
151
197
|
deleteConnectionRecords: async (connection, {tx}) => {
|
|
152
198
|
await deleteJiraInstallationByConnectionId(connection.id, {tx});
|
|
153
199
|
},
|
|
@@ -199,8 +245,10 @@ async function loadJiraModuleParts(
|
|
|
199
245
|
provider: integrationProvider,
|
|
200
246
|
workers: [
|
|
201
247
|
createJiraMaintenanceWorker({
|
|
248
|
+
jira,
|
|
202
249
|
tokenStore,
|
|
203
250
|
resolveConnection: getIntegrationConnectionById,
|
|
251
|
+
webhookUrlForConnection: jiraWebhookUrl,
|
|
204
252
|
}),
|
|
205
253
|
],
|
|
206
254
|
webhookProcessors: integrationProvider.webhookProcessors,
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import {Context} from '@temporalio/activity';
|
|
2
|
+
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
3
|
+
import {processIntegrationSecretCleanups} from '#core/secret-cleanup.js';
|
|
1
4
|
import {pruneWebhookDeliveriesActivity} from './prune-webhook-deliveries.js';
|
|
2
5
|
|
|
3
|
-
export function createIntegrationsMaintenanceActivities(
|
|
6
|
+
export function createIntegrationsMaintenanceActivities(options: {
|
|
7
|
+
registry: IntegrationProviderRegistry;
|
|
8
|
+
}) {
|
|
4
9
|
return {
|
|
5
10
|
pruneWebhookDeliveriesActivity,
|
|
11
|
+
cleanupIntegrationSecretsActivity: () =>
|
|
12
|
+
processIntegrationSecretCleanups({
|
|
13
|
+
registry: options.registry,
|
|
14
|
+
heartbeat: () => Context.current().heartbeat(),
|
|
15
|
+
}),
|
|
6
16
|
};
|
|
7
17
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {log, proxyActivities} from '@temporalio/workflow';
|
|
2
|
+
import type {createIntegrationsMaintenanceActivities} from '../activities/index.js';
|
|
3
|
+
import {CLEANUP_SECRETS_ACTIVITY_TIMEOUT_MS} from '../constants.js';
|
|
4
|
+
|
|
5
|
+
const {cleanupIntegrationSecretsActivity} = proxyActivities<
|
|
6
|
+
ReturnType<typeof createIntegrationsMaintenanceActivities>
|
|
7
|
+
>({
|
|
8
|
+
startToCloseTimeout: CLEANUP_SECRETS_ACTIVITY_TIMEOUT_MS,
|
|
9
|
+
heartbeatTimeout: '1 minute',
|
|
10
|
+
// The activity records a per-row retry schedule. A later cron run retries the
|
|
11
|
+
// row without replaying the whole sweep in Temporal.
|
|
12
|
+
retry: {maximumAttempts: 1},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export async function cleanupIntegrationSecretsCron(): Promise<void> {
|
|
16
|
+
const result = await cleanupIntegrationSecretsActivity();
|
|
17
|
+
if (result.claimed > 0) {
|
|
18
|
+
log.info('Completed integration connection secret cleanup sweep', {...result});
|
|
19
|
+
}
|
|
20
|
+
}
|
package/test/env.ts
CHANGED
|
@@ -32,3 +32,6 @@ process.env.GITEA_BASE_URL = 'https://gitea.example.com';
|
|
|
32
32
|
process.env.GITEA_SERVICE_USERNAME = 'shipfox-bot';
|
|
33
33
|
process.env.GITEA_SERVICE_TOKEN = 'test-service-token';
|
|
34
34
|
process.env.GITEA_WEBHOOK_SECRET = 'test-webhook-secret';
|
|
35
|
+
process.env.SENTRY_APP_CLIENT_ID = 'test-client-id';
|
|
36
|
+
process.env.SENTRY_APP_CLIENT_SECRET = 'test-client-secret';
|
|
37
|
+
process.env.SENTRY_APP_SLUG = 'shipfox-test';
|
package/test/globalSetup.ts
CHANGED
|
@@ -15,6 +15,7 @@ export async function setup() {
|
|
|
15
15
|
await runMigrations(db(), migrationsPath, '__drizzle_migrations_integrations');
|
|
16
16
|
await runMigrations(githubDb(), githubMigrationsPath, '__drizzle_migrations_integrations_github');
|
|
17
17
|
await db().execute(sql`TRUNCATE integrations_connections CASCADE`);
|
|
18
|
+
await db().execute(sql`TRUNCATE integrations_secret_cleanups CASCADE`);
|
|
18
19
|
await db().execute(sql`TRUNCATE integrations_webhook_deliveries CASCADE`);
|
|
19
20
|
|
|
20
21
|
closeDb();
|
package/test/route-utils.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
} from '@shipfox/api-auth-context';
|
|
7
7
|
import {type AuthMethod, ClientError, closeApp, createApp} from '@shipfox/node-fastify';
|
|
8
8
|
import {afterEach, beforeEach} from '@shipfox/vitest/vi';
|
|
9
|
+
import {sql} from 'drizzle-orm';
|
|
9
10
|
import type {FastifyInstance, FastifyRequest} from 'fastify';
|
|
11
|
+
import {db} from '#db/db.js';
|
|
10
12
|
import {createIntegrationsModule, type IntegrationProvider} from '#index.js';
|
|
11
13
|
|
|
12
14
|
let authenticatedMemberships: UserContextMembership[] = [];
|
|
@@ -95,6 +97,7 @@ export function useIntegrationRouteTest() {
|
|
|
95
97
|
|
|
96
98
|
afterEach(async () => {
|
|
97
99
|
await closeApp();
|
|
100
|
+
await db().execute(sql`TRUNCATE integrations_secret_cleanups CASCADE`);
|
|
98
101
|
});
|
|
99
102
|
|
|
100
103
|
return {
|
package/test/setup.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import './env.js';
|
|
2
|
+
import {closeDb as closeGiteaDb} from '@shipfox/api-integration-gitea';
|
|
2
3
|
import {closeDb as closeGithubDb} from '@shipfox/api-integration-github';
|
|
4
|
+
import {closeDb as closeJiraDb} from '@shipfox/api-integration-jira';
|
|
5
|
+
import {closeDb as closeLinearDb} from '@shipfox/api-integration-linear';
|
|
6
|
+
import {closeDb as closeSentryDb} from '@shipfox/api-integration-sentry';
|
|
7
|
+
import {closeDb as closeSlackDb} from '@shipfox/api-integration-slack';
|
|
3
8
|
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
4
9
|
import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
|
|
5
10
|
import {closeDb} from '#db/db.js';
|
|
@@ -15,5 +20,12 @@ afterEach(() => {
|
|
|
15
20
|
afterAll(async () => {
|
|
16
21
|
closeDb();
|
|
17
22
|
closeGithubDb();
|
|
23
|
+
// isolate:false shares provider module state across files, so every memoized
|
|
24
|
+
// Drizzle handle must be cleared before the shared pool is ended.
|
|
25
|
+
closeLinearDb();
|
|
26
|
+
closeSlackDb();
|
|
27
|
+
closeJiraDb();
|
|
28
|
+
closeSentryDb();
|
|
29
|
+
closeGiteaDb();
|
|
18
30
|
await closePostgresClient();
|
|
19
31
|
});
|