@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.
Files changed (35) hide show
  1. package/LICENSE.md +9 -0
  2. package/index.js +66 -0
  3. package/package.json +53 -0
  4. package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
  5. package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
  6. package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
  7. package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
  8. package/src/adapters/aws-scheduler-adapter.js +138 -0
  9. package/src/adapters/local-scheduler-adapter.js +103 -0
  10. package/src/adapters/scheduler-adapter-factory.js +69 -0
  11. package/src/adapters/scheduler-adapter.js +64 -0
  12. package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
  13. package/src/application/__tests__/admin-script-base.test.js +273 -0
  14. package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
  15. package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
  16. package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
  17. package/src/application/__tests__/script-factory.test.js +381 -0
  18. package/src/application/__tests__/script-runner.test.js +202 -0
  19. package/src/application/admin-frigg-commands.js +242 -0
  20. package/src/application/admin-script-base.js +138 -0
  21. package/src/application/dry-run-http-interceptor.js +296 -0
  22. package/src/application/dry-run-repository-wrapper.js +261 -0
  23. package/src/application/schedule-management-use-case.js +230 -0
  24. package/src/application/script-factory.js +161 -0
  25. package/src/application/script-runner.js +254 -0
  26. package/src/builtins/__tests__/integration-health-check.test.js +598 -0
  27. package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
  28. package/src/builtins/index.js +28 -0
  29. package/src/builtins/integration-health-check.js +279 -0
  30. package/src/builtins/oauth-token-refresh.js +221 -0
  31. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
  32. package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
  33. package/src/infrastructure/admin-auth-middleware.js +49 -0
  34. package/src/infrastructure/admin-script-router.js +311 -0
  35. 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 };