@friggframework/admin-scripts 2.0.0--canary.517.095e3f6.0 → 2.0.0--canary.517.44c158e.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
 
@@ -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
132
  | `getExecutionId()` | The current `AdminProcess` 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
 
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.44c158e.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.44c158e.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.44c158e.0",
15
+ "@friggframework/prettier-config": "2.0.0--canary.517.44c158e.0",
16
+ "@friggframework/test": "2.0.0--canary.517.44c158e.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": "44c158e7a3bbeaac4b410c1062b0ce209bffd7f0"
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
 
@@ -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
24
  * @param {string|number|null} [params.executionId] - ID of the AdminProcess 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 ====================
@@ -21,6 +21,8 @@ class ScriptRunner {
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
  /**
@@ -100,6 +103,7 @@ class ScriptRunner {
100
103
  const context = createAdminScriptContext({
101
104
  executionId,
102
105
  integrationFactory: this.integrationFactory,
106
+ commands: this.scriptCommands,
103
107
  });
104
108
 
105
109
  let output;
@@ -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,6 +178,7 @@ describe('Admin Script Router', () => {
177
178
  expect(createScriptRunner).toHaveBeenCalledWith({
178
179
  scriptFactory: mockFactory,
179
180
  integrationFactory: {},
181
+ scriptCommands: {},
180
182
  });
181
183
  });
182
184
 
@@ -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,12 +12,14 @@ 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
25
  completeAdminProcess: jest.fn().mockResolvedValue({}),
@@ -26,6 +28,7 @@ describe('Admin Script Executor Handler', () => {
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',
@@ -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',
@@ -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',
@@ -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
 
@@ -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',
@@ -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
  */