@shipfox/api-integration-jira 12.1.0 → 12.2.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/.turbo/turbo-type$colon$emit.log +1 -0
- package/CHANGELOG.md +31 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +9 -1
- package/dist/api/client.js.map +1 -1
- package/dist/core/agent-tools-provider.d.ts.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/scopes.d.ts +1 -1
- package/dist/core/scopes.d.ts.map +1 -1
- package/dist/db/db.d.ts +4 -4
- package/dist/db/schema/installations.d.ts +1 -1
- package/dist/db/schema/pending-selections.d.ts +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/integrations.d.ts.map +1 -1
- package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/api/client.test.ts +172 -1
- package/src/api/client.ts +15 -1
- package/src/index.test.ts +11 -0
- package/src/index.ts +11 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-jira",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"drizzle-orm": "^0.45.2",
|
|
28
28
|
"ky": "^2.0.0",
|
|
29
29
|
"zod": "^4.4.3",
|
|
30
|
-
"@shipfox/api-auth-context": "12.
|
|
31
|
-
"@shipfox/api-integration-spi": "1.0.
|
|
32
|
-
"@shipfox/api-integration-jira-dto": "12.
|
|
30
|
+
"@shipfox/api-auth-context": "12.2.0",
|
|
31
|
+
"@shipfox/api-integration-spi": "1.0.1",
|
|
32
|
+
"@shipfox/api-integration-jira-dto": "12.2.0",
|
|
33
33
|
"@shipfox/config": "1.2.4",
|
|
34
34
|
"@shipfox/node-drizzle": "0.3.5",
|
|
35
|
-
"@shipfox/node-fastify": "0.4.
|
|
35
|
+
"@shipfox/node-fastify": "0.4.2",
|
|
36
36
|
"@shipfox/node-jwt": "0.4.0",
|
|
37
|
-
"@shipfox/node-module": "1.0.
|
|
38
|
-
"@shipfox/node-opentelemetry": "0.6.
|
|
37
|
+
"@shipfox/node-module": "1.0.6",
|
|
38
|
+
"@shipfox/node-opentelemetry": "0.6.4",
|
|
39
39
|
"@shipfox/node-postgres": "0.5.0"
|
|
40
40
|
},
|
|
41
41
|
"imports": {
|
package/src/api/client.test.ts
CHANGED
|
@@ -2,7 +2,18 @@ import {HTTPError} from 'ky';
|
|
|
2
2
|
import type {JiraIntegrationProviderError} from '#core/errors.js';
|
|
3
3
|
import {mapJiraError} from './client.js';
|
|
4
4
|
|
|
5
|
-
const mocks = vi.hoisted(() => ({
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
delete: vi.fn(),
|
|
7
|
+
post: vi.fn(),
|
|
8
|
+
request: vi.fn(),
|
|
9
|
+
warn: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@shipfox/node-opentelemetry', () => ({
|
|
13
|
+
logger: () => ({
|
|
14
|
+
warn: mocks.warn,
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
6
17
|
|
|
7
18
|
vi.mock('ky', () => {
|
|
8
19
|
class MockHTTPError extends Error {
|
|
@@ -77,6 +88,7 @@ describe('Jira dynamic webhook API', () => {
|
|
|
77
88
|
mocks.delete.mockReset();
|
|
78
89
|
mocks.post.mockReset();
|
|
79
90
|
mocks.request.mockReset();
|
|
91
|
+
mocks.warn.mockReset();
|
|
80
92
|
});
|
|
81
93
|
|
|
82
94
|
it('registers the six curated events with the access token', async () => {
|
|
@@ -133,6 +145,165 @@ describe('Jira dynamic webhook API', () => {
|
|
|
133
145
|
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
134
146
|
});
|
|
135
147
|
|
|
148
|
+
it('logs provider validation errors before rejecting registration', async () => {
|
|
149
|
+
mocks.post.mockReturnValue(
|
|
150
|
+
resolves({
|
|
151
|
+
webhookRegistrationResult: [
|
|
152
|
+
{createdWebhookId: 123, errors: ['Webhook URL is not approved', 'Invalid JQL filter']},
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
157
|
+
|
|
158
|
+
await expect(
|
|
159
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
160
|
+
accessToken: 'access-token',
|
|
161
|
+
cloudId: 'cloud-1',
|
|
162
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
163
|
+
}),
|
|
164
|
+
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
165
|
+
|
|
166
|
+
expect(mocks.warn).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(mocks.warn).toHaveBeenCalledWith(
|
|
168
|
+
{
|
|
169
|
+
operation: 'register-dynamic-webhook',
|
|
170
|
+
providerErrors: ['Webhook URL is not approved', 'Invalid JQL filter'],
|
|
171
|
+
providerErrorCount: 2,
|
|
172
|
+
},
|
|
173
|
+
'Jira dynamic webhook registration rejected',
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('limits logged provider errors to five values', async () => {
|
|
178
|
+
mocks.post.mockReturnValue(
|
|
179
|
+
resolves({
|
|
180
|
+
webhookRegistrationResult: [
|
|
181
|
+
{
|
|
182
|
+
errors: ['one', 'two', 'three', 'four', 'five', 'six'],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
191
|
+
accessToken: 'access-token',
|
|
192
|
+
cloudId: 'cloud-1',
|
|
193
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
194
|
+
}),
|
|
195
|
+
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
196
|
+
|
|
197
|
+
expect(mocks.warn).toHaveBeenCalledWith(
|
|
198
|
+
{
|
|
199
|
+
operation: 'register-dynamic-webhook',
|
|
200
|
+
providerErrors: ['one', 'two', 'three', 'four', 'five'],
|
|
201
|
+
providerErrorCount: 6,
|
|
202
|
+
},
|
|
203
|
+
'Jira dynamic webhook registration rejected',
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('limits each logged provider error to 500 characters', async () => {
|
|
208
|
+
const longError = 'x'.repeat(501);
|
|
209
|
+
mocks.post.mockReturnValue(resolves({webhookRegistrationResult: [{errors: [longError]}]}));
|
|
210
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
211
|
+
|
|
212
|
+
await expect(
|
|
213
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
214
|
+
accessToken: 'access-token',
|
|
215
|
+
cloudId: 'cloud-1',
|
|
216
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
217
|
+
}),
|
|
218
|
+
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
219
|
+
|
|
220
|
+
expect(mocks.warn).toHaveBeenCalledWith(
|
|
221
|
+
{
|
|
222
|
+
operation: 'register-dynamic-webhook',
|
|
223
|
+
providerErrors: ['x'.repeat(500)],
|
|
224
|
+
providerErrorCount: 1,
|
|
225
|
+
},
|
|
226
|
+
'Jira dynamic webhook registration rejected',
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('omits non-string provider errors from the warning', async () => {
|
|
231
|
+
mocks.post.mockReturnValue(
|
|
232
|
+
resolves({
|
|
233
|
+
webhookRegistrationResult: [
|
|
234
|
+
{errors: ['valid message', 42, null, {message: 'raw response'}, true]},
|
|
235
|
+
],
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
239
|
+
|
|
240
|
+
await expect(
|
|
241
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
242
|
+
accessToken: 'access-token',
|
|
243
|
+
cloudId: 'cloud-1',
|
|
244
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
245
|
+
}),
|
|
246
|
+
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
247
|
+
|
|
248
|
+
expect(mocks.warn).toHaveBeenCalledWith(
|
|
249
|
+
{
|
|
250
|
+
operation: 'register-dynamic-webhook',
|
|
251
|
+
providerErrors: ['valid message'],
|
|
252
|
+
providerErrorCount: 5,
|
|
253
|
+
},
|
|
254
|
+
'Jira dynamic webhook registration rejected',
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('accepts an empty provider errors array without logging a rejection', async () => {
|
|
259
|
+
mocks.post.mockReturnValue(
|
|
260
|
+
resolves({webhookRegistrationResult: [{createdWebhookId: 123, errors: []}]}),
|
|
261
|
+
);
|
|
262
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
266
|
+
accessToken: 'access-token',
|
|
267
|
+
cloudId: 'cloud-1',
|
|
268
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
269
|
+
}),
|
|
270
|
+
).resolves.toEqual({webhookId: 123});
|
|
271
|
+
expect(mocks.warn).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('rejects a non-array provider errors value without logging it', async () => {
|
|
275
|
+
mocks.post.mockReturnValue(
|
|
276
|
+
resolves({
|
|
277
|
+
webhookRegistrationResult: [{createdWebhookId: 123, errors: {message: 'do not log me'}}],
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
281
|
+
|
|
282
|
+
await expect(
|
|
283
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
284
|
+
accessToken: 'access-token',
|
|
285
|
+
cloudId: 'cloud-1',
|
|
286
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
287
|
+
}),
|
|
288
|
+
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
289
|
+
expect(mocks.warn).not.toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('keeps the malformed provider response reason for a Jira rejection', async () => {
|
|
293
|
+
mocks.post.mockReturnValue(
|
|
294
|
+
resolves({webhookRegistrationResult: [{errors: ['Provider rejected the webhook']}]}),
|
|
295
|
+
);
|
|
296
|
+
const {createJiraApiClient} = await import('./client.js');
|
|
297
|
+
|
|
298
|
+
await expect(
|
|
299
|
+
createJiraApiClient().registerDynamicWebhook({
|
|
300
|
+
accessToken: 'access-token',
|
|
301
|
+
cloudId: 'cloud-1',
|
|
302
|
+
url: 'https://shipfox.example.com/webhooks/integrations/jira/connection-1',
|
|
303
|
+
}),
|
|
304
|
+
).rejects.toMatchObject({reason: 'malformed-provider-response'});
|
|
305
|
+
});
|
|
306
|
+
|
|
136
307
|
it('deletes a dynamic webhook by id', async () => {
|
|
137
308
|
mocks.delete.mockResolvedValue(undefined);
|
|
138
309
|
const {createJiraApiClient} = await import('./client.js');
|
package/src/api/client.ts
CHANGED
|
@@ -282,7 +282,21 @@ function parseDynamicWebhookRegistration(
|
|
|
282
282
|
createdWebhookId?: unknown;
|
|
283
283
|
errors?: unknown;
|
|
284
284
|
};
|
|
285
|
-
if (errors !== undefined &&
|
|
285
|
+
if (errors !== undefined && !Array.isArray(errors)) {
|
|
286
|
+
throw malformed('Jira webhook registration returned errors');
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
289
|
+
logger().warn(
|
|
290
|
+
{
|
|
291
|
+
operation: 'register-dynamic-webhook',
|
|
292
|
+
providerErrors: errors
|
|
293
|
+
.filter((error): error is string => typeof error === 'string')
|
|
294
|
+
.slice(0, 5)
|
|
295
|
+
.map((error) => error.slice(0, 500)),
|
|
296
|
+
providerErrorCount: errors.length,
|
|
297
|
+
},
|
|
298
|
+
'Jira dynamic webhook registration rejected',
|
|
299
|
+
);
|
|
286
300
|
throw malformed('Jira webhook registration returned errors');
|
|
287
301
|
}
|
|
288
302
|
if (
|
package/src/index.test.ts
CHANGED
|
@@ -31,6 +31,17 @@ describe('createJiraIntegrationProvider', () => {
|
|
|
31
31
|
}),
|
|
32
32
|
).toThrow('requires all webhook receiver dependencies');
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it('exposes explicit connection cleanup without requiring routes', () => {
|
|
36
|
+
const deleteConnectionRecords = vi.fn(() => Promise.resolve());
|
|
37
|
+
const deleteConnectionSecrets = vi.fn(() => Promise.resolve());
|
|
38
|
+
const provider = createJiraIntegrationProvider({
|
|
39
|
+
cleanup: {deleteConnectionRecords, deleteConnectionSecrets},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(provider.deleteConnectionRecords).toBe(deleteConnectionRecords);
|
|
43
|
+
expect(provider.deleteConnectionSecrets).toBe(deleteConnectionSecrets);
|
|
44
|
+
});
|
|
34
45
|
});
|
|
35
46
|
|
|
36
47
|
describe('createJiraMaintenanceWorker', () => {
|
package/src/index.ts
CHANGED
|
@@ -126,6 +126,7 @@ export {
|
|
|
126
126
|
updateJiraInstallationWebhook,
|
|
127
127
|
upsertJiraInstallation,
|
|
128
128
|
withJiraRefreshLock,
|
|
129
|
+
withJiraRefreshLockAndWait,
|
|
129
130
|
withJiraWebhookRegistrationLock,
|
|
130
131
|
} from '#db/installations.js';
|
|
131
132
|
export type {CreateJiraWebhookRoutesOptions} from '#presentation/routes/webhooks.js';
|
|
@@ -142,6 +143,15 @@ export interface CreateJiraIntegrationProviderOptions {
|
|
|
142
143
|
}
|
|
143
144
|
| undefined;
|
|
144
145
|
getJiraInstallationByConnectionId?: typeof getJiraInstallationByConnectionId | undefined;
|
|
146
|
+
cleanup?:
|
|
147
|
+
| {
|
|
148
|
+
deleteConnectionRecords?: (
|
|
149
|
+
connection: {id: string},
|
|
150
|
+
options: {tx: unknown},
|
|
151
|
+
) => Promise<void>;
|
|
152
|
+
deleteConnectionSecrets?: (connection: {id: string; workspaceId: string}) => Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
| undefined;
|
|
145
155
|
routes?: JiraIntegrationProviderRoutesOptions | undefined;
|
|
146
156
|
}
|
|
147
157
|
|
|
@@ -210,6 +220,7 @@ export function createJiraIntegrationProvider(options: CreateJiraIntegrationProv
|
|
|
210
220
|
async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {
|
|
211
221
|
return (await getInstallationByConnectionId(connection.id))?.siteUrl;
|
|
212
222
|
},
|
|
223
|
+
...options.cleanup,
|
|
213
224
|
routes,
|
|
214
225
|
webhookProcessors: webhookProcessor
|
|
215
226
|
? [{routeIds: ['jira'] as const, processor: webhookProcessor}]
|