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