@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
@@ -54,7 +54,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
54
54
  config: {
55
55
  timeout: 900000, // 15 minutes
56
56
  maxRetries: 0,
57
- requiresIntegrationFactory: true,
57
+ requireIntegrationInstance: true,
58
58
  },
59
59
 
60
60
  schedule: {
@@ -62,14 +62,13 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
62
62
  cronExpression: 'cron(0 6 * * ? *)', // Daily at 6 AM UTC
63
63
  },
64
64
 
65
+ // UI-specific overrides
65
66
  display: {
66
- label: 'Integration Health Check',
67
- description: 'Check health and connectivity of integrations',
68
67
  category: 'maintenance',
69
68
  },
70
69
  };
71
70
 
72
- async execute(frigg, params = {}) {
71
+ async execute(params = {}) {
73
72
  const {
74
73
  integrationIds = null,
75
74
  checkCredentials = true,
@@ -84,7 +83,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
84
83
  results: []
85
84
  };
86
85
 
87
- frigg.log('info', 'Starting integration health check', {
86
+ this.context.log('info', 'Starting integration health check', {
88
87
  checkCredentials,
89
88
  checkConnectivity,
90
89
  updateStatus,
@@ -95,17 +94,17 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
95
94
  let integrations;
96
95
  if (integrationIds && integrationIds.length > 0) {
97
96
  integrations = await Promise.all(
98
- integrationIds.map(id => frigg.findIntegrationById(id).catch(() => null))
97
+ integrationIds.map(id => this.context.findIntegrationById(id).catch(() => null))
99
98
  );
100
99
  integrations = integrations.filter(Boolean);
101
100
  } else {
102
- integrations = await this.getAllIntegrations(frigg);
101
+ integrations = await this.getAllIntegrations();
103
102
  }
104
103
 
105
- frigg.log('info', `Checking ${integrations.length} integrations`);
104
+ this.context.log('info', `Checking ${integrations.length} integrations`);
106
105
 
107
106
  for (const integration of integrations) {
108
- const result = await this.checkIntegration(frigg, integration, {
107
+ const result = await this.checkIntegration(integration, {
109
108
  checkCredentials,
110
109
  checkConnectivity
111
110
  });
@@ -124,17 +123,17 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
124
123
  if (updateStatus && result.status !== 'unknown') {
125
124
  try {
126
125
  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}`);
126
+ await this.context.updateIntegrationStatus(integration.id, newStatus);
127
+ this.context.log('info', `Updated status for ${integration.id} to ${newStatus}`);
129
128
  } catch (error) {
130
- frigg.log('warn', `Failed to update status for ${integration.id}`, {
129
+ this.context.log('warn', `Failed to update status for ${integration.id}`, {
131
130
  error: error.message
132
131
  });
133
132
  }
134
133
  }
135
134
  }
136
135
 
137
- frigg.log('info', 'Health check completed', {
136
+ this.context.log('info', 'Health check completed', {
138
137
  healthy: summary.healthy,
139
138
  unhealthy: summary.unhealthy,
140
139
  unknown: summary.unknown
@@ -143,19 +142,19 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
143
142
  return summary;
144
143
  }
145
144
 
146
- async getAllIntegrations(frigg) {
147
- return frigg.listIntegrations({});
145
+ async getAllIntegrations() {
146
+ return this.context.listIntegrations({});
148
147
  }
149
148
 
150
- async checkIntegration(frigg, integration, options) {
149
+ async checkIntegration(integration, options) {
151
150
  const { checkCredentials, checkConnectivity } = options;
152
151
  const result = this._createCheckResult(integration);
153
152
 
154
153
  try {
155
- await this._runChecks(frigg, integration, result, { checkCredentials, checkConnectivity });
154
+ await this._runChecks(integration, result, { checkCredentials, checkConnectivity });
156
155
  this._determineOverallStatus(result);
157
156
  } catch (error) {
158
- this._handleCheckError(frigg, integration, result, error);
157
+ this._handleCheckError(integration, result, error);
159
158
  }
160
159
 
161
160
  return result;
@@ -179,7 +178,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
179
178
  * Run all requested checks
180
179
  * @private
181
180
  */
182
- async _runChecks(frigg, integration, result, options) {
181
+ async _runChecks(integration, result, options) {
183
182
  const { checkCredentials, checkConnectivity } = options;
184
183
 
185
184
  if (checkCredentials) {
@@ -187,7 +186,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
187
186
  }
188
187
 
189
188
  if (checkConnectivity) {
190
- this._addCheckResult(result, 'connectivity', await this.checkApiConnectivity(frigg, integration));
189
+ this._addCheckResult(result, 'connectivity', await this.checkApiConnectivity(integration));
191
190
  }
192
191
  }
193
192
 
@@ -214,8 +213,8 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
214
213
  * Handle check error and update result
215
214
  * @private
216
215
  */
217
- _handleCheckError(frigg, integration, result, error) {
218
- frigg.log('error', `Error checking integration ${integration.id}`, {
216
+ _handleCheckError(integration, result, error) {
217
+ this.context.log('error', `Error checking integration ${integration.id}`, {
219
218
  error: error.message
220
219
  });
221
220
  result.status = 'unknown';
@@ -246,12 +245,12 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
246
245
  return result;
247
246
  }
248
247
 
249
- async checkApiConnectivity(frigg, integration) {
248
+ async checkApiConnectivity(integration) {
250
249
  const result = { valid: true, issue: null, responseTime: null };
251
250
 
252
251
  try {
253
252
  const startTime = Date.now();
254
- const instance = await frigg.instantiate(integration.id);
253
+ const instance = await this.context.instantiate(integration.id);
255
254
 
256
255
  // Try to make a simple API call
257
256
  if (instance.primary?.api?.getAuthenticationInfo) {
@@ -47,17 +47,16 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
47
47
  config: {
48
48
  timeout: 600000, // 10 minutes
49
49
  maxRetries: 1,
50
- requiresIntegrationFactory: true, // Needs to call external APIs
50
+ requireIntegrationInstance: true, // Needs to call external APIs
51
51
  },
52
52
 
53
+ // UI-specific overrides
53
54
  display: {
54
- label: 'OAuth Token Refresh',
55
- description: 'Refresh OAuth tokens before they expire',
56
55
  category: 'maintenance',
57
56
  },
58
57
  };
59
58
 
60
- async execute(frigg, params = {}) {
59
+ async execute(params = {}) {
61
60
  const {
62
61
  integrationIds = null,
63
62
  expiryThresholdHours = 24,
@@ -71,7 +70,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
71
70
  details: []
72
71
  };
73
72
 
74
- frigg.log('info', 'Starting OAuth token refresh', {
73
+ this.context.log('info', 'Starting OAuth token refresh', {
75
74
  expiryThresholdHours,
76
75
  dryRun,
77
76
  specificIds: integrationIds?.length || 'all'
@@ -81,19 +80,19 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
81
80
  let integrations;
82
81
  if (integrationIds && integrationIds.length > 0) {
83
82
  integrations = await Promise.all(
84
- integrationIds.map(id => frigg.findIntegrationById(id).catch(() => null))
83
+ integrationIds.map(id => this.context.findIntegrationById(id).catch(() => null))
85
84
  );
86
85
  integrations = integrations.filter(Boolean);
87
86
  } else {
88
87
  // Get all integrations (this would need to be paginated for large deployments)
89
- integrations = await this.getAllIntegrations(frigg);
88
+ integrations = await this.getAllIntegrations();
90
89
  }
91
90
 
92
- frigg.log('info', `Found ${integrations.length} integrations to check`);
91
+ this.context.log('info', `Found ${integrations.length} integrations to check`);
93
92
 
94
93
  for (const integration of integrations) {
95
94
  try {
96
- const detail = await this.processIntegration(frigg, integration, {
95
+ const detail = await this.processIntegration(integration, {
97
96
  expiryThresholdHours,
98
97
  dryRun
99
98
  });
@@ -108,7 +107,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
108
107
  results.failed++;
109
108
  }
110
109
  } catch (error) {
111
- frigg.log('error', `Error processing integration ${integration.id}`, {
110
+ this.context.log('error', `Error processing integration ${integration.id}`, {
112
111
  error: error.message
113
112
  });
114
113
  results.failed++;
@@ -120,7 +119,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
120
119
  }
121
120
  }
122
121
 
123
- frigg.log('info', 'OAuth token refresh completed', {
122
+ this.context.log('info', 'OAuth token refresh completed', {
124
123
  refreshed: results.refreshed,
125
124
  failed: results.failed,
126
125
  skipped: results.skipped
@@ -129,13 +128,13 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
129
128
  return results;
130
129
  }
131
130
 
132
- async getAllIntegrations(frigg) {
131
+ async getAllIntegrations() {
133
132
  // This is a simplified implementation
134
133
  // In production, would need pagination for large datasets
135
- return frigg.listIntegrations({});
134
+ return this.context.listIntegrations({});
136
135
  }
137
136
 
138
- async processIntegration(frigg, integration, options) {
137
+ async processIntegration(integration, options) {
139
138
  const { expiryThresholdHours, dryRun } = options;
140
139
 
141
140
  // Check prerequisites
@@ -146,12 +145,12 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
146
145
 
147
146
  // Handle dry run
148
147
  if (dryRun) {
149
- frigg.log('info', `[DRY RUN] Would refresh token for ${integration.id}`);
148
+ this.context.log('info', `[DRY RUN] Would refresh token for ${integration.id}`);
150
149
  return this._createResult(integration.id, 'skipped', 'Dry run - would have refreshed');
151
150
  }
152
151
 
153
152
  // Perform refresh
154
- return this._performTokenRefresh(frigg, integration);
153
+ return this._performTokenRefresh(integration);
155
154
  }
156
155
 
157
156
  /**
@@ -183,18 +182,18 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
183
182
  * Perform the actual token refresh
184
183
  * @private
185
184
  */
186
- async _performTokenRefresh(frigg, integration) {
185
+ async _performTokenRefresh(integration) {
187
186
  const expiresAt = integration.config?.credentials?.expires_at;
188
187
 
189
188
  try {
190
- const instance = await frigg.instantiate(integration.id);
189
+ const instance = await this.context.instantiate(integration.id);
191
190
 
192
191
  if (!instance.primary?.api?.refreshAccessToken) {
193
192
  return this._createResult(integration.id, 'skipped', 'API does not support token refresh');
194
193
  }
195
194
 
196
195
  await instance.primary.api.refreshAccessToken();
197
- frigg.log('info', `Refreshed token for integration ${integration.id}`);
196
+ this.context.log('info', `Refreshed token for integration ${integration.id}`);
198
197
 
199
198
  return {
200
199
  integrationId: integration.id,
@@ -202,7 +201,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
202
201
  previousExpiry: expiresAt
203
202
  };
204
203
  } catch (error) {
205
- frigg.log('error', `Failed to refresh token for ${integration.id}`, {
204
+ this.context.log('error', `Failed to refresh token for ${integration.id}`, {
206
205
  error: error.message
207
206
  });
208
207
  return this._createResult(integration.id, 'failed', error.message);
@@ -1,22 +1,17 @@
1
- const { adminAuthMiddleware } = require('../admin-auth-middleware');
1
+ const { validateAdminApiKey } = require('../admin-auth-middleware');
2
2
 
3
- // Mock the admin script commands
4
- jest.mock('@friggframework/core/application/commands/admin-script-commands', () => ({
5
- createAdminScriptCommands: jest.fn(),
6
- }));
7
-
8
- const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
9
-
10
- describe('adminAuthMiddleware', () => {
3
+ describe('validateAdminApiKey', () => {
11
4
  let mockReq;
12
5
  let mockRes;
13
6
  let mockNext;
14
- let mockCommands;
7
+ let originalEnv;
15
8
 
16
9
  beforeEach(() => {
10
+ originalEnv = process.env.ADMIN_API_KEY;
11
+ process.env.ADMIN_API_KEY = 'test-admin-key-123';
12
+
17
13
  mockReq = {
18
14
  headers: {},
19
- ip: '127.0.0.1',
20
15
  };
21
16
 
22
17
  mockRes = {
@@ -25,124 +20,66 @@ describe('adminAuthMiddleware', () => {
25
20
  };
26
21
 
27
22
  mockNext = jest.fn();
28
-
29
- mockCommands = {
30
- validateAdminApiKey: jest.fn(),
31
- };
32
-
33
- createAdminScriptCommands.mockReturnValue(mockCommands);
34
23
  });
35
24
 
36
25
  afterEach(() => {
26
+ if (originalEnv) {
27
+ process.env.ADMIN_API_KEY = originalEnv;
28
+ } else {
29
+ delete process.env.ADMIN_API_KEY;
30
+ }
37
31
  jest.clearAllMocks();
38
32
  });
39
33
 
40
- describe('Authorization header validation', () => {
41
- it('should reject request without Authorization header', async () => {
42
- await adminAuthMiddleware(mockReq, mockRes, mockNext);
43
-
44
- expect(mockRes.status).toHaveBeenCalledWith(401);
45
- expect(mockRes.json).toHaveBeenCalledWith({
46
- error: 'Missing or invalid Authorization header',
47
- code: 'MISSING_AUTH',
48
- });
49
- expect(mockNext).not.toHaveBeenCalled();
50
- });
51
-
52
- it('should reject request with invalid Authorization format', async () => {
53
- mockReq.headers.authorization = 'InvalidFormat key123';
34
+ describe('Environment configuration', () => {
35
+ it('should reject when ADMIN_API_KEY not configured', () => {
36
+ delete process.env.ADMIN_API_KEY;
54
37
 
55
- await adminAuthMiddleware(mockReq, mockRes, mockNext);
38
+ validateAdminApiKey(mockReq, mockRes, mockNext);
56
39
 
57
40
  expect(mockRes.status).toHaveBeenCalledWith(401);
58
41
  expect(mockRes.json).toHaveBeenCalledWith({
59
- error: 'Missing or invalid Authorization header',
60
- code: 'MISSING_AUTH',
42
+ error: 'Unauthorized',
43
+ message: 'Admin API key not configured',
61
44
  });
62
45
  expect(mockNext).not.toHaveBeenCalled();
63
46
  });
64
47
  });
65
48
 
66
- describe('API key validation', () => {
67
- it('should reject request with invalid API key', async () => {
68
- mockReq.headers.authorization = 'Bearer invalid-key';
69
- mockCommands.validateAdminApiKey.mockResolvedValue({
70
- error: 401,
71
- reason: 'Invalid API key',
72
- code: 'INVALID_API_KEY',
73
- });
74
-
75
- await adminAuthMiddleware(mockReq, mockRes, mockNext);
49
+ describe('Header validation', () => {
50
+ it('should reject request without x-frigg-admin-api-key header', () => {
51
+ validateAdminApiKey(mockReq, mockRes, mockNext);
76
52
 
77
- expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('invalid-key');
78
53
  expect(mockRes.status).toHaveBeenCalledWith(401);
79
54
  expect(mockRes.json).toHaveBeenCalledWith({
80
- error: 'Invalid API key',
81
- code: 'INVALID_API_KEY',
55
+ error: 'Unauthorized',
56
+ message: 'x-frigg-admin-api-key header required',
82
57
  });
83
58
  expect(mockNext).not.toHaveBeenCalled();
84
59
  });
60
+ });
85
61
 
86
- it('should reject request with expired API key', async () => {
87
- mockReq.headers.authorization = 'Bearer expired-key';
88
- mockCommands.validateAdminApiKey.mockResolvedValue({
89
- error: 401,
90
- reason: 'API key has expired',
91
- code: 'EXPIRED_API_KEY',
92
- });
62
+ describe('API key validation', () => {
63
+ it('should reject request with invalid API key', () => {
64
+ mockReq.headers['x-frigg-admin-api-key'] = 'invalid-key';
93
65
 
94
- await adminAuthMiddleware(mockReq, mockRes, mockNext);
66
+ validateAdminApiKey(mockReq, mockRes, mockNext);
95
67
 
96
- expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
97
68
  expect(mockRes.status).toHaveBeenCalledWith(401);
98
69
  expect(mockRes.json).toHaveBeenCalledWith({
99
- error: 'API key has expired',
100
- code: 'EXPIRED_API_KEY',
70
+ error: 'Unauthorized',
71
+ message: 'Invalid admin API key',
101
72
  });
102
73
  expect(mockNext).not.toHaveBeenCalled();
103
74
  });
104
75
 
105
- it('should accept request with valid API key', async () => {
106
- const validKey = 'valid-api-key-123';
107
- mockReq.headers.authorization = `Bearer ${validKey}`;
108
- mockCommands.validateAdminApiKey.mockResolvedValue({
109
- valid: true,
110
- apiKey: {
111
- id: 'key-id-1',
112
- name: 'test-key',
113
- keyLast4: 'e123',
114
- },
115
- });
76
+ it('should accept request with valid API key', () => {
77
+ mockReq.headers['x-frigg-admin-api-key'] = 'test-admin-key-123';
116
78
 
117
- await adminAuthMiddleware(mockReq, mockRes, mockNext);
79
+ validateAdminApiKey(mockReq, mockRes, mockNext);
118
80
 
119
- expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith(validKey);
120
- expect(mockReq.adminApiKey).toBeDefined();
121
- expect(mockReq.adminApiKey.name).toBe('test-key');
122
- expect(mockReq.adminAudit).toBeDefined();
123
- expect(mockReq.adminAudit.apiKeyName).toBe('test-key');
124
- expect(mockReq.adminAudit.apiKeyLast4).toBe('e123');
125
- expect(mockReq.adminAudit.ipAddress).toBe('127.0.0.1');
126
81
  expect(mockNext).toHaveBeenCalled();
127
82
  expect(mockRes.status).not.toHaveBeenCalled();
128
83
  });
129
84
  });
130
-
131
- describe('Error handling', () => {
132
- it('should handle validation errors gracefully', async () => {
133
- mockReq.headers.authorization = 'Bearer some-key';
134
- mockCommands.validateAdminApiKey.mockRejectedValue(
135
- new Error('Database error')
136
- );
137
-
138
- await adminAuthMiddleware(mockReq, mockRes, mockNext);
139
-
140
- expect(mockRes.status).toHaveBeenCalledWith(500);
141
- expect(mockRes.json).toHaveBeenCalledWith({
142
- error: 'Authentication failed',
143
- code: 'AUTH_ERROR',
144
- });
145
- expect(mockNext).not.toHaveBeenCalled();
146
- });
147
- });
148
85
  });