@friggframework/admin-scripts 2.0.0--canary.517.41839c5.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/LICENSE.md +9 -0
- package/index.js +66 -0
- package/package.json +53 -0
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
- package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
- package/src/adapters/aws-scheduler-adapter.js +138 -0
- package/src/adapters/local-scheduler-adapter.js +103 -0
- package/src/adapters/scheduler-adapter-factory.js +69 -0
- package/src/adapters/scheduler-adapter.js +64 -0
- package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
- package/src/application/__tests__/admin-script-base.test.js +273 -0
- package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
- package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
- package/src/application/__tests__/script-factory.test.js +381 -0
- package/src/application/__tests__/script-runner.test.js +202 -0
- package/src/application/admin-frigg-commands.js +242 -0
- package/src/application/admin-script-base.js +138 -0
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/schedule-management-use-case.js +230 -0
- package/src/application/script-factory.js +161 -0
- package/src/application/script-runner.js +254 -0
- package/src/builtins/__tests__/integration-health-check.test.js +598 -0
- package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
- package/src/builtins/index.js +28 -0
- package/src/builtins/integration-health-check.js +279 -0
- package/src/builtins/oauth-token-refresh.js +221 -0
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
- package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
- package/src/infrastructure/admin-auth-middleware.js +49 -0
- package/src/infrastructure/admin-script-router.js +311 -0
- package/src/infrastructure/script-executor-handler.js +75 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
const { OAuthTokenRefreshScript } = require('../oauth-token-refresh');
|
|
2
|
+
|
|
3
|
+
describe('OAuthTokenRefreshScript', () => {
|
|
4
|
+
describe('Definition', () => {
|
|
5
|
+
it('should have correct name and metadata', () => {
|
|
6
|
+
expect(OAuthTokenRefreshScript.Definition.name).toBe('oauth-token-refresh');
|
|
7
|
+
expect(OAuthTokenRefreshScript.Definition.version).toBe('1.0.0');
|
|
8
|
+
expect(OAuthTokenRefreshScript.Definition.source).toBe('BUILTIN');
|
|
9
|
+
expect(OAuthTokenRefreshScript.Definition.config.requiresIntegrationFactory).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should have valid input schema', () => {
|
|
13
|
+
const schema = OAuthTokenRefreshScript.Definition.inputSchema;
|
|
14
|
+
expect(schema.type).toBe('object');
|
|
15
|
+
expect(schema.properties.integrationIds).toBeDefined();
|
|
16
|
+
expect(schema.properties.expiryThresholdHours).toBeDefined();
|
|
17
|
+
expect(schema.properties.dryRun).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have valid output schema', () => {
|
|
21
|
+
const schema = OAuthTokenRefreshScript.Definition.outputSchema;
|
|
22
|
+
expect(schema.type).toBe('object');
|
|
23
|
+
expect(schema.properties.refreshed).toBeDefined();
|
|
24
|
+
expect(schema.properties.failed).toBeDefined();
|
|
25
|
+
expect(schema.properties.skipped).toBeDefined();
|
|
26
|
+
expect(schema.properties.details).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have appropriate timeout configuration', () => {
|
|
30
|
+
expect(OAuthTokenRefreshScript.Definition.config.timeout).toBe(600000); // 10 minutes
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('execute()', () => {
|
|
35
|
+
let script;
|
|
36
|
+
let mockFrigg;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
script = new OAuthTokenRefreshScript();
|
|
40
|
+
mockFrigg = {
|
|
41
|
+
log: jest.fn(),
|
|
42
|
+
listIntegrations: jest.fn(),
|
|
43
|
+
findIntegrationById: jest.fn(),
|
|
44
|
+
instantiate: jest.fn(),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return empty results when no integrations found', async () => {
|
|
49
|
+
mockFrigg.listIntegrations.mockResolvedValue([]);
|
|
50
|
+
|
|
51
|
+
const result = await script.execute(mockFrigg, {});
|
|
52
|
+
|
|
53
|
+
expect(result.refreshed).toBe(0);
|
|
54
|
+
expect(result.failed).toBe(0);
|
|
55
|
+
expect(result.skipped).toBe(0);
|
|
56
|
+
expect(result.details).toEqual([]);
|
|
57
|
+
expect(mockFrigg.log).toHaveBeenCalledWith('info', expect.any(String), expect.any(Object));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should skip integrations without OAuth credentials', async () => {
|
|
61
|
+
const integration = {
|
|
62
|
+
id: 'int-1',
|
|
63
|
+
config: {} // No credentials
|
|
64
|
+
};
|
|
65
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
66
|
+
|
|
67
|
+
const result = await script.execute(mockFrigg, {});
|
|
68
|
+
|
|
69
|
+
expect(result.skipped).toBe(1);
|
|
70
|
+
expect(result.refreshed).toBe(0);
|
|
71
|
+
expect(result.details[0]).toMatchObject({
|
|
72
|
+
integrationId: 'int-1',
|
|
73
|
+
action: 'skipped',
|
|
74
|
+
reason: 'No OAuth credentials found'
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should skip integrations without expiry time', async () => {
|
|
79
|
+
const integration = {
|
|
80
|
+
id: 'int-1',
|
|
81
|
+
config: {
|
|
82
|
+
credentials: {
|
|
83
|
+
access_token: 'token123'
|
|
84
|
+
// No expires_at
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
89
|
+
|
|
90
|
+
const result = await script.execute(mockFrigg, {});
|
|
91
|
+
|
|
92
|
+
expect(result.skipped).toBe(1);
|
|
93
|
+
expect(result.details[0]).toMatchObject({
|
|
94
|
+
integrationId: 'int-1',
|
|
95
|
+
action: 'skipped',
|
|
96
|
+
reason: 'No expiry time found'
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should skip tokens not near expiry', async () => {
|
|
101
|
+
const farFutureExpiry = new Date(Date.now() + 48 * 60 * 60 * 1000); // 48 hours from now
|
|
102
|
+
const integration = {
|
|
103
|
+
id: 'int-1',
|
|
104
|
+
config: {
|
|
105
|
+
credentials: {
|
|
106
|
+
access_token: 'token123',
|
|
107
|
+
expires_at: farFutureExpiry.toISOString()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
112
|
+
|
|
113
|
+
const result = await script.execute(mockFrigg, {
|
|
114
|
+
expiryThresholdHours: 24
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result.skipped).toBe(1);
|
|
118
|
+
expect(result.details[0]).toMatchObject({
|
|
119
|
+
integrationId: 'int-1',
|
|
120
|
+
action: 'skipped',
|
|
121
|
+
reason: 'Token not near expiry'
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should refresh tokens that are near expiry', async () => {
|
|
126
|
+
const soonExpiry = new Date(Date.now() + 12 * 60 * 60 * 1000); // 12 hours from now
|
|
127
|
+
const integration = {
|
|
128
|
+
id: 'int-1',
|
|
129
|
+
config: {
|
|
130
|
+
credentials: {
|
|
131
|
+
access_token: 'token123',
|
|
132
|
+
expires_at: soonExpiry.toISOString()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const mockInstance = {
|
|
138
|
+
primary: {
|
|
139
|
+
api: {
|
|
140
|
+
refreshAccessToken: jest.fn().mockResolvedValue(undefined)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
146
|
+
mockFrigg.instantiate.mockResolvedValue(mockInstance);
|
|
147
|
+
|
|
148
|
+
const result = await script.execute(mockFrigg, {
|
|
149
|
+
expiryThresholdHours: 24
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result.refreshed).toBe(1);
|
|
153
|
+
expect(result.skipped).toBe(0);
|
|
154
|
+
expect(mockInstance.primary.api.refreshAccessToken).toHaveBeenCalled();
|
|
155
|
+
expect(result.details[0]).toMatchObject({
|
|
156
|
+
integrationId: 'int-1',
|
|
157
|
+
action: 'refreshed'
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle dryRun mode correctly', async () => {
|
|
162
|
+
const soonExpiry = new Date(Date.now() + 12 * 60 * 60 * 1000);
|
|
163
|
+
const integration = {
|
|
164
|
+
id: 'int-1',
|
|
165
|
+
config: {
|
|
166
|
+
credentials: {
|
|
167
|
+
access_token: 'token123',
|
|
168
|
+
expires_at: soonExpiry.toISOString()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
174
|
+
|
|
175
|
+
const result = await script.execute(mockFrigg, {
|
|
176
|
+
expiryThresholdHours: 24,
|
|
177
|
+
dryRun: true
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.refreshed).toBe(0);
|
|
181
|
+
expect(result.skipped).toBe(1);
|
|
182
|
+
expect(mockFrigg.instantiate).not.toHaveBeenCalled();
|
|
183
|
+
expect(result.details[0]).toMatchObject({
|
|
184
|
+
integrationId: 'int-1',
|
|
185
|
+
action: 'skipped',
|
|
186
|
+
reason: 'Dry run - would have refreshed'
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle refresh failures gracefully', async () => {
|
|
191
|
+
const soonExpiry = new Date(Date.now() + 12 * 60 * 60 * 1000);
|
|
192
|
+
const integration = {
|
|
193
|
+
id: 'int-1',
|
|
194
|
+
config: {
|
|
195
|
+
credentials: {
|
|
196
|
+
access_token: 'token123',
|
|
197
|
+
expires_at: soonExpiry.toISOString()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const mockInstance = {
|
|
203
|
+
primary: {
|
|
204
|
+
api: {
|
|
205
|
+
refreshAccessToken: jest.fn().mockRejectedValue(new Error('API Error'))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
211
|
+
mockFrigg.instantiate.mockResolvedValue(mockInstance);
|
|
212
|
+
|
|
213
|
+
const result = await script.execute(mockFrigg, {
|
|
214
|
+
expiryThresholdHours: 24
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(result.failed).toBe(1);
|
|
218
|
+
expect(result.refreshed).toBe(0);
|
|
219
|
+
expect(result.details[0]).toMatchObject({
|
|
220
|
+
integrationId: 'int-1',
|
|
221
|
+
action: 'failed',
|
|
222
|
+
reason: 'API Error'
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should skip integrations without refresh support', async () => {
|
|
227
|
+
const soonExpiry = new Date(Date.now() + 12 * 60 * 60 * 1000);
|
|
228
|
+
const integration = {
|
|
229
|
+
id: 'int-1',
|
|
230
|
+
config: {
|
|
231
|
+
credentials: {
|
|
232
|
+
access_token: 'token123',
|
|
233
|
+
expires_at: soonExpiry.toISOString()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const mockInstance = {
|
|
239
|
+
primary: {
|
|
240
|
+
api: {
|
|
241
|
+
// No refreshAccessToken method
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
247
|
+
mockFrigg.instantiate.mockResolvedValue(mockInstance);
|
|
248
|
+
|
|
249
|
+
const result = await script.execute(mockFrigg, {
|
|
250
|
+
expiryThresholdHours: 24
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result.skipped).toBe(1);
|
|
254
|
+
expect(result.details[0]).toMatchObject({
|
|
255
|
+
integrationId: 'int-1',
|
|
256
|
+
action: 'skipped',
|
|
257
|
+
reason: 'API does not support token refresh'
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should filter by specific integration IDs', async () => {
|
|
262
|
+
const integration1 = {
|
|
263
|
+
id: 'int-1',
|
|
264
|
+
config: { credentials: { access_token: 'token1' } }
|
|
265
|
+
};
|
|
266
|
+
const integration2 = {
|
|
267
|
+
id: 'int-2',
|
|
268
|
+
config: { credentials: { access_token: 'token2' } }
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
mockFrigg.findIntegrationById.mockImplementation((id) => {
|
|
272
|
+
if (id === 'int-1') return Promise.resolve(integration1);
|
|
273
|
+
if (id === 'int-2') return Promise.resolve(integration2);
|
|
274
|
+
return Promise.reject(new Error('Not found'));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const result = await script.execute(mockFrigg, {
|
|
278
|
+
integrationIds: ['int-1', 'int-2']
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(mockFrigg.findIntegrationById).toHaveBeenCalledWith('int-1');
|
|
282
|
+
expect(mockFrigg.findIntegrationById).toHaveBeenCalledWith('int-2');
|
|
283
|
+
expect(mockFrigg.listIntegrations).not.toHaveBeenCalled();
|
|
284
|
+
expect(result.details).toHaveLength(2);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should handle errors when processing integrations', async () => {
|
|
288
|
+
const integration = {
|
|
289
|
+
id: 'int-1',
|
|
290
|
+
config: {
|
|
291
|
+
credentials: {
|
|
292
|
+
access_token: 'token123',
|
|
293
|
+
expires_at: new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
mockFrigg.listIntegrations.mockResolvedValue([integration]);
|
|
299
|
+
mockFrigg.instantiate.mockRejectedValue(new Error('Instantiation failed'));
|
|
300
|
+
|
|
301
|
+
const result = await script.execute(mockFrigg, {
|
|
302
|
+
expiryThresholdHours: 24
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(result.failed).toBe(1);
|
|
306
|
+
expect(result.details[0]).toMatchObject({
|
|
307
|
+
integrationId: 'int-1',
|
|
308
|
+
action: 'failed',
|
|
309
|
+
reason: 'Instantiation failed'
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('processIntegration()', () => {
|
|
315
|
+
let script;
|
|
316
|
+
let mockFrigg;
|
|
317
|
+
|
|
318
|
+
beforeEach(() => {
|
|
319
|
+
script = new OAuthTokenRefreshScript();
|
|
320
|
+
mockFrigg = {
|
|
321
|
+
log: jest.fn(),
|
|
322
|
+
instantiate: jest.fn(),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should return correct detail object for each scenario', async () => {
|
|
327
|
+
// Test various scenarios are covered in execute() tests above
|
|
328
|
+
// This test validates the method can be called directly
|
|
329
|
+
const integration = {
|
|
330
|
+
id: 'int-1',
|
|
331
|
+
config: {}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const result = await script.processIntegration(mockFrigg, integration, {
|
|
335
|
+
expiryThresholdHours: 24,
|
|
336
|
+
dryRun: false
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(result).toHaveProperty('integrationId');
|
|
340
|
+
expect(result).toHaveProperty('action');
|
|
341
|
+
expect(result).toHaveProperty('reason');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { OAuthTokenRefreshScript } = require('./oauth-token-refresh');
|
|
2
|
+
const { IntegrationHealthCheckScript } = require('./integration-health-check');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in Admin Scripts
|
|
6
|
+
*
|
|
7
|
+
* These scripts ship with @friggframework/admin-scripts and provide
|
|
8
|
+
* common maintenance and monitoring functionality.
|
|
9
|
+
*/
|
|
10
|
+
const builtinScripts = [
|
|
11
|
+
OAuthTokenRefreshScript,
|
|
12
|
+
IntegrationHealthCheckScript,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register all built-in scripts with a factory
|
|
17
|
+
* @param {ScriptFactory} factory - Script factory to register with
|
|
18
|
+
*/
|
|
19
|
+
function registerBuiltinScripts(factory) {
|
|
20
|
+
factory.registerAll(builtinScripts);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
OAuthTokenRefreshScript,
|
|
25
|
+
IntegrationHealthCheckScript,
|
|
26
|
+
builtinScripts,
|
|
27
|
+
registerBuiltinScripts,
|
|
28
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
const { AdminScriptBase } = require('../application/admin-script-base');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration Health Check Script
|
|
5
|
+
*
|
|
6
|
+
* Checks the health of integrations by verifying:
|
|
7
|
+
* - Credential validity
|
|
8
|
+
* - API connectivity
|
|
9
|
+
* - Configuration integrity
|
|
10
|
+
*/
|
|
11
|
+
class IntegrationHealthCheckScript extends AdminScriptBase {
|
|
12
|
+
static Definition = {
|
|
13
|
+
name: 'integration-health-check',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
description: 'Checks health of integrations and reports issues',
|
|
16
|
+
source: 'BUILTIN',
|
|
17
|
+
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
integrationIds: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: { type: 'string' },
|
|
24
|
+
description: 'Specific integration IDs to check (optional, defaults to all)'
|
|
25
|
+
},
|
|
26
|
+
checkCredentials: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: true,
|
|
29
|
+
description: 'Verify credential validity'
|
|
30
|
+
},
|
|
31
|
+
checkConnectivity: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: true,
|
|
34
|
+
description: 'Test API connectivity'
|
|
35
|
+
},
|
|
36
|
+
updateStatus: {
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
default: false,
|
|
39
|
+
description: 'Update integration status based on health'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
outputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
healthy: { type: 'number' },
|
|
48
|
+
unhealthy: { type: 'number' },
|
|
49
|
+
unknown: { type: 'number' },
|
|
50
|
+
results: { type: 'array' }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
config: {
|
|
55
|
+
timeout: 900000, // 15 minutes
|
|
56
|
+
maxRetries: 0,
|
|
57
|
+
requiresIntegrationFactory: true,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
schedule: {
|
|
61
|
+
enabled: false, // Can be enabled via API
|
|
62
|
+
cronExpression: 'cron(0 6 * * ? *)', // Daily at 6 AM UTC
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
display: {
|
|
66
|
+
label: 'Integration Health Check',
|
|
67
|
+
description: 'Check health and connectivity of integrations',
|
|
68
|
+
category: 'maintenance',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async execute(frigg, params = {}) {
|
|
73
|
+
const {
|
|
74
|
+
integrationIds = null,
|
|
75
|
+
checkCredentials = true,
|
|
76
|
+
checkConnectivity = true,
|
|
77
|
+
updateStatus = false
|
|
78
|
+
} = params;
|
|
79
|
+
|
|
80
|
+
const summary = {
|
|
81
|
+
healthy: 0,
|
|
82
|
+
unhealthy: 0,
|
|
83
|
+
unknown: 0,
|
|
84
|
+
results: []
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
frigg.log('info', 'Starting integration health check', {
|
|
88
|
+
checkCredentials,
|
|
89
|
+
checkConnectivity,
|
|
90
|
+
updateStatus,
|
|
91
|
+
specificIds: integrationIds?.length || 'all'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Get integrations to check
|
|
95
|
+
let integrations;
|
|
96
|
+
if (integrationIds && integrationIds.length > 0) {
|
|
97
|
+
integrations = await Promise.all(
|
|
98
|
+
integrationIds.map(id => frigg.findIntegrationById(id).catch(() => null))
|
|
99
|
+
);
|
|
100
|
+
integrations = integrations.filter(Boolean);
|
|
101
|
+
} else {
|
|
102
|
+
integrations = await this.getAllIntegrations(frigg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
frigg.log('info', `Checking ${integrations.length} integrations`);
|
|
106
|
+
|
|
107
|
+
for (const integration of integrations) {
|
|
108
|
+
const result = await this.checkIntegration(frigg, integration, {
|
|
109
|
+
checkCredentials,
|
|
110
|
+
checkConnectivity
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
summary.results.push(result);
|
|
114
|
+
|
|
115
|
+
if (result.status === 'healthy') {
|
|
116
|
+
summary.healthy++;
|
|
117
|
+
} else if (result.status === 'unhealthy') {
|
|
118
|
+
summary.unhealthy++;
|
|
119
|
+
} else {
|
|
120
|
+
summary.unknown++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Optionally update integration status
|
|
124
|
+
if (updateStatus && result.status !== 'unknown') {
|
|
125
|
+
try {
|
|
126
|
+
const newStatus = result.status === 'healthy' ? 'ACTIVE' : 'ERROR';
|
|
127
|
+
await frigg.updateIntegrationStatus(integration.id, newStatus);
|
|
128
|
+
frigg.log('info', `Updated status for ${integration.id} to ${newStatus}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
frigg.log('warn', `Failed to update status for ${integration.id}`, {
|
|
131
|
+
error: error.message
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
frigg.log('info', 'Health check completed', {
|
|
138
|
+
healthy: summary.healthy,
|
|
139
|
+
unhealthy: summary.unhealthy,
|
|
140
|
+
unknown: summary.unknown
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return summary;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getAllIntegrations(frigg) {
|
|
147
|
+
return frigg.listIntegrations({});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async checkIntegration(frigg, integration, options) {
|
|
151
|
+
const { checkCredentials, checkConnectivity } = options;
|
|
152
|
+
const result = this._createCheckResult(integration);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await this._runChecks(frigg, integration, result, { checkCredentials, checkConnectivity });
|
|
156
|
+
this._determineOverallStatus(result);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
this._handleCheckError(frigg, integration, result, error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create initial check result object
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
_createCheckResult(integration) {
|
|
169
|
+
return {
|
|
170
|
+
integrationId: integration.id,
|
|
171
|
+
integrationType: integration.config?.type || 'unknown',
|
|
172
|
+
status: 'unknown',
|
|
173
|
+
checks: {},
|
|
174
|
+
issues: []
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run all requested checks
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
async _runChecks(frigg, integration, result, options) {
|
|
183
|
+
const { checkCredentials, checkConnectivity } = options;
|
|
184
|
+
|
|
185
|
+
if (checkCredentials) {
|
|
186
|
+
this._addCheckResult(result, 'credentials', this.checkCredentialValidity(integration));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (checkConnectivity) {
|
|
190
|
+
this._addCheckResult(result, 'connectivity', await this.checkApiConnectivity(frigg, integration));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Add a check result and track any issues
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
_addCheckResult(result, checkName, checkResult) {
|
|
199
|
+
result.checks[checkName] = checkResult;
|
|
200
|
+
if (!checkResult.valid) {
|
|
201
|
+
result.issues.push(checkResult.issue);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Determine overall health status from issues
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_determineOverallStatus(result) {
|
|
210
|
+
result.status = result.issues.length === 0 ? 'healthy' : 'unhealthy';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handle check error and update result
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_handleCheckError(frigg, integration, result, error) {
|
|
218
|
+
frigg.log('error', `Error checking integration ${integration.id}`, {
|
|
219
|
+
error: error.message
|
|
220
|
+
});
|
|
221
|
+
result.status = 'unknown';
|
|
222
|
+
result.issues.push(`Check failed: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
checkCredentialValidity(integration) {
|
|
226
|
+
const result = { valid: true, issue: null };
|
|
227
|
+
|
|
228
|
+
// Check for access token
|
|
229
|
+
if (!integration.config?.credentials?.access_token) {
|
|
230
|
+
result.valid = false;
|
|
231
|
+
result.issue = 'Missing access token';
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check for expiry
|
|
236
|
+
const expiresAt = integration.config?.credentials?.expires_at;
|
|
237
|
+
if (expiresAt) {
|
|
238
|
+
const expiryTime = new Date(expiresAt);
|
|
239
|
+
if (expiryTime < new Date()) {
|
|
240
|
+
result.valid = false;
|
|
241
|
+
result.issue = 'Access token expired';
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async checkApiConnectivity(frigg, integration) {
|
|
250
|
+
const result = { valid: true, issue: null, responseTime: null };
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const startTime = Date.now();
|
|
254
|
+
const instance = await frigg.instantiate(integration.id);
|
|
255
|
+
|
|
256
|
+
// Try to make a simple API call
|
|
257
|
+
if (instance.primary?.api?.getAuthenticationInfo) {
|
|
258
|
+
await instance.primary.api.getAuthenticationInfo();
|
|
259
|
+
} else if (instance.primary?.api?.getCurrentUser) {
|
|
260
|
+
await instance.primary.api.getCurrentUser();
|
|
261
|
+
} else {
|
|
262
|
+
// No suitable health check method
|
|
263
|
+
result.valid = true;
|
|
264
|
+
result.issue = null;
|
|
265
|
+
result.note = 'No health check endpoint available';
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
result.responseTime = Date.now() - startTime;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
result.valid = false;
|
|
272
|
+
result.issue = `API connectivity failed: ${error.message}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = { IntegrationHealthCheckScript };
|