@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,49 @@
1
+ const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
2
+
3
+ /**
4
+ * Admin API Key Authentication Middleware
5
+ *
6
+ * Validates admin API keys for script endpoints.
7
+ * Expects: Authorization: Bearer <api-key>
8
+ */
9
+ async function adminAuthMiddleware(req, res, next) {
10
+ try {
11
+ const authHeader = req.headers.authorization;
12
+
13
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
14
+ return res.status(401).json({
15
+ error: 'Missing or invalid Authorization header',
16
+ code: 'MISSING_AUTH'
17
+ });
18
+ }
19
+
20
+ const apiKey = authHeader.substring(7); // Remove 'Bearer '
21
+ const commands = createAdminScriptCommands();
22
+ const result = await commands.validateAdminApiKey(apiKey);
23
+
24
+ if (result.error) {
25
+ return res.status(result.error).json({
26
+ error: result.reason,
27
+ code: result.code
28
+ });
29
+ }
30
+
31
+ // Attach validated key info to request for audit trail
32
+ req.adminApiKey = result.apiKey;
33
+ req.adminAudit = {
34
+ apiKeyName: result.apiKey.name,
35
+ apiKeyLast4: result.apiKey.keyLast4,
36
+ ipAddress: req.ip || req.connection?.remoteAddress || 'unknown'
37
+ };
38
+
39
+ next();
40
+ } catch (error) {
41
+ console.error('Admin auth middleware error:', error);
42
+ res.status(500).json({
43
+ error: 'Authentication failed',
44
+ code: 'AUTH_ERROR'
45
+ });
46
+ }
47
+ }
48
+
49
+ module.exports = { adminAuthMiddleware };
@@ -0,0 +1,311 @@
1
+ const express = require('express');
2
+ const serverless = require('serverless-http');
3
+ const { adminAuthMiddleware } = require('./admin-auth-middleware');
4
+ const { getScriptFactory } = require('../application/script-factory');
5
+ const { createScriptRunner } = require('../application/script-runner');
6
+ const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
7
+ const { QueuerUtil } = require('@friggframework/core/queues');
8
+ const { createSchedulerAdapter } = require('../adapters/scheduler-adapter-factory');
9
+ const { ScheduleManagementUseCase } = require('../application/schedule-management-use-case');
10
+
11
+ const router = express.Router();
12
+
13
+ // Apply auth middleware to all admin routes
14
+ router.use(adminAuthMiddleware);
15
+
16
+ /**
17
+ * Create ScheduleManagementUseCase instance
18
+ * @private
19
+ */
20
+ function createScheduleManagementUseCase() {
21
+ return new ScheduleManagementUseCase({
22
+ commands: createAdminScriptCommands(),
23
+ schedulerAdapter: createSchedulerAdapter(),
24
+ scriptFactory: getScriptFactory(),
25
+ });
26
+ }
27
+
28
+ /**
29
+ * GET /admin/scripts
30
+ * List all registered scripts
31
+ */
32
+ router.get('/scripts', async (req, res) => {
33
+ try {
34
+ const factory = getScriptFactory();
35
+ const scripts = factory.getAll();
36
+
37
+ res.json({
38
+ scripts: scripts.map((s) => ({
39
+ name: s.name,
40
+ version: s.definition.version,
41
+ description: s.definition.description,
42
+ category: s.definition.display?.category || 'custom',
43
+ requiresIntegrationFactory:
44
+ s.definition.config?.requiresIntegrationFactory || false,
45
+ schedule: s.definition.schedule || null,
46
+ })),
47
+ });
48
+ } catch (error) {
49
+ console.error('Error listing scripts:', error);
50
+ res.status(500).json({ error: 'Failed to list scripts' });
51
+ }
52
+ });
53
+
54
+ /**
55
+ * GET /admin/scripts/:scriptName
56
+ * Get script details
57
+ */
58
+ router.get('/scripts/:scriptName', async (req, res) => {
59
+ try {
60
+ const { scriptName } = req.params;
61
+ const factory = getScriptFactory();
62
+
63
+ if (!factory.has(scriptName)) {
64
+ return res.status(404).json({
65
+ error: `Script "${scriptName}" not found`,
66
+ code: 'SCRIPT_NOT_FOUND',
67
+ });
68
+ }
69
+
70
+ const scriptClass = factory.get(scriptName);
71
+ const definition = scriptClass.Definition;
72
+
73
+ res.json({
74
+ name: definition.name,
75
+ version: definition.version,
76
+ description: definition.description,
77
+ inputSchema: definition.inputSchema,
78
+ outputSchema: definition.outputSchema,
79
+ config: definition.config,
80
+ display: definition.display,
81
+ schedule: definition.schedule,
82
+ });
83
+ } catch (error) {
84
+ console.error('Error getting script:', error);
85
+ res.status(500).json({ error: 'Failed to get script details' });
86
+ }
87
+ });
88
+
89
+ /**
90
+ * POST /admin/scripts/:scriptName/execute
91
+ * Execute a script (sync, async, or dry-run)
92
+ */
93
+ router.post('/scripts/:scriptName/execute', async (req, res) => {
94
+ try {
95
+ const { scriptName } = req.params;
96
+ const { params = {}, mode = 'async', dryRun = false } = req.body;
97
+ const factory = getScriptFactory();
98
+
99
+ if (!factory.has(scriptName)) {
100
+ return res.status(404).json({
101
+ error: `Script "${scriptName}" not found`,
102
+ code: 'SCRIPT_NOT_FOUND',
103
+ });
104
+ }
105
+
106
+ // Dry-run always executes synchronously
107
+ if (dryRun) {
108
+ const runner = createScriptRunner();
109
+ const result = await runner.execute(scriptName, params, {
110
+ trigger: 'MANUAL',
111
+ mode: 'sync',
112
+ dryRun: true,
113
+ audit: req.adminAudit,
114
+ });
115
+ return res.json(result);
116
+ }
117
+
118
+ if (mode === 'sync') {
119
+ // Synchronous execution - wait for result
120
+ const runner = createScriptRunner();
121
+ const result = await runner.execute(scriptName, params, {
122
+ trigger: 'MANUAL',
123
+ mode: 'sync',
124
+ audit: req.adminAudit,
125
+ });
126
+ return res.json(result);
127
+ }
128
+
129
+ // Async execution - queue and return immediately
130
+ const commands = createAdminScriptCommands();
131
+ const execution = await commands.createScriptExecution({
132
+ scriptName,
133
+ scriptVersion: factory.get(scriptName).Definition.version,
134
+ trigger: 'MANUAL',
135
+ mode: 'async',
136
+ input: params,
137
+ audit: req.adminAudit,
138
+ });
139
+
140
+ // Queue the execution
141
+ await QueuerUtil.send(
142
+ {
143
+ scriptName,
144
+ executionId: execution.id,
145
+ trigger: 'MANUAL',
146
+ params,
147
+ },
148
+ process.env.ADMIN_SCRIPT_QUEUE_URL
149
+ );
150
+
151
+ res.status(202).json({
152
+ executionId: execution.id,
153
+ status: 'PENDING',
154
+ scriptName,
155
+ message: 'Script queued for execution',
156
+ });
157
+ } catch (error) {
158
+ console.error('Error executing script:', error);
159
+ res.status(500).json({ error: 'Failed to execute script' });
160
+ }
161
+ });
162
+
163
+ /**
164
+ * GET /admin/executions/:executionId
165
+ * Get execution status
166
+ */
167
+ router.get('/executions/:executionId', async (req, res) => {
168
+ try {
169
+ const { executionId } = req.params;
170
+ const commands = createAdminScriptCommands();
171
+ const execution = await commands.findScriptExecutionById(executionId);
172
+
173
+ if (execution.error) {
174
+ return res.status(execution.error).json({
175
+ error: execution.reason,
176
+ code: execution.code,
177
+ });
178
+ }
179
+
180
+ res.json(execution);
181
+ } catch (error) {
182
+ console.error('Error getting execution:', error);
183
+ res.status(500).json({ error: 'Failed to get execution' });
184
+ }
185
+ });
186
+
187
+ /**
188
+ * GET /admin/executions
189
+ * List recent executions
190
+ */
191
+ router.get('/executions', async (req, res) => {
192
+ try {
193
+ const { scriptName, status, limit = 50 } = req.query;
194
+ const commands = createAdminScriptCommands();
195
+
196
+ const executions = await commands.findRecentExecutions({
197
+ scriptName,
198
+ status,
199
+ limit: Number.parseInt(limit, 10),
200
+ });
201
+
202
+ res.json({ executions });
203
+ } catch (error) {
204
+ console.error('Error listing executions:', error);
205
+ res.status(500).json({ error: 'Failed to list executions' });
206
+ }
207
+ });
208
+
209
+ /**
210
+ * GET /admin/scripts/:scriptName/schedule
211
+ * Get effective schedule (DB override > Definition default > none)
212
+ */
213
+ router.get('/scripts/:scriptName/schedule', async (req, res) => {
214
+ try {
215
+ const { scriptName } = req.params;
216
+ const useCase = createScheduleManagementUseCase();
217
+
218
+ const result = await useCase.getEffectiveSchedule(scriptName);
219
+
220
+ res.json({
221
+ source: result.source,
222
+ scriptName,
223
+ ...result.schedule,
224
+ });
225
+ } catch (error) {
226
+ if (error.code === 'SCRIPT_NOT_FOUND') {
227
+ return res.status(404).json({
228
+ error: error.message,
229
+ code: error.code,
230
+ });
231
+ }
232
+ console.error('Error getting schedule:', error);
233
+ res.status(500).json({ error: 'Failed to get schedule' });
234
+ }
235
+ });
236
+
237
+ /**
238
+ * PUT /admin/scripts/:scriptName/schedule
239
+ * Create or update schedule override
240
+ */
241
+ router.put('/scripts/:scriptName/schedule', async (req, res) => {
242
+ try {
243
+ const { scriptName } = req.params;
244
+ const { enabled, cronExpression, timezone } = req.body;
245
+ const useCase = createScheduleManagementUseCase();
246
+
247
+ const result = await useCase.upsertSchedule(scriptName, {
248
+ enabled,
249
+ cronExpression,
250
+ timezone,
251
+ });
252
+
253
+ res.json({
254
+ success: result.success,
255
+ schedule: {
256
+ source: 'database',
257
+ ...result.schedule,
258
+ },
259
+ ...(result.schedulerWarning && { schedulerWarning: result.schedulerWarning }),
260
+ });
261
+ } catch (error) {
262
+ if (error.code === 'SCRIPT_NOT_FOUND') {
263
+ return res.status(404).json({
264
+ error: error.message,
265
+ code: error.code,
266
+ });
267
+ }
268
+ if (error.code === 'INVALID_INPUT') {
269
+ return res.status(400).json({
270
+ error: error.message,
271
+ code: error.code,
272
+ });
273
+ }
274
+ console.error('Error updating schedule:', error);
275
+ res.status(500).json({ error: 'Failed to update schedule' });
276
+ }
277
+ });
278
+
279
+ /**
280
+ * DELETE /admin/scripts/:scriptName/schedule
281
+ * Remove schedule override (revert to Definition default)
282
+ */
283
+ router.delete('/scripts/:scriptName/schedule', async (req, res) => {
284
+ try {
285
+ const { scriptName } = req.params;
286
+ const useCase = createScheduleManagementUseCase();
287
+
288
+ const result = await useCase.deleteSchedule(scriptName);
289
+
290
+ res.json(result);
291
+ } catch (error) {
292
+ if (error.code === 'SCRIPT_NOT_FOUND') {
293
+ return res.status(404).json({
294
+ error: error.message,
295
+ code: error.code,
296
+ });
297
+ }
298
+ console.error('Error deleting schedule:', error);
299
+ res.status(500).json({ error: 'Failed to delete schedule' });
300
+ }
301
+ });
302
+
303
+ // Create Express app
304
+ const app = express();
305
+ app.use(express.json());
306
+ app.use('/admin', router);
307
+
308
+ // Export for Lambda
309
+ const handler = serverless(app);
310
+
311
+ module.exports = { router, app, handler };
@@ -0,0 +1,75 @@
1
+ const { createScriptRunner } = require('../application/script-runner');
2
+ const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
3
+
4
+ /**
5
+ * SQS Queue Worker Lambda Handler
6
+ *
7
+ * Processes script execution messages from AdminScriptQueue
8
+ */
9
+ async function handler(event) {
10
+ const results = [];
11
+
12
+ for (const record of event.Records) {
13
+ const message = JSON.parse(record.body);
14
+ const { scriptName, executionId, trigger, params } = message;
15
+
16
+ console.log(`Processing script: ${scriptName}, executionId: ${executionId}`);
17
+
18
+ try {
19
+ const runner = createScriptRunner();
20
+ const commands = createAdminScriptCommands();
21
+
22
+ // If executionId provided (async from API), update existing record
23
+ if (executionId) {
24
+ await commands.updateScriptExecutionStatus(executionId, 'RUNNING');
25
+ }
26
+
27
+ const result = await runner.execute(scriptName, params, {
28
+ trigger: trigger || 'QUEUE',
29
+ mode: 'async',
30
+ executionId, // Reuse existing if provided
31
+ });
32
+
33
+ console.log(
34
+ `Script completed: ${scriptName}, status: ${result.status}`
35
+ );
36
+ results.push({
37
+ scriptName,
38
+ status: result.status,
39
+ executionId: result.executionId,
40
+ });
41
+ } catch (error) {
42
+ console.error(`Script failed: ${scriptName}`, error);
43
+
44
+ // Try to update execution status if we have an ID
45
+ if (executionId) {
46
+ const commands = createAdminScriptCommands();
47
+ await commands
48
+ .completeScriptExecution(executionId, {
49
+ status: 'FAILED',
50
+ error: {
51
+ name: error.name,
52
+ message: error.message,
53
+ stack: error.stack,
54
+ },
55
+ })
56
+ .catch((e) =>
57
+ console.error('Failed to update execution:', e)
58
+ );
59
+ }
60
+
61
+ results.push({
62
+ scriptName,
63
+ status: 'FAILED',
64
+ error: error.message,
65
+ });
66
+ }
67
+ }
68
+
69
+ return {
70
+ statusCode: 200,
71
+ body: JSON.stringify({ processed: results.length, results }),
72
+ };
73
+ }
74
+
75
+ module.exports = { handler };