@aglyn/plugins-workflows 1.0.0-beta.285 → 1.0.0-beta.287

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-workflows",
3
- "version": "1.0.0-beta.285",
3
+ "version": "1.0.0-beta.287",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,17 +25,17 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.285",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.285",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.285",
31
- "@aglyn/shared-ui-next": "1.0.0-beta.285",
32
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.285",
33
- "@aglyn/shared-util-email": "1.0.0-beta.285",
34
- "@aglyn/shared-util-http": "1.0.0-beta.285",
35
- "@aglyn/shared-util-timestamp": "1.0.0-beta.285",
36
- "@aglyn/tenant-data-admin": "1.0.0-beta.285",
37
- "@aglyn/tenant-feature-instance": "1.0.0-beta.285",
38
- "@aglyn/tenant-runtime": "1.0.0-beta.285",
28
+ "@aglyn/aglyn": "1.0.0-beta.287",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.287",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.287",
31
+ "@aglyn/shared-ui-next": "1.0.0-beta.287",
32
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.287",
33
+ "@aglyn/shared-util-email": "1.0.0-beta.287",
34
+ "@aglyn/shared-util-http": "1.0.0-beta.287",
35
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.287",
36
+ "@aglyn/tenant-data-admin": "1.0.0-beta.287",
37
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.287",
38
+ "@aglyn/tenant-runtime": "1.0.0-beta.287",
39
39
  "@swc/helpers": "0.5.23"
40
40
  },
41
41
  "peerDependencies": {
@@ -14,44 +14,6 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- /**
18
- * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).
19
- *
20
- * The Actions card's Test button runs an action with an in-page trigger as
21
- * though the trigger had fired on the site, and says what came back. A
22
- * published page reaches the same engine path through `events/dispatch`,
23
- * which is a TENANT route — served on the site's own origin, to anonymous
24
- * visitors — and the console is another app on another origin whose
25
- * dispatcher never loads it. This is the console's door to that path, and
26
- * unlike the page's it knows who is asking.
27
- *
28
- * ## Who may
29
- *
30
- * A site admin or editor, read off the host's `memberRoles`, or staff. The
31
- * publish roles rather than the content roles, because a test run is a LIVE
32
- * run: its emails are sent, its webhooks posted and its rows written, as a
33
- * visitor's would be. An author edits and makes nothing live, and so does not
34
- * run automations either.
35
- *
36
- * The site's plugin switch is the dispatcher's: the request names its site as
37
- * a top-level `hostId`, so a site with Automation switched off has no such
38
- * door.
39
- *
40
- * ## What runs
41
- *
42
- * The stored action, through `runSingleActionOutcome` — the single-action path
43
- * a page's dispatch takes — so the switch, the conditions, the plan gate and
44
- * the month's run allowance are the ones a real run meets, the run is metered
45
- * on the site's and the workspace's `actionRuns`, and it lands in the action's
46
- * Runs history like any other. It is marked rather than simulated: see
47
- * `actionTestRunPayload`.
48
- * When a gate stops it, the refusal names the gate, because a test that ran
49
- * nothing must not read as one that ran.
50
- *
51
- * Only an in-page trigger: a server event (a form submitted, an order placed)
52
- * carries fields no sample stands in for, and its runs come from the doors
53
- * that raise it.
54
- */
55
17
  import { type PluginApiHandler } from '@aglyn/aglyn/server';
56
18
  import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
57
19
  import type { SingleActionOutcome } from '../engine/run-event-actions';
@@ -51,7 +51,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
51
51
  * Only an in-page trigger: a server event (a form submitted, an order placed)
52
52
  * carries fields no sample stands in for, and its runs come from the doors
53
53
  * that raise it.
54
- */ import { hostRoleCanPublish, isSiteEventType, planLabelGrantingFeature } from "@aglyn/aglyn/server";
54
+ */ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
55
+ import { isSiteEventType, planLabelGrantingFeature } from "@aglyn/aglyn/server";
55
56
  /*
56
57
  * The MODULES, not the barrel, as for the org automation doors beside this
57
58
  * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.
@@ -183,7 +184,7 @@ function defaultDeps() {
183
184
  });
184
185
  }
185
186
  try {
186
- var _host_get, _ref2, _outcome_skipped;
187
+ var _ref2, _outcome_skipped;
187
188
  var _stored_data_trigger, _stored_data, _req_body2;
188
189
  const firestore = deps.firestore();
189
190
  const hostRef = firestore.collection('hosts').doc(hostId);
@@ -191,10 +192,9 @@ function defaultDeps() {
191
192
  if (!host.exists) return res.status(404).json({
192
193
  error: 'Unknown site'
193
194
  });
194
- const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[caller.uid];
195
- if (caller.staff !== true && !hostRoleCanPublish(role)) {
195
+ if (caller.staff !== true && !holdsSiteKey(host, caller.uid, 'site.publish')) {
196
196
  return res.status(403).json({
197
- error: 'Only a site admin or editor can test an action'
197
+ error: siteKeyRefusal('site.publish')
198
198
  });
199
199
  }
200
200
  const rate = await deps.consumeRateLimit(`workflows-test-run:${hostId}:${caller.uid}`, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/action-test-run-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).\n *\n * The Actions card's Test button runs an action with an in-page trigger as\n * though the trigger had fired on the site, and says what came back. A\n * published page reaches the same engine path through `events/dispatch`,\n * which is a TENANT route — served on the site's own origin, to anonymous\n * visitors — and the console is another app on another origin whose\n * dispatcher never loads it. This is the console's door to that path, and\n * unlike the page's it knows who is asking.\n *\n * ## Who may\n *\n * A site admin or editor, read off the host's `memberRoles`, or staff. The\n * publish roles rather than the content roles, because a test run is a LIVE\n * run: its emails are sent, its webhooks posted and its rows written, as a\n * visitor's would be. An author edits and makes nothing live, and so does not\n * run automations either.\n *\n * The site's plugin switch is the dispatcher's: the request names its site as\n * a top-level `hostId`, so a site with Automation switched off has no such\n * door.\n *\n * ## What runs\n *\n * The stored action, through `runSingleActionOutcome` — the single-action path\n * a page's dispatch takes — so the switch, the conditions, the plan gate and\n * the month's run allowance are the ones a real run meets, the run is metered\n * on the site's and the workspace's `actionRuns`, and it lands in the action's\n * Runs history like any other. It is marked rather than simulated: see\n * `actionTestRunPayload`.\n * When a gate stops it, the refusal names the gate, because a test that ran\n * nothing must not read as one that ran.\n *\n * Only an in-page trigger: a server event (a form submitted, an order placed)\n * carries fields no sample stands in for, and its runs come from the doors\n * that raise it.\n */\n\nimport {\n hostRoleCanPublish,\n isSiteEventType,\n type PluginApiHandler,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel, as for the org automation doors beside this\n * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { consumeRateLimit } from '@aglyn/tenant-data-admin/server/rate-limit-store'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport type {\n SingleActionOutcome,\n SingleActionSkip,\n} from '../engine/run-event-actions'\nimport { actionTestRunPayload } from '../model/action-test-run'\nimport type { HostAction } from '../model/host-actions'\n\nexport { ACTION_TEST_RUN_API_ROUTE } from '../model/action-test-run'\n\n/**\n * Test runs one person may start on one site in a minute.\n *\n * Every test is a real run with real sends, so this is the feature's own\n * bucket beneath the console dispatcher's general one: a person iterating on\n * an action tests every few seconds at most, and past this is a script.\n */\nexport const ACTION_TEST_RUNS_PER_MINUTE = 10\n\nconst ACTION_TEST_RUN_WINDOW_MS = 60_000\n\n/** What the door reaches outside this module; specs hand in their own. */\nexport interface ActionTestRunDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; staff?: unknown }>\n consumeRateLimit(\n key: string,\n options: { limit: number; windowMs: number },\n ): Promise<{ allowed: boolean; resetMs: number }>\n runAction(\n hostId: string,\n actionId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<SingleActionOutcome>\n now(): number\n}\n\nfunction defaultDeps(): ActionTestRunDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, staff: decoded['staff'] }\n },\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n // Imported by the first test run rather than with the console surface,\n // as the host-event listener imports it.\n runAction: async (hostId, actionId, event, payload) => {\n const { runSingleActionOutcome } = await import('../engine/run-event-actions')\n return await runSingleActionOutcome(hostId, actionId, event, payload)\n },\n now: () => Date.now(),\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\n/** The status and words for each gate that can stop a test run. */\nfunction refusalFor(\n skipped: SingleActionSkip,\n outcome: SingleActionOutcome,\n payload: HostEventPayload,\n): { status: number; error: string } {\n switch (skipped) {\n case 'missing':\n return { status: 404, error: 'That action no longer exists' }\n case 'disabled':\n return { status: 409, error: 'Switch the action on to test it' }\n case 'event':\n return {\n status: 409,\n error: 'The action changed while it was being tested — try again',\n }\n case 'conditions':\n return {\n status: 422,\n error:\n `Nothing ran: a test run on ${String(payload['path'])} does not ` +\n 'meet this action’s conditions',\n }\n case 'plan':\n return {\n status: 403,\n error:\n `Actions need the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade',\n }\n case 'allowance':\n return {\n status: 402,\n error: `This site has used its ${outcome.limit} action runs for the month`,\n }\n default:\n return { status: 500, error: 'The test run could not be completed. Try again.' }\n }\n}\n\n/**\n * `automations/actions/test-run`: run one of a site's actions now.\n *\n * Body `{ hostId, actionId, payload? }`. Answers `{ ok: true, alerts }` when\n * the action's steps ran — the site alerts they produced, for the card to show\n * — and otherwise an `error` saying what stopped it.\n */\nexport function createActionTestRunHandler(\n overrides: Partial<ActionTestRunDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const actionId = String(req.body?.actionId ?? '')\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(actionId)) {\n return res.status(400).json({ error: 'Invalid actionId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n let caller: { uid: string; staff?: unknown }\n try {\n caller = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[workflows] test run could not verify the caller', error)\n return res\n .status(500)\n .json({ error: 'The sign-in could not be checked. Try again.' })\n }\n\n try {\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[caller.uid]\n if (caller.staff !== true && !hostRoleCanPublish(role)) {\n return res\n .status(403)\n .json({ error: 'Only a site admin or editor can test an action' })\n }\n\n const rate = await deps.consumeRateLimit(\n `workflows-test-run:${hostId}:${caller.uid}`,\n { limit: ACTION_TEST_RUNS_PER_MINUTE, windowMs: ACTION_TEST_RUN_WINDOW_MS },\n )\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))),\n )\n return res\n .status(429)\n .json({ error: 'Too many test runs — wait a minute and try again' })\n }\n\n const stored = await hostRef.collection('actions').doc(actionId).get()\n if (!stored.exists || stored.get('deletedAt')) {\n return res.status(404).json({ error: 'That action no longer exists' })\n }\n const event = String(\n (stored.data() as HostAction | undefined)?.trigger?.event ?? '',\n )\n if (!isSiteEventType(event)) {\n return res.status(400).json({\n error: 'Only an action with an in-page trigger can be tested here',\n })\n }\n\n const payload = actionTestRunPayload(req.body?.payload)\n const outcome = await deps.runAction(hostId, actionId, event, payload)\n if (outcome.ran) {\n return res.status(200).json({ ok: true, alerts: outcome.alerts })\n }\n const refusal = refusalFor(outcome.skipped ?? 'failed', outcome, payload)\n return res.status(refusal.status).json({ error: refusal.error })\n } catch (error) {\n console.error('[workflows] test run failed', hostId, actionId, error)\n return res\n .status(500)\n .json({ error: 'The test run could not be completed. Try again.' })\n }\n }\n}\n\nexport const actionTestRunHandler = createActionTestRunHandler()\n"],"names":["hostRoleCanPublish","isSiteEventType","planLabelGrantingFeature","isDocumentId","firebaseAdmin","isRefusedIdToken","consumeRateLimit","actionTestRunPayload","ACTION_TEST_RUN_API_ROUTE","ACTION_TEST_RUNS_PER_MINUTE","ACTION_TEST_RUN_WINDOW_MS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","staff","key","options","runAction","hostId","actionId","event","payload","runSingleActionOutcome","now","Date","bearerToken","headers","authorization","String","startsWith","slice","length","refusalFor","skipped","outcome","status","error","limit","createActionTestRunHandler","overrides","req","res","deps","method","json","body","caller","console","host","stored","hostRef","collection","doc","get","exists","role","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/action-test-run-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).\n *\n * The Actions card's Test button runs an action with an in-page trigger as\n * though the trigger had fired on the site, and says what came back. A\n * published page reaches the same engine path through `events/dispatch`,\n * which is a TENANT route — served on the site's own origin, to anonymous\n * visitors — and the console is another app on another origin whose\n * dispatcher never loads it. This is the console's door to that path, and\n * unlike the page's it knows who is asking.\n *\n * ## Who may\n *\n * A site admin or editor, read off the host's `memberRoles`, or staff. The\n * publish roles rather than the content roles, because a test run is a LIVE\n * run: its emails are sent, its webhooks posted and its rows written, as a\n * visitor's would be. An author edits and makes nothing live, and so does not\n * run automations either.\n *\n * The site's plugin switch is the dispatcher's: the request names its site as\n * a top-level `hostId`, so a site with Automation switched off has no such\n * door.\n *\n * ## What runs\n *\n * The stored action, through `runSingleActionOutcome` — the single-action path\n * a page's dispatch takes — so the switch, the conditions, the plan gate and\n * the month's run allowance are the ones a real run meets, the run is metered\n * on the site's and the workspace's `actionRuns`, and it lands in the action's\n * Runs history like any other. It is marked rather than simulated: see\n * `actionTestRunPayload`.\n * When a gate stops it, the refusal names the gate, because a test that ran\n * nothing must not read as one that ran.\n *\n * Only an in-page trigger: a server event (a form submitted, an order placed)\n * carries fields no sample stands in for, and its runs come from the doors\n * that raise it.\n */\n\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport {\n isSiteEventType,\n type PluginApiHandler,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel, as for the org automation doors beside this\n * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { consumeRateLimit } from '@aglyn/tenant-data-admin/server/rate-limit-store'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport type {\n SingleActionOutcome,\n SingleActionSkip,\n} from '../engine/run-event-actions'\nimport { actionTestRunPayload } from '../model/action-test-run'\nimport type { HostAction } from '../model/host-actions'\n\nexport { ACTION_TEST_RUN_API_ROUTE } from '../model/action-test-run'\n\n/**\n * Test runs one person may start on one site in a minute.\n *\n * Every test is a real run with real sends, so this is the feature's own\n * bucket beneath the console dispatcher's general one: a person iterating on\n * an action tests every few seconds at most, and past this is a script.\n */\nexport const ACTION_TEST_RUNS_PER_MINUTE = 10\n\nconst ACTION_TEST_RUN_WINDOW_MS = 60_000\n\n/** What the door reaches outside this module; specs hand in their own. */\nexport interface ActionTestRunDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; staff?: unknown }>\n consumeRateLimit(\n key: string,\n options: { limit: number; windowMs: number },\n ): Promise<{ allowed: boolean; resetMs: number }>\n runAction(\n hostId: string,\n actionId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<SingleActionOutcome>\n now(): number\n}\n\nfunction defaultDeps(): ActionTestRunDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, staff: decoded['staff'] }\n },\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n // Imported by the first test run rather than with the console surface,\n // as the host-event listener imports it.\n runAction: async (hostId, actionId, event, payload) => {\n const { runSingleActionOutcome } = await import('../engine/run-event-actions')\n return await runSingleActionOutcome(hostId, actionId, event, payload)\n },\n now: () => Date.now(),\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\n/** The status and words for each gate that can stop a test run. */\nfunction refusalFor(\n skipped: SingleActionSkip,\n outcome: SingleActionOutcome,\n payload: HostEventPayload,\n): { status: number; error: string } {\n switch (skipped) {\n case 'missing':\n return { status: 404, error: 'That action no longer exists' }\n case 'disabled':\n return { status: 409, error: 'Switch the action on to test it' }\n case 'event':\n return {\n status: 409,\n error: 'The action changed while it was being tested — try again',\n }\n case 'conditions':\n return {\n status: 422,\n error:\n `Nothing ran: a test run on ${String(payload['path'])} does not ` +\n 'meet this action’s conditions',\n }\n case 'plan':\n return {\n status: 403,\n error:\n `Actions need the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade',\n }\n case 'allowance':\n return {\n status: 402,\n error: `This site has used its ${outcome.limit} action runs for the month`,\n }\n default:\n return { status: 500, error: 'The test run could not be completed. Try again.' }\n }\n}\n\n/**\n * `automations/actions/test-run`: run one of a site's actions now.\n *\n * Body `{ hostId, actionId, payload? }`. Answers `{ ok: true, alerts }` when\n * the action's steps ran — the site alerts they produced, for the card to show\n * — and otherwise an `error` saying what stopped it.\n */\nexport function createActionTestRunHandler(\n overrides: Partial<ActionTestRunDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const actionId = String(req.body?.actionId ?? '')\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(actionId)) {\n return res.status(400).json({ error: 'Invalid actionId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n let caller: { uid: string; staff?: unknown }\n try {\n caller = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[workflows] test run could not verify the caller', error)\n return res\n .status(500)\n .json({ error: 'The sign-in could not be checked. Try again.' })\n }\n\n try {\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n if (caller.staff !== true && !holdsSiteKey(host, caller.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\n }\n\n const rate = await deps.consumeRateLimit(\n `workflows-test-run:${hostId}:${caller.uid}`,\n { limit: ACTION_TEST_RUNS_PER_MINUTE, windowMs: ACTION_TEST_RUN_WINDOW_MS },\n )\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))),\n )\n return res\n .status(429)\n .json({ error: 'Too many test runs — wait a minute and try again' })\n }\n\n const stored = await hostRef.collection('actions').doc(actionId).get()\n if (!stored.exists || stored.get('deletedAt')) {\n return res.status(404).json({ error: 'That action no longer exists' })\n }\n const event = String(\n (stored.data() as HostAction | undefined)?.trigger?.event ?? '',\n )\n if (!isSiteEventType(event)) {\n return res.status(400).json({\n error: 'Only an action with an in-page trigger can be tested here',\n })\n }\n\n const payload = actionTestRunPayload(req.body?.payload)\n const outcome = await deps.runAction(hostId, actionId, event, payload)\n if (outcome.ran) {\n return res.status(200).json({ ok: true, alerts: outcome.alerts })\n }\n const refusal = refusalFor(outcome.skipped ?? 'failed', outcome, payload)\n return res.status(refusal.status).json({ error: refusal.error })\n } catch (error) {\n console.error('[workflows] test run failed', hostId, actionId, error)\n return res\n .status(500)\n .json({ error: 'The test run could not be completed. Try again.' })\n }\n }\n}\n\nexport const actionTestRunHandler = createActionTestRunHandler()\n"],"names":["holdsSiteKey","siteKeyRefusal","isSiteEventType","planLabelGrantingFeature","isDocumentId","firebaseAdmin","isRefusedIdToken","consumeRateLimit","actionTestRunPayload","ACTION_TEST_RUN_API_ROUTE","ACTION_TEST_RUNS_PER_MINUTE","ACTION_TEST_RUN_WINDOW_MS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","staff","key","options","runAction","hostId","actionId","event","payload","runSingleActionOutcome","now","Date","bearerToken","headers","authorization","String","startsWith","slice","length","refusalFor","skipped","outcome","status","error","limit","createActionTestRunHandler","overrides","req","res","deps","method","json","body","caller","console","stored","hostRef","collection","doc","host","get","exists","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
@@ -50,7 +50,8 @@ export declare const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
50
50
  /** A draft's name when the caller asks for none. */
51
51
  export declare const AUTOMATION_DRAFT_DEFAULT_NAME = "New automation";
52
52
  /** The resources route's refusal for a member who may not write the site. */
53
- export declare const AUTOMATION_DRAFT_ROLE_REFUSAL = "Editing requires the editor role";
53
+ /** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */
54
+ export declare const AUTOMATION_DRAFT_ROLE_REFUSAL: string;
54
55
  /** What a person is told when their plan has no actions builder. It names the page that sells one. */
55
56
  export declare const AUTOMATION_DRAFT_PLAN_REFUSAL = "Automations are not included on this workspace\u2019s plan. Upgrade in Billing to build them.";
56
57
  /** The resources route's refusal at the cap. */
@@ -14,8 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
- import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
17
+ */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
19
19
  import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
20
20
  import { firebaseAdmin } from "@aglyn/tenant-data-admin";
21
21
  import { BUNDLE_ID } from "./constants/bundle-common.js";
@@ -25,7 +25,7 @@ import { interactionPlaceholders } from "@aglyn/aglyn/app-utils/draft-placeholde
25
25
  /** The resource name this writer is registered under. */ export const AUTOMATION_DRAFT_RESOURCE = 'automation';
26
26
  /** The longest name the Actions editor stores. */ export const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
27
27
  /** A draft's name when the caller asks for none. */ export const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation';
28
- /** The resources route's refusal for a member who may not write the site. */ export const AUTOMATION_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role';
28
+ /** The resources route's refusal for a member who may not write the site. */ /** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */ export const AUTOMATION_DRAFT_ROLE_REFUSAL = siteKeyRefusal('site.edit');
29
29
  /** What a person is told when their plan has no actions builder. It names the page that sells one. */ export const AUTOMATION_DRAFT_PLAN_REFUSAL = 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.';
30
30
  /** The resources route's refusal at the cap. */ export const AUTOMATION_DRAFT_LIMIT_REFUSAL = `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` + 'delete some to make room';
31
31
  /** A trigger bound to one element, which makes an action an interaction of its page. */ const LEAF_SELECTOR = /^\[data-aglyn="leaf:.+"\]$/;
@@ -324,9 +324,7 @@ function recordOf(snapshot) {
324
324
  };
325
325
  }
326
326
  function roleRefusal(host, uid) {
327
- var _host_get;
328
- const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
329
- return hostRoleCanWrite(role) ? null : {
327
+ return holdsSiteKey(host, uid, 'site.edit') ? null : {
330
328
  status: 403,
331
329
  error: AUTOMATION_DRAFT_ROLE_REFUSAL
332
330
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server-automation-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n ACTIONS_MAX_PER_HOST,\n siteInteractionDocument,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './model/host-actions'\nimport { interactionPlaceholders } from '@aglyn/aglyn/app-utils/draft-placeholders'\n\n/**\n * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).\n *\n * The draft writer this plugin registers for the `automation` resource on the\n * core's resource-drafts seam. A plugin that produces an automation — a\n * generator working from a description, an importer bringing flows over from\n * another tool — asks for the writer by name and gets this plugin's rules,\n * never a copy of them:\n *\n * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the\n * Actions editor saves (`siteInteractionDocument`), with only the fields each\n * step type holds. It is listed on the Automation page's Actions, opens in\n * that editor, and runs through the same executor as one a person made.\n * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,\n * so nothing it describes runs until a person switches it on.\n * - THE SCHEMA is the editor's own validator, `validateHostAction`, which the\n * executor's writers share. A placeholder the draft could not fill — a\n * bracketed list name, a bracketed condition value — passes it, because it\n * is a value a person replaces, and the facts name every one.\n * - THE ROOM is the flat platform cap on live actions, counted inside the\n * transaction with the arithmetic `/api/hosts/resources` uses.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content. THE PLAN is the Actions editor's: the `actions` entitlement.\n *\n * Nothing is enabled, sent or run, and the writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const AUTOMATION_DRAFT_RESOURCE = 'automation'\n\n/** The longest name the Actions editor stores. */\nexport const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60\n\n/** A draft's name when the caller asks for none. */\nexport const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation'\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const AUTOMATION_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** What a person is told when their plan has no actions builder. It names the page that sells one. */\nexport const AUTOMATION_DRAFT_PLAN_REFUSAL =\n 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.'\n\n/** The resources route's refusal at the cap. */\nexport const AUTOMATION_DRAFT_LIMIT_REFUSAL =\n `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` +\n 'delete some to make room'\n\n/** A trigger bound to one element, which makes an action an interaction of its page. */\nconst LEAF_SELECTOR = /^\\[data-aglyn=\"leaf:.+\"\\]$/\n\n/**\n * The fields each step type holds, besides `type` and the step's own `when`.\n * Total over the step types, so a type the model gains is a compile error here\n * until its fields are named, rather than a draft that silently drops them.\n */\nexport const AUTOMATION_STEP_FIELDS: Readonly<Record<HostActionStepType, readonly string[]>> = {\n runWorkflow: ['workflowId', 'workflowName'],\n siteAlert: ['message', 'severity'],\n customEvent: ['eventName'],\n datasetAppend: ['datasetId', 'datasetName'],\n webhookPost: ['webhookId', 'webhookName'],\n showOverlay: ['overlayId', 'overlayName'],\n stickyNav: ['selector'],\n addClass: ['selector', 'className'],\n removeClass: ['selector', 'className'],\n toggleClass: ['selector', 'className'],\n showElement: ['selector', 'delayMs', 'dismissOn'],\n hideElement: ['selector', 'delayMs'],\n toggleElement: ['selector', 'delayMs', 'dismissOn'],\n setAttribute: ['selector', 'name', 'value'],\n removeAttribute: ['selector', 'name'],\n scrollTo: ['selector', 'behavior', 'offsetPx'],\n playVideo: ['selector'],\n openDrawer: ['drawerNodeId'],\n closeDrawer: ['drawerNodeId'],\n toggleDrawer: ['drawerNodeId'],\n openMenu: ['menuNodeId'],\n closeMenu: ['menuNodeId'],\n toggleMenu: ['menuNodeId'],\n showHtml: ['html'],\n runJs: ['code'],\n redirect: ['url', 'screenId'],\n trackGaEvent: ['eventName', 'params'],\n sendEmail: ['subject', 'body', 'toField', 'topicId', 'transactional'],\n notifyAdmins: ['title', 'body'],\n enrollList: ['listId', 'listName'],\n updateDataset: ['datasetId', 'datasetName'],\n assignCampaign: ['campaignId', 'campaignName'],\n wait: ['delayMinutes'],\n waitForEvent: ['eventName', 'timeoutMinutes'],\n exitFlow: [],\n setContactStage: ['lifecycleStage'],\n addContactTag: ['tag'],\n assignContactOwner: ['ownerUid', 'ownerEmail', 'roundRobin'],\n createCrmTask: ['title', 'kind', 'priority', 'dueInDays', 'assigneeUid', 'assigneeEmail'],\n logCrmActivity: ['kind', 'body', 'direction'],\n}\n\n/** The trigger keys an automation stores. */\nconst TRIGGER_FIELDS = [\n 'event',\n 'filter',\n 'conditions',\n 'combinator',\n 'selector',\n 'threshold',\n 'pathPattern',\n 'oncePerVisitor',\n 'oncePerSession',\n 'cooldownMinutes',\n 'everyTime',\n] as const\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The keys of `source` that `keys` names and that hold something. */\nfunction picked(source: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n if (source[key] !== undefined && source[key] !== null) out[key] = source[key]\n }\n return out\n}\n\nfunction conditionOf(value: unknown): HostActionTriggerCondition | null {\n if (!isRecord(value)) return null\n return picked(value, ['field', 'op', 'value']) as unknown as HostActionTriggerCondition\n}\n\nexport type AutomationDraftContentRead =\n | { ok: true; action: HostAction }\n | { ok: false; problems: string[] }\n\n/**\n * What a caller sends as `content` — `{ action }` — read into the action this\n * writer would store: the trigger's and each step's own fields and nothing\n * else, named, OFF, and held to the editor's validator.\n */\nexport function readAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): AutomationDraftContentRead {\n const raw = content['action']\n if (!isRecord(raw)) return { ok: false, problems: ['The draft holds no automation'] }\n const trigger = raw['trigger']\n const steps = raw['steps']\n if (!isRecord(trigger)) return { ok: false, problems: ['Pick a trigger event'] }\n if (!Array.isArray(steps)) return { ok: false, problems: ['Add at least one step'] }\n const problems: string[] = []\n const knownTypes = new Set(Object.keys(AUTOMATION_STEP_FIELDS))\n const cleanSteps: HostActionStep[] = []\n steps.forEach((step, index) => {\n const type = isRecord(step) ? step['type'] : undefined\n if (typeof type !== 'string' || !knownTypes.has(type)) {\n problems.push(`Step ${index + 1}: not a step the Actions editor offers`)\n return\n }\n const source = step as Record<string, unknown>\n const when = isRecord(source['when'])\n ? {\n conditions: (Array.isArray(source['when']['conditions']) ? source['when']['conditions'] : [])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null),\n ...(typeof source['when']['combinator'] === 'string'\n ? { combinator: source['when']['combinator'] }\n : {}),\n }\n : null\n cleanSteps.push({\n type,\n ...picked(source, AUTOMATION_STEP_FIELDS[type as HostActionStepType]),\n ...(when && when.conditions.length ? { when } : {}),\n } as HostActionStep)\n })\n const cleanTrigger = picked(trigger, TRIGGER_FIELDS)\n if (Array.isArray(cleanTrigger['conditions'])) {\n cleanTrigger['conditions'] = (cleanTrigger['conditions'] as unknown[])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null)\n }\n // An element's own interaction belongs to the page that holds the element,\n // and is authored there; the Actions list does not show one.\n if (typeof cleanTrigger['selector'] === 'string' && LEAF_SELECTOR.test(cleanTrigger['selector'])) {\n problems.push('An interaction on one element is set up on that element, not as an automation')\n }\n const name = String(raw['name'] ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS)\n const action: HostAction = {\n name: name || AUTOMATION_DRAFT_DEFAULT_NAME,\n trigger: cleanTrigger as unknown as HostAction['trigger'],\n steps: cleanSteps,\n enabled: false,\n }\n const invalid = validateHostAction(action)\n if (invalid) problems.push(invalid)\n return problems.length ? { ok: false, problems } : { ok: true, action }\n}\n\n/** What the writer reports about an automation beyond its identity. */\nfunction factsOf(action: Pick<HostAction, 'trigger' | 'steps' | 'enabled'>): Record<string, unknown> {\n return {\n event: action.trigger?.event ?? '',\n steps: (action.steps ?? []).length,\n enabled: action.enabled === true,\n placeholders: interactionPlaceholders(action).length,\n }\n}\n\n/** Whether content is an automation this site could store: well-formed, off, and valid. */\nexport function checkAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): PluginDraftCheck {\n const read = readAutomationDraftContent(content)\n return read.ok === false ? read : { ok: true, facts: factsOf(read.action) }\n}\n\nfunction recordOf(snapshot: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (snapshot.data() ?? {}) as HostAction\n return {\n id: snapshot.id,\n name: String(data.name ?? ''),\n versionId: null,\n facts: factsOf(data),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: AUTOMATION_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: Readonly<Record<string, unknown>> | null): PluginDraftRefusal | null {\n return checkEntitlement(org as Partial<AglynOrgBilling> | null, 'actions')\n ? null\n : { status: 403, error: AUTOMATION_DRAFT_PLAN_REFUSAL }\n}\n\n/** The live actions the site holds: a soft-deleted one frees its slot. */\nfunction liveCount(rows: FirebaseFirestore.QuerySnapshot): number {\n return rows.docs.filter((row) => row.get('deletedAt') == null).length\n}\n\nfunction roomRefusal(live: number): PluginDraftRefusal | null {\n return live >= ACTIONS_MAX_PER_HOST ? { status: 403, error: AUTOMATION_DRAFT_LIMIT_REFUSAL } : null\n}\n\nexport interface AutomationDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createAutomationDraftWriter(\n deps: AutomationDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n planRefusal(context.org) ??\n roomRefusal(liveCount(await hostRef.collection('actions').select('deletedAt').get()))\n )\n },\n\n check: (content) => checkAutomationDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const snapshot = await firestore().collection('hosts').doc(hostId).collection('actions').doc(id).get()\n return snapshot.exists ? recordOf(snapshot) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readAutomationDraftContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const actionRef = hostRef.collection('actions').doc(request.id)\n const name =\n String(request.name ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS) || read.action.name\n const action: HostAction = { ...read.action, name, enabled: false, recipe: null }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(actionRef),\n tx.get(hostRef.collection('actions').select('deletedAt')),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const refusal =\n roleRefusal(host, request.uid) ?? planRefusal(request.org) ?? roomRefusal(liveCount(rows))\n if (refusal) return { ok: false, ...refusal }\n tx.create(actionRef, {\n ...siteInteractionDocument(action),\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return { ok: true, replayed: false, id: request.id, name, versionId: null, facts: factsOf(action) }\n })\n },\n }\n}\n\nexport const automationDraftWriter = createAutomationDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerAutomationDraftWriter(): void {\n registerPluginResourceDraftWriter(AUTOMATION_DRAFT_RESOURCE, automationDraftWriter, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","ACTIONS_MAX_PER_HOST","siteInteractionDocument","validateHostAction","interactionPlaceholders","AUTOMATION_DRAFT_RESOURCE","AUTOMATION_DRAFT_NAME_MAX_CHARS","AUTOMATION_DRAFT_DEFAULT_NAME","AUTOMATION_DRAFT_ROLE_REFUSAL","AUTOMATION_DRAFT_PLAN_REFUSAL","AUTOMATION_DRAFT_LIMIT_REFUSAL","LEAF_SELECTOR","AUTOMATION_STEP_FIELDS","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","removeClass","toggleClass","showElement","hideElement","toggleElement","setAttribute","removeAttribute","scrollTo","playVideo","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","TRIGGER_FIELDS","isRecord","value","Array","isArray","picked","source","keys","out","key","undefined","conditionOf","readAutomationDraftContent","content","raw","ok","problems","trigger","steps","knownTypes","Set","Object","cleanSteps","forEach","step","index","type","has","push","when","conditions","map","filter","clause","combinator","length","cleanTrigger","test","name","String","replace","trim","slice","action","enabled","invalid","factsOf","event","placeholders","checkAutomationDraftContent","read","facts","recordOf","snapshot","data","id","versionId","roleRefusal","host","uid","role","get","status","error","planRefusal","org","liveCount","rows","docs","row","roomRefusal","live","createAutomationDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","doc","hostId","exists","select","check","write","request","db","actionRef","recipe","runTransaction","tx","existing","Promise","all","replayed","create","createdAt","now","updatedAt","createdBy","automationDraftWriter","registerAutomationDraftWriter","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server-automation-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n ACTIONS_MAX_PER_HOST,\n siteInteractionDocument,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './model/host-actions'\nimport { interactionPlaceholders } from '@aglyn/aglyn/app-utils/draft-placeholders'\n\n/**\n * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).\n *\n * The draft writer this plugin registers for the `automation` resource on the\n * core's resource-drafts seam. A plugin that produces an automation — a\n * generator working from a description, an importer bringing flows over from\n * another tool — asks for the writer by name and gets this plugin's rules,\n * never a copy of them:\n *\n * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the\n * Actions editor saves (`siteInteractionDocument`), with only the fields each\n * step type holds. It is listed on the Automation page's Actions, opens in\n * that editor, and runs through the same executor as one a person made.\n * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,\n * so nothing it describes runs until a person switches it on.\n * - THE SCHEMA is the editor's own validator, `validateHostAction`, which the\n * executor's writers share. A placeholder the draft could not fill — a\n * bracketed list name, a bracketed condition value — passes it, because it\n * is a value a person replaces, and the facts name every one.\n * - THE ROOM is the flat platform cap on live actions, counted inside the\n * transaction with the arithmetic `/api/hosts/resources` uses.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content. THE PLAN is the Actions editor's: the `actions` entitlement.\n *\n * Nothing is enabled, sent or run, and the writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const AUTOMATION_DRAFT_RESOURCE = 'automation'\n\n/** The longest name the Actions editor stores. */\nexport const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60\n\n/** A draft's name when the caller asks for none. */\nexport const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation'\n\n/** The resources route's refusal for a member who may not write the site. */\n/** Names \"Edit site content\" (`site.edit`, AGL-4047), never a role. */\nexport const AUTOMATION_DRAFT_ROLE_REFUSAL = siteKeyRefusal('site.edit')\n\n/** What a person is told when their plan has no actions builder. It names the page that sells one. */\nexport const AUTOMATION_DRAFT_PLAN_REFUSAL =\n 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.'\n\n/** The resources route's refusal at the cap. */\nexport const AUTOMATION_DRAFT_LIMIT_REFUSAL =\n `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` +\n 'delete some to make room'\n\n/** A trigger bound to one element, which makes an action an interaction of its page. */\nconst LEAF_SELECTOR = /^\\[data-aglyn=\"leaf:.+\"\\]$/\n\n/**\n * The fields each step type holds, besides `type` and the step's own `when`.\n * Total over the step types, so a type the model gains is a compile error here\n * until its fields are named, rather than a draft that silently drops them.\n */\nexport const AUTOMATION_STEP_FIELDS: Readonly<Record<HostActionStepType, readonly string[]>> = {\n runWorkflow: ['workflowId', 'workflowName'],\n siteAlert: ['message', 'severity'],\n customEvent: ['eventName'],\n datasetAppend: ['datasetId', 'datasetName'],\n webhookPost: ['webhookId', 'webhookName'],\n showOverlay: ['overlayId', 'overlayName'],\n stickyNav: ['selector'],\n addClass: ['selector', 'className'],\n removeClass: ['selector', 'className'],\n toggleClass: ['selector', 'className'],\n showElement: ['selector', 'delayMs', 'dismissOn'],\n hideElement: ['selector', 'delayMs'],\n toggleElement: ['selector', 'delayMs', 'dismissOn'],\n setAttribute: ['selector', 'name', 'value'],\n removeAttribute: ['selector', 'name'],\n scrollTo: ['selector', 'behavior', 'offsetPx'],\n playVideo: ['selector'],\n openDrawer: ['drawerNodeId'],\n closeDrawer: ['drawerNodeId'],\n toggleDrawer: ['drawerNodeId'],\n openMenu: ['menuNodeId'],\n closeMenu: ['menuNodeId'],\n toggleMenu: ['menuNodeId'],\n showHtml: ['html'],\n runJs: ['code'],\n redirect: ['url', 'screenId'],\n trackGaEvent: ['eventName', 'params'],\n sendEmail: ['subject', 'body', 'toField', 'topicId', 'transactional'],\n notifyAdmins: ['title', 'body'],\n enrollList: ['listId', 'listName'],\n updateDataset: ['datasetId', 'datasetName'],\n assignCampaign: ['campaignId', 'campaignName'],\n wait: ['delayMinutes'],\n waitForEvent: ['eventName', 'timeoutMinutes'],\n exitFlow: [],\n setContactStage: ['lifecycleStage'],\n addContactTag: ['tag'],\n assignContactOwner: ['ownerUid', 'ownerEmail', 'roundRobin'],\n createCrmTask: ['title', 'kind', 'priority', 'dueInDays', 'assigneeUid', 'assigneeEmail'],\n logCrmActivity: ['kind', 'body', 'direction'],\n}\n\n/** The trigger keys an automation stores. */\nconst TRIGGER_FIELDS = [\n 'event',\n 'filter',\n 'conditions',\n 'combinator',\n 'selector',\n 'threshold',\n 'pathPattern',\n 'oncePerVisitor',\n 'oncePerSession',\n 'cooldownMinutes',\n 'everyTime',\n] as const\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The keys of `source` that `keys` names and that hold something. */\nfunction picked(source: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n if (source[key] !== undefined && source[key] !== null) out[key] = source[key]\n }\n return out\n}\n\nfunction conditionOf(value: unknown): HostActionTriggerCondition | null {\n if (!isRecord(value)) return null\n return picked(value, ['field', 'op', 'value']) as unknown as HostActionTriggerCondition\n}\n\nexport type AutomationDraftContentRead =\n | { ok: true; action: HostAction }\n | { ok: false; problems: string[] }\n\n/**\n * What a caller sends as `content` — `{ action }` — read into the action this\n * writer would store: the trigger's and each step's own fields and nothing\n * else, named, OFF, and held to the editor's validator.\n */\nexport function readAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): AutomationDraftContentRead {\n const raw = content['action']\n if (!isRecord(raw)) return { ok: false, problems: ['The draft holds no automation'] }\n const trigger = raw['trigger']\n const steps = raw['steps']\n if (!isRecord(trigger)) return { ok: false, problems: ['Pick a trigger event'] }\n if (!Array.isArray(steps)) return { ok: false, problems: ['Add at least one step'] }\n const problems: string[] = []\n const knownTypes = new Set(Object.keys(AUTOMATION_STEP_FIELDS))\n const cleanSteps: HostActionStep[] = []\n steps.forEach((step, index) => {\n const type = isRecord(step) ? step['type'] : undefined\n if (typeof type !== 'string' || !knownTypes.has(type)) {\n problems.push(`Step ${index + 1}: not a step the Actions editor offers`)\n return\n }\n const source = step as Record<string, unknown>\n const when = isRecord(source['when'])\n ? {\n conditions: (Array.isArray(source['when']['conditions']) ? source['when']['conditions'] : [])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null),\n ...(typeof source['when']['combinator'] === 'string'\n ? { combinator: source['when']['combinator'] }\n : {}),\n }\n : null\n cleanSteps.push({\n type,\n ...picked(source, AUTOMATION_STEP_FIELDS[type as HostActionStepType]),\n ...(when && when.conditions.length ? { when } : {}),\n } as HostActionStep)\n })\n const cleanTrigger = picked(trigger, TRIGGER_FIELDS)\n if (Array.isArray(cleanTrigger['conditions'])) {\n cleanTrigger['conditions'] = (cleanTrigger['conditions'] as unknown[])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null)\n }\n // An element's own interaction belongs to the page that holds the element,\n // and is authored there; the Actions list does not show one.\n if (typeof cleanTrigger['selector'] === 'string' && LEAF_SELECTOR.test(cleanTrigger['selector'])) {\n problems.push('An interaction on one element is set up on that element, not as an automation')\n }\n const name = String(raw['name'] ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS)\n const action: HostAction = {\n name: name || AUTOMATION_DRAFT_DEFAULT_NAME,\n trigger: cleanTrigger as unknown as HostAction['trigger'],\n steps: cleanSteps,\n enabled: false,\n }\n const invalid = validateHostAction(action)\n if (invalid) problems.push(invalid)\n return problems.length ? { ok: false, problems } : { ok: true, action }\n}\n\n/** What the writer reports about an automation beyond its identity. */\nfunction factsOf(action: Pick<HostAction, 'trigger' | 'steps' | 'enabled'>): Record<string, unknown> {\n return {\n event: action.trigger?.event ?? '',\n steps: (action.steps ?? []).length,\n enabled: action.enabled === true,\n placeholders: interactionPlaceholders(action).length,\n }\n}\n\n/** Whether content is an automation this site could store: well-formed, off, and valid. */\nexport function checkAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): PluginDraftCheck {\n const read = readAutomationDraftContent(content)\n return read.ok === false ? read : { ok: true, facts: factsOf(read.action) }\n}\n\nfunction recordOf(snapshot: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (snapshot.data() ?? {}) as HostAction\n return {\n id: snapshot.id,\n name: String(data.name ?? ''),\n versionId: null,\n facts: factsOf(data),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n return holdsSiteKey(host, uid, 'site.edit') ? null : { status: 403, error: AUTOMATION_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: Readonly<Record<string, unknown>> | null): PluginDraftRefusal | null {\n return checkEntitlement(org as Partial<AglynOrgBilling> | null, 'actions')\n ? null\n : { status: 403, error: AUTOMATION_DRAFT_PLAN_REFUSAL }\n}\n\n/** The live actions the site holds: a soft-deleted one frees its slot. */\nfunction liveCount(rows: FirebaseFirestore.QuerySnapshot): number {\n return rows.docs.filter((row) => row.get('deletedAt') == null).length\n}\n\nfunction roomRefusal(live: number): PluginDraftRefusal | null {\n return live >= ACTIONS_MAX_PER_HOST ? { status: 403, error: AUTOMATION_DRAFT_LIMIT_REFUSAL } : null\n}\n\nexport interface AutomationDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createAutomationDraftWriter(\n deps: AutomationDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n planRefusal(context.org) ??\n roomRefusal(liveCount(await hostRef.collection('actions').select('deletedAt').get()))\n )\n },\n\n check: (content) => checkAutomationDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const snapshot = await firestore().collection('hosts').doc(hostId).collection('actions').doc(id).get()\n return snapshot.exists ? recordOf(snapshot) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readAutomationDraftContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const actionRef = hostRef.collection('actions').doc(request.id)\n const name =\n String(request.name ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS) || read.action.name\n const action: HostAction = { ...read.action, name, enabled: false, recipe: null }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(actionRef),\n tx.get(hostRef.collection('actions').select('deletedAt')),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const refusal =\n roleRefusal(host, request.uid) ?? planRefusal(request.org) ?? roomRefusal(liveCount(rows))\n if (refusal) return { ok: false, ...refusal }\n tx.create(actionRef, {\n ...siteInteractionDocument(action),\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return { ok: true, replayed: false, id: request.id, name, versionId: null, facts: factsOf(action) }\n })\n },\n }\n}\n\nexport const automationDraftWriter = createAutomationDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerAutomationDraftWriter(): void {\n registerPluginResourceDraftWriter(AUTOMATION_DRAFT_RESOURCE, automationDraftWriter, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["checkEntitlement","holdsSiteKey","siteKeyRefusal","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","ACTIONS_MAX_PER_HOST","siteInteractionDocument","validateHostAction","interactionPlaceholders","AUTOMATION_DRAFT_RESOURCE","AUTOMATION_DRAFT_NAME_MAX_CHARS","AUTOMATION_DRAFT_DEFAULT_NAME","AUTOMATION_DRAFT_ROLE_REFUSAL","AUTOMATION_DRAFT_PLAN_REFUSAL","AUTOMATION_DRAFT_LIMIT_REFUSAL","LEAF_SELECTOR","AUTOMATION_STEP_FIELDS","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","removeClass","toggleClass","showElement","hideElement","toggleElement","setAttribute","removeAttribute","scrollTo","playVideo","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","TRIGGER_FIELDS","isRecord","value","Array","isArray","picked","source","keys","out","key","undefined","conditionOf","readAutomationDraftContent","content","raw","ok","problems","trigger","steps","knownTypes","Set","Object","cleanSteps","forEach","step","index","type","has","push","when","conditions","map","filter","clause","combinator","length","cleanTrigger","test","name","String","replace","trim","slice","action","enabled","invalid","factsOf","event","placeholders","checkAutomationDraftContent","read","facts","recordOf","snapshot","data","id","versionId","roleRefusal","host","uid","status","error","planRefusal","org","liveCount","rows","docs","row","get","roomRefusal","live","createAutomationDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","doc","hostId","exists","select","check","write","request","db","actionRef","recipe","runTransaction","tx","existing","Promise","all","replayed","create","createdAt","now","updatedAt","createdBy","automationDraftWriter","registerAutomationDraftWriter","pluginId"],"mappings":"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"}