@friggframework/admin-scripts 2.0.0--canary.517.a37d697.0 → 2.0.0--canary.522.cbd3d5a.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.
package/index.js CHANGED
@@ -12,7 +12,7 @@ const { AdminFriggCommands, createAdminFriggCommands } = require('./src/applicat
12
12
  const { ScriptRunner, createScriptRunner } = require('./src/application/script-runner');
13
13
 
14
14
  // Infrastructure
15
- const { validateAdminApiKey } = require('./src/infrastructure/admin-auth-middleware');
15
+ const { adminAuthMiddleware } = require('./src/infrastructure/admin-auth-middleware');
16
16
  const { router, app, handler: routerHandler } = require('./src/infrastructure/admin-script-router');
17
17
  const { handler: executorHandler } = require('./src/infrastructure/script-executor-handler');
18
18
 
@@ -45,7 +45,7 @@ module.exports = {
45
45
  createScriptRunner,
46
46
 
47
47
  // Infrastructure layer
48
- validateAdminApiKey,
48
+ adminAuthMiddleware,
49
49
  router,
50
50
  app,
51
51
  routerHandler,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.517.a37d697.0",
4
+ "version": "2.0.0--canary.522.cbd3d5a.0",
5
5
  "description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
6
6
  "dependencies": {
7
7
  "@aws-sdk/client-scheduler": "^3.588.0",
8
- "@friggframework/core": "2.0.0--canary.517.a37d697.0",
8
+ "@friggframework/core": "2.0.0--canary.522.cbd3d5a.0",
9
9
  "bcryptjs": "^2.4.3",
10
10
  "express": "^4.18.2",
11
11
  "lodash": "4.17.21",
@@ -14,9 +14,9 @@
14
14
  "uuid": "^9.0.1"
15
15
  },
16
16
  "devDependencies": {
17
- "@friggframework/eslint-config": "2.0.0--canary.517.a37d697.0",
18
- "@friggframework/prettier-config": "2.0.0--canary.517.a37d697.0",
19
- "@friggframework/test": "2.0.0--canary.517.a37d697.0",
17
+ "@friggframework/eslint-config": "2.0.0--canary.522.cbd3d5a.0",
18
+ "@friggframework/prettier-config": "2.0.0--canary.522.cbd3d5a.0",
19
+ "@friggframework/test": "2.0.0--canary.522.cbd3d5a.0",
20
20
  "chai": "^4.3.6",
21
21
  "eslint": "^8.22.0",
22
22
  "jest": "^29.7.0",
@@ -49,5 +49,5 @@
49
49
  "maintenance",
50
50
  "operations"
51
51
  ],
52
- "gitHead": "a37d697613667fff814938b1f30ee2d834c4ffcd"
52
+ "gitHead": "cbd3d5a81315519842f308fc0bd786a3f8786b79"
53
53
  }
@@ -5,7 +5,7 @@ jest.mock('@friggframework/core/integrations/repositories/integration-repository
5
5
  jest.mock('@friggframework/core/user/repositories/user-repository-factory');
6
6
  jest.mock('@friggframework/core/modules/repositories/module-repository-factory');
7
7
  jest.mock('@friggframework/core/credential/repositories/credential-repository-factory');
8
- jest.mock('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
8
+ jest.mock('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
9
9
  jest.mock('@friggframework/core/queues');
10
10
 
11
11
  describe('AdminFriggCommands', () => {
@@ -13,7 +13,7 @@ describe('AdminFriggCommands', () => {
13
13
  let mockUserRepo;
14
14
  let mockModuleRepo;
15
15
  let mockCredentialRepo;
16
- let mockAdminProcessRepo;
16
+ let mockScriptExecutionRepo;
17
17
  let mockQueuerUtil;
18
18
 
19
19
  beforeEach(() => {
@@ -46,8 +46,8 @@ describe('AdminFriggCommands', () => {
46
46
  updateCredential: jest.fn(),
47
47
  };
48
48
 
49
- mockAdminProcessRepo = {
50
- appendProcessLog: jest.fn().mockResolvedValue(undefined),
49
+ mockScriptExecutionRepo = {
50
+ appendExecutionLog: jest.fn().mockResolvedValue(undefined),
51
51
  };
52
52
 
53
53
  mockQueuerUtil = {
@@ -60,14 +60,14 @@ describe('AdminFriggCommands', () => {
60
60
  const { createUserRepository } = require('@friggframework/core/user/repositories/user-repository-factory');
61
61
  const { createModuleRepository } = require('@friggframework/core/modules/repositories/module-repository-factory');
62
62
  const { createCredentialRepository } = require('@friggframework/core/credential/repositories/credential-repository-factory');
63
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
63
+ const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
64
64
  const { QueuerUtil } = require('@friggframework/core/queues');
65
65
 
66
66
  createIntegrationRepository.mockReturnValue(mockIntegrationRepo);
67
67
  createUserRepository.mockReturnValue(mockUserRepo);
68
68
  createModuleRepository.mockReturnValue(mockModuleRepo);
69
69
  createCredentialRepository.mockReturnValue(mockCredentialRepo);
70
- createAdminProcessRepository.mockReturnValue(mockAdminProcessRepo);
70
+ createScriptExecutionRepository.mockReturnValue(mockScriptExecutionRepo);
71
71
 
72
72
  // Mock QueuerUtil methods
73
73
  QueuerUtil.send = mockQueuerUtil.send;
@@ -158,16 +158,16 @@ describe('AdminFriggCommands', () => {
158
158
  expect(repo).toBe(mockCredentialRepo);
159
159
  });
160
160
 
161
- it('creates adminProcessRepository on first access', () => {
161
+ it('creates scriptExecutionRepository on first access', () => {
162
162
  const commands = new AdminFriggCommands();
163
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
163
+ const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
164
164
 
165
- expect(createAdminProcessRepository).not.toHaveBeenCalled();
165
+ expect(createScriptExecutionRepository).not.toHaveBeenCalled();
166
166
 
167
- const repo = commands.adminProcessRepository;
167
+ const repo = commands.scriptExecutionRepository;
168
168
 
169
- expect(createAdminProcessRepository).toHaveBeenCalledTimes(1);
170
- expect(repo).toBe(mockAdminProcessRepo);
169
+ expect(createScriptExecutionRepository).toHaveBeenCalledTimes(1);
170
+ expect(repo).toBe(mockScriptExecutionRepo);
171
171
  });
172
172
  });
173
173
 
@@ -551,15 +551,15 @@ describe('AdminFriggCommands', () => {
551
551
  it('log() persists if executionId set', async () => {
552
552
  const commands = new AdminFriggCommands({ executionId: 'exec_123' });
553
553
  // Force repository creation
554
- commands.adminProcessRepository;
554
+ commands.scriptExecutionRepository;
555
555
 
556
556
  commands.log('warn', 'Warning message', { detail: 'xyz' });
557
557
 
558
558
  // Give async operation a chance to execute
559
559
  await new Promise(resolve => setImmediate(resolve));
560
560
 
561
- expect(mockAdminProcessRepo.appendProcessLog).toHaveBeenCalled();
562
- const callArgs = mockAdminProcessRepo.appendProcessLog.mock.calls[0];
561
+ expect(mockScriptExecutionRepo.appendExecutionLog).toHaveBeenCalled();
562
+ const callArgs = mockScriptExecutionRepo.appendExecutionLog.mock.calls[0];
563
563
  expect(callArgs[0]).toBe('exec_123');
564
564
  expect(callArgs[1].level).toBe('warn');
565
565
  expect(callArgs[1].message).toBe('Warning message');
@@ -572,14 +572,14 @@ describe('AdminFriggCommands', () => {
572
572
 
573
573
  await new Promise(resolve => setImmediate(resolve));
574
574
 
575
- expect(mockAdminProcessRepo.appendProcessLog).not.toHaveBeenCalled();
575
+ expect(mockScriptExecutionRepo.appendExecutionLog).not.toHaveBeenCalled();
576
576
  });
577
577
 
578
578
  it('log() handles persistence failure gracefully', async () => {
579
579
  const commands = new AdminFriggCommands({ executionId: 'exec_123' });
580
580
  // Force repository creation
581
- commands.adminProcessRepository;
582
- mockAdminProcessRepo.appendProcessLog.mockRejectedValue(new Error('DB Error'));
581
+ commands.scriptExecutionRepository;
582
+ mockScriptExecutionRepo.appendExecutionLog.mockRejectedValue(new Error('DB Error'));
583
583
 
584
584
  // Should not throw
585
585
  expect(() => commands.log('error', 'Test error')).not.toThrow();
@@ -36,9 +36,9 @@ describe('ScriptRunner', () => {
36
36
  scriptFactory = new ScriptFactory([TestScript]);
37
37
 
38
38
  mockCommands = {
39
- createAdminProcess: jest.fn(),
40
- updateAdminProcessState: jest.fn(),
41
- completeAdminProcess: jest.fn(),
39
+ createScriptExecution: jest.fn(),
40
+ updateScriptExecutionStatus: jest.fn(),
41
+ completeScriptExecution: jest.fn(),
42
42
  };
43
43
 
44
44
  mockFrigg = {
@@ -49,11 +49,11 @@ describe('ScriptRunner', () => {
49
49
  createAdminScriptCommands.mockReturnValue(mockCommands);
50
50
  createAdminFriggCommands.mockReturnValue(mockFrigg);
51
51
 
52
- mockCommands.createAdminProcess.mockResolvedValue({
52
+ mockCommands.createScriptExecution.mockResolvedValue({
53
53
  id: 'exec-123',
54
54
  });
55
- mockCommands.updateAdminProcessState.mockResolvedValue({});
56
- mockCommands.completeAdminProcess.mockResolvedValue({ success: true });
55
+ mockCommands.updateScriptExecutionStatus.mockResolvedValue({});
56
+ mockCommands.completeScriptExecution.mockResolvedValue({ success: true });
57
57
  });
58
58
 
59
59
  afterEach(() => {
@@ -76,7 +76,7 @@ describe('ScriptRunner', () => {
76
76
  expect(result.executionId).toBe('exec-123');
77
77
  expect(result.metrics.durationMs).toBeGreaterThanOrEqual(0);
78
78
 
79
- expect(mockCommands.createAdminProcess).toHaveBeenCalledWith({
79
+ expect(mockCommands.createScriptExecution).toHaveBeenCalledWith({
80
80
  scriptName: 'test-script',
81
81
  scriptVersion: '1.0.0',
82
82
  trigger: 'MANUAL',
@@ -85,12 +85,12 @@ describe('ScriptRunner', () => {
85
85
  audit: { apiKeyName: 'test-key' },
86
86
  });
87
87
 
88
- expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
88
+ expect(mockCommands.updateScriptExecutionStatus).toHaveBeenCalledWith(
89
89
  'exec-123',
90
90
  'RUNNING'
91
91
  );
92
92
 
93
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
93
+ expect(mockCommands.completeScriptExecution).toHaveBeenCalledWith(
94
94
  'exec-123',
95
95
  expect.objectContaining({
96
96
  status: 'COMPLETED',
@@ -128,7 +128,7 @@ describe('ScriptRunner', () => {
128
128
  expect(result.scriptName).toBe('failing-script');
129
129
  expect(result.error.message).toBe('Script failed');
130
130
 
131
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
131
+ expect(mockCommands.completeScriptExecution).toHaveBeenCalledWith(
132
132
  'exec-123',
133
133
  expect.objectContaining({
134
134
  status: 'FAILED',
@@ -178,8 +178,8 @@ describe('ScriptRunner', () => {
178
178
  });
179
179
 
180
180
  expect(result.executionId).toBe('existing-exec-456');
181
- expect(mockCommands.createAdminProcess).not.toHaveBeenCalled();
182
- expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
181
+ expect(mockCommands.createScriptExecution).not.toHaveBeenCalled();
182
+ expect(mockCommands.updateScriptExecutionStatus).toHaveBeenCalledWith(
183
183
  'existing-exec-456',
184
184
  'RUNNING'
185
185
  );
@@ -25,7 +25,7 @@ class AdminFriggCommands {
25
25
  this._userRepository = null;
26
26
  this._moduleRepository = null;
27
27
  this._credentialRepository = null;
28
- this._adminProcessRepository = null;
28
+ this._scriptExecutionRepository = null;
29
29
  }
30
30
 
31
31
  // ==================== LAZY-LOADED REPOSITORIES ====================
@@ -62,12 +62,12 @@ class AdminFriggCommands {
62
62
  return this._credentialRepository;
63
63
  }
64
64
 
65
- get adminProcessRepository() {
66
- if (!this._adminProcessRepository) {
67
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
68
- this._adminProcessRepository = createAdminProcessRepository();
65
+ get scriptExecutionRepository() {
66
+ if (!this._scriptExecutionRepository) {
67
+ const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
68
+ this._scriptExecutionRepository = createScriptExecutionRepository();
69
69
  }
70
- return this._adminProcessRepository;
70
+ return this._scriptExecutionRepository;
71
71
  }
72
72
 
73
73
  // ==================== INTEGRATION QUERIES ====================
@@ -209,7 +209,7 @@ class AdminFriggCommands {
209
209
 
210
210
  // Persist to execution record if we have an executionId
211
211
  if (this.executionId) {
212
- this.adminProcessRepository.appendProcessLog(this.executionId, entry)
212
+ this.scriptExecutionRepository.appendExecutionLog(this.executionId, entry)
213
213
  .catch(err => console.error('Failed to persist log:', err));
214
214
  }
215
215
 
@@ -1,4 +1,5 @@
1
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
1
+ const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
2
+ const { createAdminApiKeyRepository } = require('@friggframework/core/admin-scripts/repositories/admin-api-key-repository-factory');
2
3
 
3
4
  /**
4
5
  * Admin Script Base Class
@@ -86,7 +87,8 @@ class AdminScriptBase {
86
87
  this.integrationFactory = params.integrationFactory || null;
87
88
 
88
89
  // OPTIONAL: Injected repositories (for testing or custom implementations)
89
- this.adminProcessRepository = params.adminProcessRepository || null;
90
+ this.scriptExecutionRepository = params.scriptExecutionRepository || null;
91
+ this.adminApiKeyRepository = params.adminApiKeyRepository || null;
90
92
  }
91
93
 
92
94
  /**
@@ -50,7 +50,7 @@ class ScriptRunner {
50
50
 
51
51
  // Create execution record if not provided
52
52
  if (!executionId) {
53
- const execution = await this.commands.createAdminProcess({
53
+ const execution = await this.commands.createScriptExecution({
54
54
  scriptName,
55
55
  scriptVersion: definition.version,
56
56
  trigger,
@@ -66,7 +66,7 @@ class ScriptRunner {
66
66
  try {
67
67
  // Update status to RUNNING (skip in dry-run)
68
68
  if (!dryRun) {
69
- await this.commands.updateAdminProcessState(executionId, 'RUNNING');
69
+ await this.commands.updateScriptExecutionStatus(executionId, 'RUNNING');
70
70
  }
71
71
 
72
72
  // Create frigg commands for the script
@@ -99,7 +99,7 @@ class ScriptRunner {
99
99
 
100
100
  // Complete execution (skip in dry-run)
101
101
  if (!dryRun) {
102
- await this.commands.completeAdminProcess(executionId, {
102
+ await this.commands.completeScriptExecution(executionId, {
103
103
  status: 'COMPLETED',
104
104
  output,
105
105
  metrics: {
@@ -140,7 +140,7 @@ class ScriptRunner {
140
140
 
141
141
  // Record failure (skip in dry-run)
142
142
  if (!dryRun) {
143
- await this.commands.completeAdminProcess(executionId, {
143
+ await this.commands.completeScriptExecution(executionId, {
144
144
  status: 'FAILED',
145
145
  error: {
146
146
  name: error.name,
@@ -1,17 +1,22 @@
1
- const { validateAdminApiKey } = require('../admin-auth-middleware');
1
+ const { adminAuthMiddleware } = require('../admin-auth-middleware');
2
2
 
3
- describe('validateAdminApiKey', () => {
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', () => {
4
11
  let mockReq;
5
12
  let mockRes;
6
13
  let mockNext;
7
- let originalEnv;
14
+ let mockCommands;
8
15
 
9
16
  beforeEach(() => {
10
- originalEnv = process.env.ADMIN_API_KEY;
11
- process.env.ADMIN_API_KEY = 'test-admin-key-123';
12
-
13
17
  mockReq = {
14
18
  headers: {},
19
+ ip: '127.0.0.1',
15
20
  };
16
21
 
17
22
  mockRes = {
@@ -20,66 +25,124 @@ describe('validateAdminApiKey', () => {
20
25
  };
21
26
 
22
27
  mockNext = jest.fn();
28
+
29
+ mockCommands = {
30
+ validateAdminApiKey: jest.fn(),
31
+ };
32
+
33
+ createAdminScriptCommands.mockReturnValue(mockCommands);
23
34
  });
24
35
 
25
36
  afterEach(() => {
26
- if (originalEnv) {
27
- process.env.ADMIN_API_KEY = originalEnv;
28
- } else {
29
- delete process.env.ADMIN_API_KEY;
30
- }
31
37
  jest.clearAllMocks();
32
38
  });
33
39
 
34
- describe('Environment configuration', () => {
35
- it('should reject when ADMIN_API_KEY not configured', () => {
36
- delete process.env.ADMIN_API_KEY;
37
-
38
- validateAdminApiKey(mockReq, mockRes, mockNext);
40
+ describe('Authorization header validation', () => {
41
+ it('should reject request without Authorization header', async () => {
42
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
39
43
 
40
44
  expect(mockRes.status).toHaveBeenCalledWith(401);
41
45
  expect(mockRes.json).toHaveBeenCalledWith({
42
- error: 'Unauthorized',
43
- message: 'Admin API key not configured',
46
+ error: 'Missing or invalid Authorization header',
47
+ code: 'MISSING_AUTH',
44
48
  });
45
49
  expect(mockNext).not.toHaveBeenCalled();
46
50
  });
47
- });
48
51
 
49
- describe('Header validation', () => {
50
- it('should reject request without x-frigg-admin-api-key header', () => {
51
- validateAdminApiKey(mockReq, mockRes, mockNext);
52
+ it('should reject request with invalid Authorization format', async () => {
53
+ mockReq.headers.authorization = 'InvalidFormat key123';
54
+
55
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
52
56
 
53
57
  expect(mockRes.status).toHaveBeenCalledWith(401);
54
58
  expect(mockRes.json).toHaveBeenCalledWith({
55
- error: 'Unauthorized',
56
- message: 'x-frigg-admin-api-key header required',
59
+ error: 'Missing or invalid Authorization header',
60
+ code: 'MISSING_AUTH',
57
61
  });
58
62
  expect(mockNext).not.toHaveBeenCalled();
59
63
  });
60
64
  });
61
65
 
62
66
  describe('API key validation', () => {
63
- it('should reject request with invalid API key', () => {
64
- mockReq.headers['x-frigg-admin-api-key'] = 'invalid-key';
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
+ });
65
85
 
66
- validateAdminApiKey(mockReq, mockRes, mockNext);
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
+ });
67
93
 
94
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
95
+
96
+ expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
68
97
  expect(mockRes.status).toHaveBeenCalledWith(401);
69
98
  expect(mockRes.json).toHaveBeenCalledWith({
70
- error: 'Unauthorized',
71
- message: 'Invalid admin API key',
99
+ error: 'API key has expired',
100
+ code: 'EXPIRED_API_KEY',
72
101
  });
73
102
  expect(mockNext).not.toHaveBeenCalled();
74
103
  });
75
104
 
76
- it('should accept request with valid API key', () => {
77
- mockReq.headers['x-frigg-admin-api-key'] = 'test-admin-key-123';
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
+ });
78
116
 
79
- validateAdminApiKey(mockReq, mockRes, mockNext);
117
+ await adminAuthMiddleware(mockReq, mockRes, mockNext);
80
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');
81
126
  expect(mockNext).toHaveBeenCalled();
82
127
  expect(mockRes.status).not.toHaveBeenCalled();
83
128
  });
84
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
+ });
85
148
  });
@@ -4,8 +4,13 @@ const { AdminScriptBase } = require('../../application/admin-script-base');
4
4
 
5
5
  // Mock dependencies
6
6
  jest.mock('../admin-auth-middleware', () => ({
7
- validateAdminApiKey: (req, res, next) => {
8
- // Mock auth - no audit trail with simplified auth
7
+ adminAuthMiddleware: (req, res, next) => {
8
+ // Mock auth - attach admin audit info
9
+ req.adminAudit = {
10
+ apiKeyName: 'test-key',
11
+ apiKeyLast4: '1234',
12
+ ipAddress: '127.0.0.1',
13
+ };
9
14
  next();
10
15
  },
11
16
  }));
@@ -54,8 +59,8 @@ describe('Admin Script Router', () => {
54
59
  };
55
60
 
56
61
  mockCommands = {
57
- createAdminProcess: jest.fn(),
58
- findAdminProcessById: jest.fn(),
62
+ createScriptExecution: jest.fn(),
63
+ findScriptExecutionById: jest.fn(),
59
64
  findRecentExecutions: jest.fn(),
60
65
  };
61
66
 
@@ -138,7 +143,7 @@ describe('Admin Script Router', () => {
138
143
  });
139
144
  });
140
145
 
141
- describe('POST /admin/scripts/:scriptName', () => {
146
+ describe('POST /admin/scripts/:scriptName/execute', () => {
142
147
  it('should execute script synchronously', async () => {
143
148
  mockRunner.execute.mockResolvedValue({
144
149
  executionId: 'exec-123',
@@ -149,7 +154,7 @@ describe('Admin Script Router', () => {
149
154
  });
150
155
 
151
156
  const response = await request(app)
152
- .post('/admin/scripts/test-script')
157
+ .post('/admin/scripts/test-script/execute')
153
158
  .send({
154
159
  params: { foo: 'bar' },
155
160
  mode: 'sync',
@@ -169,12 +174,12 @@ describe('Admin Script Router', () => {
169
174
  });
170
175
 
171
176
  it('should queue script for async execution', async () => {
172
- mockCommands.createAdminProcess.mockResolvedValue({
177
+ mockCommands.createScriptExecution.mockResolvedValue({
173
178
  id: 'exec-456',
174
179
  });
175
180
 
176
181
  const response = await request(app)
177
- .post('/admin/scripts/test-script')
182
+ .post('/admin/scripts/test-script/execute')
178
183
  .send({
179
184
  params: { foo: 'bar' },
180
185
  mode: 'async',
@@ -193,12 +198,12 @@ describe('Admin Script Router', () => {
193
198
  });
194
199
 
195
200
  it('should default to async mode', async () => {
196
- mockCommands.createAdminProcess.mockResolvedValue({
201
+ mockCommands.createScriptExecution.mockResolvedValue({
197
202
  id: 'exec-789',
198
203
  });
199
204
 
200
205
  const response = await request(app)
201
- .post('/admin/scripts/test-script')
206
+ .post('/admin/scripts/test-script/execute')
202
207
  .send({
203
208
  params: { foo: 'bar' },
204
209
  });
@@ -211,7 +216,7 @@ describe('Admin Script Router', () => {
211
216
  mockFactory.has.mockReturnValue(false);
212
217
 
213
218
  const response = await request(app)
214
- .post('/admin/scripts/non-existent')
219
+ .post('/admin/scripts/non-existent/execute')
215
220
  .send({
216
221
  params: {},
217
222
  });
@@ -221,15 +226,15 @@ describe('Admin Script Router', () => {
221
226
  });
222
227
  });
223
228
 
224
- describe('GET /admin/scripts/:scriptName/executions/:executionId', () => {
229
+ describe('GET /admin/executions/:executionId', () => {
225
230
  it('should return execution details', async () => {
226
- mockCommands.findAdminProcessById.mockResolvedValue({
231
+ mockCommands.findScriptExecutionById.mockResolvedValue({
227
232
  id: 'exec-123',
228
233
  scriptName: 'test-script',
229
234
  status: 'COMPLETED',
230
235
  });
231
236
 
232
- const response = await request(app).get('/admin/scripts/test-script/executions/exec-123');
237
+ const response = await request(app).get('/admin/executions/exec-123');
233
238
 
234
239
  expect(response.status).toBe(200);
235
240
  expect(response.body.id).toBe('exec-123');
@@ -237,14 +242,14 @@ describe('Admin Script Router', () => {
237
242
  });
238
243
 
239
244
  it('should return 404 for non-existent execution', async () => {
240
- mockCommands.findAdminProcessById.mockResolvedValue({
245
+ mockCommands.findScriptExecutionById.mockResolvedValue({
241
246
  error: 404,
242
247
  reason: 'Execution not found',
243
248
  code: 'EXECUTION_NOT_FOUND',
244
249
  });
245
250
 
246
251
  const response = await request(app).get(
247
- '/admin/scripts/test-script/executions/non-existent'
252
+ '/admin/executions/non-existent'
248
253
  );
249
254
 
250
255
  expect(response.status).toBe(404);
@@ -252,28 +257,24 @@ describe('Admin Script Router', () => {
252
257
  });
253
258
  });
254
259
 
255
- describe('GET /admin/scripts/:scriptName/executions', () => {
256
- it('should list executions for specific script', async () => {
260
+ describe('GET /admin/executions', () => {
261
+ it('should list recent executions', async () => {
257
262
  mockCommands.findRecentExecutions.mockResolvedValue([
258
263
  { id: 'exec-1', scriptName: 'test-script', status: 'COMPLETED' },
259
264
  { id: 'exec-2', scriptName: 'test-script', status: 'RUNNING' },
260
265
  ]);
261
266
 
262
- const response = await request(app).get('/admin/scripts/test-script/executions');
267
+ const response = await request(app).get('/admin/executions');
263
268
 
264
269
  expect(response.status).toBe(200);
265
270
  expect(response.body.executions).toHaveLength(2);
266
- expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
267
- scriptName: 'test-script',
268
- limit: 50,
269
- });
270
271
  });
271
272
 
272
273
  it('should accept query parameters', async () => {
273
274
  mockCommands.findRecentExecutions.mockResolvedValue([]);
274
275
 
275
276
  await request(app).get(
276
- '/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
277
+ '/admin/executions?scriptName=test-script&status=COMPLETED&limit=10'
277
278
  );
278
279
 
279
280
  expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
@@ -1,11 +1,49 @@
1
+ const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
2
+
1
3
  /**
2
4
  * Admin API Key Authentication Middleware
3
5
  *
4
- * Re-exports shared admin auth middleware from @friggframework/core.
5
- * Uses simple ENV-based API key validation.
6
- * Expects: x-frigg-admin-api-key header
6
+ * Validates admin API keys for script endpoints.
7
+ * Expects: Authorization: Bearer <api-key>
7
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
+ };
8
38
 
9
- const { validateAdminApiKey } = require('@friggframework/core/handlers/middleware/admin-auth');
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
+ }
10
48
 
11
- module.exports = { validateAdminApiKey };
49
+ module.exports = { adminAuthMiddleware };
@@ -1,6 +1,6 @@
1
1
  const express = require('express');
2
2
  const serverless = require('serverless-http');
3
- const { validateAdminApiKey } = require('./admin-auth-middleware');
3
+ const { adminAuthMiddleware } = require('./admin-auth-middleware');
4
4
  const { getScriptFactory } = require('../application/script-factory');
5
5
  const { createScriptRunner } = require('../application/script-runner');
6
6
  const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
@@ -11,7 +11,7 @@ const { ScheduleManagementUseCase } = require('../application/schedule-managemen
11
11
  const router = express.Router();
12
12
 
13
13
  // Apply auth middleware to all admin routes
14
- router.use(validateAdminApiKey);
14
+ router.use(adminAuthMiddleware);
15
15
 
16
16
  /**
17
17
  * Create ScheduleManagementUseCase instance
@@ -87,10 +87,10 @@ router.get('/scripts/:scriptName', async (req, res) => {
87
87
  });
88
88
 
89
89
  /**
90
- * POST /admin/scripts/:scriptName
90
+ * POST /admin/scripts/:scriptName/execute
91
91
  * Execute a script (sync, async, or dry-run)
92
92
  */
93
- router.post('/scripts/:scriptName', async (req, res) => {
93
+ router.post('/scripts/:scriptName/execute', async (req, res) => {
94
94
  try {
95
95
  const { scriptName } = req.params;
96
96
  const { params = {}, mode = 'async', dryRun = false } = req.body;
@@ -110,6 +110,7 @@ router.post('/scripts/:scriptName', async (req, res) => {
110
110
  trigger: 'MANUAL',
111
111
  mode: 'sync',
112
112
  dryRun: true,
113
+ audit: req.adminAudit,
113
114
  });
114
115
  return res.json(result);
115
116
  }
@@ -120,18 +121,20 @@ router.post('/scripts/:scriptName', async (req, res) => {
120
121
  const result = await runner.execute(scriptName, params, {
121
122
  trigger: 'MANUAL',
122
123
  mode: 'sync',
124
+ audit: req.adminAudit,
123
125
  });
124
126
  return res.json(result);
125
127
  }
126
128
 
127
129
  // Async execution - queue and return immediately
128
130
  const commands = createAdminScriptCommands();
129
- const execution = await commands.createAdminProcess({
131
+ const execution = await commands.createScriptExecution({
130
132
  scriptName,
131
133
  scriptVersion: factory.get(scriptName).Definition.version,
132
134
  trigger: 'MANUAL',
133
135
  mode: 'async',
134
136
  input: params,
137
+ audit: req.adminAudit,
135
138
  });
136
139
 
137
140
  // Queue the execution
@@ -158,14 +161,14 @@ router.post('/scripts/:scriptName', async (req, res) => {
158
161
  });
159
162
 
160
163
  /**
161
- * GET /admin/scripts/:scriptName/executions/:executionId
162
- * Get execution status for specific script
164
+ * GET /admin/executions/:executionId
165
+ * Get execution status
163
166
  */
164
- router.get('/scripts/:scriptName/executions/:executionId', async (req, res) => {
167
+ router.get('/executions/:executionId', async (req, res) => {
165
168
  try {
166
169
  const { executionId } = req.params;
167
170
  const commands = createAdminScriptCommands();
168
- const execution = await commands.findAdminProcessById(executionId);
171
+ const execution = await commands.findScriptExecutionById(executionId);
169
172
 
170
173
  if (execution.error) {
171
174
  return res.status(execution.error).json({
@@ -182,13 +185,12 @@ router.get('/scripts/:scriptName/executions/:executionId', async (req, res) => {
182
185
  });
183
186
 
184
187
  /**
185
- * GET /admin/scripts/:scriptName/executions
186
- * List recent executions for specific script
188
+ * GET /admin/executions
189
+ * List recent executions
187
190
  */
188
- router.get('/scripts/:scriptName/executions', async (req, res) => {
191
+ router.get('/executions', async (req, res) => {
189
192
  try {
190
- const { scriptName } = req.params;
191
- const { status, limit = 50 } = req.query;
193
+ const { scriptName, status, limit = 50 } = req.query;
192
194
  const commands = createAdminScriptCommands();
193
195
 
194
196
  const executions = await commands.findRecentExecutions({
@@ -21,7 +21,7 @@ async function handler(event) {
21
21
 
22
22
  // If executionId provided (async from API), update existing record
23
23
  if (executionId) {
24
- await commands.updateAdminProcessState(executionId, 'RUNNING');
24
+ await commands.updateScriptExecutionStatus(executionId, 'RUNNING');
25
25
  }
26
26
 
27
27
  const result = await runner.execute(scriptName, params, {
@@ -45,7 +45,7 @@ async function handler(event) {
45
45
  if (executionId) {
46
46
  const commands = createAdminScriptCommands();
47
47
  await commands
48
- .completeAdminProcess(executionId, {
48
+ .completeScriptExecution(executionId, {
49
49
  status: 'FAILED',
50
50
  error: {
51
51
  name: error.name,