@friggframework/admin-scripts 2.0.0--canary.517.8eaf5df.0 → 2.0.0--canary.517.c197eb5.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
@@ -36,7 +36,6 @@ const Definition = {
36
36
  adminScripts: [AttioHealingScript],
37
37
 
38
38
  admin: {
39
- includeBuiltinScripts: true, // register oauth-token-refresh + integration-health-check
40
39
  enableScheduling: true, // provision EventBridge Scheduler resources
41
40
  },
42
41
  };
@@ -44,7 +43,7 @@ const Definition = {
44
43
  module.exports = { Definition };
45
44
  ```
46
45
 
47
- At deploy time the framework's `AdminScriptBuilder` provisions the SQS queue, the router + worker Lambdas, and (when `enableScheduling` is set) the EventBridge Scheduler group and IAM role. At runtime the router/worker load this app definition and register your scripts (plus the built-ins) into the script registry.
46
+ At deploy time the framework's `AdminScriptBuilder` provisions the SQS queue, the router + worker Lambdas, and (when `enableScheduling` is set) the EventBridge Scheduler group and IAM role. At runtime the router/worker load this app definition and register your scripts into the script registry.
48
47
 
49
48
  ---
50
49
 
@@ -96,9 +95,14 @@ class AttioHealingScript extends AdminScriptBase {
96
95
  throw new Error(`Integration ${integrationId} not found`);
97
96
  }
98
97
 
99
- // Call the live integration/API when you need it
98
+ // Call the live integration when you need to hit an external API.
99
+ // Modules are attached to the instance by their own name (from the
100
+ // module's getName(), e.g. `instance.attio`) and each module's `.api`
101
+ // is its authenticated client. Iterate `instance.modules` if you don't
102
+ // want to hard-code a module name. The methods on `.api` are defined by
103
+ // that specific API module.
100
104
  const instance = await this.context.instantiate(integrationId);
101
- await instance.primary.api.refreshMetadata();
105
+ await instance.attio.api.refreshEntityConfig(); // example — use your module's real method
102
106
 
103
107
  this.context.log('info', 'Healing complete', { integrationId });
104
108
  return { healed: true, integrationId };
@@ -159,10 +163,10 @@ curl -X POST https://<your-app>/admin/scripts/attio-healing \
159
163
  **Sync** — runs inline and returns the result. Only for fast scripts: in a deployed environment, sync is rejected (`400 SYNC_TIMEOUT_TOO_LONG`) when the script's `config.timeout` exceeds the API Lambda budget (~25s). Use `async` for anything longer.
160
164
 
161
165
  ```bash
162
- curl -X POST https://<your-app>/admin/scripts/integration-health-check \
166
+ curl -X POST https://<your-app>/admin/scripts/attio-healing \
163
167
  -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
164
168
  -H "Content-Type: application/json" \
165
- -d '{ "params": {}, "mode": "sync" }'
169
+ -d '{ "params": { "integrationId": "abc123" }, "mode": "sync" }'
166
170
 
167
171
  # 200 OK
168
172
  # { "executionId": "...", "status": "COMPLETED", "output": { ... }, "metrics": { "durationMs": 812 } }
@@ -203,29 +207,20 @@ A script can ship a default schedule in its `Definition.schedule`, and operators
203
207
 
204
208
  ```bash
205
209
  # Enable a daily 6am UTC run
206
- curl -X PUT https://<your-app>/admin/scripts/integration-health-check/schedule \
210
+ curl -X PUT https://<your-app>/admin/scripts/attio-healing/schedule \
207
211
  -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
208
212
  -H "Content-Type: application/json" \
209
213
  -d '{ "enabled": true, "cronExpression": "cron(0 6 * * ? *)", "timezone": "UTC" }'
210
214
 
211
215
  # Inspect / remove
212
- curl https://<your-app>/admin/scripts/integration-health-check/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
213
- curl -X DELETE https://<your-app>/admin/scripts/integration-health-check/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
216
+ curl https://<your-app>/admin/scripts/attio-healing/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
217
+ curl -X DELETE https://<your-app>/admin/scripts/attio-healing/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
214
218
  ```
215
219
 
216
220
  Scheduling requires `admin.enableScheduling: true` in the app definition (so the EventBridge Scheduler group, IAM role, and env vars are provisioned). In a deployed environment the router refuses to fall back to the in-memory local scheduler, returning `503 SCHEDULER_NOT_CONFIGURED` if the provider isn't wired.
217
221
 
218
222
  ---
219
223
 
220
- ## Built-in scripts
221
-
222
- Set `admin.includeBuiltinScripts: true` to register:
223
-
224
- - **`oauth-token-refresh`** — refreshes OAuth tokens for integrations nearing expiry. Params: `integrationIds?`, `expiryThresholdHours` (default 24), `dryRun`.
225
- - **`integration-health-check`** — checks credential validity and API connectivity. Params: `integrationIds?`, `checkCredentials` (default true), `checkConnectivity` (default true), `updateStatus` (default false).
226
-
227
- ---
228
-
229
224
  ## Script chaining
230
225
 
231
226
  Long or fan-out work can enqueue follow-up scripts. Continuations are tracked with `parentExecutionId` so you can trace the lineage:
package/index.js CHANGED
@@ -37,14 +37,6 @@ const {
37
37
  handler: executorHandler,
38
38
  } = require('./src/infrastructure/script-executor-handler');
39
39
 
40
- // Built-in Scripts
41
- const {
42
- OAuthTokenRefreshScript,
43
- IntegrationHealthCheckScript,
44
- builtinScripts,
45
- registerBuiltinScripts,
46
- } = require('./src/builtins');
47
-
48
40
  // Adapters
49
41
  const { SchedulerAdapter } = require('./src/adapters/scheduler-adapter');
50
42
  const { AWSSchedulerAdapter } = require('./src/adapters/aws-scheduler-adapter');
@@ -76,12 +68,6 @@ module.exports = {
76
68
  routerHandler,
77
69
  executorHandler,
78
70
 
79
- // Built-in scripts
80
- OAuthTokenRefreshScript,
81
- IntegrationHealthCheckScript,
82
- builtinScripts,
83
- registerBuiltinScripts,
84
-
85
71
  // Adapters
86
72
  SchedulerAdapter,
87
73
  AWSSchedulerAdapter,
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.517.8eaf5df.0",
4
+ "version": "2.0.0--canary.517.c197eb5.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.8eaf5df.0",
8
+ "@friggframework/core": "2.0.0--canary.517.c197eb5.0",
9
+ "@hapi/boom": "^10.0.1",
9
10
  "express": "^4.18.2",
10
11
  "serverless-http": "^3.2.0"
11
12
  },
12
13
  "devDependencies": {
13
- "@friggframework/eslint-config": "2.0.0--canary.517.8eaf5df.0",
14
- "@friggframework/prettier-config": "2.0.0--canary.517.8eaf5df.0",
15
- "@friggframework/test": "2.0.0--canary.517.8eaf5df.0",
14
+ "@friggframework/eslint-config": "2.0.0--canary.517.c197eb5.0",
15
+ "@friggframework/prettier-config": "2.0.0--canary.517.c197eb5.0",
16
+ "@friggframework/test": "2.0.0--canary.517.c197eb5.0",
16
17
  "eslint": "^8.22.0",
17
18
  "jest": "^29.7.0",
18
19
  "prettier": "^2.7.1",
@@ -43,5 +44,5 @@
43
44
  "maintenance",
44
45
  "operations"
45
46
  ],
46
- "gitHead": "8eaf5df0746c979fc5cb7877d51a861becd5fbce"
47
+ "gitHead": "c197eb5c237e263a8d1b65a94f213ee4732b3c57"
47
48
  }
@@ -170,7 +170,7 @@ describe('DeleteScheduleUseCase', () => {
170
170
  try {
171
171
  await useCase.execute('non-existent');
172
172
  } catch (error) {
173
- expect(error.code).toBe('SCRIPT_NOT_FOUND');
173
+ expect(error.output.statusCode).toBe(404);
174
174
  }
175
175
  });
176
176
  });
@@ -112,7 +112,7 @@ describe('GetEffectiveScheduleUseCase', () => {
112
112
  try {
113
113
  await useCase.execute('non-existent');
114
114
  } catch (error) {
115
- expect(error.code).toBe('SCRIPT_NOT_FOUND');
115
+ expect(error.output.statusCode).toBe(404);
116
116
  }
117
117
  });
118
118
  });
@@ -153,7 +153,7 @@ describe('UpsertScheduleUseCase', () => {
153
153
  try {
154
154
  await useCase.execute('non-existent', { enabled: true });
155
155
  } catch (error) {
156
- expect(error.code).toBe('SCRIPT_NOT_FOUND');
156
+ expect(error.output.statusCode).toBe(404);
157
157
  }
158
158
  });
159
159
 
@@ -167,7 +167,7 @@ describe('UpsertScheduleUseCase', () => {
167
167
  try {
168
168
  await useCase.execute('test-script', { enabled: 'yes' });
169
169
  } catch (error) {
170
- expect(error.code).toBe('INVALID_INPUT');
170
+ expect(error.output.statusCode).toBe(400);
171
171
  }
172
172
  });
173
173
 
@@ -183,7 +183,7 @@ describe('UpsertScheduleUseCase', () => {
183
183
  try {
184
184
  await useCase.execute('test-script', { enabled: true });
185
185
  } catch (error) {
186
- expect(error.code).toBe('INVALID_INPUT');
186
+ expect(error.output.statusCode).toBe(400);
187
187
  }
188
188
  });
189
189
 
@@ -1,3 +1,5 @@
1
+ const Boom = require('@hapi/boom');
2
+
1
3
  /**
2
4
  * Delete Schedule Use Case
3
5
  *
@@ -51,9 +53,7 @@ class DeleteScheduleUseCase {
51
53
  */
52
54
  _validateScriptExists(scriptName) {
53
55
  if (!this.scriptFactory.has(scriptName)) {
54
- const error = new Error(`Script "${scriptName}" not found`);
55
- error.code = 'SCRIPT_NOT_FOUND';
56
- throw error;
56
+ throw Boom.notFound(`Script "${scriptName}" not found`);
57
57
  }
58
58
  }
59
59
 
@@ -1,3 +1,5 @@
1
+ const Boom = require('@hapi/boom');
2
+
1
3
  /**
2
4
  * Get Effective Schedule Use Case
3
5
  *
@@ -62,9 +64,7 @@ class GetEffectiveScheduleUseCase {
62
64
  */
63
65
  _validateScriptExists(scriptName) {
64
66
  if (!this.scriptFactory.has(scriptName)) {
65
- const error = new Error(`Script "${scriptName}" not found`);
66
- error.code = 'SCRIPT_NOT_FOUND';
67
- throw error;
67
+ throw Boom.notFound(`Script "${scriptName}" not found`);
68
68
  }
69
69
  }
70
70
 
@@ -1,3 +1,5 @@
1
+ const Boom = require('@hapi/boom');
2
+
1
3
  /**
2
4
  * Upsert Schedule Use Case
3
5
  *
@@ -65,9 +67,7 @@ class UpsertScheduleUseCase {
65
67
  */
66
68
  _validateScriptExists(scriptName) {
67
69
  if (!this.scriptFactory.has(scriptName)) {
68
- const error = new Error(`Script "${scriptName}" not found`);
69
- error.code = 'SCRIPT_NOT_FOUND';
70
- throw error;
70
+ throw Boom.notFound(`Script "${scriptName}" not found`);
71
71
  }
72
72
  }
73
73
 
@@ -76,17 +76,13 @@ class UpsertScheduleUseCase {
76
76
  */
77
77
  _validateInput(enabled, cronExpression) {
78
78
  if (typeof enabled !== 'boolean') {
79
- const error = new Error('enabled must be a boolean');
80
- error.code = 'INVALID_INPUT';
81
- throw error;
79
+ throw Boom.badRequest('enabled must be a boolean');
82
80
  }
83
81
 
84
82
  if (enabled && !cronExpression) {
85
- const error = new Error(
83
+ throw Boom.badRequest(
86
84
  'cronExpression is required when enabled is true'
87
85
  );
88
- error.code = 'INVALID_INPUT';
89
- throw error;
90
86
  }
91
87
  }
92
88
 
@@ -404,7 +404,7 @@ describe('Admin Script Router', () => {
404
404
  );
405
405
 
406
406
  expect(response.status).toBe(404);
407
- expect(response.body.code).toBe('SCRIPT_NOT_FOUND');
407
+ expect(response.body.error).toMatch(/not found/i);
408
408
  });
409
409
  });
410
410
 
@@ -481,7 +481,6 @@ describe('Admin Script Router', () => {
481
481
  });
482
482
 
483
483
  expect(response.status).toBe(400);
484
- expect(response.body.code).toBe('INVALID_INPUT');
485
484
  expect(response.body.error).toContain('enabled');
486
485
  });
487
486
 
@@ -493,7 +492,6 @@ describe('Admin Script Router', () => {
493
492
  });
494
493
 
495
494
  expect(response.status).toBe(400);
496
- expect(response.body.code).toBe('INVALID_INPUT');
497
495
  expect(response.body.error).toContain('cronExpression');
498
496
  });
499
497
 
@@ -508,7 +506,7 @@ describe('Admin Script Router', () => {
508
506
  });
509
507
 
510
508
  expect(response.status).toBe(404);
511
- expect(response.body.code).toBe('SCRIPT_NOT_FOUND');
509
+ expect(response.body.error).toMatch(/not found/i);
512
510
  });
513
511
 
514
512
  it('should provision EventBridge schedule when enabled', async () => {
@@ -712,7 +710,7 @@ describe('Admin Script Router', () => {
712
710
  );
713
711
 
714
712
  expect(response.status).toBe(404);
715
- expect(response.body.code).toBe('SCRIPT_NOT_FOUND');
713
+ expect(response.body.error).toMatch(/not found/i);
716
714
  });
717
715
 
718
716
  it('should delete EventBridge schedule when external rule exists', async () => {
@@ -1,5 +1,6 @@
1
1
  const express = require('express');
2
2
  const serverless = require('serverless-http');
3
+ const Boom = require('@hapi/boom');
3
4
  const { validateAdminApiKey } = require('./admin-auth-middleware');
4
5
  const { getScriptFactory } = require('../application/script-factory');
5
6
  const { createScriptRunner } = require('../application/script-runner');
@@ -26,13 +27,27 @@ const router = express.Router();
26
27
  // Apply auth middleware to all admin routes
27
28
  router.use(validateAdminApiKey);
28
29
 
29
- // Register the host app's admin scripts (and built-ins) before handling requests.
30
+ // Register the host app's admin scripts before handling requests.
30
31
  // Memoized, so this only does work on the first request per process.
31
32
  router.use((_req, _res, next) => {
32
33
  bootstrapAdminScripts();
33
34
  next();
34
35
  });
35
36
 
37
+ /**
38
+ * Translate a thrown error into an HTTP response. Boom errors (thrown by the
39
+ * schedule use cases) carry their own status code; anything else is an
40
+ * unexpected 500. Mirrors the framework's app-handler-helpers convention.
41
+ * @private
42
+ */
43
+ function sendError(res, error, fallbackMessage) {
44
+ if (error.isBoom) {
45
+ return res.status(error.output.statusCode).json({ error: error.message });
46
+ }
47
+ console.error(fallbackMessage, error);
48
+ return res.status(500).json({ error: fallbackMessage });
49
+ }
50
+
36
51
  /**
37
52
  * Build audit metadata for an execution from the request.
38
53
  * @private
@@ -62,11 +77,9 @@ function createScheduleUseCases() {
62
77
  process.env.SCHEDULER_PROVIDER ||
63
78
  (process.env.AWS_LAMBDA_FUNCTION_NAME ? null : 'local');
64
79
  if (!schedulerType) {
65
- const error = new Error(
80
+ throw Boom.serverUnavailable(
66
81
  'SCHEDULER_PROVIDER is not configured. Set it (e.g. "aws") via appDefinition.admin.enableScheduling.'
67
82
  );
68
- error.code = 'SCHEDULER_NOT_CONFIGURED';
69
- throw error;
70
83
  }
71
84
 
72
85
  const schedulerAdapter = createSchedulerAdapter({
@@ -354,19 +367,7 @@ router.get('/scripts/:scriptName/schedule', async (req, res) => {
354
367
  ...result.schedule,
355
368
  });
356
369
  } catch (error) {
357
- if (error.code === 'SCRIPT_NOT_FOUND') {
358
- return res.status(404).json({
359
- error: error.message,
360
- code: error.code,
361
- });
362
- }
363
- if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
364
- return res
365
- .status(503)
366
- .json({ error: error.message, code: error.code });
367
- }
368
- console.error('Error getting schedule:', error);
369
- res.status(500).json({ error: 'Failed to get schedule' });
370
+ return sendError(res, error, 'Failed to get schedule');
370
371
  }
371
372
  });
372
373
 
@@ -397,25 +398,7 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
397
398
  }),
398
399
  });
399
400
  } catch (error) {
400
- if (error.code === 'SCRIPT_NOT_FOUND') {
401
- return res.status(404).json({
402
- error: error.message,
403
- code: error.code,
404
- });
405
- }
406
- if (error.code === 'INVALID_INPUT') {
407
- return res.status(400).json({
408
- error: error.message,
409
- code: error.code,
410
- });
411
- }
412
- if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
413
- return res
414
- .status(503)
415
- .json({ error: error.message, code: error.code });
416
- }
417
- console.error('Error updating schedule:', error);
418
- res.status(500).json({ error: 'Failed to update schedule' });
401
+ return sendError(res, error, 'Failed to update schedule');
419
402
  }
420
403
  });
421
404
 
@@ -432,19 +415,7 @@ router.delete('/scripts/:scriptName/schedule', async (req, res) => {
432
415
 
433
416
  res.json(result);
434
417
  } catch (error) {
435
- if (error.code === 'SCRIPT_NOT_FOUND') {
436
- return res.status(404).json({
437
- error: error.message,
438
- code: error.code,
439
- });
440
- }
441
- if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
442
- return res
443
- .status(503)
444
- .json({ error: error.message, code: error.code });
445
- }
446
- console.error('Error deleting schedule:', error);
447
- res.status(500).json({ error: 'Failed to delete schedule' });
418
+ return sendError(res, error, 'Failed to delete schedule');
448
419
  }
449
420
  });
450
421
 
@@ -55,15 +55,10 @@ function bootstrapAdminScripts() {
55
55
  const {
56
56
  loadAppDefinition,
57
57
  } = require('@friggframework/core/handlers/app-definition-loader');
58
- const { adminScripts = [], admin = {} } = loadAppDefinition();
58
+ const { adminScripts = [] } = loadAppDefinition();
59
59
 
60
60
  const factory = getScriptFactory();
61
61
  registerScripts(factory, adminScripts);
62
-
63
- if (admin.includeBuiltinScripts) {
64
- const { builtinScripts } = require('../builtins');
65
- registerScripts(factory, builtinScripts);
66
- }
67
62
  } catch (error) {
68
63
  console.error(
69
64
  '[admin-scripts] bootstrap: could not load app definition:',