@friggframework/admin-scripts 2.0.0--canary.517.095e3f6.0 → 2.0.0--canary.517.06fe141.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @friggframework/admin-scripts
2
2
 
3
- Admin Script Runner for Frigg — write and run operational/maintenance scripts inside your deployed Frigg app, with VPC/KMS-secured database access, the same repositories your integrations use, sync or async (queued) execution, dry-run validation, and optional cron scheduling via AWS EventBridge Scheduler.
3
+ Admin Script Runner for Frigg — write and run operational/maintenance scripts inside your deployed Frigg app, with VPC/KMS-secured database access through the same Frigg commands your integrations use, sync or async (queued) execution, dry-run validation, and optional cron scheduling via AWS EventBridge Scheduler.
4
4
 
5
5
  Typical use cases:
6
6
 
@@ -8,7 +8,7 @@ Typical use cases:
8
8
  - **Recurring maintenance** — refresh webhooks/subscriptions before they expire.
9
9
  - **Built-in utilities** — OAuth token refresh, integration health checks.
10
10
 
11
- > Admin scripts are a **high-privilege** surface. Every endpoint is protected by an admin API key (`x-frigg-admin-api-key`), scripts run in your private VPC subnets, and every execution is tracked in the `AdminProcess` table. Never expose the admin API key to browsers or end users.
11
+ > Admin scripts are a **high-privilege** surface. Every endpoint is protected by an admin API key (`x-frigg-admin-api-key`), scripts run in your private VPC subnets, and every execution is tracked in the `AdminScriptExecution` table. Never expose the admin API key to browsers or end users.
12
12
 
13
13
  ---
14
14
 
@@ -87,12 +87,17 @@ class AttioHealingScript extends AdminScriptBase {
87
87
 
88
88
  this.context.log('info', 'Starting Attio healing', { integrationId });
89
89
 
90
+ // Read persisted data through Frigg commands (never repositories).
91
+ // Commands return the data on success, or an { error, reason } object
92
+ // on failure — check `.error` yourself.
90
93
  const integration =
91
- await this.context.integrationRepository.findIntegrationById(
94
+ await this.context.commands.integrations.findIntegrationById(
92
95
  integrationId
93
96
  );
94
- if (!integration) {
95
- throw new Error(`Integration ${integrationId} not found`);
97
+ if (integration.error) {
98
+ throw new Error(
99
+ `Integration ${integrationId} not found: ${integration.reason}`
100
+ );
96
101
  }
97
102
 
98
103
  // Call the live integration when you need to hit an external API.
@@ -117,16 +122,16 @@ module.exports = { AttioHealingScript };
117
122
  | Member | Description |
118
123
  | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
119
124
  | `log(level, message, data?)` | Records a structured log entry (`level`: `debug`/`info`/`warn`/`error`). Logs are persisted to the execution's `results.logs`. |
120
- | `integrationRepository` | Read/query integrations. |
121
- | `userRepository` | Read/query users. |
122
- | `moduleRepository` | Read/query modules. |
123
- | `credentialRepository` | Read credentials (decrypted transparently). |
125
+ | `commands.users` | User commands (`findIndividualUserById`, `createUser`, `updateUser`, …). |
126
+ | `commands.credentials` | Credential commands (`findCredential`, `updateCredential`, …); secrets decrypted transparently. |
127
+ | `commands.entities` | Entity/module commands (`findEntityById`, `findEntitiesByUserId`, …). |
128
+ | `commands.integrations` | Integration reads (`findIntegrationById`, `listIntegrations({ type, status })`). |
124
129
  | `instantiate(integrationId)` | Returns a hydrated integration instance for calling external APIs. Requires `config.requireIntegrationInstance: true`. |
125
130
  | `queueScript(name, params?)` | Enqueue another script as a follow-up (tracked as a child of the current execution). |
126
131
  | `queueScriptBatch(entries)` | Enqueue many follow-up scripts at once. |
127
- | `getExecutionId()` | The current `AdminProcess` record id. |
132
+ | `getExecutionId()` | The current `AdminScriptExecution` record id. |
128
133
 
129
- Repositories return **plain, decrypted data** field-level encryption is handled transparently.
134
+ Commands return **plain, decrypted data** on success (field-level encryption is handled transparently) or an `{ error, reason, code }` object on failure — scripts check `.error` themselves. Scripts have no direct repository access; all database interaction goes through the command layer.
130
135
 
131
136
  ---
132
137
 
@@ -242,7 +247,7 @@ async execute(params) {
242
247
  - **Sync** (`mode: 'sync'`) — runs in the API Lambda, result returned in the response. Capped by the API timeout.
243
248
  - **Async** (`mode: 'async'`, default) — queued to SQS and run by the worker Lambda (15-min budget). The SQS queue has a redrive policy (up to 3 receives) to a dead-letter queue, so a crashed invocation is retried at the infrastructure level. There is no application-level per-script retry — model idempotency accordingly.
244
249
 
245
- Every execution is persisted as an `AdminProcess` record with its `state` (`PENDING` → `RUNNING` → `COMPLETED`/`FAILED`), input, output, metrics, and logs.
250
+ Every execution is persisted as an `AdminScriptExecution` record with its `state` (`PENDING` → `RUNNING` → `COMPLETED`/`FAILED`), input, output, metrics, and logs.
246
251
 
247
252
  ---
248
253
 
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.517.095e3f6.0",
4
+ "version": "2.0.0--canary.517.06fe141.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.095e3f6.0",
8
+ "@friggframework/core": "2.0.0--canary.517.06fe141.0",
9
9
  "@hapi/boom": "^10.0.1",
10
10
  "express": "^4.18.2",
11
11
  "serverless-http": "^3.2.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@friggframework/eslint-config": "2.0.0--canary.517.095e3f6.0",
15
- "@friggframework/prettier-config": "2.0.0--canary.517.095e3f6.0",
16
- "@friggframework/test": "2.0.0--canary.517.095e3f6.0",
14
+ "@friggframework/eslint-config": "2.0.0--canary.517.06fe141.0",
15
+ "@friggframework/prettier-config": "2.0.0--canary.517.06fe141.0",
16
+ "@friggframework/test": "2.0.0--canary.517.06fe141.0",
17
17
  "eslint": "^8.22.0",
18
18
  "jest": "^29.7.0",
19
19
  "prettier": "^2.7.1",
@@ -44,5 +44,5 @@
44
44
  "maintenance",
45
45
  "operations"
46
46
  ],
47
- "gitHead": "095e3f64ffc59fd7afc713a537c69a0ec48eef7b"
47
+ "gitHead": "06fe14103a72028ce7bf622631580fb24a4abaaf"
48
48
  }
@@ -1,80 +1,24 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const {
2
4
  AdminScriptContext,
3
5
  createAdminScriptContext,
4
6
  } = require('../admin-script-context');
5
7
 
6
- // Mock all repository factories
7
- jest.mock(
8
- '@friggframework/core/integrations/repositories/integration-repository-factory'
9
- );
10
- jest.mock('@friggframework/core/user/repositories/user-repository-factory');
11
- jest.mock(
12
- '@friggframework/core/modules/repositories/module-repository-factory'
13
- );
14
- jest.mock(
15
- '@friggframework/core/credential/repositories/credential-repository-factory'
16
- );
17
8
  jest.mock('@friggframework/core/queues');
18
9
 
19
10
  describe('AdminScriptContext', () => {
20
- let mockIntegrationRepo;
21
- let mockUserRepo;
22
- let mockModuleRepo;
23
- let mockCredentialRepo;
24
11
  let mockQueuerUtil;
25
12
 
26
13
  beforeEach(() => {
27
14
  jest.clearAllMocks();
28
15
 
29
- mockIntegrationRepo = {
30
- findIntegrations: jest.fn(),
31
- findIntegrationById: jest.fn(),
32
- findIntegrationsByUserId: jest.fn(),
33
- updateIntegrationConfig: jest.fn(),
34
- updateIntegrationStatus: jest.fn(),
35
- };
36
-
37
- mockUserRepo = {
38
- findIndividualUserById: jest.fn(),
39
- findIndividualUserByAppUserId: jest.fn(),
40
- findIndividualUserByUsername: jest.fn(),
41
- };
42
-
43
- mockModuleRepo = {
44
- findEntity: jest.fn(),
45
- findEntityById: jest.fn(),
46
- findEntitiesByUserId: jest.fn(),
47
- };
48
-
49
- mockCredentialRepo = {
50
- findCredential: jest.fn(),
51
- updateCredential: jest.fn(),
52
- };
53
-
54
16
  mockQueuerUtil = {
55
17
  send: jest.fn().mockResolvedValue(undefined),
56
18
  batchSend: jest.fn().mockResolvedValue(undefined),
57
19
  };
58
20
 
59
- const {
60
- createIntegrationRepository,
61
- } = require('@friggframework/core/integrations/repositories/integration-repository-factory');
62
- const {
63
- createUserRepository,
64
- } = require('@friggframework/core/user/repositories/user-repository-factory');
65
- const {
66
- createModuleRepository,
67
- } = require('@friggframework/core/modules/repositories/module-repository-factory');
68
- const {
69
- createCredentialRepository,
70
- } = require('@friggframework/core/credential/repositories/credential-repository-factory');
71
21
  const { QueuerUtil } = require('@friggframework/core/queues');
72
-
73
- createIntegrationRepository.mockReturnValue(mockIntegrationRepo);
74
- createUserRepository.mockReturnValue(mockUserRepo);
75
- createModuleRepository.mockReturnValue(mockModuleRepo);
76
- createCredentialRepository.mockReturnValue(mockCredentialRepo);
77
-
78
22
  QueuerUtil.send = mockQueuerUtil.send;
79
23
  QueuerUtil.batchSend = mockQueuerUtil.batchSend;
80
24
  });
@@ -86,6 +30,19 @@ describe('AdminScriptContext', () => {
86
30
  expect(ctx.executionId).toBe('exec_123');
87
31
  expect(ctx.logs).toEqual([]);
88
32
  expect(ctx.integrationFactory).toBeNull();
33
+ expect(ctx.commands).toBeNull();
34
+ });
35
+
36
+ it('exposes the injected command bundle as context.commands', () => {
37
+ const commands = {
38
+ users: {},
39
+ credentials: {},
40
+ entities: {},
41
+ integrations: {},
42
+ };
43
+ const ctx = new AdminScriptContext({ commands });
44
+
45
+ expect(ctx.commands).toBe(commands);
89
46
  });
90
47
 
91
48
  it('creates with integrationFactory', () => {
@@ -106,71 +63,23 @@ describe('AdminScriptContext', () => {
106
63
  });
107
64
  });
108
65
 
109
- describe('Lazy Repository Loading', () => {
110
- it('creates integrationRepository on first access', () => {
66
+ describe('command surface (no direct repository access)', () => {
67
+ it('does not expose repository getters', () => {
111
68
  const ctx = new AdminScriptContext();
112
- const {
113
- createIntegrationRepository,
114
- } = require('@friggframework/core/integrations/repositories/integration-repository-factory');
115
-
116
- expect(createIntegrationRepository).not.toHaveBeenCalled();
117
69
 
118
- const repo = ctx.integrationRepository;
119
-
120
- expect(createIntegrationRepository).toHaveBeenCalledTimes(1);
121
- expect(repo).toBe(mockIntegrationRepo);
70
+ expect(ctx.integrationRepository).toBeUndefined();
71
+ expect(ctx.userRepository).toBeUndefined();
72
+ expect(ctx.moduleRepository).toBeUndefined();
73
+ expect(ctx.credentialRepository).toBeUndefined();
122
74
  });
123
75
 
124
- it('returns same instance on subsequent access', () => {
125
- const ctx = new AdminScriptContext();
126
-
127
- const repo1 = ctx.integrationRepository;
128
- const repo2 = ctx.integrationRepository;
129
-
130
- expect(repo1).toBe(repo2);
131
- expect(repo1).toBe(mockIntegrationRepo);
132
- });
133
-
134
- it('creates userRepository on first access', () => {
135
- const ctx = new AdminScriptContext();
136
- const {
137
- createUserRepository,
138
- } = require('@friggframework/core/user/repositories/user-repository-factory');
139
-
140
- expect(createUserRepository).not.toHaveBeenCalled();
141
-
142
- const repo = ctx.userRepository;
143
-
144
- expect(createUserRepository).toHaveBeenCalledTimes(1);
145
- expect(repo).toBe(mockUserRepo);
146
- });
147
-
148
- it('creates moduleRepository on first access', () => {
149
- const ctx = new AdminScriptContext();
150
- const {
151
- createModuleRepository,
152
- } = require('@friggframework/core/modules/repositories/module-repository-factory');
153
-
154
- expect(createModuleRepository).not.toHaveBeenCalled();
155
-
156
- const repo = ctx.moduleRepository;
157
-
158
- expect(createModuleRepository).toHaveBeenCalledTimes(1);
159
- expect(repo).toBe(mockModuleRepo);
160
- });
161
-
162
- it('creates credentialRepository on first access', () => {
163
- const ctx = new AdminScriptContext();
164
- const {
165
- createCredentialRepository,
166
- } = require('@friggframework/core/credential/repositories/credential-repository-factory');
167
-
168
- expect(createCredentialRepository).not.toHaveBeenCalled();
169
-
170
- const repo = ctx.credentialRepository;
76
+ it('the context module never imports a repository factory', () => {
77
+ const source = fs.readFileSync(
78
+ path.join(__dirname, '..', 'admin-script-context.js'),
79
+ 'utf8'
80
+ );
171
81
 
172
- expect(createCredentialRepository).toHaveBeenCalledTimes(1);
173
- expect(repo).toBe(mockCredentialRepo);
82
+ expect(source).not.toMatch(/repository-factory/);
174
83
  });
175
84
  });
176
85
 
@@ -38,9 +38,9 @@ describe('ScriptRunner', () => {
38
38
  scriptFactory = new ScriptFactory([TestScript]);
39
39
 
40
40
  mockCommands = {
41
- createAdminProcess: jest.fn(),
42
- updateAdminProcessState: jest.fn(),
43
- completeAdminProcess: jest.fn(),
41
+ createExecution: jest.fn(),
42
+ updateExecutionState: jest.fn(),
43
+ completeExecution: jest.fn(),
44
44
  };
45
45
 
46
46
  mockContext = {
@@ -52,11 +52,11 @@ describe('ScriptRunner', () => {
52
52
  createAdminScriptCommands.mockReturnValue(mockCommands);
53
53
  createAdminScriptContext.mockReturnValue(mockContext);
54
54
 
55
- mockCommands.createAdminProcess.mockResolvedValue({
55
+ mockCommands.createExecution.mockResolvedValue({
56
56
  id: 'exec-123',
57
57
  });
58
- mockCommands.updateAdminProcessState.mockResolvedValue({});
59
- mockCommands.completeAdminProcess.mockResolvedValue({ success: true });
58
+ mockCommands.updateExecutionState.mockResolvedValue({});
59
+ mockCommands.completeExecution.mockResolvedValue({ success: true });
60
60
  });
61
61
 
62
62
  afterEach(() => {
@@ -89,7 +89,7 @@ describe('ScriptRunner', () => {
89
89
  expect(result.executionId).toBe('exec-123');
90
90
  expect(result.metrics.durationMs).toBeGreaterThanOrEqual(0);
91
91
 
92
- expect(mockCommands.createAdminProcess).toHaveBeenCalledWith({
92
+ expect(mockCommands.createExecution).toHaveBeenCalledWith({
93
93
  scriptName: 'test-script',
94
94
  scriptVersion: '1.0.0',
95
95
  trigger: 'MANUAL',
@@ -98,12 +98,12 @@ describe('ScriptRunner', () => {
98
98
  audit: { apiKeyName: 'test-key' },
99
99
  });
100
100
 
101
- expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
101
+ expect(mockCommands.updateExecutionState).toHaveBeenCalledWith(
102
102
  'exec-123',
103
103
  'RUNNING'
104
104
  );
105
105
 
106
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
106
+ expect(mockCommands.completeExecution).toHaveBeenCalledWith(
107
107
  'exec-123',
108
108
  expect.objectContaining({
109
109
  state: 'COMPLETED',
@@ -170,7 +170,7 @@ describe('ScriptRunner', () => {
170
170
  expect(result.scriptName).toBe('failing-script');
171
171
  expect(result.error.message).toBe('Script failed');
172
172
 
173
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
173
+ expect(mockCommands.completeExecution).toHaveBeenCalledWith(
174
174
  'exec-123',
175
175
  expect.objectContaining({
176
176
  state: 'FAILED',
@@ -227,8 +227,8 @@ describe('ScriptRunner', () => {
227
227
  );
228
228
 
229
229
  expect(result.executionId).toBe('existing-exec-456');
230
- expect(mockCommands.createAdminProcess).not.toHaveBeenCalled();
231
- expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
230
+ expect(mockCommands.createExecution).not.toHaveBeenCalled();
231
+ expect(mockCommands.updateExecutionState).toHaveBeenCalledWith(
232
232
  'existing-exec-456',
233
233
  'RUNNING'
234
234
  );
@@ -237,7 +237,7 @@ describe('ScriptRunner', () => {
237
237
  it('reports COMPLETED even when persisting completion fails', async () => {
238
238
  // Commands return an error object (never throw). A successful script
239
239
  // must not be misreported as FAILED if the completion write fails.
240
- mockCommands.completeAdminProcess.mockResolvedValue({
240
+ mockCommands.completeExecution.mockResolvedValue({
241
241
  error: 500,
242
242
  reason: 'DB write failed',
243
243
  });
@@ -257,7 +257,7 @@ describe('ScriptRunner', () => {
257
257
  });
258
258
 
259
259
  it('throws when the execution record cannot be created', async () => {
260
- mockCommands.createAdminProcess.mockResolvedValue({
260
+ mockCommands.createExecution.mockResolvedValue({
261
261
  error: 500,
262
262
  reason: 'DB down',
263
263
  });
@@ -282,7 +282,7 @@ describe('ScriptRunner', () => {
282
282
 
283
283
  await runner.execute('test-script', {}, { trigger: 'MANUAL' });
284
284
 
285
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
285
+ expect(mockCommands.completeExecution).toHaveBeenCalledWith(
286
286
  'exec-123',
287
287
  expect.objectContaining({
288
288
  logs: [{ level: 'info', message: 'hi' }],
@@ -3,23 +3,27 @@ const { QueuerUtil } = require('@friggframework/core/queues');
3
3
  /**
4
4
  * AdminScriptContext - Execution environment for admin scripts
5
5
  *
6
- * Provides a controlled surface area for scripts to interact with
7
- * the Frigg platform. Unique capabilities vs direct repo access:
6
+ * Provides a controlled surface area for scripts to interact with the Frigg
7
+ * platform. Scripts touch the database only through injected Frigg commands
8
+ * (`context.commands`) — never through repositories directly. Capabilities:
8
9
  *
10
+ * - **Frigg commands**: `commands.users`, `commands.credentials`,
11
+ * `commands.entities`, and `commands.integrations` expose the framework's
12
+ * command layer. Each command returns data on success or an `{ error }`
13
+ * object on failure — scripts check `.error` themselves.
9
14
  * - **Admin bypass**: `instantiate()` passes `_isAdminContext: true` to
10
15
  * skip user-ownership checks when loading integration instances
11
16
  * - **Script chaining**: `queueScript()` / `queueScriptBatch()` let scripts
12
17
  * enqueue follow-up work with parent execution tracking
13
18
  * - **Execution-scoped logging**: `log()` collects structured entries tied
14
19
  * to the current execution for post-run inspection
15
- * - **Lazy-loaded repositories**: Repos are exposed directly as getters
16
- * so scripts can query any data they need without wrapper indirection
17
20
  */
18
21
  class AdminScriptContext {
19
22
  /**
20
23
  * @param {Object} [params={}] - Context configuration
21
- * @param {string|number|null} [params.executionId] - ID of the AdminProcess record this context is scoped to (used for log persistence and script chaining)
24
+ * @param {string|number|null} [params.executionId] - ID of the AdminScriptExecution record this context is scoped to (used for log persistence and script chaining)
22
25
  * @param {Object|null} [params.integrationFactory] - Factory used to hydrate integration instances; required for scripts that call instantiate()
26
+ * @param {Object|null} [params.commands] - Frigg command bundle ({ users, credentials, entities, integrations }) injected by the composition root and exposed to scripts as context.commands
23
27
  */
24
28
  constructor(params = {}) {
25
29
  this.executionId = params.executionId || null;
@@ -27,54 +31,10 @@ class AdminScriptContext {
27
31
 
28
32
  this.integrationFactory = params.integrationFactory || null;
29
33
 
30
- // Repositories are created on first use so the Prisma client is only
31
- // initialized (and only for the repos a script actually touches) when needed.
32
- this._integrationRepository = null;
33
- this._userRepository = null;
34
- this._moduleRepository = null;
35
- this._credentialRepository = null;
36
- }
37
-
38
- // ==================== LAZY-LOADED REPOSITORIES ====================
39
-
40
- get integrationRepository() {
41
- if (!this._integrationRepository) {
42
- const {
43
- createIntegrationRepository,
44
- } = require('@friggframework/core/integrations/repositories/integration-repository-factory');
45
- this._integrationRepository = createIntegrationRepository();
46
- }
47
- return this._integrationRepository;
48
- }
49
-
50
- get userRepository() {
51
- if (!this._userRepository) {
52
- const {
53
- createUserRepository,
54
- } = require('@friggframework/core/user/repositories/user-repository-factory');
55
- this._userRepository = createUserRepository();
56
- }
57
- return this._userRepository;
58
- }
59
-
60
- get moduleRepository() {
61
- if (!this._moduleRepository) {
62
- const {
63
- createModuleRepository,
64
- } = require('@friggframework/core/modules/repositories/module-repository-factory');
65
- this._moduleRepository = createModuleRepository();
66
- }
67
- return this._moduleRepository;
68
- }
69
-
70
- get credentialRepository() {
71
- if (!this._credentialRepository) {
72
- const {
73
- createCredentialRepository,
74
- } = require('@friggframework/core/credential/repositories/credential-repository-factory');
75
- this._credentialRepository = createCredentialRepository();
76
- }
77
- return this._credentialRepository;
34
+ // Scripts interact with the database only through these Frigg commands.
35
+ // Injected by bootstrap.js the context never reaches for repositories
36
+ // or command factories itself.
37
+ this.commands = params.commands || null;
78
38
  }
79
39
 
80
40
  // ==================== INTEGRATION INSTANTIATION ====================
@@ -17,10 +17,12 @@ class ScriptRunner {
17
17
  * @param {Object} params
18
18
  * @param {ScriptFactory} params.scriptFactory - Required. The registry used
19
19
  * to resolve and instantiate scripts by name (built by bootstrap.js).
20
- * @param {Object} [params.commands] - Admin process command layer; defaults
20
+ * @param {Object} [params.commands] - Admin script execution command layer; defaults
21
21
  * to a fresh createAdminScriptCommands().
22
22
  * @param {Object} [params.integrationFactory] - Hydrates integration
23
23
  * instances for scripts that need them.
24
+ * @param {Object} [params.scriptCommands] - Frigg command bundle exposed to
25
+ * scripts as context.commands (built by bootstrap.js).
24
26
  */
25
27
  constructor(params = {}) {
26
28
  if (!params.scriptFactory) {
@@ -29,6 +31,7 @@ class ScriptRunner {
29
31
  this.scriptFactory = params.scriptFactory;
30
32
  this.commands = params.commands || createAdminScriptCommands();
31
33
  this.integrationFactory = params.integrationFactory || null;
34
+ this.scriptCommands = params.scriptCommands || null;
32
35
  }
33
36
 
34
37
  /**
@@ -39,8 +42,8 @@ class ScriptRunner {
39
42
  * @param {string} options.trigger - 'MANUAL' | 'SCHEDULED' | 'QUEUE'
40
43
  * @param {string} options.mode - 'sync' | 'async'
41
44
  * @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
42
- * @param {string} options.executionId - Reuse existing AdminProcess record ID (NOT the Lambda execution ID).
43
- * This is the database ID from the AdminProcess collection/table that tracks script executions.
45
+ * @param {string} options.executionId - Reuse existing AdminScriptExecution record ID (NOT the Lambda execution ID).
46
+ * This is the database ID from the AdminScriptExecution collection/table that tracks script executions.
44
47
  * Pass this when resuming a queued execution to continue using the same execution record.
45
48
  */
46
49
  async execute(scriptName, params = {}, options = {}) {
@@ -75,7 +78,7 @@ class ScriptRunner {
75
78
 
76
79
  // Create execution record if not provided
77
80
  if (!executionId) {
78
- const execution = await this.commands.createAdminProcess({
81
+ const execution = await this.commands.createExecution({
79
82
  scriptName,
80
83
  scriptVersion: definition.version,
81
84
  trigger,
@@ -88,7 +91,8 @@ class ScriptRunner {
88
91
  // than tracking an `undefined` execution id.
89
92
  if (execution.error) {
90
93
  throw new Error(
91
- execution.reason || 'Failed to create admin process record'
94
+ execution.reason ||
95
+ 'Failed to create admin script execution record'
92
96
  );
93
97
  }
94
98
  executionId = execution.id;
@@ -100,11 +104,12 @@ class ScriptRunner {
100
104
  const context = createAdminScriptContext({
101
105
  executionId,
102
106
  integrationFactory: this.integrationFactory,
107
+ commands: this.scriptCommands,
103
108
  });
104
109
 
105
110
  let output;
106
111
  try {
107
- await this.commands.updateAdminProcessState(executionId, 'RUNNING');
112
+ await this.commands.updateExecutionState(executionId, 'RUNNING');
108
113
 
109
114
  // Create script instance with context injected via constructor
110
115
  const script = this.scriptFactory.createInstance(scriptName, {
@@ -118,7 +123,7 @@ class ScriptRunner {
118
123
  } catch (error) {
119
124
  const durationMs = new Date() - startTime;
120
125
 
121
- const completion = await this.commands.completeAdminProcess(
126
+ const completion = await this.commands.completeExecution(
122
127
  executionId,
123
128
  {
124
129
  state: 'FAILED',
@@ -165,7 +170,7 @@ class ScriptRunner {
165
170
  metrics: { durationMs },
166
171
  };
167
172
 
168
- const completion = await this.commands.completeAdminProcess(
173
+ const completion = await this.commands.completeExecution(
169
174
  executionId,
170
175
  {
171
176
  state: 'COMPLETED',
@@ -58,9 +58,9 @@ describe('Admin Script Router', () => {
58
58
  };
59
59
 
60
60
  mockCommands = {
61
- createAdminProcess: jest.fn(),
62
- findAdminProcessById: jest.fn(),
63
- findAdminProcessesByName: jest.fn(),
61
+ createExecution: jest.fn(),
62
+ findExecutionById: jest.fn(),
63
+ findExecutionsByName: jest.fn(),
64
64
  };
65
65
 
66
66
  mockSchedulerAdapter = {
@@ -72,6 +72,7 @@ describe('Admin Script Router', () => {
72
72
  bootstrapAdminScripts.mockReturnValue({
73
73
  scriptFactory: mockFactory,
74
74
  integrationFactory: {},
75
+ scriptCommands: {},
75
76
  });
76
77
  createScriptRunner.mockReturnValue(mockRunner);
77
78
  createAdminScriptCommands.mockReturnValue(mockCommands);
@@ -177,13 +178,14 @@ describe('Admin Script Router', () => {
177
178
  expect(createScriptRunner).toHaveBeenCalledWith({
178
179
  scriptFactory: mockFactory,
179
180
  integrationFactory: {},
181
+ scriptCommands: {},
180
182
  });
181
183
  });
182
184
 
183
185
  it('should queue script for async execution', async () => {
184
186
  process.env.ADMIN_SCRIPT_QUEUE_URL =
185
187
  'https://sqs.us-east-1.amazonaws.com/123/test-queue';
186
- mockCommands.createAdminProcess.mockResolvedValue({
188
+ mockCommands.createExecution.mockResolvedValue({
187
189
  id: 'exec-456',
188
190
  });
189
191
 
@@ -210,7 +212,7 @@ describe('Admin Script Router', () => {
210
212
  it('should default to async mode', async () => {
211
213
  process.env.ADMIN_SCRIPT_QUEUE_URL =
212
214
  'https://sqs.us-east-1.amazonaws.com/123/test-queue';
213
- mockCommands.createAdminProcess.mockResolvedValue({
215
+ mockCommands.createExecution.mockResolvedValue({
214
216
  id: 'exec-789',
215
217
  });
216
218
 
@@ -255,7 +257,7 @@ describe('Admin Script Router', () => {
255
257
 
256
258
  describe('GET /admin/scripts/:scriptName/executions/:executionId', () => {
257
259
  it('should return execution details', async () => {
258
- mockCommands.findAdminProcessById.mockResolvedValue({
260
+ mockCommands.findExecutionById.mockResolvedValue({
259
261
  id: 'exec-123',
260
262
  scriptName: 'test-script',
261
263
  status: 'COMPLETED',
@@ -271,7 +273,7 @@ describe('Admin Script Router', () => {
271
273
  });
272
274
 
273
275
  it('should return 404 for non-existent execution', async () => {
274
- mockCommands.findAdminProcessById.mockResolvedValue({
276
+ mockCommands.findExecutionById.mockResolvedValue({
275
277
  error: 404,
276
278
  reason: 'Execution not found',
277
279
  code: 'EXECUTION_NOT_FOUND',
@@ -288,7 +290,7 @@ describe('Admin Script Router', () => {
288
290
 
289
291
  describe('GET /admin/scripts/:scriptName/executions', () => {
290
292
  it('should list executions for specific script', async () => {
291
- mockCommands.findAdminProcessesByName.mockResolvedValue([
293
+ mockCommands.findExecutionsByName.mockResolvedValue([
292
294
  { id: 'exec-1', name: 'test-script', state: 'COMPLETED' },
293
295
  { id: 'exec-2', name: 'test-script', state: 'RUNNING' },
294
296
  ]);
@@ -299,7 +301,7 @@ describe('Admin Script Router', () => {
299
301
 
300
302
  expect(response.status).toBe(200);
301
303
  expect(response.body.executions).toHaveLength(2);
302
- expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
304
+ expect(mockCommands.findExecutionsByName).toHaveBeenCalledWith(
303
305
  'test-script',
304
306
  {
305
307
  limit: 50,
@@ -308,13 +310,13 @@ describe('Admin Script Router', () => {
308
310
  });
309
311
 
310
312
  it('should accept query parameters (status maps to state, limit is bounded)', async () => {
311
- mockCommands.findAdminProcessesByName.mockResolvedValue([]);
313
+ mockCommands.findExecutionsByName.mockResolvedValue([]);
312
314
 
313
315
  await request(app).get(
314
316
  '/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
315
317
  );
316
318
 
317
- expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
319
+ expect(mockCommands.findExecutionsByName).toHaveBeenCalledWith(
318
320
  'test-script',
319
321
  {
320
322
  limit: 10,
@@ -3,6 +3,27 @@ const { AdminScriptBase } = require('../../application/admin-script-base');
3
3
 
4
4
  jest.mock('@friggframework/core/handlers/app-definition-loader');
5
5
 
6
+ // The command bundle is built from these factories; mock them so bootstrap
7
+ // doesn't need a configured database to construct the command groups.
8
+ jest.mock('@friggframework/core/application/commands/user-commands', () => ({
9
+ createUserCommands: jest.fn(() => ({ __kind: 'users' })),
10
+ }));
11
+ jest.mock(
12
+ '@friggframework/core/application/commands/credential-commands',
13
+ () => ({
14
+ createCredentialCommands: jest.fn(() => ({ __kind: 'credentials' })),
15
+ })
16
+ );
17
+ jest.mock('@friggframework/core/application/commands/entity-commands', () => ({
18
+ createEntityCommands: jest.fn(() => ({ __kind: 'entities' })),
19
+ }));
20
+ jest.mock(
21
+ '@friggframework/core/application/commands/integration-commands',
22
+ () => ({
23
+ createIntegrationCommands: jest.fn(() => ({ __kind: 'integrations' })),
24
+ })
25
+ );
26
+
6
27
  const {
7
28
  loadAppDefinition,
8
29
  } = require('@friggframework/core/handlers/app-definition-loader');
@@ -64,9 +85,23 @@ describe('bootstrapAdminScripts', () => {
64
85
 
65
86
  expect(second.scriptFactory).toBe(first.scriptFactory);
66
87
  expect(second.integrationFactory).toBe(first.integrationFactory);
88
+ expect(second.scriptCommands).toBe(first.scriptCommands);
67
89
  expect(loadAppDefinition).toHaveBeenCalledTimes(1);
68
90
  });
69
91
 
92
+ it('builds the injectable command bundle (users, credentials, entities, integrations)', () => {
93
+ loadAppDefinition.mockReturnValue({ adminScripts: [] });
94
+
95
+ const { scriptCommands } = bootstrapAdminScripts();
96
+
97
+ expect(scriptCommands).toEqual({
98
+ users: { __kind: 'users' },
99
+ credentials: { __kind: 'credentials' },
100
+ entities: { __kind: 'entities' },
101
+ integrations: { __kind: 'integrations' },
102
+ });
103
+ });
104
+
70
105
  it('tolerates an app definition with no adminScripts', () => {
71
106
  loadAppDefinition.mockReturnValue({});
72
107
 
@@ -12,20 +12,23 @@ const { handler } = require('../script-executor-handler');
12
12
  describe('Admin Script Executor Handler', () => {
13
13
  let mockScriptFactory;
14
14
  let mockIntegrationFactory;
15
+ let mockScriptCommands;
15
16
  let mockRunner;
16
17
  let mockCommands;
17
18
 
18
19
  beforeEach(() => {
19
20
  mockScriptFactory = { id: 'script-factory' };
20
21
  mockIntegrationFactory = { id: 'integration-factory' };
22
+ mockScriptCommands = { id: 'script-commands' };
21
23
  mockRunner = { execute: jest.fn() };
22
24
  mockCommands = {
23
- completeAdminProcess: jest.fn().mockResolvedValue({}),
25
+ completeExecution: jest.fn().mockResolvedValue({}),
24
26
  };
25
27
 
26
28
  bootstrapAdminScripts.mockReturnValue({
27
29
  scriptFactory: mockScriptFactory,
28
30
  integrationFactory: mockIntegrationFactory,
31
+ scriptCommands: mockScriptCommands,
29
32
  });
30
33
  createScriptRunner.mockReturnValue(mockRunner);
31
34
  createAdminScriptCommands.mockReturnValue(mockCommands);
@@ -56,6 +59,7 @@ describe('Admin Script Executor Handler', () => {
56
59
  expect(createScriptRunner).toHaveBeenCalledWith({
57
60
  scriptFactory: mockScriptFactory,
58
61
  integrationFactory: mockIntegrationFactory,
62
+ scriptCommands: mockScriptCommands,
59
63
  });
60
64
  expect(mockRunner.execute).toHaveBeenCalledWith(
61
65
  'my-script',
@@ -85,7 +89,7 @@ describe('Admin Script Executor Handler', () => {
85
89
  expect(response.statusCode).toBe(200);
86
90
  const body = JSON.parse(response.body);
87
91
  expect(body.results[0].status).toBe('FAILED');
88
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
92
+ expect(mockCommands.completeExecution).toHaveBeenCalledWith(
89
93
  'exec-2',
90
94
  expect.objectContaining({ state: 'FAILED' })
91
95
  );
@@ -114,6 +118,7 @@ describe('Admin Script Executor Handler', () => {
114
118
  expect(createScriptRunner).toHaveBeenCalledWith({
115
119
  scriptFactory: mockScriptFactory,
116
120
  integrationFactory: mockIntegrationFactory,
121
+ scriptCommands: mockScriptCommands,
117
122
  });
118
123
  expect(mockRunner.execute).toHaveBeenCalledWith(
119
124
  'my-script',
@@ -154,7 +159,7 @@ describe('Admin Script Executor Handler', () => {
154
159
  expect(body.processed).toBe(2);
155
160
  expect(body.results[0].status).toBe('FAILED');
156
161
  expect(body.results[1].status).toBe('COMPLETED');
157
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
162
+ expect(mockCommands.completeExecution).toHaveBeenCalledWith(
158
163
  'exec-bad',
159
164
  expect.objectContaining({ state: 'FAILED' })
160
165
  );
@@ -23,19 +23,12 @@ const {
23
23
 
24
24
  const router = express.Router();
25
25
 
26
- // Apply auth middleware to all admin routes
26
+ // Apply auth middleware to all admin routes. Each handler then calls
27
+ // bootstrapAdminScripts() (memoized, so it runs once per process) to obtain the
28
+ // scriptFactory / integrationFactory / scriptCommands it needs — no global, and
29
+ // endpoints that don't touch scripts skip the work entirely.
27
30
  router.use(validateAdminApiKey);
28
31
 
29
- // Build (once, memoized) and inject the per-process dependencies onto the
30
- // request, so route handlers consume them explicitly instead of reaching for a
31
- // global. Registers the host app's admin scripts on the first request.
32
- router.use((req, _res, next) => {
33
- const { scriptFactory, integrationFactory } = bootstrapAdminScripts();
34
- req.scriptFactory = scriptFactory;
35
- req.integrationFactory = integrationFactory;
36
- next();
37
- });
38
-
39
32
  /**
40
33
  * Translate a thrown error into an HTTP response. Boom errors (thrown by the
41
34
  * schedule use cases) carry their own status code; anything else is an
@@ -116,9 +109,9 @@ function createScheduleUseCases(scriptFactory) {
116
109
  * GET /admin/scripts
117
110
  * List all registered scripts
118
111
  */
119
- router.get('/scripts', async (req, res) => {
112
+ router.get('/scripts', async (_req, res) => {
120
113
  try {
121
- const factory = req.scriptFactory;
114
+ const { scriptFactory: factory } = bootstrapAdminScripts();
122
115
  const scripts = factory.getAll();
123
116
 
124
117
  res.json({
@@ -144,7 +137,7 @@ router.get('/scripts', async (req, res) => {
144
137
  router.get('/scripts/:scriptName', async (req, res) => {
145
138
  try {
146
139
  const { scriptName } = req.params;
147
- const factory = req.scriptFactory;
140
+ const { scriptFactory: factory } = bootstrapAdminScripts();
148
141
 
149
142
  if (!factory.has(scriptName)) {
150
143
  return res.status(404).json({
@@ -179,7 +172,7 @@ router.post('/scripts/:scriptName/validate', async (req, res) => {
179
172
  try {
180
173
  const { scriptName } = req.params;
181
174
  const { params = {} } = req.body;
182
- const factory = req.scriptFactory;
175
+ const { scriptFactory: factory } = bootstrapAdminScripts();
183
176
 
184
177
  if (!factory.has(scriptName)) {
185
178
  return res.status(404).json({
@@ -204,7 +197,11 @@ router.post('/scripts/:scriptName', async (req, res) => {
204
197
  try {
205
198
  const { scriptName } = req.params;
206
199
  const { params = {}, mode = 'async' } = req.body;
207
- const factory = req.scriptFactory;
200
+ const {
201
+ scriptFactory: factory,
202
+ integrationFactory,
203
+ scriptCommands,
204
+ } = bootstrapAdminScripts();
208
205
 
209
206
  if (!factory.has(scriptName)) {
210
207
  return res.status(404).json({
@@ -242,8 +239,9 @@ router.post('/scripts/:scriptName', async (req, res) => {
242
239
  }
243
240
 
244
241
  const runner = createScriptRunner({
245
- scriptFactory: req.scriptFactory,
246
- integrationFactory: req.integrationFactory,
242
+ scriptFactory: factory,
243
+ integrationFactory,
244
+ scriptCommands,
247
245
  });
248
246
  const result = await runner.execute(scriptName, params, {
249
247
  trigger: 'MANUAL',
@@ -263,7 +261,7 @@ router.post('/scripts/:scriptName', async (req, res) => {
263
261
  }
264
262
 
265
263
  const commands = createAdminScriptCommands();
266
- const execution = await commands.createAdminProcess({
264
+ const execution = await commands.createExecution({
267
265
  scriptName,
268
266
  scriptVersion: definition.version,
269
267
  trigger: 'MANUAL',
@@ -311,7 +309,7 @@ router.get('/scripts/:scriptName/executions/:executionId', async (req, res) => {
311
309
  try {
312
310
  const { executionId } = req.params;
313
311
  const commands = createAdminScriptCommands();
314
- const execution = await commands.findAdminProcessById(executionId);
312
+ const execution = await commands.findExecutionById(executionId);
315
313
 
316
314
  if (execution.error) {
317
315
  return res.status(execution.error).json({
@@ -342,7 +340,7 @@ router.get('/scripts/:scriptName/executions', async (req, res) => {
342
340
  ? 50
343
341
  : Math.min(Math.max(parsedLimit, 1), 200);
344
342
 
345
- const executions = await commands.findAdminProcessesByName(scriptName, {
343
+ const executions = await commands.findExecutionsByName(scriptName, {
346
344
  limit: safeLimit,
347
345
  ...(status && { state: status }),
348
346
  });
@@ -361,9 +359,8 @@ router.get('/scripts/:scriptName/executions', async (req, res) => {
361
359
  router.get('/scripts/:scriptName/schedule', async (req, res) => {
362
360
  try {
363
361
  const { scriptName } = req.params;
364
- const { getEffectiveSchedule } = createScheduleUseCases(
365
- req.scriptFactory
366
- );
362
+ const { scriptFactory } = bootstrapAdminScripts();
363
+ const { getEffectiveSchedule } = createScheduleUseCases(scriptFactory);
367
364
 
368
365
  const result = await getEffectiveSchedule.execute(scriptName);
369
366
 
@@ -385,7 +382,8 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
385
382
  try {
386
383
  const { scriptName } = req.params;
387
384
  const { enabled, cronExpression, timezone } = req.body;
388
- const { upsertSchedule } = createScheduleUseCases(req.scriptFactory);
385
+ const { scriptFactory } = bootstrapAdminScripts();
386
+ const { upsertSchedule } = createScheduleUseCases(scriptFactory);
389
387
 
390
388
  const result = await upsertSchedule.execute(scriptName, {
391
389
  enabled,
@@ -415,7 +413,8 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
415
413
  router.delete('/scripts/:scriptName/schedule', async (req, res) => {
416
414
  try {
417
415
  const { scriptName } = req.params;
418
- const { deleteSchedule } = createScheduleUseCases(req.scriptFactory);
416
+ const { scriptFactory } = bootstrapAdminScripts();
417
+ const { deleteSchedule } = createScheduleUseCases(scriptFactory);
419
418
 
420
419
  const result = await deleteSchedule.execute(scriptName);
421
420
 
@@ -15,6 +15,7 @@ const { ScriptFactory } = require('../application/script-factory');
15
15
  */
16
16
  let bootstrapped = false;
17
17
  let scriptFactory = null;
18
+ let scriptCommands = null;
18
19
  let integrationFactory = null;
19
20
 
20
21
  function registerScripts(factory, scriptClasses) {
@@ -45,11 +46,41 @@ function createIntegrationFactory() {
45
46
  }
46
47
 
47
48
  /**
48
- * @returns {{ scriptFactory: ScriptFactory, integrationFactory: object }}
49
+ * Build the command bundle injected into every AdminScriptContext. Scripts
50
+ * interact with the database only through these Frigg commands — never through
51
+ * repositories directly. The require is lazy so the package keeps no load-time
52
+ * coupling to core's command/repository modules.
53
+ */
54
+ function buildScriptCommands() {
55
+ const {
56
+ createUserCommands,
57
+ } = require('@friggframework/core/application/commands/user-commands');
58
+ const {
59
+ createCredentialCommands,
60
+ } = require('@friggframework/core/application/commands/credential-commands');
61
+ const {
62
+ createEntityCommands,
63
+ } = require('@friggframework/core/application/commands/entity-commands');
64
+ const {
65
+ createIntegrationCommands,
66
+ } = require('@friggframework/core/application/commands/integration-commands');
67
+
68
+ return {
69
+ users: createUserCommands(),
70
+ credentials: createCredentialCommands(),
71
+ entities: createEntityCommands(),
72
+ // Class-agnostic reads (findIntegrationById, listIntegrations). Scripts
73
+ // that need class-scoped integration ops build their own commands.
74
+ integrations: createIntegrationCommands(),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * @returns {{ scriptFactory: ScriptFactory, scriptCommands: object, integrationFactory: object }}
49
80
  */
50
81
  function bootstrapAdminScripts() {
51
82
  if (bootstrapped) {
52
- return { scriptFactory, integrationFactory };
83
+ return { scriptFactory, scriptCommands, integrationFactory };
53
84
  }
54
85
  bootstrapped = true;
55
86
 
@@ -72,14 +103,26 @@ function bootstrapAdminScripts() {
72
103
  );
73
104
  }
74
105
 
106
+ // Built in its own try so a command-layer failure never blocks script
107
+ // registration (and vice versa) — both honor the never-throw contract.
108
+ try {
109
+ scriptCommands = buildScriptCommands();
110
+ } catch (error) {
111
+ console.error(
112
+ '[admin-scripts] bootstrap: could not build command bundle:',
113
+ error.message
114
+ );
115
+ }
116
+
75
117
  integrationFactory = createIntegrationFactory();
76
- return { scriptFactory, integrationFactory };
118
+ return { scriptFactory, scriptCommands, integrationFactory };
77
119
  }
78
120
 
79
121
  /** Test-only: reset memoized bootstrap state. */
80
122
  function _resetBootstrapForTests() {
81
123
  bootstrapped = false;
82
124
  scriptFactory = null;
125
+ scriptCommands = null;
83
126
  integrationFactory = null;
84
127
  }
85
128
 
@@ -8,7 +8,7 @@ const { bootstrapAdminScripts } = require('./bootstrap');
8
8
  * Run a single execution message through the ScriptRunner.
9
9
  * @param {Object} message - Parsed execution message.
10
10
  * @param {string} message.scriptName - Name of the registered script to run (required).
11
- * @param {string} [message.executionId] - Existing AdminProcess id to resume; when
11
+ * @param {string} [message.executionId] - Existing AdminScriptExecution id to resume; when
12
12
  * absent, ScriptRunner creates a new record.
13
13
  * @param {string} [message.trigger] - Execution trigger; defaults to 'QUEUE'.
14
14
  * @param {Object} [message.params] - Parameters passed to the script.
@@ -16,10 +16,14 @@ const { bootstrapAdminScripts } = require('./bootstrap');
16
16
  * @param {Object} deps
17
17
  * @param {ScriptFactory} deps.scriptFactory - Registry used to resolve and instantiate the script.
18
18
  * @param {Object} deps.integrationFactory - Hydrates integration instances for scripts that need them.
19
+ * @param {Object} deps.scriptCommands - Frigg command bundle exposed to the script as context.commands.
19
20
  * @returns {Promise<{ scriptName: string, status: string, executionId: string }>}
20
21
  * @private
21
22
  */
22
- async function runMessage(message, { scriptFactory, integrationFactory }) {
23
+ async function runMessage(
24
+ message,
25
+ { scriptFactory, integrationFactory, scriptCommands }
26
+ ) {
23
27
  const { scriptName, executionId, trigger, params, parentExecutionId } =
24
28
  message;
25
29
 
@@ -33,7 +37,11 @@ async function runMessage(message, { scriptFactory, integrationFactory }) {
33
37
  }`
34
38
  );
35
39
 
36
- const runner = createScriptRunner({ scriptFactory, integrationFactory });
40
+ const runner = createScriptRunner({
41
+ scriptFactory,
42
+ integrationFactory,
43
+ scriptCommands,
44
+ });
37
45
  const result = await runner.execute(scriptName, params, {
38
46
  trigger: trigger || 'QUEUE',
39
47
  mode: 'async',
@@ -51,7 +59,7 @@ async function runMessage(message, { scriptFactory, integrationFactory }) {
51
59
  }
52
60
 
53
61
  /**
54
- * Mark an admin process FAILED when the worker itself blows up (parse error,
62
+ * Mark an admin script execution FAILED when the worker itself blows up (parse error,
55
63
  * runner construction), so the record doesn't stay stuck in a non-terminal
56
64
  * state. No-op when there is no execution id.
57
65
  * @private
@@ -60,7 +68,7 @@ async function markFailed(executionId, error) {
60
68
  if (!executionId) return;
61
69
  try {
62
70
  const commands = createAdminScriptCommands();
63
- await commands.completeAdminProcess(executionId, {
71
+ await commands.completeExecution(executionId, {
64
72
  state: 'FAILED',
65
73
  error: {
66
74
  name: error.name,
@@ -80,7 +88,7 @@ async function markFailed(executionId, error) {
80
88
  * Handle an EventBridge Scheduler direct invoke: the event itself is a single
81
89
  * execution message (no `Records` wrapper).
82
90
  * @param {Object} event - The execution message.
83
- * @param {{ scriptFactory: ScriptFactory, integrationFactory: object }} deps
91
+ * @param {{ scriptFactory: ScriptFactory, integrationFactory: object, scriptCommands: object }} deps
84
92
  * @returns {Promise<{ statusCode: number, body: string }>}
85
93
  * @private
86
94
  */
@@ -118,7 +126,7 @@ async function handleScheduledInvoke(event, deps) {
118
126
  * (manual async executions and queueScript continuations). Failures are isolated
119
127
  * per record so one bad message doesn't drop the rest of the batch.
120
128
  * @param {Object} event - The SQS event with a `Records` array.
121
- * @param {{ scriptFactory: ScriptFactory, integrationFactory: object }} deps
129
+ * @param {{ scriptFactory: ScriptFactory, integrationFactory: object, scriptCommands: object }} deps
122
130
  * @returns {Promise<{ statusCode: number, body: string }>}
123
131
  * @private
124
132
  */