@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
@@ -15,12 +15,19 @@ jest.mock('../../application/script-runner');
15
15
  jest.mock('@friggframework/core/application/commands/admin-script-commands');
16
16
  jest.mock('@friggframework/core/queues');
17
17
  jest.mock('../../adapters/scheduler-adapter-factory');
18
+ jest.mock('../bootstrap', () => ({
19
+ bootstrapAdminScripts: () => ({ integrationFactory: {} }),
20
+ }));
18
21
 
19
22
  const { getScriptFactory } = require('../../application/script-factory');
20
23
  const { createScriptRunner } = require('../../application/script-runner');
21
- const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
24
+ const {
25
+ createAdminScriptCommands,
26
+ } = require('@friggframework/core/application/commands/admin-script-commands');
22
27
  const { QueuerUtil } = require('@friggframework/core/queues');
23
- const { createSchedulerAdapter } = require('../../adapters/scheduler-adapter-factory');
28
+ const {
29
+ createSchedulerAdapter,
30
+ } = require('../../adapters/scheduler-adapter-factory');
24
31
 
25
32
  describe('Admin Script Router', () => {
26
33
  let mockFactory;
@@ -56,7 +63,7 @@ describe('Admin Script Router', () => {
56
63
  mockCommands = {
57
64
  createAdminProcess: jest.fn(),
58
65
  findAdminProcessById: jest.fn(),
59
- findRecentExecutions: jest.fn(),
66
+ findAdminProcessesByName: jest.fn(),
60
67
  };
61
68
 
62
69
  mockSchedulerAdapter = {
@@ -118,7 +125,9 @@ describe('Admin Script Router', () => {
118
125
 
119
126
  describe('GET /admin/scripts/:scriptName', () => {
120
127
  it('should return script details', async () => {
121
- const response = await request(app).get('/admin/scripts/test-script');
128
+ const response = await request(app).get(
129
+ '/admin/scripts/test-script'
130
+ );
122
131
 
123
132
  expect(response.status).toBe(200);
124
133
  expect(response.body.name).toBe('test-script');
@@ -169,7 +178,8 @@ describe('Admin Script Router', () => {
169
178
  });
170
179
 
171
180
  it('should queue script for async execution', async () => {
172
- process.env.ADMIN_SCRIPT_QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123/test-queue';
181
+ process.env.ADMIN_SCRIPT_QUEUE_URL =
182
+ 'https://sqs.us-east-1.amazonaws.com/123/test-queue';
173
183
  mockCommands.createAdminProcess.mockResolvedValue({
174
184
  id: 'exec-456',
175
185
  });
@@ -195,7 +205,8 @@ describe('Admin Script Router', () => {
195
205
  });
196
206
 
197
207
  it('should default to async mode', async () => {
198
- process.env.ADMIN_SCRIPT_QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123/test-queue';
208
+ process.env.ADMIN_SCRIPT_QUEUE_URL =
209
+ 'https://sqs.us-east-1.amazonaws.com/123/test-queue';
199
210
  mockCommands.createAdminProcess.mockResolvedValue({
200
211
  id: 'exec-789',
201
212
  });
@@ -247,7 +258,9 @@ describe('Admin Script Router', () => {
247
258
  status: 'COMPLETED',
248
259
  });
249
260
 
250
- const response = await request(app).get('/admin/scripts/test-script/executions/exec-123');
261
+ const response = await request(app).get(
262
+ '/admin/scripts/test-script/executions/exec-123'
263
+ );
251
264
 
252
265
  expect(response.status).toBe(200);
253
266
  expect(response.body.id).toBe('exec-123');
@@ -272,33 +285,39 @@ describe('Admin Script Router', () => {
272
285
 
273
286
  describe('GET /admin/scripts/:scriptName/executions', () => {
274
287
  it('should list executions for specific script', async () => {
275
- mockCommands.findRecentExecutions.mockResolvedValue([
276
- { id: 'exec-1', scriptName: 'test-script', status: 'COMPLETED' },
277
- { id: 'exec-2', scriptName: 'test-script', status: 'RUNNING' },
288
+ mockCommands.findAdminProcessesByName.mockResolvedValue([
289
+ { id: 'exec-1', name: 'test-script', state: 'COMPLETED' },
290
+ { id: 'exec-2', name: 'test-script', state: 'RUNNING' },
278
291
  ]);
279
292
 
280
- const response = await request(app).get('/admin/scripts/test-script/executions');
293
+ const response = await request(app).get(
294
+ '/admin/scripts/test-script/executions'
295
+ );
281
296
 
282
297
  expect(response.status).toBe(200);
283
298
  expect(response.body.executions).toHaveLength(2);
284
- expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
285
- scriptName: 'test-script',
286
- limit: 50,
287
- });
299
+ expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
300
+ 'test-script',
301
+ {
302
+ limit: 50,
303
+ }
304
+ );
288
305
  });
289
306
 
290
- it('should accept query parameters', async () => {
291
- mockCommands.findRecentExecutions.mockResolvedValue([]);
307
+ it('should accept query parameters (status maps to state, limit is bounded)', async () => {
308
+ mockCommands.findAdminProcessesByName.mockResolvedValue([]);
292
309
 
293
310
  await request(app).get(
294
311
  '/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
295
312
  );
296
313
 
297
- expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
298
- scriptName: 'test-script',
299
- status: 'COMPLETED',
300
- limit: 10,
301
- });
314
+ expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
315
+ 'test-script',
316
+ {
317
+ limit: 10,
318
+ state: 'COMPLETED',
319
+ }
320
+ );
302
321
  });
303
322
  });
304
323
 
@@ -311,15 +330,20 @@ describe('Admin Script Router', () => {
311
330
  timezone: 'America/New_York',
312
331
  lastTriggeredAt: new Date('2025-01-01T09:00:00Z'),
313
332
  nextTriggerAt: new Date('2025-01-02T09:00:00Z'),
314
- externalScheduleId: 'arn:aws:events:us-east-1:123456789012:rule/test',
333
+ externalScheduleId:
334
+ 'arn:aws:events:us-east-1:123456789012:rule/test',
315
335
  externalScheduleName: 'test-script-schedule',
316
336
  createdAt: new Date('2025-01-01T00:00:00Z'),
317
337
  updatedAt: new Date('2025-01-01T00:00:00Z'),
318
338
  };
319
339
 
320
- mockCommands.getScheduleByScriptName = jest.fn().mockResolvedValue(dbSchedule);
340
+ mockCommands.getScheduleByScriptName = jest
341
+ .fn()
342
+ .mockResolvedValue(dbSchedule);
321
343
 
322
- const response = await request(app).get('/admin/scripts/test-script/schedule');
344
+ const response = await request(app).get(
345
+ '/admin/scripts/test-script/schedule'
346
+ );
323
347
 
324
348
  expect(response.status).toBe(200);
325
349
  expect(response.body.source).toBe('database');
@@ -329,7 +353,9 @@ describe('Admin Script Router', () => {
329
353
  });
330
354
 
331
355
  it('should return definition schedule when no database override', async () => {
332
- mockCommands.getScheduleByScriptName = jest.fn().mockResolvedValue(null);
356
+ mockCommands.getScheduleByScriptName = jest
357
+ .fn()
358
+ .mockResolvedValue(null);
333
359
 
334
360
  // Update test script to include schedule
335
361
  class ScheduledTestScript extends TestScript {
@@ -345,7 +371,9 @@ describe('Admin Script Router', () => {
345
371
 
346
372
  mockFactory.get.mockReturnValue(ScheduledTestScript);
347
373
 
348
- const response = await request(app).get('/admin/scripts/test-script/schedule');
374
+ const response = await request(app).get(
375
+ '/admin/scripts/test-script/schedule'
376
+ );
349
377
 
350
378
  expect(response.status).toBe(200);
351
379
  expect(response.body.source).toBe('definition');
@@ -355,9 +383,13 @@ describe('Admin Script Router', () => {
355
383
  });
356
384
 
357
385
  it('should return none when no schedule configured', async () => {
358
- mockCommands.getScheduleByScriptName = jest.fn().mockResolvedValue(null);
386
+ mockCommands.getScheduleByScriptName = jest
387
+ .fn()
388
+ .mockResolvedValue(null);
359
389
 
360
- const response = await request(app).get('/admin/scripts/test-script/schedule');
390
+ const response = await request(app).get(
391
+ '/admin/scripts/test-script/schedule'
392
+ );
361
393
 
362
394
  expect(response.status).toBe(200);
363
395
  expect(response.body.source).toBe('none');
@@ -389,7 +421,9 @@ describe('Admin Script Router', () => {
389
421
  updatedAt: new Date(),
390
422
  };
391
423
 
392
- mockCommands.upsertSchedule = jest.fn().mockResolvedValue(newSchedule);
424
+ mockCommands.upsertSchedule = jest
425
+ .fn()
426
+ .mockResolvedValue(newSchedule);
393
427
 
394
428
  const response = await request(app)
395
429
  .put('/admin/scripts/test-script/schedule')
@@ -424,7 +458,9 @@ describe('Admin Script Router', () => {
424
458
  updatedAt: new Date(),
425
459
  };
426
460
 
427
- mockCommands.upsertSchedule = jest.fn().mockResolvedValue(updatedSchedule);
461
+ mockCommands.upsertSchedule = jest
462
+ .fn()
463
+ .mockResolvedValue(updatedSchedule);
428
464
 
429
465
  const response = await request(app)
430
466
  .put('/admin/scripts/test-script/schedule')
@@ -487,10 +523,15 @@ describe('Admin Script Router', () => {
487
523
  updatedAt: new Date(),
488
524
  };
489
525
 
490
- mockCommands.upsertSchedule = jest.fn().mockResolvedValue(newSchedule);
491
- mockCommands.updateScheduleExternalInfo = jest.fn().mockResolvedValue(newSchedule);
526
+ mockCommands.upsertSchedule = jest
527
+ .fn()
528
+ .mockResolvedValue(newSchedule);
529
+ mockCommands.updateScheduleExternalInfo = jest
530
+ .fn()
531
+ .mockResolvedValue(newSchedule);
492
532
  mockSchedulerAdapter.createSchedule.mockResolvedValue({
493
- scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
533
+ scheduleArn:
534
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
494
535
  scheduleName: 'frigg-script-test-script',
495
536
  });
496
537
 
@@ -508,11 +549,16 @@ describe('Admin Script Router', () => {
508
549
  cronExpression: '0 12 * * *',
509
550
  timezone: 'America/Los_Angeles',
510
551
  });
511
- expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalledWith('test-script', {
512
- externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
552
+ expect(
553
+ mockCommands.updateScheduleExternalInfo
554
+ ).toHaveBeenCalledWith('test-script', {
555
+ externalScheduleId:
556
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
513
557
  externalScheduleName: 'frigg-script-test-script',
514
558
  });
515
- expect(response.body.schedule.externalScheduleId).toBe('arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script');
559
+ expect(response.body.schedule.externalScheduleId).toBe(
560
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script'
561
+ );
516
562
  });
517
563
 
518
564
  it('should delete EventBridge schedule when disabling existing schedule', async () => {
@@ -521,14 +567,19 @@ describe('Admin Script Router', () => {
521
567
  enabled: false,
522
568
  cronExpression: null,
523
569
  timezone: 'UTC',
524
- externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
570
+ externalScheduleId:
571
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
525
572
  externalScheduleName: 'frigg-script-test-script',
526
573
  createdAt: new Date(),
527
574
  updatedAt: new Date(),
528
575
  };
529
576
 
530
- mockCommands.upsertSchedule = jest.fn().mockResolvedValue(existingSchedule);
531
- mockCommands.updateScheduleExternalInfo = jest.fn().mockResolvedValue(existingSchedule);
577
+ mockCommands.upsertSchedule = jest
578
+ .fn()
579
+ .mockResolvedValue(existingSchedule);
580
+ mockCommands.updateScheduleExternalInfo = jest
581
+ .fn()
582
+ .mockResolvedValue(existingSchedule);
532
583
  mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
533
584
 
534
585
  const response = await request(app)
@@ -538,8 +589,12 @@ describe('Admin Script Router', () => {
538
589
  });
539
590
 
540
591
  expect(response.status).toBe(200);
541
- expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
542
- expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalledWith('test-script', {
592
+ expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
593
+ 'test-script'
594
+ );
595
+ expect(
596
+ mockCommands.updateScheduleExternalInfo
597
+ ).toHaveBeenCalledWith('test-script', {
543
598
  externalScheduleId: null,
544
599
  externalScheduleName: null,
545
600
  });
@@ -555,8 +610,12 @@ describe('Admin Script Router', () => {
555
610
  updatedAt: new Date(),
556
611
  };
557
612
 
558
- mockCommands.upsertSchedule = jest.fn().mockResolvedValue(newSchedule);
559
- mockSchedulerAdapter.createSchedule.mockRejectedValue(new Error('AWS Scheduler API error'));
613
+ mockCommands.upsertSchedule = jest
614
+ .fn()
615
+ .mockResolvedValue(newSchedule);
616
+ mockSchedulerAdapter.createSchedule.mockRejectedValue(
617
+ new Error('AWS Scheduler API error')
618
+ );
560
619
 
561
620
  const response = await request(app)
562
621
  .put('/admin/scripts/test-script/schedule')
@@ -568,7 +627,9 @@ describe('Admin Script Router', () => {
568
627
  // Request should succeed despite scheduler error
569
628
  expect(response.status).toBe(200);
570
629
  expect(response.body.success).toBe(true);
571
- expect(response.body.schedulerWarning).toBe('AWS Scheduler API error');
630
+ expect(response.body.schedulerWarning).toBe(
631
+ 'AWS Scheduler API error'
632
+ );
572
633
  });
573
634
  });
574
635
 
@@ -592,7 +653,9 @@ describe('Admin Script Router', () => {
592
653
  expect(response.body.success).toBe(true);
593
654
  expect(response.body.deletedCount).toBe(1);
594
655
  expect(response.body.message).toContain('removed');
595
- expect(mockCommands.deleteSchedule).toHaveBeenCalledWith('test-script');
656
+ expect(mockCommands.deleteSchedule).toHaveBeenCalledWith(
657
+ 'test-script'
658
+ );
596
659
  });
597
660
 
598
661
  it('should return definition schedule after deleting override', async () => {
@@ -636,7 +699,9 @@ describe('Admin Script Router', () => {
636
699
 
637
700
  expect(response.status).toBe(200);
638
701
  expect(response.body.deletedCount).toBe(0);
639
- expect(response.body.message).toContain('No schedule override found');
702
+ expect(response.body.message).toContain(
703
+ 'No schedule override found'
704
+ );
640
705
  });
641
706
 
642
707
  it('should return 404 for non-existent script', async () => {
@@ -658,7 +723,8 @@ describe('Admin Script Router', () => {
658
723
  scriptName: 'test-script',
659
724
  enabled: true,
660
725
  cronExpression: '0 12 * * *',
661
- externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
726
+ externalScheduleId:
727
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
662
728
  externalScheduleName: 'frigg-script-test-script',
663
729
  },
664
730
  });
@@ -669,7 +735,9 @@ describe('Admin Script Router', () => {
669
735
  );
670
736
 
671
737
  expect(response.status).toBe(200);
672
- expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
738
+ expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
739
+ 'test-script'
740
+ );
673
741
  });
674
742
 
675
743
  it('should not call scheduler when no external rule exists', async () => {
@@ -700,10 +768,13 @@ describe('Admin Script Router', () => {
700
768
  scriptName: 'test-script',
701
769
  enabled: true,
702
770
  cronExpression: '0 12 * * *',
703
- externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
771
+ externalScheduleId:
772
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
704
773
  },
705
774
  });
706
- mockSchedulerAdapter.deleteSchedule.mockRejectedValue(new Error('Scheduler delete failed'));
775
+ mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
776
+ new Error('Scheduler delete failed')
777
+ );
707
778
 
708
779
  const response = await request(app).delete(
709
780
  '/admin/scripts/test-script/schedule'
@@ -712,7 +783,9 @@ describe('Admin Script Router', () => {
712
783
  // Request should succeed despite scheduler error
713
784
  expect(response.status).toBe(200);
714
785
  expect(response.body.success).toBe(true);
715
- expect(response.body.schedulerWarning).toBe('Scheduler delete failed');
786
+ expect(response.body.schedulerWarning).toBe(
787
+ 'Scheduler delete failed'
788
+ );
716
789
  });
717
790
  });
718
791
  });
@@ -6,6 +6,8 @@
6
6
  * Expects: x-frigg-admin-api-key header
7
7
  */
8
8
 
9
- const { validateAdminApiKey } = require('@friggframework/core/handlers/middleware/admin-auth');
9
+ const {
10
+ validateAdminApiKey,
11
+ } = require('@friggframework/core/handlers/middleware/admin-auth');
10
12
 
11
13
  module.exports = { validateAdminApiKey };
@@ -3,10 +3,18 @@ const serverless = require('serverless-http');
3
3
  const { validateAdminApiKey } = require('./admin-auth-middleware');
4
4
  const { getScriptFactory } = require('../application/script-factory');
5
5
  const { createScriptRunner } = require('../application/script-runner');
6
- const { validateScriptInput } = require('../application/validate-script-input');
7
- const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
6
+ const {
7
+ validateScriptInput,
8
+ validateParams,
9
+ } = require('../application/validate-script-input');
10
+ const {
11
+ createAdminScriptCommands,
12
+ } = require('@friggframework/core/application/commands/admin-script-commands');
8
13
  const { QueuerUtil } = require('@friggframework/core/queues');
9
- const { createSchedulerAdapter } = require('../adapters/scheduler-adapter-factory');
14
+ const {
15
+ createSchedulerAdapter,
16
+ } = require('../adapters/scheduler-adapter-factory');
17
+ const { bootstrapAdminScripts } = require('./bootstrap');
10
18
  const {
11
19
  GetEffectiveScheduleUseCase,
12
20
  UpsertScheduleUseCase,
@@ -18,14 +26,51 @@ const router = express.Router();
18
26
  // Apply auth middleware to all admin routes
19
27
  router.use(validateAdminApiKey);
20
28
 
29
+ // Register the host app's admin scripts (and built-ins) before handling requests.
30
+ // Memoized, so this only does work on the first request per process.
31
+ router.use((_req, _res, next) => {
32
+ bootstrapAdminScripts();
33
+ next();
34
+ });
35
+
36
+ /**
37
+ * Build audit metadata for an execution from the request.
38
+ * @private
39
+ */
40
+ function buildAudit(req) {
41
+ const apiKey = req.headers['x-frigg-admin-api-key'];
42
+ const forwardedFor = (req.headers['x-forwarded-for'] || '')
43
+ .split(',')[0]
44
+ .trim();
45
+ return {
46
+ ipAddress: forwardedFor || req.ip || null,
47
+ apiKeyLast4: apiKey ? String(apiKey).slice(-4) : null,
48
+ };
49
+ }
50
+
21
51
  /**
22
52
  * Create schedule use case instances
23
53
  * @private
24
54
  */
25
55
  function createScheduleUseCases() {
26
56
  const commands = createAdminScriptCommands();
57
+
58
+ // The local adapter is in-memory only (schedules vanish on cold start), so it
59
+ // must never be the silent default in a deployed Lambda. Require an explicit
60
+ // provider when running on AWS; fall back to 'local' only for local dev/tests.
61
+ const schedulerType =
62
+ process.env.SCHEDULER_PROVIDER ||
63
+ (process.env.AWS_LAMBDA_FUNCTION_NAME ? null : 'local');
64
+ if (!schedulerType) {
65
+ const error = new Error(
66
+ 'SCHEDULER_PROVIDER is not configured. Set it (e.g. "aws") via appDefinition.admin.enableScheduling.'
67
+ );
68
+ error.code = 'SCHEDULER_NOT_CONFIGURED';
69
+ throw error;
70
+ }
71
+
27
72
  const schedulerAdapter = createSchedulerAdapter({
28
- type: process.env.SCHEDULER_PROVIDER || 'local',
73
+ type: schedulerType,
29
74
  targetLambdaArn: process.env.ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN,
30
75
  scheduleGroupName: process.env.ADMIN_SCRIPT_SCHEDULE_GROUP,
31
76
  roleArn: process.env.SCHEDULER_ROLE_ARN,
@@ -33,9 +78,20 @@ function createScheduleUseCases() {
33
78
  const scriptFactory = getScriptFactory();
34
79
 
35
80
  return {
36
- getEffectiveSchedule: new GetEffectiveScheduleUseCase({ commands, scriptFactory }),
37
- upsertSchedule: new UpsertScheduleUseCase({ commands, schedulerAdapter, scriptFactory }),
38
- deleteSchedule: new DeleteScheduleUseCase({ commands, schedulerAdapter, scriptFactory }),
81
+ getEffectiveSchedule: new GetEffectiveScheduleUseCase({
82
+ commands,
83
+ scriptFactory,
84
+ }),
85
+ upsertSchedule: new UpsertScheduleUseCase({
86
+ commands,
87
+ schedulerAdapter,
88
+ scriptFactory,
89
+ }),
90
+ deleteSchedule: new DeleteScheduleUseCase({
91
+ commands,
92
+ schedulerAdapter,
93
+ scriptFactory,
94
+ }),
39
95
  };
40
96
  }
41
97
 
@@ -142,11 +198,40 @@ router.post('/scripts/:scriptName', async (req, res) => {
142
198
  });
143
199
  }
144
200
 
201
+ const definition = factory.get(scriptName).Definition;
202
+
203
+ // Fail fast on invalid input instead of starting an execution that will error
204
+ const validation = validateParams(definition, params);
205
+ if (!validation.valid) {
206
+ return res.status(400).json({
207
+ error: `Invalid input: ${validation.errors.join(', ')}`,
208
+ code: 'INVALID_INPUT',
209
+ details: validation.errors,
210
+ });
211
+ }
212
+
213
+ const audit = buildAudit(req);
214
+
145
215
  if (mode === 'sync') {
146
- const runner = createScriptRunner();
216
+ // Sync runs inside the 30s API Lambda; long scripts must use async
217
+ const timeout = definition.config?.timeout;
218
+ if (
219
+ process.env.AWS_LAMBDA_FUNCTION_NAME &&
220
+ timeout &&
221
+ timeout > 25000
222
+ ) {
223
+ return res.status(400).json({
224
+ error: `Script "${scriptName}" timeout (${timeout}ms) exceeds the sync API limit. Use mode: "async".`,
225
+ code: 'SYNC_TIMEOUT_TOO_LONG',
226
+ });
227
+ }
228
+
229
+ const { integrationFactory } = bootstrapAdminScripts();
230
+ const runner = createScriptRunner({ integrationFactory });
147
231
  const result = await runner.execute(scriptName, params, {
148
232
  trigger: 'MANUAL',
149
233
  mode: 'sync',
234
+ audit,
150
235
  });
151
236
  return res.json(result);
152
237
  }
@@ -163,12 +248,21 @@ router.post('/scripts/:scriptName', async (req, res) => {
163
248
  const commands = createAdminScriptCommands();
164
249
  const execution = await commands.createAdminProcess({
165
250
  scriptName,
166
- scriptVersion: factory.get(scriptName).Definition.version,
251
+ scriptVersion: definition.version,
167
252
  trigger: 'MANUAL',
168
253
  mode: 'async',
169
254
  input: params,
255
+ audit,
170
256
  });
171
257
 
258
+ // Commands return an error object (never throw) — don't queue a broken execution
259
+ if (execution.error) {
260
+ return res.status(execution.error).json({
261
+ error: execution.reason || 'Failed to create execution record',
262
+ code: execution.code,
263
+ });
264
+ }
265
+
172
266
  // Queue the execution
173
267
  await QueuerUtil.send(
174
268
  {
@@ -226,10 +320,14 @@ router.get('/scripts/:scriptName/executions', async (req, res) => {
226
320
  const { status, limit = 50 } = req.query;
227
321
  const commands = createAdminScriptCommands();
228
322
 
229
- const executions = await commands.findRecentExecutions({
230
- scriptName,
231
- status,
232
- limit: Number.parseInt(limit, 10),
323
+ const parsedLimit = Number.parseInt(limit, 10);
324
+ const safeLimit = Number.isNaN(parsedLimit)
325
+ ? 50
326
+ : Math.min(Math.max(parsedLimit, 1), 200);
327
+
328
+ const executions = await commands.findAdminProcessesByName(scriptName, {
329
+ limit: safeLimit,
330
+ ...(status && { state: status }),
233
331
  });
234
332
 
235
333
  res.json({ executions });
@@ -262,6 +360,11 @@ router.get('/scripts/:scriptName/schedule', async (req, res) => {
262
360
  code: error.code,
263
361
  });
264
362
  }
363
+ if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
364
+ return res
365
+ .status(503)
366
+ .json({ error: error.message, code: error.code });
367
+ }
265
368
  console.error('Error getting schedule:', error);
266
369
  res.status(500).json({ error: 'Failed to get schedule' });
267
370
  }
@@ -289,7 +392,9 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
289
392
  source: 'database',
290
393
  ...result.schedule,
291
394
  },
292
- ...(result.schedulerWarning && { schedulerWarning: result.schedulerWarning }),
395
+ ...(result.schedulerWarning && {
396
+ schedulerWarning: result.schedulerWarning,
397
+ }),
293
398
  });
294
399
  } catch (error) {
295
400
  if (error.code === 'SCRIPT_NOT_FOUND') {
@@ -304,6 +409,11 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
304
409
  code: error.code,
305
410
  });
306
411
  }
412
+ if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
413
+ return res
414
+ .status(503)
415
+ .json({ error: error.message, code: error.code });
416
+ }
307
417
  console.error('Error updating schedule:', error);
308
418
  res.status(500).json({ error: 'Failed to update schedule' });
309
419
  }
@@ -328,6 +438,11 @@ router.delete('/scripts/:scriptName/schedule', async (req, res) => {
328
438
  code: error.code,
329
439
  });
330
440
  }
441
+ if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
442
+ return res
443
+ .status(503)
444
+ .json({ error: error.message, code: error.code });
445
+ }
331
446
  console.error('Error deleting schedule:', error);
332
447
  res.status(500).json({ error: 'Failed to delete schedule' });
333
448
  }