@shipfox/api-integration-jira 12.2.0 → 12.3.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 +8 -7
- package/.turbo/turbo-type$colon$emit.log +0 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -1
- package/dist/api/client.d.ts +10 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +55 -11
- package/dist/api/client.js.map +1 -1
- package/dist/core/disconnect.d.ts +12 -0
- package/dist/core/disconnect.d.ts.map +1 -1
- package/dist/core/disconnect.js +45 -1
- package/dist/core/disconnect.js.map +1 -1
- package/dist/core/install.d.ts +1 -0
- package/dist/core/install.d.ts.map +1 -1
- package/dist/core/install.js +2 -1
- package/dist/core/install.js.map +1 -1
- package/dist/core/tokens.d.ts +2 -0
- package/dist/core/tokens.d.ts.map +1 -1
- package/dist/core/tokens.js +2 -2
- package/dist/core/tokens.js.map +1 -1
- package/dist/core/webhook-registration.d.ts +1 -0
- package/dist/core/webhook-registration.d.ts.map +1 -1
- package/dist/core/webhook-registration.js +3 -1
- package/dist/core/webhook-registration.js.map +1 -1
- package/dist/core/webhook-url.d.ts +3 -0
- package/dist/core/webhook-url.d.ts.map +1 -0
- package/dist/core/webhook-url.js +8 -0
- package/dist/core/webhook-url.js.map +1 -0
- package/dist/db/installations.d.ts +13 -0
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +18 -1
- package/dist/db/installations.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -7
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +2 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +2 -1
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/temporal/activities/index.d.ts +8 -1
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +8 -1
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/temporal/activities/renew-jira-webhooks.d.ts +26 -0
- package/dist/temporal/activities/renew-jira-webhooks.d.ts.map +1 -0
- package/dist/temporal/activities/renew-jira-webhooks.js +96 -0
- package/dist/temporal/activities/renew-jira-webhooks.js.map +1 -0
- package/dist/temporal/constants.d.ts +2 -0
- package/dist/temporal/constants.d.ts.map +1 -1
- package/dist/temporal/constants.js +2 -0
- package/dist/temporal/constants.js.map +1 -1
- package/dist/temporal/worker.d.ts +3 -0
- package/dist/temporal/worker.d.ts.map +1 -1
- package/dist/temporal/worker.js +5 -0
- package/dist/temporal/worker.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +38 -2
- 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/temporal/workflows/renew-jira-webhooks-cron.d.ts +2 -0
- package/dist/temporal/workflows/renew-jira-webhooks-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/renew-jira-webhooks-cron.js +18 -0
- package/dist/temporal/workflows/renew-jira-webhooks-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api/client.test.ts +81 -2
- package/src/api/client.ts +68 -7
- package/src/core/disconnect.test.ts +107 -1
- package/src/core/disconnect.ts +64 -1
- package/src/core/install.test.ts +23 -0
- package/src/core/install.ts +5 -2
- package/src/core/tokens-refresh.test.ts +39 -0
- package/src/core/tokens.ts +4 -2
- package/src/core/webhook-registration.test.ts +32 -0
- package/src/core/webhook-registration.ts +4 -1
- package/src/core/webhook-url.ts +9 -0
- package/src/db/installations.test.ts +82 -0
- package/src/db/installations.ts +54 -1
- package/src/index.test.ts +21 -1
- package/src/index.ts +21 -9
- package/src/presentation/routes/webhooks.ts +3 -1
- package/src/temporal/activities/index.ts +20 -3
- package/src/temporal/activities/renew-jira-webhooks.test.ts +215 -0
- package/src/temporal/activities/renew-jira-webhooks.ts +144 -0
- package/src/temporal/constants.ts +2 -0
- package/src/temporal/worker.ts +11 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/renew-jira-webhooks-cron.ts +17 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type {IntegrationConnection} from '@shipfox/api-integration-spi';
|
|
2
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
3
|
+
import type {JiraApiClient} from '#api/client.js';
|
|
4
|
+
import type {JiraTokenStore} from '#core/tokens.js';
|
|
5
|
+
import {registerJiraWebhook} from '#core/webhook-registration.js';
|
|
6
|
+
import {
|
|
7
|
+
type JiraInstallation,
|
|
8
|
+
listJiraInstallationsDueForWebhookRenewal,
|
|
9
|
+
updateJiraInstallationWebhookIfUnchanged,
|
|
10
|
+
} from '#db/installations.js';
|
|
11
|
+
import {
|
|
12
|
+
JIRA_WEBHOOK_RENEWAL_SWEEP_BATCH_SIZE,
|
|
13
|
+
JIRA_WEBHOOK_RENEWAL_THRESHOLD_MS,
|
|
14
|
+
} from '#temporal/constants.js';
|
|
15
|
+
|
|
16
|
+
const JIRA_WEBHOOK_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
17
|
+
|
|
18
|
+
export interface JiraWebhookRenewalActivityResult {
|
|
19
|
+
renewed: number;
|
|
20
|
+
reregistered: number;
|
|
21
|
+
skipped: number;
|
|
22
|
+
failed: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateJiraWebhookRenewalActivitiesOptions {
|
|
26
|
+
tokenStore: Pick<JiraTokenStore, 'getAccessToken'>;
|
|
27
|
+
jira?:
|
|
28
|
+
| Pick<
|
|
29
|
+
JiraApiClient,
|
|
30
|
+
'refreshDynamicWebhooks' | 'registerDynamicWebhook' | 'deleteDynamicWebhook'
|
|
31
|
+
>
|
|
32
|
+
| undefined;
|
|
33
|
+
webhookUrlForConnection?: ((connectionId: string) => string) | undefined;
|
|
34
|
+
resolveConnection(
|
|
35
|
+
connectionId: string,
|
|
36
|
+
): Promise<Pick<IntegrationConnection, 'lifecycleStatus'> | undefined>;
|
|
37
|
+
listInstallations?: typeof listJiraInstallationsDueForWebhookRenewal | undefined;
|
|
38
|
+
updateInstallation?: typeof updateJiraInstallationWebhookIfUnchanged | undefined;
|
|
39
|
+
registerWebhook?: typeof registerJiraWebhook | undefined;
|
|
40
|
+
now?: (() => Date) | undefined;
|
|
41
|
+
heartbeat?: (() => void) | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createJiraWebhookRenewalActivities(
|
|
45
|
+
options: CreateJiraWebhookRenewalActivitiesOptions,
|
|
46
|
+
) {
|
|
47
|
+
const listInstallations = options.listInstallations ?? listJiraInstallationsDueForWebhookRenewal;
|
|
48
|
+
const updateInstallation = options.updateInstallation ?? updateJiraInstallationWebhookIfUnchanged;
|
|
49
|
+
const registerWebhook = options.registerWebhook ?? registerJiraWebhook;
|
|
50
|
+
const now = options.now ?? (() => new Date());
|
|
51
|
+
const heartbeat = options.heartbeat ?? (() => undefined);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
async renewJiraWebhooksActivity(): Promise<JiraWebhookRenewalActivityResult> {
|
|
55
|
+
const renewalDeadline = new Date(now().getTime() + JIRA_WEBHOOK_RENEWAL_THRESHOLD_MS);
|
|
56
|
+
const installations = await listInstallations({
|
|
57
|
+
before: renewalDeadline,
|
|
58
|
+
limit: JIRA_WEBHOOK_RENEWAL_SWEEP_BATCH_SIZE,
|
|
59
|
+
});
|
|
60
|
+
let renewed = 0;
|
|
61
|
+
let reregistered = 0;
|
|
62
|
+
let skipped = 0;
|
|
63
|
+
let failed = 0;
|
|
64
|
+
|
|
65
|
+
for (const installation of installations) {
|
|
66
|
+
const heartbeatInterval = setInterval(heartbeat, JIRA_WEBHOOK_HEARTBEAT_INTERVAL_MS);
|
|
67
|
+
try {
|
|
68
|
+
heartbeat();
|
|
69
|
+
if (!isDueForRenewal(installation, renewalDeadline)) {
|
|
70
|
+
skipped += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const connection = await options.resolveConnection(installation.connectionId);
|
|
75
|
+
if (connection?.lifecycleStatus !== 'active') {
|
|
76
|
+
skipped += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!options.jira || !options.webhookUrlForConnection) {
|
|
81
|
+
throw new Error('Jira webhook renewal dependencies are not configured');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const accessToken = await options.tokenStore.getAccessToken({
|
|
85
|
+
connectionId: installation.connectionId,
|
|
86
|
+
});
|
|
87
|
+
const webhookExpiresAt =
|
|
88
|
+
installation.webhookIds.length === 0
|
|
89
|
+
? undefined
|
|
90
|
+
: await options.jira.refreshDynamicWebhooks({
|
|
91
|
+
accessToken,
|
|
92
|
+
cloudId: installation.cloudId,
|
|
93
|
+
webhookIds: installation.webhookIds,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (webhookExpiresAt === undefined) {
|
|
97
|
+
await registerWebhook({
|
|
98
|
+
jira: options.jira,
|
|
99
|
+
connectionId: installation.connectionId,
|
|
100
|
+
cloudId: installation.cloudId,
|
|
101
|
+
accessToken,
|
|
102
|
+
webhookUrl: options.webhookUrlForConnection(installation.connectionId),
|
|
103
|
+
now,
|
|
104
|
+
replaceExistingWebhooks: true,
|
|
105
|
+
});
|
|
106
|
+
reregistered += 1;
|
|
107
|
+
} else {
|
|
108
|
+
const updated = await updateInstallation({
|
|
109
|
+
connectionId: installation.connectionId,
|
|
110
|
+
webhookIds: installation.webhookIds,
|
|
111
|
+
webhookExpiresAt,
|
|
112
|
+
expectedWebhookIds: installation.webhookIds,
|
|
113
|
+
expectedWebhookExpiresAt: installation.webhookExpiresAt,
|
|
114
|
+
});
|
|
115
|
+
if (!updated) {
|
|
116
|
+
skipped += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
renewed += 1;
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
failed += 1;
|
|
123
|
+
logger().warn(
|
|
124
|
+
{err: error, connectionId: installation.connectionId},
|
|
125
|
+
'Jira webhook renewal failed',
|
|
126
|
+
);
|
|
127
|
+
} finally {
|
|
128
|
+
clearInterval(heartbeatInterval);
|
|
129
|
+
heartbeat();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {renewed, reregistered, skipped, failed};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isDueForRenewal(installation: JiraInstallation, renewalDeadline: Date): boolean {
|
|
139
|
+
return (
|
|
140
|
+
installation.status === 'installed' &&
|
|
141
|
+
installation.webhookExpiresAt !== null &&
|
|
142
|
+
installation.webhookExpiresAt.getTime() <= renewalDeadline.getTime()
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
2
2
|
|
|
3
|
+
export const JIRA_WEBHOOK_RENEWAL_THRESHOLD_MS = 7 * MS_PER_DAY;
|
|
4
|
+
export const JIRA_WEBHOOK_RENEWAL_SWEEP_BATCH_SIZE = 20;
|
|
3
5
|
export const JIRA_REFRESH_TOKEN_IDLE_WINDOW_MS = 90 * MS_PER_DAY;
|
|
4
6
|
export const JIRA_REFRESH_TOKEN_REFRESH_LEAD_TIME_MS = 14 * MS_PER_DAY;
|
|
5
7
|
export const JIRA_REFRESH_TOKEN_PROACTIVE_REFRESH_AFTER_MS =
|
package/src/temporal/worker.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {fileURLToPath} from 'node:url';
|
|
|
3
3
|
import type {IntegrationConnection} from '@shipfox/api-integration-spi';
|
|
4
4
|
import type {ModuleWorker} from '@shipfox/node-module';
|
|
5
5
|
import {Context} from '@temporalio/activity';
|
|
6
|
+
import type {JiraApiClient} from '#api/client.js';
|
|
6
7
|
import type {JiraTokenStore} from '#core/tokens.js';
|
|
7
8
|
import {createJiraMaintenanceActivities} from '#temporal/activities/index.js';
|
|
8
9
|
import {JIRA_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';
|
|
@@ -12,6 +13,11 @@ const workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
|
|
|
12
13
|
|
|
13
14
|
export interface CreateJiraMaintenanceWorkerOptions {
|
|
14
15
|
tokenStore: Pick<JiraTokenStore, 'getAccessToken'>;
|
|
16
|
+
jira: Pick<
|
|
17
|
+
JiraApiClient,
|
|
18
|
+
'refreshDynamicWebhooks' | 'registerDynamicWebhook' | 'deleteDynamicWebhook'
|
|
19
|
+
>;
|
|
20
|
+
webhookUrlForConnection: (connectionId: string) => string;
|
|
15
21
|
resolveConnection(
|
|
16
22
|
connectionId: string,
|
|
17
23
|
): Promise<Pick<IntegrationConnection, 'lifecycleStatus'> | undefined>;
|
|
@@ -34,6 +40,11 @@ export function createJiraMaintenanceWorker(
|
|
|
34
40
|
id: 'jira-refresh-tokens',
|
|
35
41
|
cronSchedule: '0 */6 * * *',
|
|
36
42
|
},
|
|
43
|
+
{
|
|
44
|
+
name: 'renewJiraWebhooksCron',
|
|
45
|
+
id: 'jira-renew-webhooks',
|
|
46
|
+
cronSchedule: '0 */6 * * *',
|
|
47
|
+
},
|
|
37
48
|
],
|
|
38
49
|
};
|
|
39
50
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {log, proxyActivities} from '@temporalio/workflow';
|
|
2
|
+
import type {createJiraMaintenanceActivities} from '../activities/index.js';
|
|
3
|
+
|
|
4
|
+
const {renewJiraWebhooksActivity} = proxyActivities<
|
|
5
|
+
ReturnType<typeof createJiraMaintenanceActivities>
|
|
6
|
+
>({
|
|
7
|
+
startToCloseTimeout: '5 minutes',
|
|
8
|
+
heartbeatTimeout: '1 minute',
|
|
9
|
+
retry: {maximumAttempts: 1},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export async function renewJiraWebhooksCron(): Promise<void> {
|
|
13
|
+
const result = await renewJiraWebhooksActivity();
|
|
14
|
+
if (result.renewed > 0 || result.reregistered > 0 || result.failed > 0 || result.skipped > 0) {
|
|
15
|
+
log.info('Completed Jira webhook renewal', {...result});
|
|
16
|
+
}
|
|
17
|
+
}
|