@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.21b69ac.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 (31) hide show
  1. package/PR_517_REVIEW_TRACKER.md +56 -0
  2. package/index.js +12 -3
  3. package/package.json +6 -9
  4. package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
  5. package/src/application/__tests__/admin-script-base.test.js +100 -84
  6. package/src/application/__tests__/script-runner.test.js +146 -16
  7. package/src/application/admin-frigg-commands.js +20 -32
  8. package/src/application/admin-script-base.js +20 -99
  9. package/src/application/script-runner.js +131 -135
  10. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
  11. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
  12. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
  13. package/src/application/use-cases/delete-schedule-use-case.js +108 -0
  14. package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
  15. package/src/application/use-cases/index.js +18 -0
  16. package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
  17. package/src/builtins/__tests__/integration-health-check.test.js +67 -60
  18. package/src/builtins/__tests__/oauth-token-refresh.test.js +45 -37
  19. package/src/builtins/integration-health-check.js +23 -24
  20. package/src/builtins/oauth-token-refresh.js +19 -20
  21. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
  22. package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
  23. package/src/infrastructure/admin-auth-middleware.js +5 -43
  24. package/src/infrastructure/admin-script-router.js +38 -32
  25. package/src/infrastructure/script-executor-handler.js +2 -2
  26. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  27. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  28. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  29. package/src/application/dry-run-http-interceptor.js +0 -296
  30. package/src/application/dry-run-repository-wrapper.js +0 -261
  31. package/src/application/schedule-management-use-case.js +0 -230
@@ -1,276 +0,0 @@
1
- const { ScheduleManagementUseCase } = require('../schedule-management-use-case');
2
-
3
- describe('ScheduleManagementUseCase', () => {
4
- let useCase;
5
- let mockCommands;
6
- let mockSchedulerAdapter;
7
- let mockScriptFactory;
8
-
9
- beforeEach(() => {
10
- mockCommands = {
11
- getScheduleByScriptName: jest.fn(),
12
- upsertSchedule: jest.fn(),
13
- updateScheduleAwsInfo: jest.fn(),
14
- deleteSchedule: jest.fn(),
15
- };
16
-
17
- mockSchedulerAdapter = {
18
- createSchedule: jest.fn(),
19
- deleteSchedule: jest.fn(),
20
- };
21
-
22
- mockScriptFactory = {
23
- has: jest.fn(),
24
- get: jest.fn(),
25
- };
26
-
27
- useCase = new ScheduleManagementUseCase({
28
- commands: mockCommands,
29
- schedulerAdapter: mockSchedulerAdapter,
30
- scriptFactory: mockScriptFactory,
31
- });
32
- });
33
-
34
- describe('getEffectiveSchedule', () => {
35
- it('should return database schedule when override exists', async () => {
36
- const dbSchedule = {
37
- scriptName: 'test-script',
38
- enabled: true,
39
- cronExpression: '0 9 * * *',
40
- timezone: 'UTC',
41
- };
42
-
43
- mockScriptFactory.has.mockReturnValue(true);
44
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
45
- mockCommands.getScheduleByScriptName.mockResolvedValue(dbSchedule);
46
-
47
- const result = await useCase.getEffectiveSchedule('test-script');
48
-
49
- expect(result.source).toBe('database');
50
- expect(result.schedule).toEqual(dbSchedule);
51
- });
52
-
53
- it('should return definition schedule when no database override', async () => {
54
- const definitionSchedule = {
55
- enabled: true,
56
- cronExpression: '0 12 * * *',
57
- timezone: 'America/New_York',
58
- };
59
-
60
- mockScriptFactory.has.mockReturnValue(true);
61
- mockScriptFactory.get.mockReturnValue({
62
- Definition: { schedule: definitionSchedule },
63
- });
64
- mockCommands.getScheduleByScriptName.mockResolvedValue(null);
65
-
66
- const result = await useCase.getEffectiveSchedule('test-script');
67
-
68
- expect(result.source).toBe('definition');
69
- expect(result.schedule.enabled).toBe(true);
70
- expect(result.schedule.cronExpression).toBe('0 12 * * *');
71
- });
72
-
73
- it('should return none when no schedule configured', async () => {
74
- mockScriptFactory.has.mockReturnValue(true);
75
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
76
- mockCommands.getScheduleByScriptName.mockResolvedValue(null);
77
-
78
- const result = await useCase.getEffectiveSchedule('test-script');
79
-
80
- expect(result.source).toBe('none');
81
- expect(result.schedule.enabled).toBe(false);
82
- });
83
-
84
- it('should throw error when script not found', async () => {
85
- mockScriptFactory.has.mockReturnValue(false);
86
-
87
- await expect(useCase.getEffectiveSchedule('non-existent'))
88
- .rejects.toThrow('Script "non-existent" not found');
89
- });
90
- });
91
-
92
- describe('upsertSchedule', () => {
93
- it('should create schedule and provision EventBridge when enabled', async () => {
94
- const savedSchedule = {
95
- scriptName: 'test-script',
96
- enabled: true,
97
- cronExpression: '0 12 * * *',
98
- timezone: 'UTC',
99
- };
100
-
101
- mockScriptFactory.has.mockReturnValue(true);
102
- mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
103
- mockSchedulerAdapter.createSchedule.mockResolvedValue({
104
- scheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
105
- scheduleName: 'frigg-script-test-script',
106
- });
107
- mockCommands.updateScheduleAwsInfo.mockResolvedValue({
108
- ...savedSchedule,
109
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
110
- });
111
-
112
- const result = await useCase.upsertSchedule('test-script', {
113
- enabled: true,
114
- cronExpression: '0 12 * * *',
115
- timezone: 'UTC',
116
- });
117
-
118
- expect(result.success).toBe(true);
119
- expect(result.schedule.scriptName).toBe('test-script');
120
- expect(mockSchedulerAdapter.createSchedule).toHaveBeenCalledWith({
121
- scriptName: 'test-script',
122
- cronExpression: '0 12 * * *',
123
- timezone: 'UTC',
124
- });
125
- expect(mockCommands.updateScheduleAwsInfo).toHaveBeenCalled();
126
- });
127
-
128
- it('should delete EventBridge schedule when disabling', async () => {
129
- const existingSchedule = {
130
- scriptName: 'test-script',
131
- enabled: false,
132
- cronExpression: null,
133
- timezone: 'UTC',
134
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
135
- };
136
-
137
- mockScriptFactory.has.mockReturnValue(true);
138
- mockCommands.upsertSchedule.mockResolvedValue(existingSchedule);
139
- mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
140
- mockCommands.updateScheduleAwsInfo.mockResolvedValue({
141
- ...existingSchedule,
142
- awsScheduleArn: null,
143
- });
144
-
145
- const result = await useCase.upsertSchedule('test-script', {
146
- enabled: false,
147
- });
148
-
149
- expect(result.success).toBe(true);
150
- expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
151
- });
152
-
153
- it('should handle scheduler errors gracefully', async () => {
154
- const savedSchedule = {
155
- scriptName: 'test-script',
156
- enabled: true,
157
- cronExpression: '0 12 * * *',
158
- timezone: 'UTC',
159
- };
160
-
161
- mockScriptFactory.has.mockReturnValue(true);
162
- mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
163
- mockSchedulerAdapter.createSchedule.mockRejectedValue(
164
- new Error('AWS Scheduler API error')
165
- );
166
-
167
- const result = await useCase.upsertSchedule('test-script', {
168
- enabled: true,
169
- cronExpression: '0 12 * * *',
170
- });
171
-
172
- // Should succeed with warning, not fail
173
- expect(result.success).toBe(true);
174
- expect(result.schedulerWarning).toBe('AWS Scheduler API error');
175
- });
176
-
177
- it('should throw error when script not found', async () => {
178
- mockScriptFactory.has.mockReturnValue(false);
179
-
180
- await expect(useCase.upsertSchedule('non-existent', { enabled: true }))
181
- .rejects.toThrow('Script "non-existent" not found');
182
- });
183
-
184
- it('should throw error when enabled without cronExpression', async () => {
185
- mockScriptFactory.has.mockReturnValue(true);
186
-
187
- await expect(useCase.upsertSchedule('test-script', { enabled: true }))
188
- .rejects.toThrow('cronExpression is required when enabled is true');
189
- });
190
- });
191
-
192
- describe('deleteSchedule', () => {
193
- it('should delete schedule and EventBridge rule', async () => {
194
- const deletedSchedule = {
195
- scriptName: 'test-script',
196
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
197
- };
198
-
199
- mockScriptFactory.has.mockReturnValue(true);
200
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
201
- mockCommands.deleteSchedule.mockResolvedValue({
202
- deletedCount: 1,
203
- deleted: deletedSchedule,
204
- });
205
- mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
206
-
207
- const result = await useCase.deleteSchedule('test-script');
208
-
209
- expect(result.success).toBe(true);
210
- expect(result.deletedCount).toBe(1);
211
- expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
212
- });
213
-
214
- it('should not call scheduler when no AWS rule exists', async () => {
215
- mockScriptFactory.has.mockReturnValue(true);
216
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
217
- mockCommands.deleteSchedule.mockResolvedValue({
218
- deletedCount: 1,
219
- deleted: { scriptName: 'test-script' }, // No awsScheduleArn
220
- });
221
-
222
- const result = await useCase.deleteSchedule('test-script');
223
-
224
- expect(result.success).toBe(true);
225
- expect(mockSchedulerAdapter.deleteSchedule).not.toHaveBeenCalled();
226
- });
227
-
228
- it('should handle scheduler delete errors gracefully', async () => {
229
- mockScriptFactory.has.mockReturnValue(true);
230
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
231
- mockCommands.deleteSchedule.mockResolvedValue({
232
- deletedCount: 1,
233
- deleted: {
234
- scriptName: 'test-script',
235
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
236
- },
237
- });
238
- mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
239
- new Error('AWS delete failed')
240
- );
241
-
242
- const result = await useCase.deleteSchedule('test-script');
243
-
244
- expect(result.success).toBe(true);
245
- expect(result.schedulerWarning).toBe('AWS delete failed');
246
- });
247
-
248
- it('should return effective schedule after deletion', async () => {
249
- const definitionSchedule = {
250
- enabled: true,
251
- cronExpression: '0 6 * * *',
252
- };
253
-
254
- mockScriptFactory.has.mockReturnValue(true);
255
- mockScriptFactory.get.mockReturnValue({
256
- Definition: { schedule: definitionSchedule },
257
- });
258
- mockCommands.deleteSchedule.mockResolvedValue({
259
- deletedCount: 1,
260
- deleted: { scriptName: 'test-script' },
261
- });
262
-
263
- const result = await useCase.deleteSchedule('test-script');
264
-
265
- expect(result.effectiveSchedule.source).toBe('definition');
266
- expect(result.effectiveSchedule.enabled).toBe(true);
267
- });
268
-
269
- it('should throw error when script not found', async () => {
270
- mockScriptFactory.has.mockReturnValue(false);
271
-
272
- await expect(useCase.deleteSchedule('non-existent'))
273
- .rejects.toThrow('Script "non-existent" not found');
274
- });
275
- });
276
- });
@@ -1,296 +0,0 @@
1
- /**
2
- * Dry-Run HTTP Interceptor
3
- *
4
- * Creates a mock HTTP client that logs requests instead of executing them.
5
- * Used to intercept API module calls during dry-run.
6
- */
7
-
8
- /**
9
- * Sanitize headers to remove authentication tokens
10
- * @param {Object} headers - HTTP headers
11
- * @returns {Object} Sanitized headers
12
- */
13
- function sanitizeHeaders(headers) {
14
- if (!headers || typeof headers !== 'object') {
15
- return {};
16
- }
17
-
18
- const safe = { ...headers };
19
-
20
- // Remove common auth headers
21
- const sensitiveHeaders = [
22
- 'authorization',
23
- 'Authorization',
24
- 'x-api-key',
25
- 'X-API-Key',
26
- 'x-auth-token',
27
- 'X-Auth-Token',
28
- 'api-key',
29
- 'API-Key',
30
- 'apikey',
31
- 'ApiKey',
32
- 'token',
33
- 'Token',
34
- ];
35
-
36
- for (const header of sensitiveHeaders) {
37
- if (safe[header]) {
38
- safe[header] = '[REDACTED]';
39
- }
40
- }
41
-
42
- return safe;
43
- }
44
-
45
- /**
46
- * Detect service name from base URL
47
- * @param {string} baseURL - Base URL of the API
48
- * @returns {string} Service name
49
- */
50
- function detectService(baseURL) {
51
- if (!baseURL) return 'unknown';
52
-
53
- const url = baseURL.toLowerCase();
54
-
55
- // CRM Systems
56
- if (url.includes('hubspot') || url.includes('hubapi')) return 'HubSpot';
57
- if (url.includes('salesforce')) return 'Salesforce';
58
- if (url.includes('pipedrive')) return 'Pipedrive';
59
- if (url.includes('zoho')) return 'Zoho CRM';
60
- if (url.includes('attio')) return 'Attio';
61
-
62
- // Communication
63
- if (url.includes('slack')) return 'Slack';
64
- if (url.includes('discord')) return 'Discord';
65
- if (url.includes('teams.microsoft')) return 'Microsoft Teams';
66
-
67
- // Project Management
68
- if (url.includes('asana')) return 'Asana';
69
- if (url.includes('monday')) return 'Monday.com';
70
- if (url.includes('trello')) return 'Trello';
71
- if (url.includes('clickup')) return 'ClickUp';
72
-
73
- // Storage
74
- if (url.includes('googleapis.com/drive')) return 'Google Drive';
75
- if (url.includes('dropbox')) return 'Dropbox';
76
- if (url.includes('box.com')) return 'Box';
77
-
78
- // Email & Marketing
79
- if (url.includes('sendgrid')) return 'SendGrid';
80
- if (url.includes('mailchimp')) return 'Mailchimp';
81
- if (url.includes('gmail')) return 'Gmail';
82
-
83
- // Accounting
84
- if (url.includes('quickbooks')) return 'QuickBooks';
85
- if (url.includes('xero')) return 'Xero';
86
-
87
- // Other
88
- if (url.includes('stripe')) return 'Stripe';
89
- if (url.includes('shopify')) return 'Shopify';
90
- if (url.includes('github')) return 'GitHub';
91
- if (url.includes('gitlab')) return 'GitLab';
92
-
93
- return 'unknown';
94
- }
95
-
96
- /**
97
- * Sanitize request data to remove sensitive information
98
- * @param {*} data - Request data
99
- * @returns {*} Sanitized data
100
- */
101
- function sanitizeData(data) {
102
- if (data === null || data === undefined) {
103
- return data;
104
- }
105
-
106
- if (typeof data !== 'object') {
107
- return data;
108
- }
109
-
110
- if (Array.isArray(data)) {
111
- return data.map(sanitizeData);
112
- }
113
-
114
- const sanitized = {};
115
- for (const [key, value] of Object.entries(data)) {
116
- const lowerKey = key.toLowerCase();
117
-
118
- // Check if this is a leaf node that should be redacted
119
- const isSensitiveField =
120
- lowerKey === 'password' ||
121
- lowerKey === 'token' ||
122
- lowerKey === 'secret' ||
123
- lowerKey === 'apikey' ||
124
- lowerKey.endsWith('password') ||
125
- lowerKey.endsWith('token') ||
126
- lowerKey.endsWith('secret') ||
127
- lowerKey.endsWith('key') && !lowerKey.endsWith('publickey');
128
-
129
- // Only redact if it's a primitive value (not an object/array)
130
- if (isSensitiveField && typeof value !== 'object') {
131
- sanitized[key] = '[REDACTED]';
132
- continue;
133
- }
134
-
135
- // Recursively sanitize nested objects
136
- if (typeof value === 'object' && value !== null) {
137
- sanitized[key] = sanitizeData(value);
138
- } else {
139
- sanitized[key] = value;
140
- }
141
- }
142
-
143
- return sanitized;
144
- }
145
-
146
- /**
147
- * Create a dry-run HTTP client
148
- *
149
- * @param {Array} operationLog - Array to append logged HTTP requests
150
- * @returns {Object} Mock HTTP client compatible with axios interface
151
- */
152
- function createDryRunHttpClient(operationLog) {
153
- /**
154
- * Mock HTTP request handler
155
- * @param {Object} config - Request configuration
156
- * @returns {Promise<Object>} Mock response
157
- */
158
- const mockRequest = async (config) => {
159
- // Build full URL
160
- let fullUrl = config.url;
161
- if (config.baseURL && !config.url.startsWith('http')) {
162
- fullUrl = `${config.baseURL}${config.url.startsWith('/') ? '' : '/'}${config.url}`;
163
- }
164
-
165
- // Log the request that WOULD have been made
166
- const logEntry = {
167
- operation: 'HTTP_REQUEST',
168
- method: (config.method || 'GET').toUpperCase(),
169
- url: fullUrl,
170
- baseURL: config.baseURL,
171
- path: config.url,
172
- service: detectService(config.baseURL || fullUrl),
173
- headers: sanitizeHeaders(config.headers),
174
- timestamp: new Date().toISOString(),
175
- };
176
-
177
- // Include request data for write operations
178
- if (config.data && ['POST', 'PUT', 'PATCH'].includes(logEntry.method)) {
179
- logEntry.data = sanitizeData(config.data);
180
- }
181
-
182
- // Include query params
183
- if (config.params) {
184
- logEntry.params = sanitizeData(config.params);
185
- }
186
-
187
- operationLog.push(logEntry);
188
-
189
- // Return mock response
190
- return {
191
- status: 200,
192
- statusText: 'OK (Dry-Run)',
193
- data: {
194
- _dryRun: true,
195
- _message: 'This is a dry-run mock response',
196
- _wouldHaveExecuted: `${logEntry.method} ${fullUrl}`,
197
- _service: logEntry.service,
198
- },
199
- headers: {
200
- 'content-type': 'application/json',
201
- 'x-dry-run': 'true',
202
- },
203
- config,
204
- };
205
- };
206
-
207
- // Return axios-compatible interface
208
- return {
209
- request: mockRequest,
210
- get: (url, config = {}) => mockRequest({ ...config, method: 'GET', url }),
211
- post: (url, data, config = {}) => mockRequest({ ...config, method: 'POST', url, data }),
212
- put: (url, data, config = {}) => mockRequest({ ...config, method: 'PUT', url, data }),
213
- patch: (url, data, config = {}) =>
214
- mockRequest({ ...config, method: 'PATCH', url, data }),
215
- delete: (url, config = {}) => mockRequest({ ...config, method: 'DELETE', url }),
216
- head: (url, config = {}) => mockRequest({ ...config, method: 'HEAD', url }),
217
- options: (url, config = {}) => mockRequest({ ...config, method: 'OPTIONS', url }),
218
-
219
- // Axios-specific properties
220
- defaults: {
221
- headers: {
222
- common: {},
223
- get: {},
224
- post: {},
225
- put: {},
226
- patch: {},
227
- delete: {},
228
- },
229
- },
230
-
231
- // Interceptors (no-op in dry-run)
232
- interceptors: {
233
- request: { use: () => {}, eject: () => {} },
234
- response: { use: () => {}, eject: () => {} },
235
- },
236
- };
237
- }
238
-
239
- /**
240
- * Inject dry-run HTTP client into an integration instance
241
- *
242
- * @param {Object} integrationInstance - Integration instance from integrationFactory
243
- * @param {Object} dryRunHttpClient - Dry-run HTTP client
244
- */
245
- function injectDryRunHttpClient(integrationInstance, dryRunHttpClient) {
246
- if (!integrationInstance) {
247
- return;
248
- }
249
-
250
- // Inject into primary API module
251
- if (integrationInstance.primary?.api) {
252
- injectIntoApiModule(integrationInstance.primary.api, dryRunHttpClient);
253
- }
254
-
255
- // Inject into target API module
256
- if (integrationInstance.target?.api) {
257
- injectIntoApiModule(integrationInstance.target.api, dryRunHttpClient);
258
- }
259
- }
260
-
261
- /**
262
- * Inject dry-run HTTP client into an API module
263
- * @param {Object} apiModule - API module instance
264
- * @param {Object} dryRunHttpClient - Dry-run HTTP client
265
- */
266
- function injectIntoApiModule(apiModule, dryRunHttpClient) {
267
- // Common property names for HTTP clients in API modules
268
- const httpClientProps = [
269
- '_httpClient',
270
- 'httpClient',
271
- 'client',
272
- 'axios',
273
- 'request',
274
- 'api',
275
- 'http',
276
- ];
277
-
278
- for (const prop of httpClientProps) {
279
- if (apiModule[prop] && typeof apiModule[prop] === 'object') {
280
- apiModule[prop] = dryRunHttpClient;
281
- }
282
- }
283
-
284
- // Also check if the API module itself has request methods
285
- if (typeof apiModule.request === 'function') {
286
- Object.assign(apiModule, dryRunHttpClient);
287
- }
288
- }
289
-
290
- module.exports = {
291
- createDryRunHttpClient,
292
- injectDryRunHttpClient,
293
- sanitizeHeaders,
294
- sanitizeData,
295
- detectService,
296
- };