@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,221 @@
1
+ const { AdminScriptBase } = require('../application/admin-script-base');
2
+
3
+ /**
4
+ * OAuth Token Refresh Script
5
+ *
6
+ * Refreshes OAuth tokens for integrations that are near expiry.
7
+ * This helps prevent authentication failures due to expired tokens.
8
+ */
9
+ class OAuthTokenRefreshScript extends AdminScriptBase {
10
+ static Definition = {
11
+ name: 'oauth-token-refresh',
12
+ version: '1.0.0',
13
+ description: 'Refreshes OAuth tokens for integrations near expiry',
14
+ source: 'BUILTIN',
15
+
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ integrationIds: {
20
+ type: 'array',
21
+ items: { type: 'string' },
22
+ description: 'Specific integration IDs to refresh (optional, defaults to all)'
23
+ },
24
+ expiryThresholdHours: {
25
+ type: 'number',
26
+ default: 24,
27
+ description: 'Refresh tokens expiring within this many hours'
28
+ },
29
+ dryRun: {
30
+ type: 'boolean',
31
+ default: false,
32
+ description: 'Preview without making changes'
33
+ }
34
+ }
35
+ },
36
+
37
+ outputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ refreshed: { type: 'number' },
41
+ failed: { type: 'number' },
42
+ skipped: { type: 'number' },
43
+ details: { type: 'array' }
44
+ }
45
+ },
46
+
47
+ config: {
48
+ timeout: 600000, // 10 minutes
49
+ maxRetries: 1,
50
+ requiresIntegrationFactory: true, // Needs to call external APIs
51
+ },
52
+
53
+ display: {
54
+ label: 'OAuth Token Refresh',
55
+ description: 'Refresh OAuth tokens before they expire',
56
+ category: 'maintenance',
57
+ },
58
+ };
59
+
60
+ async execute(frigg, params = {}) {
61
+ const {
62
+ integrationIds = null,
63
+ expiryThresholdHours = 24,
64
+ dryRun = false
65
+ } = params;
66
+
67
+ const results = {
68
+ refreshed: 0,
69
+ failed: 0,
70
+ skipped: 0,
71
+ details: []
72
+ };
73
+
74
+ frigg.log('info', 'Starting OAuth token refresh', {
75
+ expiryThresholdHours,
76
+ dryRun,
77
+ specificIds: integrationIds?.length || 'all'
78
+ });
79
+
80
+ // Get integrations to check
81
+ let integrations;
82
+ if (integrationIds && integrationIds.length > 0) {
83
+ integrations = await Promise.all(
84
+ integrationIds.map(id => frigg.findIntegrationById(id).catch(() => null))
85
+ );
86
+ integrations = integrations.filter(Boolean);
87
+ } else {
88
+ // Get all integrations (this would need to be paginated for large deployments)
89
+ integrations = await this.getAllIntegrations(frigg);
90
+ }
91
+
92
+ frigg.log('info', `Found ${integrations.length} integrations to check`);
93
+
94
+ for (const integration of integrations) {
95
+ try {
96
+ const detail = await this.processIntegration(frigg, integration, {
97
+ expiryThresholdHours,
98
+ dryRun
99
+ });
100
+
101
+ results.details.push(detail);
102
+
103
+ if (detail.action === 'refreshed') {
104
+ results.refreshed++;
105
+ } else if (detail.action === 'skipped') {
106
+ results.skipped++;
107
+ } else if (detail.action === 'failed') {
108
+ results.failed++;
109
+ }
110
+ } catch (error) {
111
+ frigg.log('error', `Error processing integration ${integration.id}`, {
112
+ error: error.message
113
+ });
114
+ results.failed++;
115
+ results.details.push({
116
+ integrationId: integration.id,
117
+ action: 'failed',
118
+ reason: error.message
119
+ });
120
+ }
121
+ }
122
+
123
+ frigg.log('info', 'OAuth token refresh completed', {
124
+ refreshed: results.refreshed,
125
+ failed: results.failed,
126
+ skipped: results.skipped
127
+ });
128
+
129
+ return results;
130
+ }
131
+
132
+ async getAllIntegrations(frigg) {
133
+ // This is a simplified implementation
134
+ // In production, would need pagination for large datasets
135
+ return frigg.listIntegrations({});
136
+ }
137
+
138
+ async processIntegration(frigg, integration, options) {
139
+ const { expiryThresholdHours, dryRun } = options;
140
+
141
+ // Check prerequisites
142
+ const skipReason = this._checkRefreshPrerequisites(integration, expiryThresholdHours);
143
+ if (skipReason) {
144
+ return this._createResult(integration.id, 'skipped', skipReason);
145
+ }
146
+
147
+ // Handle dry run
148
+ if (dryRun) {
149
+ frigg.log('info', `[DRY RUN] Would refresh token for ${integration.id}`);
150
+ return this._createResult(integration.id, 'skipped', 'Dry run - would have refreshed');
151
+ }
152
+
153
+ // Perform refresh
154
+ return this._performTokenRefresh(frigg, integration);
155
+ }
156
+
157
+ /**
158
+ * Check if integration meets prerequisites for token refresh
159
+ * @private
160
+ * @returns {string|null} Skip reason or null if eligible
161
+ */
162
+ _checkRefreshPrerequisites(integration, expiryThresholdHours) {
163
+ if (!integration.config?.credentials?.access_token) {
164
+ return 'No OAuth credentials found';
165
+ }
166
+
167
+ const expiresAt = integration.config?.credentials?.expires_at;
168
+ if (!expiresAt) {
169
+ return 'No expiry time found';
170
+ }
171
+
172
+ const expiryTime = new Date(expiresAt);
173
+ const thresholdTime = new Date(Date.now() + (expiryThresholdHours * 60 * 60 * 1000));
174
+
175
+ if (expiryTime > thresholdTime) {
176
+ return 'Token not near expiry';
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Perform the actual token refresh
184
+ * @private
185
+ */
186
+ async _performTokenRefresh(frigg, integration) {
187
+ const expiresAt = integration.config?.credentials?.expires_at;
188
+
189
+ try {
190
+ const instance = await frigg.instantiate(integration.id);
191
+
192
+ if (!instance.primary?.api?.refreshAccessToken) {
193
+ return this._createResult(integration.id, 'skipped', 'API does not support token refresh');
194
+ }
195
+
196
+ await instance.primary.api.refreshAccessToken();
197
+ frigg.log('info', `Refreshed token for integration ${integration.id}`);
198
+
199
+ return {
200
+ integrationId: integration.id,
201
+ action: 'refreshed',
202
+ previousExpiry: expiresAt
203
+ };
204
+ } catch (error) {
205
+ frigg.log('error', `Failed to refresh token for ${integration.id}`, {
206
+ error: error.message
207
+ });
208
+ return this._createResult(integration.id, 'failed', error.message);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Create a result object
214
+ * @private
215
+ */
216
+ _createResult(integrationId, action, reason) {
217
+ return { integrationId, action, reason };
218
+ }
219
+ }
220
+
221
+ module.exports = { OAuthTokenRefreshScript };
@@ -0,0 +1,148 @@
1
+ const { adminAuthMiddleware } = require('../admin-auth-middleware');
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', () => {
11
+ let mockReq;
12
+ let mockRes;
13
+ let mockNext;
14
+ let mockCommands;
15
+
16
+ beforeEach(() => {
17
+ mockReq = {
18
+ headers: {},
19
+ ip: '127.0.0.1',
20
+ };
21
+
22
+ mockRes = {
23
+ status: jest.fn().mockReturnThis(),
24
+ json: jest.fn(),
25
+ };
26
+
27
+ mockNext = jest.fn();
28
+
29
+ mockCommands = {
30
+ validateAdminApiKey: jest.fn(),
31
+ };
32
+
33
+ createAdminScriptCommands.mockReturnValue(mockCommands);
34
+ });
35
+
36
+ afterEach(() => {
37
+ jest.clearAllMocks();
38
+ });
39
+
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';
54
+
55
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
56
+
57
+ expect(mockRes.status).toHaveBeenCalledWith(401);
58
+ expect(mockRes.json).toHaveBeenCalledWith({
59
+ error: 'Missing or invalid Authorization header',
60
+ code: 'MISSING_AUTH',
61
+ });
62
+ expect(mockNext).not.toHaveBeenCalled();
63
+ });
64
+ });
65
+
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);
76
+
77
+ expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('invalid-key');
78
+ expect(mockRes.status).toHaveBeenCalledWith(401);
79
+ expect(mockRes.json).toHaveBeenCalledWith({
80
+ error: 'Invalid API key',
81
+ code: 'INVALID_API_KEY',
82
+ });
83
+ expect(mockNext).not.toHaveBeenCalled();
84
+ });
85
+
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
+ });
93
+
94
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
95
+
96
+ expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
97
+ expect(mockRes.status).toHaveBeenCalledWith(401);
98
+ expect(mockRes.json).toHaveBeenCalledWith({
99
+ error: 'API key has expired',
100
+ code: 'EXPIRED_API_KEY',
101
+ });
102
+ expect(mockNext).not.toHaveBeenCalled();
103
+ });
104
+
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
+ });
116
+
117
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
118
+
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
+ expect(mockNext).toHaveBeenCalled();
127
+ expect(mockRes.status).not.toHaveBeenCalled();
128
+ });
129
+ });
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
+ });