@aglyn/plugins-workflows 1.0.0-beta.221 → 1.0.0-beta.223

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.
Files changed (101) hide show
  1. package/README.md +22 -14
  2. package/package.json +12 -12
  3. package/src/lib/components/automation-step-fields.component.d.ts +16 -16
  4. package/src/lib/components/automation-step-fields.component.js +146 -12
  5. package/src/lib/components/automation-step-fields.component.js.map +1 -1
  6. package/src/lib/components/automation-trigger-conditions.component.d.ts +25 -3
  7. package/src/lib/components/automation-trigger-conditions.component.js +89 -10
  8. package/src/lib/components/automation-trigger-conditions.component.js.map +1 -1
  9. package/src/lib/components/host-actions-card.component.js +126 -84
  10. package/src/lib/components/host-actions-card.component.js.map +1 -1
  11. package/src/lib/components/host-webhooks-card.component.js +2 -1
  12. package/src/lib/components/host-webhooks-card.component.js.map +1 -1
  13. package/src/lib/components/host-workflows-card.component.js +31 -10
  14. package/src/lib/components/host-workflows-card.component.js.map +1 -1
  15. package/src/lib/components/org-automation-editor.component.d.ts +2 -1
  16. package/src/lib/components/org-automation-editor.component.js +20 -11
  17. package/src/lib/components/org-automation-editor.component.js.map +1 -1
  18. package/src/lib/components/org-automations-card.component.js +2 -1
  19. package/src/lib/components/org-automations-card.component.js.map +1 -1
  20. package/src/lib/components/run-quota-line.component.d.ts +17 -4
  21. package/src/lib/components/run-quota-line.component.js +16 -7
  22. package/src/lib/components/run-quota-line.component.js.map +1 -1
  23. package/src/lib/components/site-org-automations-panel.component.js +2 -1
  24. package/src/lib/components/site-org-automations-panel.component.js.map +1 -1
  25. package/src/lib/components/workflows-console-page.js +18 -7
  26. package/src/lib/components/workflows-console-page.js.map +1 -1
  27. package/src/lib/declarations.d.ts +28 -0
  28. package/src/lib/declarations.js +37 -0
  29. package/src/lib/declarations.js.map +1 -0
  30. package/src/lib/declarations.server.js +17 -0
  31. package/src/lib/declarations.server.js.map +1 -1
  32. package/src/lib/engine/email-merge.d.ts +69 -0
  33. package/src/lib/engine/email-merge.js +98 -0
  34. package/src/lib/engine/email-merge.js.map +1 -0
  35. package/src/lib/engine/run-event-actions.d.ts +35 -10
  36. package/src/lib/engine/run-event-actions.js +256 -369
  37. package/src/lib/engine/run-event-actions.js.map +1 -1
  38. package/src/lib/engine/run-event-automations.d.ts +1 -1
  39. package/src/lib/engine/run-event-automations.js.map +1 -1
  40. package/src/lib/engine/run-event-workflows.d.ts +2 -1
  41. package/src/lib/engine/run-event-workflows.js +71 -30
  42. package/src/lib/engine/run-event-workflows.js.map +1 -1
  43. package/src/lib/engine/run-meter.d.ts +98 -0
  44. package/src/lib/engine/run-meter.js +121 -0
  45. package/src/lib/engine/run-meter.js.map +1 -0
  46. package/src/lib/engine/run-org-automations.d.ts +3 -2
  47. package/src/lib/engine/run-org-automations.js +17 -12
  48. package/src/lib/engine/run-org-automations.js.map +1 -1
  49. package/src/lib/engine/triggered-docs.d.ts +39 -0
  50. package/src/lib/engine/triggered-docs.js +47 -0
  51. package/src/lib/engine/triggered-docs.js.map +1 -0
  52. package/src/lib/engine/workflow-steps.d.ts +1 -1
  53. package/src/lib/engine/workflow-steps.js +3 -2
  54. package/src/lib/engine/workflow-steps.js.map +1 -1
  55. package/src/lib/model/host-actions.d.ts +284 -0
  56. package/src/lib/model/host-actions.js +316 -0
  57. package/src/lib/model/host-actions.js.map +1 -0
  58. package/src/lib/model/org-automations.d.ts +8 -5
  59. package/src/lib/model/org-automations.js +14 -8
  60. package/src/lib/model/org-automations.js.map +1 -1
  61. package/src/lib/model/step-outcomes.d.ts +2 -2
  62. package/src/lib/model/step-outcomes.js +1 -1
  63. package/src/lib/model/step-outcomes.js.map +1 -1
  64. package/src/lib/plugin.js +10 -0
  65. package/src/lib/plugin.js.map +1 -1
  66. package/src/lib/server/action-test-run-route.d.ts +3 -2
  67. package/src/lib/server/action-test-run-route.js +3 -2
  68. package/src/lib/server/action-test-run-route.js.map +1 -1
  69. package/src/lib/server/org-automations-routes.d.ts +42 -1
  70. package/src/lib/server/org-automations-routes.js +120 -72
  71. package/src/lib/server/org-automations-routes.js.map +1 -1
  72. package/src/lib/server-automation-drafts.d.ts +2 -2
  73. package/src/lib/server-automation-drafts.js +11 -7
  74. package/src/lib/server-automation-drafts.js.map +1 -1
  75. package/src/lib/server.js +19 -13
  76. package/src/lib/server.js.map +1 -1
  77. package/src/lib/testing/stand-in-crm-steps.d.ts +45 -0
  78. package/src/lib/testing/stand-in-crm-steps.js +97 -0
  79. package/src/lib/testing/stand-in-crm-steps.js.map +1 -0
  80. package/src/lib/testing/stand-in-dataset-list.js +11 -3
  81. package/src/lib/testing/stand-in-dataset-list.js.map +1 -1
  82. package/src/lib/testing/stand-in-dataset-steps.d.ts +43 -0
  83. package/src/lib/testing/stand-in-dataset-steps.js +65 -0
  84. package/src/lib/testing/stand-in-dataset-steps.js.map +1 -0
  85. package/src/lib/testing/stand-in-person-records.d.ts +35 -0
  86. package/src/lib/testing/stand-in-person-records.js +54 -0
  87. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  88. package/src/lib/testing/stand-in-recipes.d.ts +36 -0
  89. package/src/lib/testing/stand-in-recipes.js +206 -0
  90. package/src/lib/testing/stand-in-recipes.js.map +1 -0
  91. package/src/lib/transfer/org-automations-package.d.ts +54 -0
  92. package/src/lib/transfer/org-automations-package.js +167 -0
  93. package/src/lib/transfer/org-automations-package.js.map +1 -0
  94. package/src/lib/transfer/org-automations-package.server.d.ts +38 -0
  95. package/src/lib/transfer/org-automations-package.server.js +296 -0
  96. package/src/lib/transfer/org-automations-package.server.js.map +1 -0
  97. package/src/lib/usage-axes.js +5 -1
  98. package/src/lib/usage-axes.js.map +1 -1
  99. package/src/lib/engine/crm-action-steps.d.ts +0 -142
  100. package/src/lib/engine/crm-action-steps.js +0 -452
  101. package/src/lib/engine/crm-action-steps.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/org-automations-routes.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 * WRITING AN ORG AUTOMATION, and pausing one on a site (AGL-3302).\n *\n * `orgs/{orgId}/automations` is closed to every client write, the way the\n * outreach collections are: an org automation runs on every site it is\n * placed on, as that site, so its trigger, its steps and its placement are\n * decisions the server checks — the vocabulary, the organization's sites, the\n * plan — and nothing a browser may write around.\n *\n * ## Two doors\n *\n * `automations/manage` is the organization's: it creates, edits, switches on\n * and off, and deletes. It names the org and NO site, so it declares a\n * subject resolver for the dispatchers' release gate and asks the org's\n * plugin switch itself; the caller must be an org-wide owner, admin or\n * editor.\n *\n * `automations/pause` is the SITE's — the host level control. It names the\n * site, so the dispatcher applies that site's plugin switch as for any site\n * request, and it changes one thing: whether this automation is paused on\n * THIS site. The site's own admins and editors may use it, and so may an\n * org-wide editor; nobody may pause a site through it but the one named.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostIdsFromScope, visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n type AglynOrgMember,\n checkEntitlement,\n isOrgWideMember,\n type PluginApiHandler,\n type PluginApiRequestSubject,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel: `@aglyn/tenant-data-admin`'s index reaches the\n * Next render cache, and everything these doors need is the Admin SDK handle,\n * the id predicate and the org reads.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n getOrgDoc,\n logHostActivity,\n logOrgActivity,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n ORG_AUTOMATION_ACTIVITY_TARGET,\n ORG_AUTOMATION_API_ROUTES,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n orgAutomationPausedHostIds,\n prunePausedHostIds,\n readOrgAutomation,\n} from '../model/org-automations'\n\nexport { ORG_AUTOMATION_API_ROUTES }\n\n/** What `automations/manage` may be asked to do. */\nexport const ORG_AUTOMATION_MANAGE_ACTIONS = [\n 'create',\n 'update',\n 'setEnabled',\n 'delete',\n] as const\n\ntype ManageAction = (typeof ORG_AUTOMATION_MANAGE_ACTIONS)[number]\n\n/** The org roles that may write the organization's automations. */\nconst ORG_MANAGE_ROLES = ['owner', 'admin', 'editor'] as const\n\n/** The site roles that may pause an org automation on their own site. */\nconst SITE_PAUSE_ROLES = ['admin', 'editor'] as const\n\n/** What the doors reach outside this module; specs hand in their own. */\nexport interface OrgAutomationRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; email?: string | null }>\n resolveOrgMembership(\n uid: string,\n orgId: string,\n ): Promise<{ orgId: string; member: Partial<AglynOrgMember> } | null>\n getOrgDoc(orgId: string): Promise<Record<string, unknown> | null>\n resolveOrgIdForHost(hostId: string): Promise<string | null>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof ORG_AUTOMATION_ACTIVITY_TARGET; id: string; name?: string },\n ): Promise<void>\n logHostActivity(\n hostId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: 'workflow'; id: string; name?: string },\n ): Promise<void>\n newId(): string\n}\n\nfunction defaultDeps(): OrgAutomationRouteDeps {\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, email: decoded.email ?? null }\n },\n resolveOrgMembership: async (uid, orgId) =>\n (await resolveOrgMembership(uid, orgId)) as never,\n getOrgDoc: async (orgId) =>\n ((await getOrgDoc(orgId)) as Record<string, unknown> | null) ?? null,\n resolveOrgIdForHost,\n logOrgActivity: (orgId, actor, action, target) =>\n logOrgActivity(orgId, actor, action, target),\n logHostActivity: (hostId, actor, action, target) =>\n logHostActivity(hostId, actor, action, target),\n newId: createResourceUid,\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\nfunction automationsOf(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n}\n\n/**\n * Whether every site a placement names belongs to this organization — asked\n * of `hostIndex`, the mirror `resolveOrgIdForHost` reads, in one round trip.\n * `['org']` names no site and always answers yes.\n */\nasync function placementIsTheOrgs(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n visibleTo: readonly string[],\n): Promise<boolean> {\n const hostIds = hostIdsFromScope(visibleTo)\n if (!hostIds.length) return true\n if (!hostIds.every(isDocumentId)) return false\n const entries = await firestore.getAll(\n ...hostIds.map((hostId) => firestore.collection('hostIndex').doc(hostId)),\n )\n return entries.every((entry) => entry.exists && entry.get('orgId') === orgId)\n}\n\n/**\n * The organization a `automations/manage` request is for, read off its JSON\n * body, so the dispatchers' release gate asks about the right organization.\n * Consulted only for a request that names no site. Unverified, exactly as a\n * `hostId` is: the handler refuses anyone who is not an org-wide member.\n */\nexport async function orgAutomationsManageSubject(\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n if (request.method !== 'POST') return null\n const body = (await request.json().catch(() => null)) as {\n orgId?: unknown\n } | null\n const orgId = typeof body?.orgId === 'string' ? body.orgId : ''\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/**\n * `automations/manage`: create, edit, switch on or off, and delete one of the\n * organization's automations.\n *\n * A DELETE is soft — `deletedAt` stamped and the automation switched off —\n * because the id is what every run history row and every waiting person\n * names: the rows keep a name to show, and the people waiting inside it are\n * stopped by the resume's kill switch on their next beat. Deleting and\n * switching off are never refused for want of a plan, so an organization\n * that downgraded can always stop what it built.\n */\nexport function createOrgAutomationsManageHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\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 orgId = String(req.body?.orgId ?? '')\n const action = String(req.body?.action ?? '') as ManageAction\n if (!isDocumentId(orgId)) {\n return res.status(400).json({ error: 'Invalid orgId' })\n }\n if (!(ORG_AUTOMATION_MANAGE_ACTIONS as readonly string[]).includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const automationId = String(req.body?.automationId ?? '')\n if (action !== 'create' && !isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n if (\n !member ||\n !isOrgWideMember(member) ||\n !(ORG_MANAGE_ROLES as readonly string[]).includes(String(member.role ?? ''))\n ) {\n return res\n .status(403)\n .json({ error: 'Not an organization owner, admin or editor' })\n }\n /*\n * A request naming no site reaches here past only the release gate —\n * the dispatcher's per-site switch needs a site — so the organization's\n * own switch is asked here.\n */\n const org = await deps.getOrgDoc(orgId)\n if (!org || !isPluginEnabled(org as never, BUNDLE_ID)) {\n return res.status(404).json({ error: 'Not found' })\n }\n const enabling =\n action === 'create' ||\n action === 'update' ||\n (action === 'setEnabled' && req.body?.enabled === true)\n if (enabling && !checkEntitlement(org as never, 'actions')) {\n return res.status(403).json({\n error: `Org automations need the ${planLabelGrantingFeature('actions') ?? 'Pro'} plan`,\n })\n }\n\n const firestore = deps.firestore()\n const collection = automationsOf(firestore, orgId)\n const actor = { uid: caller.uid, email: caller.email ?? null }\n const logged = (verb: string, id: string, name: string) =>\n deps.logOrgActivity(orgId, actor, verb, {\n type: ORG_AUTOMATION_ACTIVITY_TARGET,\n id,\n name,\n })\n\n if (action === 'create' || action === 'update') {\n const read = readOrgAutomation(req.body?.automation)\n if (read.ok === false) {\n return res.status(400).json({ error: read.problem })\n }\n const fields = read.value\n if (!(await placementIsTheOrgs(firestore, orgId, fields.visibleTo))) {\n return res\n .status(400)\n .json({ error: 'A site you chose is not in this organization' })\n }\n if (action === 'create') {\n /*\n * Counted before the write rather than inside a transaction. The\n * cap bounds storage, not money, so the overshoot two simultaneous\n * saves can buy is one document — the trade the site actions cap\n * makes for the same reason.\n */\n const live = (\n await collection.where('deletedAt', '==', null).count().get()\n ).data().count\n if (live >= ORG_AUTOMATIONS_MAX) {\n return res.status(409).json({\n error:\n `An organization holds at most ${ORG_AUTOMATIONS_MAX} org ` +\n 'automations — delete one you no longer use first',\n })\n }\n const id = deps.newId()\n await collection.doc(id).create({\n ...fields,\n pausedHostIds: [],\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n createdBy: caller.uid,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n await logged('Created an org automation', id, fields.name)\n return res.status(200).json({ automationId: id })\n }\n const outcome = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(automationId)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return 'missing'\n transaction.update(ref, {\n ...fields,\n // A site the automation no longer runs on has nothing left to\n // pause: its entry goes, so a later placement starts unpaused.\n pausedHostIds: prunePausedHostIds(\n orgAutomationPausedHostIds(current.data()),\n fields.visibleTo,\n ),\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n return 'updated'\n })\n if (outcome === 'missing') {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n await logged('Edited an org automation', automationId, fields.name)\n return res.status(200).json({ automationId })\n }\n\n if (action === 'setEnabled') {\n if (typeof req.body?.enabled !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is enabled' })\n }\n const enabled = req.body.enabled as boolean\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(automationId)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return null\n transaction.update(ref, {\n enabled,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n await logged(\n enabled ? 'Switched on an org automation' : 'Switched off an org automation',\n automationId,\n name,\n )\n return res.status(200).json({ automationId, enabled })\n }\n\n // delete\n const deleted = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(automationId)\n const current = await transaction.get(ref)\n if (!current.exists) return null\n // Deleting twice is the same request answered again.\n if (current.get('deletedAt')) return { name: '', again: true }\n transaction.update(ref, {\n deletedAt: FieldValue.serverTimestamp(),\n deletedBy: caller.uid,\n // Off as well, so the engine's query — which asks for switched-on\n // automations only — never spends one of its places on it.\n enabled: false,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n return { name: String(current.get('name') ?? ''), again: false }\n })\n if (!deleted) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n if (!deleted.again) {\n await logged('Deleted an org automation', automationId, deleted.name)\n }\n return res.status(200).json({ automationId, deleted: true })\n } catch (error) {\n console.error('[org automations] manage failed', orgId, action, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\n/**\n * `automations/pause`: pause or resume one org automation on ONE site.\n *\n * `pausedHostIds` is changed with `arrayUnion`/`arrayRemove` of the named\n * site alone, inside a transaction that first confirms the automation runs\n * there — so a site's editor can take their own site out of an org\n * automation and put it back, and can never touch another site's entry or\n * the automation itself. Pausing stops the people already waiting inside it\n * on this site as well, on their next beat: the resume reads a pause as the\n * kill switch.\n *\n * Never refused for want of a plan: a pause is how a site stops something,\n * and stopping is always allowed.\n */\nexport function createOrgAutomationPauseHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\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 automationId = String(req.body?.automationId ?? '')\n const paused = req.body?.paused\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n if (typeof paused !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is paused' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const firestore = deps.firestore()\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const orgId = await deps.resolveOrgIdForHost(hostId)\n if (!orgId) {\n return res.status(409).json({\n error: 'This site is not part of an organization, so it runs no org automations.',\n })\n }\n const requestedOrgId = String(req.body?.orgId ?? '')\n if (requestedOrgId && requestedOrgId !== orgId) {\n return res.status(400).json({ error: 'That site is not in this organization' })\n }\n /*\n * The site's own admins and editors, read off the host's `memberRoles`\n * projection — the same answer every site door asks — or an org-wide\n * editor, asked of the membership itself so a projection that has not\n * caught up cannot refuse the organization's own managers.\n */\n const siteRole = String((host.get('memberRoles') ?? {})[caller.uid] ?? '')\n let allowed = (SITE_PAUSE_ROLES as readonly string[]).includes(siteRole)\n if (!allowed) {\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n allowed =\n Boolean(member) &&\n isOrgWideMember(member) &&\n (ORG_MANAGE_ROLES as readonly string[]).includes(String(member?.role ?? ''))\n }\n if (!allowed) {\n return res\n .status(403)\n .json({ error: 'Not an admin or editor of this site' })\n }\n\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, orgId).doc(automationId)\n const current = await transaction.get(ref)\n if (\n !current.exists ||\n current.get('deletedAt') ||\n !visibleToHost(current.get('visibleTo') as string[] | undefined, hostId)\n ) {\n return null\n }\n transaction.update(ref, {\n pausedHostIds: paused\n ? FieldValue.arrayUnion(hostId)\n : FieldValue.arrayRemove(hostId),\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res\n .status(404)\n .json({ error: 'No org automation by that id runs on this site' })\n }\n await deps.logHostActivity(\n hostId,\n { uid: caller.uid, email: caller.email ?? null },\n paused\n ? 'Paused an org automation on this site'\n : 'Resumed an org automation on this site',\n { type: 'workflow', id: automationId, name },\n )\n return res.status(200).json({ automationId, hostId, paused })\n } catch (error) {\n console.error('[org automations] pause failed', hostId, automationId, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\nexport const orgAutomationsManageHandler = createOrgAutomationsManageHandler()\nexport const orgAutomationPauseHandler = createOrgAutomationPauseHandler()\n"],"names":["createResourceUid","hostIdsFromScope","visibleToHost","isPluginEnabled","checkEntitlement","isOrgWideMember","planLabelGrantingFeature","isDocumentId","firebaseAdmin","getOrgDoc","logHostActivity","logOrgActivity","resolveOrgIdForHost","resolveOrgMembership","FieldValue","BUNDLE_ID","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_API_ROUTES","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","orgAutomationPausedHostIds","prunePausedHostIds","readOrgAutomation","ORG_AUTOMATION_MANAGE_ACTIONS","ORG_MANAGE_ROLES","SITE_PAUSE_ROLES","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","email","orgId","actor","action","target","hostId","newId","bearerToken","headers","authorization","String","startsWith","slice","length","automationsOf","collection","doc","placementIsTheOrgs","visibleTo","hostIds","every","entries","getAll","map","entry","exists","get","orgAutomationsManageSubject","request","method","body","json","catch","createOrgAutomationsManageHandler","overrides","req","res","deps","status","error","includes","automationId","member","caller","membership","role","org","enabling","enabled","logged","verb","id","name","type","read","automation","ok","problem","fields","value","live","where","count","data","create","pausedHostIds","deletedAt","createdAt","serverTimestamp","createdBy","updatedAt","updatedBy","outcome","runTransaction","transaction","ref","current","update","deleted","again","deletedBy","console","createOrgAutomationPauseHandler","paused","host","requestedOrgId","siteRole","allowed","Boolean","arrayUnion","arrayRemove","orgAutomationsManageHandler","orgAutomationPauseHandler"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/org-automations-routes.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 * WRITING AN ORG AUTOMATION, and pausing one on a site (AGL-3302).\n *\n * `orgs/{orgId}/automations` is closed to every client write, the way the\n * outreach collections are: an org automation runs on every site it is\n * placed on, as that site, so its trigger, its steps and its placement are\n * decisions the server checks — the vocabulary, the organization's sites, the\n * plan — and nothing a browser may write around.\n *\n * ## Two doors\n *\n * `automations/manage` is the organization's: it creates, edits, switches on\n * and off, and deletes. It names the org and NO site, so it declares a\n * subject resolver for the dispatchers' release gate and asks the org's\n * plugin switch itself; the caller must be an org-wide owner, admin or\n * editor.\n *\n * `automations/pause` is the SITE's — the host level control. It names the\n * site, so the dispatcher applies that site's plugin switch as for any site\n * request, and it changes one thing: whether this automation is paused on\n * THIS site. The site's own admins and editors may use it, and so may an\n * org-wide editor; nobody may pause a site through it but the one named.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostIdsFromScope, visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n type AglynOrgMember,\n checkEntitlement,\n isOrgWideMember,\n type PluginApiHandler,\n type PluginApiRequestSubject,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel: `@aglyn/tenant-data-admin`'s index reaches the\n * Next render cache, and everything these doors need is the Admin SDK handle,\n * the id predicate and the org reads.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n getOrgDoc,\n logHostActivity,\n logOrgActivity,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n ORG_AUTOMATION_ACTIVITY_TARGET,\n ORG_AUTOMATION_API_ROUTES,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n orgAutomationPausedHostIds,\n prunePausedHostIds,\n readOrgAutomation,\n type OrgAutomationFields,\n} from '../model/org-automations'\n\nexport { ORG_AUTOMATION_API_ROUTES }\n\n/** What `automations/manage` may be asked to do. */\nexport const ORG_AUTOMATION_MANAGE_ACTIONS = [\n 'create',\n 'update',\n 'setEnabled',\n 'delete',\n] as const\n\ntype ManageAction = (typeof ORG_AUTOMATION_MANAGE_ACTIONS)[number]\n\n/** The org roles that may write the organization's automations. */\nconst ORG_MANAGE_ROLES = ['owner', 'admin', 'editor'] as const\n\n/** The site roles that may pause an org automation on their own site. */\nconst SITE_PAUSE_ROLES = ['admin', 'editor'] as const\n\n/** What the doors reach outside this module; specs hand in their own. */\nexport interface OrgAutomationRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; email?: string | null }>\n resolveOrgMembership(\n uid: string,\n orgId: string,\n ): Promise<{ orgId: string; member: Partial<AglynOrgMember> } | null>\n getOrgDoc(orgId: string): Promise<Record<string, unknown> | null>\n resolveOrgIdForHost(hostId: string): Promise<string | null>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof ORG_AUTOMATION_ACTIVITY_TARGET; id: string; name?: string },\n ): Promise<void>\n logHostActivity(\n hostId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: 'workflow'; id: string; name?: string },\n ): Promise<void>\n newId(): string\n}\n\nfunction defaultDeps(): OrgAutomationRouteDeps {\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, email: decoded.email ?? null }\n },\n resolveOrgMembership: async (uid, orgId) =>\n (await resolveOrgMembership(uid, orgId)) as never,\n getOrgDoc: async (orgId) =>\n ((await getOrgDoc(orgId)) as Record<string, unknown> | null) ?? null,\n resolveOrgIdForHost,\n logOrgActivity: (orgId, actor, action, target) =>\n logOrgActivity(orgId, actor, action, target),\n logHostActivity: (hostId, actor, action, target) =>\n logHostActivity(hostId, actor, action, target),\n newId: createResourceUid,\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\nfunction automationsOf(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n}\n\n/**\n * Whether every site a placement names belongs to this organization — asked\n * of `hostIndex`, the mirror `resolveOrgIdForHost` reads, in one round trip.\n * `['org']` names no site and always answers yes.\n */\nasync function placementIsTheOrgs(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n visibleTo: readonly string[],\n): Promise<boolean> {\n const hostIds = hostIdsFromScope(visibleTo)\n if (!hostIds.length) return true\n if (!hostIds.every(isDocumentId)) return false\n const entries = await firestore.getAll(\n ...hostIds.map((hostId) => firestore.collection('hostIndex').doc(hostId)),\n )\n return entries.every((entry) => entry.exists && entry.get('orgId') === orgId)\n}\n\n/**\n * The organization a `automations/manage` request is for, read off its JSON\n * body, so the dispatchers' release gate asks about the right organization.\n * Consulted only for a request that names no site. Unverified, exactly as a\n * `hostId` is: the handler refuses anyone who is not an org-wide member.\n */\nexport async function orgAutomationsManageSubject(\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n if (request.method !== 'POST') return null\n const body = (await request.json().catch(() => null)) as {\n orgId?: unknown\n } | null\n const orgId = typeof body?.orgId === 'string' ? body.orgId : ''\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/** What writing an org automation came to: its id, or the refusal the route answers with. */\nexport type OrgAutomationWrite = { ok: true; id: string } | { ok: false; status: number; error: string }\n\n/**\n * Creates an org automation as `automations/manage` does: its sites must be\n * the organization's, and the organization holds at most\n * {@link ORG_AUTOMATIONS_MAX} live ones. The route and the package import\n * (AGL-3535) both create through here.\n */\nexport async function createOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; fields: OrgAutomationFields; actorUid: string },\n): Promise<OrgAutomationWrite> {\n if (!(await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo))) {\n return { ok: false, status: 400, error: 'A site you chose is not in this organization' }\n }\n const collection = automationsOf(firestore, input.orgId)\n /*\n * Counted before the write rather than inside a transaction. The\n * cap bounds storage, not money, so the overshoot two simultaneous\n * saves can buy is one document — the trade the site actions cap\n * makes for the same reason.\n */\n const live = (await collection.where('deletedAt', '==', null).count().get()).data().count\n if (live >= ORG_AUTOMATIONS_MAX) {\n return {\n ok: false,\n status: 409,\n error: `An organization holds at most ${ORG_AUTOMATIONS_MAX} org automations — delete one you no longer use first`,\n }\n }\n await collection.doc(input.id).create({\n ...input.fields,\n pausedHostIds: [],\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n createdBy: input.actorUid,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return { ok: true, id: input.id }\n}\n\n/** Rewrites a live org automation as `automations/manage` does; a site it no longer runs on leaves its pause list. */\nexport async function updateOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; fields: OrgAutomationFields; actorUid: string },\n): Promise<OrgAutomationWrite> {\n if (!(await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo))) {\n return { ok: false, status: 400, error: 'A site you chose is not in this organization' }\n }\n const collection = automationsOf(firestore, input.orgId)\n const outcome = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(input.id)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return 'missing'\n transaction.update(ref, {\n ...input.fields,\n // A site the automation no longer runs on has nothing left to\n // pause: its entry goes, so a later placement starts unpaused.\n pausedHostIds: prunePausedHostIds(orgAutomationPausedHostIds(current.data()), input.fields.visibleTo),\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return 'updated'\n })\n return outcome === 'missing' ? { ok: false, status: 404, error: 'Unknown org automation' } : { ok: true, id: input.id }\n}\n\n/**\n * Deletes an org automation as `automations/manage` does — softly, switched\n * off (see the handler). `null` for one that does not exist; `again` for\n * one already deleted.\n */\nexport async function deleteOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; actorUid: string },\n): Promise<{ name: string; again: boolean } | null> {\n return firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, input.orgId).doc(input.id)\n const current = await transaction.get(ref)\n if (!current.exists) return null\n // Deleting twice is the same request answered again.\n if (current.get('deletedAt')) return { name: '', again: true }\n transaction.update(ref, {\n deletedAt: FieldValue.serverTimestamp(),\n deletedBy: input.actorUid,\n // Off as well, so the engine's query — which asks for switched-on\n // automations only — never spends one of its places on it.\n enabled: false,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return { name: String(current.get('name') ?? ''), again: false }\n })\n}\n\n/**\n * `automations/manage`: create, edit, switch on or off, and delete one of the\n * organization's automations.\n *\n * A DELETE is soft — `deletedAt` stamped and the automation switched off —\n * because the id is what every run history row and every waiting person\n * names: the rows keep a name to show, and the people waiting inside it are\n * stopped by the resume's kill switch on their next beat. Deleting and\n * switching off are never refused for want of a plan, so an organization\n * that downgraded can always stop what it built.\n */\nexport function createOrgAutomationsManageHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\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 orgId = String(req.body?.orgId ?? '')\n const action = String(req.body?.action ?? '') as ManageAction\n if (!isDocumentId(orgId)) {\n return res.status(400).json({ error: 'Invalid orgId' })\n }\n if (!(ORG_AUTOMATION_MANAGE_ACTIONS as readonly string[]).includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const automationId = String(req.body?.automationId ?? '')\n if (action !== 'create' && !isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n if (\n !member ||\n !isOrgWideMember(member) ||\n !(ORG_MANAGE_ROLES as readonly string[]).includes(String(member.role ?? ''))\n ) {\n return res\n .status(403)\n .json({ error: 'Not an organization owner, admin or editor' })\n }\n /*\n * A request naming no site reaches here past only the release gate —\n * the dispatcher's per-site switch needs a site — so the organization's\n * own switch is asked here.\n */\n const org = await deps.getOrgDoc(orgId)\n if (!org || !isPluginEnabled(org as never, BUNDLE_ID)) {\n return res.status(404).json({ error: 'Not found' })\n }\n const enabling =\n action === 'create' ||\n action === 'update' ||\n (action === 'setEnabled' && req.body?.enabled === true)\n if (enabling && !checkEntitlement(org as never, 'actions')) {\n return res.status(403).json({\n error: `Org automations need the ${planLabelGrantingFeature('actions') ?? 'Pro'} plan`,\n })\n }\n\n const firestore = deps.firestore()\n const collection = automationsOf(firestore, orgId)\n const actor = { uid: caller.uid, email: caller.email ?? null }\n const logged = (verb: string, id: string, name: string) =>\n deps.logOrgActivity(orgId, actor, verb, {\n type: ORG_AUTOMATION_ACTIVITY_TARGET,\n id,\n name,\n })\n\n if (action === 'create' || action === 'update') {\n const read = readOrgAutomation(req.body?.automation)\n if (read.ok === false) {\n return res.status(400).json({ error: read.problem })\n }\n const written =\n action === 'create'\n ? await createOrgAutomationRecord(firestore, { orgId, id: deps.newId(), fields: read.value, actorUid: caller.uid })\n : await updateOrgAutomationRecord(firestore, { orgId, id: automationId, fields: read.value, actorUid: caller.uid })\n if (written.ok === false) {\n return res.status(written.status).json({ error: written.error })\n }\n await logged(action === 'create' ? 'Created an org automation' : 'Edited an org automation', written.id, read.value.name)\n return res.status(200).json({ automationId: written.id })\n }\n\n if (action === 'setEnabled') {\n if (typeof req.body?.enabled !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is enabled' })\n }\n const enabled = req.body.enabled as boolean\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(automationId)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return null\n transaction.update(ref, {\n enabled,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n await logged(\n enabled ? 'Switched on an org automation' : 'Switched off an org automation',\n automationId,\n name,\n )\n return res.status(200).json({ automationId, enabled })\n }\n\n // delete\n const deleted = await deleteOrgAutomationRecord(firestore, { orgId, id: automationId, actorUid: caller.uid })\n if (!deleted) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n if (!deleted.again) {\n await logged('Deleted an org automation', automationId, deleted.name)\n }\n return res.status(200).json({ automationId, deleted: true })\n } catch (error) {\n console.error('[org automations] manage failed', orgId, action, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\n/**\n * `automations/pause`: pause or resume one org automation on ONE site.\n *\n * `pausedHostIds` is changed with `arrayUnion`/`arrayRemove` of the named\n * site alone, inside a transaction that first confirms the automation runs\n * there — so a site's editor can take their own site out of an org\n * automation and put it back, and can never touch another site's entry or\n * the automation itself. Pausing stops the people already waiting inside it\n * on this site as well, on their next beat: the resume reads a pause as the\n * kill switch.\n *\n * Never refused for want of a plan: a pause is how a site stops something,\n * and stopping is always allowed.\n */\nexport function createOrgAutomationPauseHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\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 automationId = String(req.body?.automationId ?? '')\n const paused = req.body?.paused\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n if (typeof paused !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is paused' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const firestore = deps.firestore()\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const orgId = await deps.resolveOrgIdForHost(hostId)\n if (!orgId) {\n return res.status(409).json({\n error: 'This site is not part of an organization, so it runs no org automations.',\n })\n }\n const requestedOrgId = String(req.body?.orgId ?? '')\n if (requestedOrgId && requestedOrgId !== orgId) {\n return res.status(400).json({ error: 'That site is not in this organization' })\n }\n /*\n * The site's own admins and editors, read off the host's `memberRoles`\n * projection — the same answer every site door asks — or an org-wide\n * editor, asked of the membership itself so a projection that has not\n * caught up cannot refuse the organization's own managers.\n */\n const siteRole = String((host.get('memberRoles') ?? {})[caller.uid] ?? '')\n let allowed = (SITE_PAUSE_ROLES as readonly string[]).includes(siteRole)\n if (!allowed) {\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n allowed =\n Boolean(member) &&\n isOrgWideMember(member) &&\n (ORG_MANAGE_ROLES as readonly string[]).includes(String(member?.role ?? ''))\n }\n if (!allowed) {\n return res\n .status(403)\n .json({ error: 'Not an admin or editor of this site' })\n }\n\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, orgId).doc(automationId)\n const current = await transaction.get(ref)\n if (\n !current.exists ||\n current.get('deletedAt') ||\n !visibleToHost(current.get('visibleTo') as string[] | undefined, hostId)\n ) {\n return null\n }\n transaction.update(ref, {\n pausedHostIds: paused\n ? FieldValue.arrayUnion(hostId)\n : FieldValue.arrayRemove(hostId),\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res\n .status(404)\n .json({ error: 'No org automation by that id runs on this site' })\n }\n await deps.logHostActivity(\n hostId,\n { uid: caller.uid, email: caller.email ?? null },\n paused\n ? 'Paused an org automation on this site'\n : 'Resumed an org automation on this site',\n { type: 'workflow', id: automationId, name },\n )\n return res.status(200).json({ automationId, hostId, paused })\n } catch (error) {\n console.error('[org automations] pause failed', hostId, automationId, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\nexport const orgAutomationsManageHandler = createOrgAutomationsManageHandler()\nexport const orgAutomationPauseHandler = createOrgAutomationPauseHandler()\n"],"names":["createResourceUid","hostIdsFromScope","visibleToHost","isPluginEnabled","checkEntitlement","isOrgWideMember","planLabelGrantingFeature","isDocumentId","firebaseAdmin","getOrgDoc","logHostActivity","logOrgActivity","resolveOrgIdForHost","resolveOrgMembership","FieldValue","BUNDLE_ID","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_API_ROUTES","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","orgAutomationPausedHostIds","prunePausedHostIds","readOrgAutomation","ORG_AUTOMATION_MANAGE_ACTIONS","ORG_MANAGE_ROLES","SITE_PAUSE_ROLES","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","email","orgId","actor","action","target","hostId","newId","bearerToken","headers","authorization","String","startsWith","slice","length","automationsOf","collection","doc","placementIsTheOrgs","visibleTo","hostIds","every","entries","getAll","map","entry","exists","get","orgAutomationsManageSubject","request","method","body","json","catch","createOrgAutomationRecord","input","fields","ok","status","error","live","where","count","data","id","create","pausedHostIds","deletedAt","createdAt","serverTimestamp","createdBy","actorUid","updatedAt","updatedBy","updateOrgAutomationRecord","outcome","runTransaction","transaction","ref","current","update","deleteOrgAutomationRecord","name","again","deletedBy","enabled","createOrgAutomationsManageHandler","overrides","req","res","deps","includes","automationId","member","caller","membership","role","org","enabling","logged","verb","type","read","automation","problem","written","value","deleted","console","createOrgAutomationPauseHandler","paused","host","requestedOrgId","siteRole","allowed","Boolean","arrayUnion","arrayRemove","orgAutomationsManageHandler","orgAutomationPauseHandler"],"mappings":"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"}
@@ -14,8 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type HostAction, type HostActionStepType } from '@aglyn/aglyn/app-utils/actions';
18
17
  import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ import { type HostAction, type HostActionStepType } from './model/host-actions';
19
19
  /**
20
20
  * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).
21
21
  *
@@ -26,7 +26,7 @@ import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/ag
26
26
  * never a copy of them:
27
27
  *
28
28
  * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the
29
- * Actions editor saves (`hostActionDocument`), with only the fields each
29
+ * Actions editor saves (`siteInteractionDocument`), with only the fields each
30
30
  * step type holds. It is listed on the Automation page's Actions, opens in
31
31
  * that editor, and runs through the same executor as one a person made.
32
32
  * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,
@@ -14,13 +14,14 @@ 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 { ACTIONS_MAX_PER_HOST, hostActionDocument, validateHostAction } from "@aglyn/aglyn/app-utils/actions";
18
- import { automationPlaceholders } from "@aglyn/aglyn/app-utils/automation-placeholders";
19
- import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
17
+ */ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
20
18
  import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
19
  import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
22
20
  import { firebaseAdmin } from "@aglyn/tenant-data-admin";
23
21
  import { BUNDLE_ID } from "./constants/bundle-common.js";
22
+ import { ACTIONS_MAX_PER_HOST, siteInteractionDocument } from "@aglyn/aglyn/app-utils/site-interactions";
23
+ import { validateHostAction } from "./model/host-actions.js";
24
+ import { interactionPlaceholders } from "@aglyn/aglyn/app-utils/draft-placeholders";
24
25
  /** The resource name this writer is registered under. */ export const AUTOMATION_DRAFT_RESOURCE = 'automation';
25
26
  /** The longest name the Actions editor stores. */ export const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
26
27
  /** A draft's name when the caller asks for none. */ export const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation';
@@ -138,7 +139,8 @@ import { BUNDLE_ID } from "./constants/bundle-common.js";
138
139
  'subject',
139
140
  'body',
140
141
  'toField',
141
- 'topicId'
142
+ 'topicId',
143
+ 'transactional'
142
144
  ],
143
145
  notifyAdmins: [
144
146
  'title',
@@ -178,13 +180,15 @@ import { BUNDLE_ID } from "./constants/bundle-common.js";
178
180
  createCrmTask: [
179
181
  'title',
180
182
  'kind',
183
+ 'priority',
181
184
  'dueInDays',
182
185
  'assigneeUid',
183
186
  'assigneeEmail'
184
187
  ],
185
188
  logCrmActivity: [
186
189
  'kind',
187
- 'body'
190
+ 'body',
191
+ 'direction'
188
192
  ]
189
193
  };
190
194
  /** The trigger keys an automation stores. */ const TRIGGER_FIELDS = [
@@ -299,7 +303,7 @@ function conditionOf(value) {
299
303
  event: (_ref = (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event) != null ? _ref : '',
300
304
  steps: ((_action_steps = action.steps) != null ? _action_steps : []).length,
301
305
  enabled: action.enabled === true,
302
- placeholders: automationPlaceholders(action).length
306
+ placeholders: interactionPlaceholders(action).length
303
307
  };
304
308
  }
305
309
  /** Whether content is an automation this site could store: well-formed, off, and valid. */ export function checkAutomationDraftContent(content) {
@@ -399,7 +403,7 @@ export function createAutomationDraftWriter(deps = {}) {
399
403
  if (refusal) return _extends({
400
404
  ok: false
401
405
  }, refusal);
402
- tx.create(actionRef, _extends({}, hostActionDocument(action), {
406
+ tx.create(actionRef, _extends({}, siteInteractionDocument(action), {
403
407
  createdAt: request.now,
404
408
  updatedAt: request.now,
405
409
  createdBy: request.uid
@@ -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 {\n ACTIONS_MAX_PER_HOST,\n hostActionDocument,\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport { automationPlaceholders } from '@aglyn/aglyn/app-utils/automation-placeholders'\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'\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 (`hostActionDocument`), 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'],\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', 'dueInDays', 'assigneeUid', 'assigneeEmail'],\n logCrmActivity: ['kind', 'body'],\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: automationPlaceholders(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 ...hostActionDocument(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":["ACTIONS_MAX_PER_HOST","hostActionDocument","validateHostAction","automationPlaceholders","hostRoleCanWrite","checkEntitlement","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,EACpBC,kBAAkB,EAKlBC,kBAAkB,QACb,iCAAgC;AACvC,SAASC,sBAAsB,QAAQ,iDAAgD;AACvF,SAASC,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,gBAAgB,QAAQ,2CAA0C;AAE3E,SACEC,iCAAiC,QAM5B,qDAAoD;AAC3D,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,+BAA2B;AA+BrD,uDAAuD,GACvD,OAAO,MAAMC,4BAA4B,aAAY;AAErD,gDAAgD,GAChD,OAAO,MAAMC,kCAAkC,GAAE;AAEjD,kDAAkD,GAClD,OAAO,MAAMC,gCAAgC,iBAAgB;AAE7D,2EAA2E,GAC3E,OAAO,MAAMC,gCAAgC,mCAAkC;AAE/E,oGAAoG,GACpG,OAAO,MAAMC,gCACX,2FAA0F;AAE5F,8CAA8C,GAC9C,OAAO,MAAMC,iCACX,CAAC,uCAAuC,EAAEd,qBAAqB,YAAY,CAAC,GAC5E,2BAA0B;AAE5B,sFAAsF,GACtF,MAAMe,gBAAgB;AAEtB;;;;CAIC,GACD,OAAO,MAAMC,yBAAkF;IAC7FC,aAAa;QAAC;QAAc;KAAe;IAC3CC,WAAW;QAAC;QAAW;KAAW;IAClCC,aAAa;QAAC;KAAY;IAC1BC,eAAe;QAAC;QAAa;KAAc;IAC3CC,aAAa;QAAC;QAAa;KAAc;IACzCC,aAAa;QAAC;QAAa;KAAc;IACzCC,WAAW;QAAC;KAAW;IACvBC,UAAU;QAAC;QAAY;KAAY;IACnCC,aAAa;QAAC;QAAY;KAAY;IACtCC,aAAa;QAAC;QAAY;KAAY;IACtCC,aAAa;QAAC;QAAY;QAAW;KAAY;IACjDC,aAAa;QAAC;QAAY;KAAU;IACpCC,eAAe;QAAC;QAAY;QAAW;KAAY;IACnDC,cAAc;QAAC;QAAY;QAAQ;KAAQ;IAC3CC,iBAAiB;QAAC;QAAY;KAAO;IACrCC,UAAU;QAAC;QAAY;QAAY;KAAW;IAC9CC,WAAW;QAAC;KAAW;IACvBC,YAAY;QAAC;KAAe;IAC5BC,aAAa;QAAC;KAAe;IAC7BC,cAAc;QAAC;KAAe;IAC9BC,UAAU;QAAC;KAAa;IACxBC,WAAW;QAAC;KAAa;IACzBC,YAAY;QAAC;KAAa;IAC1BC,UAAU;QAAC;KAAO;IAClBC,OAAO;QAAC;KAAO;IACfC,UAAU;QAAC;QAAO;KAAW;IAC7BC,cAAc;QAAC;QAAa;KAAS;IACrCC,WAAW;QAAC;QAAW;QAAQ;QAAW;KAAU;IACpDC,cAAc;QAAC;QAAS;KAAO;IAC/BC,YAAY;QAAC;QAAU;KAAW;IAClCC,eAAe;QAAC;QAAa;KAAc;IAC3CC,gBAAgB;QAAC;QAAc;KAAe;IAC9CC,MAAM;QAAC;KAAe;IACtBC,cAAc;QAAC;QAAa;KAAiB;IAC7CC,UAAU,EAAE;IACZC,iBAAiB;QAAC;KAAiB;IACnCC,eAAe;QAAC;KAAM;IACtBC,oBAAoB;QAAC;QAAY;QAAc;KAAa;IAC5DC,eAAe;QAAC;QAAS;QAAQ;QAAa;QAAe;KAAgB;IAC7EC,gBAAgB;QAAC;QAAQ;KAAO;AAClC,EAAC;AAED,2CAA2C,GAC3C,MAAMC,iBAAiB;IACrB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,SAASC,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AACvE;AAEA,oEAAoE,GACpE,SAASG,OAAOC,MAA+B,EAAEC,IAAuB;IACtE,MAAMC,MAA+B,CAAC;IACtC,KAAK,MAAMC,OAAOF,KAAM;QACtB,IAAID,MAAM,CAACG,IAAI,KAAKC,aAAaJ,MAAM,CAACG,IAAI,KAAK,MAAMD,GAAG,CAACC,IAAI,GAAGH,MAAM,CAACG,IAAI;IAC/E;IACA,OAAOD;AACT;AAEA,SAASG,YAAYT,KAAc;IACjC,IAAI,CAACD,SAASC,QAAQ,OAAO;IAC7B,OAAOG,OAAOH,OAAO;QAAC;QAAS;QAAM;KAAQ;AAC/C;AAMA;;;;CAIC,GACD,OAAO,SAASU,2BACdC,OAA0C;QA6CtBC;IA3CpB,MAAMA,MAAMD,OAAO,CAAC,SAAS;IAC7B,IAAI,CAACZ,SAASa,MAAM,OAAO;QAAEC,IAAI;QAAOC,UAAU;YAAC;SAAgC;IAAC;IACpF,MAAMC,UAAUH,GAAG,CAAC,UAAU;IAC9B,MAAMI,QAAQJ,GAAG,CAAC,QAAQ;IAC1B,IAAI,CAACb,SAASgB,UAAU,OAAO;QAAEF,IAAI;QAAOC,UAAU;YAAC;SAAuB;IAAC;IAC/E,IAAI,CAACb,MAAMC,OAAO,CAACc,QAAQ,OAAO;QAAEH,IAAI;QAAOC,UAAU;YAAC;SAAwB;IAAC;IACnF,MAAMA,WAAqB,EAAE;IAC7B,MAAMG,aAAa,IAAIC,IAAIC,OAAOd,IAAI,CAAChD;IACvC,MAAM+D,aAA+B,EAAE;IACvCJ,MAAMK,OAAO,CAAC,CAACC,MAAMC;QACnB,MAAMC,OAAOzB,SAASuB,QAAQA,IAAI,CAAC,OAAO,GAAGd;QAC7C,IAAI,OAAOgB,SAAS,YAAY,CAACP,WAAWQ,GAAG,CAACD,OAAO;YACrDV,SAASY,IAAI,CAAC,CAAC,KAAK,EAAEH,QAAQ,EAAE,sCAAsC,CAAC;YACvE;QACF;QACA,MAAMnB,SAASkB;QACf,MAAMK,OAAO5B,SAASK,MAAM,CAAC,OAAO,IAChC;YACEwB,YAAY,AAAC3B,CAAAA,MAAMC,OAAO,CAACE,MAAM,CAAC,OAAO,CAAC,aAAa,IAAIA,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,EAAE,AAAD,EACxFyB,GAAG,CAACpB,aACJqB,MAAM,CAAC,CAACC,SAAiDA,WAAW;WACnE,OAAO3B,MAAM,CAAC,OAAO,CAAC,aAAa,KAAK,WACxC;YAAE4B,YAAY5B,MAAM,CAAC,OAAO,CAAC,aAAa;QAAC,IAC3C,CAAC,KAEP;QACJgB,WAAWM,IAAI,CAAC;YACdF;WACGrB,OAAOC,QAAQ/C,sBAAsB,CAACmE,KAA2B,GAChEG,QAAQA,KAAKC,UAAU,CAACK,MAAM,GAAG;YAAEN;QAAK,IAAI,CAAC;IAErD;IACA,MAAMO,eAAe/B,OAAOY,SAASjB;IACrC,IAAIG,MAAMC,OAAO,CAACgC,YAAY,CAAC,aAAa,GAAG;QAC7CA,YAAY,CAAC,aAAa,GAAG,AAACA,YAAY,CAAC,aAAa,CACrDL,GAAG,CAACpB,aACJqB,MAAM,CAAC,CAACC,SAAiDA,WAAW;IACzE;IACA,2EAA2E;IAC3E,6DAA6D;IAC7D,IAAI,OAAOG,YAAY,CAAC,WAAW,KAAK,YAAY9E,cAAc+E,IAAI,CAACD,YAAY,CAAC,WAAW,GAAG;QAChGpB,SAASY,IAAI,CAAC;IAChB;IACA,MAAMU,OAAOC,QAAOzB,YAAAA,GAAG,CAAC,OAAO,YAAXA,YAAe,IAChC0B,OAAO,CAAC,QAAQ,KAChBC,IAAI,GACJC,KAAK,CAAC,GAAGzF;IACZ,MAAM0F,SAAqB;QACzBL,MAAMA,QAAQpF;QACd+D,SAASmB;QACTlB,OAAOI;QACPsB,SAAS;IACX;IACA,MAAMC,UAAUpG,mBAAmBkG;IACnC,IAAIE,SAAS7B,SAASY,IAAI,CAACiB;IAC3B,OAAO7B,SAASmB,MAAM,GAAG;QAAEpB,IAAI;QAAOC;IAAS,IAAI;QAAED,IAAI;QAAM4B;IAAO;AACxE;AAEA,qEAAqE,GACrE,SAASG,QAAQH,MAAyD;cAG9DA;QADDA;IADT,OAAO;QACLI,KAAK,WAAEJ,kBAAAA,OAAO1B,OAAO,qBAAd0B,gBAAgBI,KAAK,mBAAI;QAChC7B,OAAO,EAACyB,gBAAAA,OAAOzB,KAAK,YAAZyB,gBAAgB,EAAE,EAAER,MAAM;QAClCS,SAASD,OAAOC,OAAO,KAAK;QAC5BI,cAActG,uBAAuBiG,QAAQR,MAAM;IACrD;AACF;AAEA,yFAAyF,GACzF,OAAO,SAASc,4BACdpC,OAA0C;IAE1C,MAAMqC,OAAOtC,2BAA2BC;IACxC,OAAOqC,KAAKnC,EAAE,KAAK,QAAQmC,OAAO;QAAEnC,IAAI;QAAMoC,OAAOL,QAAQI,KAAKP,MAAM;IAAE;AAC5E;AAEA,SAASS,SAASC,QAA4C;QAC9CA,gBAGCC;IAHf,MAAMA,QAAQD,iBAAAA,SAASC,IAAI,cAAbD,iBAAmB,CAAC;IAClC,OAAO;QACLE,IAAIF,SAASE,EAAE;QACfjB,MAAMC,QAAOe,aAAAA,KAAKhB,IAAI,YAATgB,aAAa;QAC1BE,WAAW;QACXL,OAAOL,QAAQQ;IACjB;AACF;AAEA,SAASG,YAAYC,IAAwC,EAAEC,GAAW;QAC1DD;IAAd,MAAME,OAAO,EAACF,YAAAA,KAAKG,GAAG,CAAC,0BAATH,YAA2B,CAAC,EAAE,CAACC,IAAI;IACjD,OAAOhH,iBAAiBiH,QAAQ,OAAO;QAAEE,QAAQ;QAAKC,OAAO5G;IAA8B;AAC7F;AAEA,SAAS6G,YAAYC,GAA6C;IAChE,OAAOrH,iBAAiBqH,KAAwC,aAC5D,OACA;QAAEH,QAAQ;QAAKC,OAAO3G;IAA8B;AAC1D;AAEA,wEAAwE,GACxE,SAAS8G,UAAUC,IAAqC;IACtD,OAAOA,KAAKC,IAAI,CAACpC,MAAM,CAAC,CAACqC,MAAQA,IAAIR,GAAG,CAAC,gBAAgB,MAAM1B,MAAM;AACvE;AAEA,SAASmC,YAAYC,IAAY;IAC/B,OAAOA,QAAQhI,uBAAuB;QAAEuH,QAAQ;QAAKC,OAAO1G;IAA+B,IAAI;AACjG;AAOA,OAAO,SAASmH,4BACdC,OAAkC,CAAC,CAAC;QAElBA;IAAlB,MAAMC,aAAYD,kBAAAA,KAAKC,SAAS,YAAdD,kBAAmB,IAAM3H,cAAc6H,GAAG,GAAGD,SAAS;IACxE,OAAO;QACLE,SAAS,OAAOC;gBAKZpB,MAAAA;YAJF,MAAMqB,UAAUJ,YAAYK,UAAU,CAAC,SAASC,GAAG,CAACH,QAAQI,MAAM;YAClE,MAAMvB,OAAO,MAAMoB,QAAQjB,GAAG;YAC9B,IAAI,CAACH,KAAKwB,MAAM,EAAE,OAAO;gBAAEpB,QAAQ;gBAAKC,OAAO;YAAe;YAC9D,QACEN,QAAAA,eAAAA,YAAYC,MAAMmB,QAAQlB,GAAG,aAA7BF,eACAO,YAAYa,QAAQZ,GAAG,aADvBR,OAEAa,YAAYJ,UAAU,MAAMY,QAAQC,UAAU,CAAC,WAAWI,MAAM,CAAC,aAAatB,GAAG;QAErF;QAEAuB,OAAO,CAACvE,UAAYoC,4BAA4BpC;QAEhDqC,MAAM,OAAO,EAAE+B,MAAM,EAAE1B,EAAE,EAAE;YACzB,MAAMF,WAAW,MAAMqB,YAAYK,UAAU,CAAC,SAASC,GAAG,CAACC,QAAQF,UAAU,CAAC,WAAWC,GAAG,CAACzB,IAAIM,GAAG;YACpG,OAAOR,SAAS6B,MAAM,GAAG9B,SAASC,YAAY;QAChD;QAEAgC,OAAO,OAAOC;gBAOHA;YANT,MAAMpC,OAAOtC,2BAA2B0E,QAAQzE,OAAO;YACvD,IAAIqC,KAAKnC,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAO+C,QAAQ;gBAAKC,OAAOb,KAAKlC,QAAQ,CAAC,EAAE;YAAC;YAChF,MAAMuE,KAAKb;YACX,MAAMI,UAAUS,GAAGR,UAAU,CAAC,SAASC,GAAG,CAACM,QAAQL,MAAM;YACzD,MAAMO,YAAYV,QAAQC,UAAU,CAAC,WAAWC,GAAG,CAACM,QAAQ/B,EAAE;YAC9D,MAAMjB,OACJC,QAAO+C,gBAAAA,QAAQhD,IAAI,YAAZgD,gBAAgB,IACpB9C,OAAO,CAAC,QAAQ,KAChBC,IAAI,GACJC,KAAK,CAAC,GAAGzF,oCAAoCiG,KAAKP,MAAM,CAACL,IAAI;YAClE,MAAMK,SAAqB,aAAKO,KAAKP,MAAM;gBAAEL;gBAAMM,SAAS;gBAAO6C,QAAQ;;YAC3E,OAAOF,GAAGG,cAAc,CAAC,OAAOC;oBAU5BlC,MAAAA;gBATF,yDAAyD;gBACzD,MAAM,CAACC,MAAMkC,UAAUzB,KAAK,GAAG,MAAM0B,QAAQC,GAAG,CAAC;oBAC/CH,GAAG9B,GAAG,CAACiB;oBACPa,GAAG9B,GAAG,CAAC2B;oBACPG,GAAG9B,GAAG,CAACiB,QAAQC,UAAU,CAAC,WAAWI,MAAM,CAAC;iBAC7C;gBACD,IAAI,CAACzB,KAAKwB,MAAM,EAAE,OAAO;oBAAEnE,IAAI;oBAAO+C,QAAQ;oBAAKC,OAAO;gBAAe;gBACzE,IAAI6B,SAASV,MAAM,EAAE,OAAO;oBAAEnE,IAAI;oBAAMgF,UAAU;mBAAS3C,SAASwC;gBACpE,MAAMhB,WACJnB,QAAAA,eAAAA,YAAYC,MAAM4B,QAAQ3B,GAAG,aAA7BF,eAAkCO,YAAYsB,QAAQrB,GAAG,aAAzDR,OAA8Da,YAAYJ,UAAUC;gBACtF,IAAIS,SAAS,OAAO;oBAAE7D,IAAI;mBAAU6D;gBACpCe,GAAGK,MAAM,CAACR,WAAW,aAChBhJ,mBAAmBmG;oBACtBsD,WAAWX,QAAQY,GAAG;oBACtBC,WAAWb,QAAQY,GAAG;oBACtBE,WAAWd,QAAQ3B,GAAG;;gBAExB,OAAO;oBAAE5C,IAAI;oBAAMgF,UAAU;oBAAOxC,IAAI+B,QAAQ/B,EAAE;oBAAEjB;oBAAMkB,WAAW;oBAAML,OAAOL,QAAQH;gBAAQ;YACpG;QACF;IACF;AACF;AAEA,OAAO,MAAM0D,wBAAwB7B,8BAA6B;AAElE;;;CAGC,GACD,OAAO,SAAS8B;IACdzJ,kCAAkCG,2BAA2BqJ,uBAAuB;QAClFE,UAAUxJ;IACZ;AACF"}
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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"}
package/src/lib/server.js CHANGED
@@ -23,6 +23,7 @@ import { FieldValue } from "firebase-admin/firestore";
23
23
  import { runSummaryFields } from "./model/run-history.js";
24
24
  import { runWorkflow } from "./model/workflows.js";
25
25
  import { eventRunSuspension } from "./engine/site-suspension.js";
26
+ import { recordRuns, runMonthKey, runsUsedThisMonth } from "./engine/run-meter.js";
26
27
  import { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from "./constants/bundle-common.js";
27
28
  import { registerWorkflowsServerDeclarations } from "./declarations.server.js";
28
29
  import { automationRunEnv, executeWorkflow, runDueFlowEnrollments, workflowContextFromDocs } from "./engine/run-event-actions.js";
@@ -86,9 +87,9 @@ function secretsMatch(expected, provided) {
86
87
  * `x-aglyn-secret`; a valid call enrolls the configured workflow with the
87
88
  * payload's top-level primitives in scope. Business tier (`webhooks`
88
89
  * flag); runs bill against the workflow-runs meter like any other run —
89
- * `workflowRunsPerMonth` is checked before the run and
90
- * `hosts/{id}/counters/workflowRuns` is incremented after it (AGL-2228).
91
- * That sentence was here before either half of it was true.
90
+ * `workflowRunsPerMonth` is checked against the workspace's runs before the
91
+ * run, and the site's and the workspace's counters are incremented after it
92
+ * (AGL-2228, AGL-3472).
92
93
  */ const inboundHookHandler = async (req, res)=>{
93
94
  var _req_query_hostId, _req_query_hookId;
94
95
  if (req.method !== 'POST') {
@@ -109,7 +110,7 @@ function secretsMatch(expected, provided) {
109
110
  });
110
111
  }
111
112
  try {
112
- var _req_headers_xaglynsecret, _hook_secret, _get, _req_body, _ref, _hook_name, _hook_name1;
113
+ var _req_headers_xaglynsecret, _hook_secret, _req_body, _ref, _hook_name, _hook_name1;
113
114
  var _hook_workflowName;
114
115
  const firestore = firebaseAdmin.app().firestore();
115
116
  const hostRef = firestore.collection('hosts').doc(hostId);
@@ -175,13 +176,20 @@ function secretsMatch(expected, provided) {
175
176
  * bounded by concurrency rather than unbounded by design. Making it
176
177
  * atomic means a transaction around the whole run and is the same trade
177
178
  * `runEventWorkflows` declined.
178
- */ const monthKey = new Date().toISOString().slice(0, 7);
179
- const runCounterRef = hostRef.collection('counters').doc('workflowRuns');
179
+ */ // The band is the WORKSPACE's (AGL-3472), so the count it is held to is
180
+ // every site's runs, off the org's counter — see `run-meter.ts`.
181
+ const meter = {
182
+ firestore,
183
+ hostRef,
184
+ orgId: owner == null ? void 0 : owner.orgId,
185
+ counter: 'workflowRuns',
186
+ month: runMonthKey()
187
+ };
180
188
  const runLimit = resolveOrgEntitlements(org).workflowRunsPerMonth;
181
- const runsUsed = Number((_get = (await runCounterRef.get()).get(monthKey)) != null ? _get : 0);
189
+ const runsUsed = await runsUsedThisMonth(meter);
182
190
  if (!(runsUsed + 1 <= runLimit)) {
183
191
  return res.status(402).json({
184
- error: `This site has used its ${runLimit} workflow runs for the month`
192
+ error: `This workspace has used its ${runLimit} workflow runs for the month`
185
193
  });
186
194
  }
187
195
  // Top-level primitives from the JSON body join the workflow scope.
@@ -289,11 +297,9 @@ function secretsMatch(expected, provided) {
289
297
  // `runEventWorkflows` counts its failures. A run that executed and threw
290
298
  // spent the same compute as one that returned a value; only a run that
291
299
  // never started (the cap above, a missing workflow) must not count.
292
- await runCounterRef.set({
293
- [monthKey]: FieldValue.increment(1)
294
- }, {
295
- merge: true
296
- }).catch(()=>undefined);
300
+ await recordRuns(_extends({}, meter, {
301
+ count: 1
302
+ }));
297
303
  if (failed) return res.status(422).json({
298
304
  error: outcome.error
299
305
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server.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 {\n checkEntitlement,\n type HostFunction,\n type HostVariable,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport {\n registerPluginApiRoute,\n registerPluginJob,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { timingSafeEqual } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from './model/run-history'\nimport { type HostWorkflow, runWorkflow } from './model/workflows'\nimport type { HostWebhook } from './model/webhooks'\nimport { eventRunSuspension } from './engine/site-suspension'\nimport { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsServerDeclarations } from './declarations.server'\nimport {\n automationRunEnv,\n executeWorkflow,\n runDueFlowEnrollments,\n workflowContextFromDocs,\n} from './engine/run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './engine/workflow-steps'\n\n/*==========================================\n * THE BEAT THAT MAKES A WAIT STEP REAL.\n *\n * A `wait` step suspends a run into a `flowEnrollments` row and returns; this\n * is the only thing that ever comes back for it. Without a registration here\n * the wait step would be a delay that never ends — the exact failure\n * `process-abandoned.ts` documents next door, where a Pro entitlement sat\n * behind an HTTP door nothing ever POSTed to.\n *\n * Module scope, like the commerce and bookings registrations: the runner\n * route reaches plugin jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside `registerWorkflowsApi()` would depend on which entry\n * point happened to be loaded.\n *\n * ## Every minute, and why that is cheap\n *\n * `FLOW_WAIT_MIN_MINUTES` is one, so the beat has to be one — a fifteen-minute\n * sweep would make the shortest wait anybody can author wrong by up to\n * fourteen minutes. It costs one collection-group query per minute, which\n * returns nothing at all on a platform with no flow due, because the query\n * asks for `resumeAtMs <= now` rather than for the enrolled population.\n *\n * Bounded and idempotent, the two properties the runner requires: the scan\n * budget bounds one pass, and a transactional claim means an overlapping or\n * repeated beat cannot run the same person's step twice.\n *=========================================*/\nregisterPluginJob({\n pluginId: WORKFLOWS_BUNDLE_ID,\n name: 'resume-flow-waits',\n intervalMinutes: 1,\n description:\n 'Continue automations whose wait step has ended, and time out those ' +\n 'waiting for an event that never arrived.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n await runDueFlowEnrollments(gate)\n },\n})\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentByHook = new Map<string, number[]>()\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 30\n\nfunction rateLimited(key: string): boolean {\n const now = Date.now()\n const hits = (recentByHook.get(key) ?? []).filter(\n (t) => now - t < RATE_WINDOW_MS,\n )\n hits.push(now)\n recentByHook.set(key, hits)\n return hits.length > RATE_MAX\n}\n\nfunction secretsMatch(expected: string, provided: string): boolean {\n if (!expected || expected.length !== provided.length) return false\n return timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(provided)),\n )\n}\n\n/**\n * Inbound webhook endpoint (AGL-149): external systems POST JSON to\n * `/api/hooks/{hostId}/{hookId}` with the hook's secret in\n * `x-aglyn-secret`; a valid call enrolls the configured workflow with the\n * payload's top-level primitives in scope. Business tier (`webhooks`\n * flag); runs bill against the workflow-runs meter like any other run —\n * `workflowRunsPerMonth` is checked before the run and\n * `hosts/{id}/counters/workflowRuns` is incremented after it (AGL-2228).\n * That sentence was here before either half of it was true.\n */\nconst inboundHookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const hookId = String(req.query['hookId'] ?? '')\n if (!hostId || !hookId) {\n return res.status(400).json({ error: 'Invalid webhook address' })\n }\n if (rateLimited(`${hostId}/${hookId}`)) {\n return res.status(429).json({ error: 'Too many requests' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hookSnapshot = await hostRef.collection('webhooks').doc(hookId).get()\n const hook = hookSnapshot.data() as HostWebhook | undefined\n if (\n !hook ||\n hookSnapshot.get('deletedAt') ||\n hook.direction !== 'inbound' ||\n hook.enabled === false\n ) {\n return res.status(404).json({ error: 'Unknown webhook' })\n }\n const provided = String(req.headers['x-aglyn-secret'] ?? '')\n if (!secretsMatch(hook.secret ?? '', provided)) {\n return res.status(401).json({ error: 'Bad secret' })\n }\n\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owner = await getOrgForHost(hostId)\n const org = owner?.org\n /*\n * A suspended site takes no inbound run (AGL-3356). The tenant\n * dispatcher's lockdown gate reads `hostId` from the query or body, and\n * this route carries it in the PATH, so the engine's own question is\n * asked here. A 423 rather than a 200: the caller is an integration, and\n * one told a run happened keeps sending.\n */\n const suspended = await eventRunSuspension(hostRef, org)\n if (suspended) {\n return res.status(423).json({ error: 'locked', scope: suspended.scope })\n }\n if (!checkEntitlement(org as any, 'webhooks')) {\n return res\n .status(403)\n .json({ error: 'Webhooks are not enabled on this site' })\n }\n /*\n * The MONTHLY RUN CAP, which this handler's own docblock claimed for\n * years — \"runs bill against the workflow-runs meter like any other run\"\n * — and did not have. It read neither `workflowRunsPerMonth` nor\n * `hosts/{id}/counters/workflowRuns`, and it wrote neither, so an inbound\n * hook was a workflow execution that no cap could refuse and no meter\n * could see: the customer's usage card, the usage-alerts cron and the\n * COGS rollup all read a counter this path never moved.\n *\n * The refusal is a 402 rather than the silent `return` of\n * `runEventWorkflows`: that path is fired from a visitor request whose\n * success must not depend on the workflow, where this one is an\n * integration calling us on purpose and a caller that gets a 200 for a\n * run that did not happen will keep sending. `Retry-After` is deliberately\n * absent — the month, not a delay, is what clears this.\n *\n * The plan-less/unreadable org resolves as FREE\n * (`workflowRunsPerMonth: 0`), so an org doc we cannot read refuses rather\n * than runs. `checkEntitlement(webhooks)` above already refuses free on\n * today's price list; this is not a second gate on the same fact, because\n * a staff `entitlementOverrides` can grant `webhooks` without granting\n * runs, and because the cap is what bounds a BUSINESS org's spend.\n *\n * Read-then-write, like the event path, and bounded the same way: the\n * per-hook limiter above is 30/60s, so the overshoot a race can buy is\n * bounded by concurrency rather than unbounded by design. Making it\n * atomic means a transaction around the whole run and is the same trade\n * `runEventWorkflows` declined.\n */\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n const runLimit = resolveOrgEntitlements(org as any).workflowRunsPerMonth\n const runsUsed = Number((await runCounterRef.get()).get(monthKey) ?? 0)\n if (!(runsUsed + 1 <= runLimit)) {\n return res.status(402).json({\n error: `This site has used its ${runLimit} workflow runs for the month`,\n })\n }\n\n // Top-level primitives from the JSON body join the workflow scope.\n const body =\n typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n const scope: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(body)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n scope[key.slice(0, 64)] =\n typeof value === 'string' ? value.slice(0, 2000) : value\n }\n }\n\n const [functionDocs, variableDocs, workflowDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n hostRef.collection('workflows').limit(100).get(),\n ])\n const byName = <T extends { name?: string; deletedAt?: unknown }>(\n docs: FirebaseFirestore.QueryDocumentSnapshot[],\n ) => {\n const map: Record<string, T> = {}\n for (const doc of docs) {\n const data = doc.data() as T\n if (data?.name && !data.deletedAt) map[data.name] = data\n }\n return map\n }\n const workflows = byName<HostWorkflow>(workflowDocs.docs)\n const workflow = workflows[hook.workflowName?.trim() ?? '']\n if (!workflow) {\n return res.status(422).json({ error: 'No workflow bound to this hook' })\n }\n\n const hookEvent = `hook:${hook.name ?? hookId}`\n /*\n * A workflow of function calls is evaluated, as it always has been. One\n * with Actions steps is PERFORMED by the engine's executor — the posted\n * JSON is its payload — under the Actions tier gates, step by step, and\n * it is still one run on the meter below. Its working set is built from\n * the documents read above rather than read again.\n */\n const outcome: { error: string | null; value: unknown; summary: string } =\n workflowHasActionSteps(workflow)\n ? await (async () => {\n const workflowDoc = workflowDocs.docs.find(\n (doc) =>\n !doc.get('deletedAt') &&\n doc.get('name') === hook.workflowName?.trim(),\n )\n const execution = await executeWorkflow(\n automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth: 0,\n loadWorkflowContext: async () =>\n workflowContextFromDocs(\n functionDocs.docs,\n variableDocs.docs,\n workflowDocs.docs,\n ),\n }),\n {\n kind: 'workflow',\n id: workflowDoc?.id ?? '',\n name: workflow.name ?? '',\n },\n workflow as AutomationWorkflow,\n hookEvent,\n scope,\n )\n return {\n error: execution.errors.length\n ? execution.errors.join('; ')\n : null,\n value: execution.value,\n summary: execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : `Ran ${hook.workflowName}`,\n }\n })()\n : (() => {\n const run = runWorkflow(\n workflow,\n byName<HostFunction>(functionDocs.docs),\n byName<HostVariable>(variableDocs.docs),\n { event: hookEvent, ...scope },\n { workflows },\n )\n return run.ok === false\n ? { error: run.error, value: undefined, summary: '' }\n : {\n error: null,\n value: run.value,\n summary: `Ran ${hook.workflowName}`,\n }\n })()\n const failed = outcome.error !== null\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // An outside system called the hook's URL; the hook is who it was.\n triggeredBy: { kind: 'webhook', name: hook.workflowName },\n action: failed\n ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300)\n : `Inbound webhook ran \"${hook.workflowName}\"`,\n // The run-history shape (AGL-2222) — without `result` this execution\n // is invisible to the Runs table, which reads a verdict and not prose.\n result: failed ? 'failed' : 'succeeded',\n trigger: hookEvent,\n ...runSummaryFields(\n failed\n ? String(outcome.error).slice(0, 300)\n : outcome.summary.slice(0, 300),\n ),\n target: { type: 'workflow', id: hookId, name: hook.name ?? '' },\n // The site log's search finds a run by its hook's name (AGL-3321).\n searchTokens: activitySearchTokens({ target: { name: hook.name } }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n // The run happened, so it counts — including a FAILED one, exactly as\n // `runEventWorkflows` counts its failures. A run that executed and threw\n // spent the same compute as one that returned a value; only a run that\n // never started (the cap above, a missing workflow) must not count.\n await runCounterRef\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n\n if (failed) return res.status(422).json({ error: outcome.error })\n return res.status(200).json({ ok: true, value: outcome.value })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Webhook failed' })\n }\n}\n\n/**\n * Registers the workflows plugin's public API routes (AGL-396), and the\n * engine's host-event listener beside them for a process whose boot did not\n * (see `declarations.server.ts`).\n */\nexport function registerWorkflowsApi(): void {\n registerWorkflowsServerDeclarations()\n registerPluginApiRoute('hooks/[hostId]/[hookId]', inboundHookHandler)\n}\n\n/**\n * The console half (AGL-2919), re-exported so one entry point serves both\n * surfaces: the manifest generator resolves every server surface through\n * `@aglyn/plugins-workflows/server`, and a second entry point would be a second\n * thing to keep in step with plugins.config.json.\n */\nexport { registerWorkflowsConsoleApi } from './server-console'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { workflowsPlanEntitlements } from './plan-entitlements'\n"],"names":["checkEntitlement","resolveOrgEntitlements","registerPluginApiRoute","registerPluginJob","activitySearchTokens","firebaseAdmin","getOrgForHost","timingSafeEqual","FieldValue","runSummaryFields","runWorkflow","eventRunSuspension","BUNDLE_ID","WORKFLOWS_BUNDLE_ID","registerWorkflowsServerDeclarations","automationRunEnv","executeWorkflow","runDueFlowEnrollments","workflowContextFromDocs","workflowHasActionSteps","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","recentByHook","Map","RATE_WINDOW_MS","RATE_MAX","rateLimited","key","now","Date","hits","get","filter","t","push","set","length","secretsMatch","expected","provided","Uint8Array","Buffer","from","inboundHookHandler","req","res","method","status","json","error","hostId","String","query","hookId","hook","firestore","app","hostRef","collection","doc","hookSnapshot","data","direction","enabled","headers","secret","owner","org","suspended","monthKey","toISOString","slice","runCounterRef","runLimit","workflowRunsPerMonth","runsUsed","Number","body","JSON","parse","value","Object","entries","functionDocs","variableDocs","workflowDocs","Promise","all","limit","byName","docs","map","deletedAt","workflows","workflow","workflowName","trim","hookEvent","outcome","workflowDoc","find","execution","depth","loadWorkflowContext","kind","id","errors","join","summary","outcomes","run","event","ok","undefined","failed","add","actorId","actorEmail","triggeredBy","action","result","trigger","target","type","searchTokens","createdAt","serverTimestamp","catch","increment","merge","console","registerWorkflowsApi","registerWorkflowsConsoleApi"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server.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 {\n checkEntitlement,\n type HostFunction,\n type HostVariable,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/server'\nimport {\n registerPluginApiRoute,\n registerPluginJob,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { timingSafeEqual } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from './model/run-history'\nimport { type HostWorkflow, runWorkflow } from './model/workflows'\nimport type { HostWebhook } from './model/webhooks'\nimport { eventRunSuspension } from './engine/site-suspension'\nimport {\n recordRuns,\n type RunMeterScope,\n runMonthKey,\n runsUsedThisMonth,\n} from './engine/run-meter'\nimport { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsServerDeclarations } from './declarations.server'\nimport {\n automationRunEnv,\n executeWorkflow,\n runDueFlowEnrollments,\n workflowContextFromDocs,\n} from './engine/run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './engine/workflow-steps'\n\n/*==========================================\n * THE BEAT THAT MAKES A WAIT STEP REAL.\n *\n * A `wait` step suspends a run into a `flowEnrollments` row and returns; this\n * is the only thing that ever comes back for it. Without a registration here\n * the wait step would be a delay that never ends — the exact failure\n * `process-abandoned.ts` documents next door, where a Pro entitlement sat\n * behind an HTTP door nothing ever POSTed to.\n *\n * Module scope, like the commerce and bookings registrations: the runner\n * route reaches plugin jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside `registerWorkflowsApi()` would depend on which entry\n * point happened to be loaded.\n *\n * ## Every minute, and why that is cheap\n *\n * `FLOW_WAIT_MIN_MINUTES` is one, so the beat has to be one — a fifteen-minute\n * sweep would make the shortest wait anybody can author wrong by up to\n * fourteen minutes. It costs one collection-group query per minute, which\n * returns nothing at all on a platform with no flow due, because the query\n * asks for `resumeAtMs <= now` rather than for the enrolled population.\n *\n * Bounded and idempotent, the two properties the runner requires: the scan\n * budget bounds one pass, and a transactional claim means an overlapping or\n * repeated beat cannot run the same person's step twice.\n *=========================================*/\nregisterPluginJob({\n pluginId: WORKFLOWS_BUNDLE_ID,\n name: 'resume-flow-waits',\n intervalMinutes: 1,\n description:\n 'Continue automations whose wait step has ended, and time out those ' +\n 'waiting for an event that never arrived.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n await runDueFlowEnrollments(gate)\n },\n})\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentByHook = new Map<string, number[]>()\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 30\n\nfunction rateLimited(key: string): boolean {\n const now = Date.now()\n const hits = (recentByHook.get(key) ?? []).filter(\n (t) => now - t < RATE_WINDOW_MS,\n )\n hits.push(now)\n recentByHook.set(key, hits)\n return hits.length > RATE_MAX\n}\n\nfunction secretsMatch(expected: string, provided: string): boolean {\n if (!expected || expected.length !== provided.length) return false\n return timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(provided)),\n )\n}\n\n/**\n * Inbound webhook endpoint (AGL-149): external systems POST JSON to\n * `/api/hooks/{hostId}/{hookId}` with the hook's secret in\n * `x-aglyn-secret`; a valid call enrolls the configured workflow with the\n * payload's top-level primitives in scope. Business tier (`webhooks`\n * flag); runs bill against the workflow-runs meter like any other run —\n * `workflowRunsPerMonth` is checked against the workspace's runs before the\n * run, and the site's and the workspace's counters are incremented after it\n * (AGL-2228, AGL-3472).\n */\nconst inboundHookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const hookId = String(req.query['hookId'] ?? '')\n if (!hostId || !hookId) {\n return res.status(400).json({ error: 'Invalid webhook address' })\n }\n if (rateLimited(`${hostId}/${hookId}`)) {\n return res.status(429).json({ error: 'Too many requests' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hookSnapshot = await hostRef.collection('webhooks').doc(hookId).get()\n const hook = hookSnapshot.data() as HostWebhook | undefined\n if (\n !hook ||\n hookSnapshot.get('deletedAt') ||\n hook.direction !== 'inbound' ||\n hook.enabled === false\n ) {\n return res.status(404).json({ error: 'Unknown webhook' })\n }\n const provided = String(req.headers['x-aglyn-secret'] ?? '')\n if (!secretsMatch(hook.secret ?? '', provided)) {\n return res.status(401).json({ error: 'Bad secret' })\n }\n\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owner = await getOrgForHost(hostId)\n const org = owner?.org\n /*\n * A suspended site takes no inbound run (AGL-3356). The tenant\n * dispatcher's lockdown gate reads `hostId` from the query or body, and\n * this route carries it in the PATH, so the engine's own question is\n * asked here. A 423 rather than a 200: the caller is an integration, and\n * one told a run happened keeps sending.\n */\n const suspended = await eventRunSuspension(hostRef, org)\n if (suspended) {\n return res.status(423).json({ error: 'locked', scope: suspended.scope })\n }\n if (!checkEntitlement(org as any, 'webhooks')) {\n return res\n .status(403)\n .json({ error: 'Webhooks are not enabled on this site' })\n }\n /*\n * The MONTHLY RUN CAP, which this handler's own docblock claimed for\n * years — \"runs bill against the workflow-runs meter like any other run\"\n * — and did not have. It read neither `workflowRunsPerMonth` nor\n * `hosts/{id}/counters/workflowRuns`, and it wrote neither, so an inbound\n * hook was a workflow execution that no cap could refuse and no meter\n * could see: the customer's usage card, the usage-alerts cron and the\n * COGS rollup all read a counter this path never moved.\n *\n * The refusal is a 402 rather than the silent `return` of\n * `runEventWorkflows`: that path is fired from a visitor request whose\n * success must not depend on the workflow, where this one is an\n * integration calling us on purpose and a caller that gets a 200 for a\n * run that did not happen will keep sending. `Retry-After` is deliberately\n * absent — the month, not a delay, is what clears this.\n *\n * The plan-less/unreadable org resolves as FREE\n * (`workflowRunsPerMonth: 0`), so an org doc we cannot read refuses rather\n * than runs. `checkEntitlement(webhooks)` above already refuses free on\n * today's price list; this is not a second gate on the same fact, because\n * a staff `entitlementOverrides` can grant `webhooks` without granting\n * runs, and because the cap is what bounds a BUSINESS org's spend.\n *\n * Read-then-write, like the event path, and bounded the same way: the\n * per-hook limiter above is 30/60s, so the overshoot a race can buy is\n * bounded by concurrency rather than unbounded by design. Making it\n * atomic means a transaction around the whole run and is the same trade\n * `runEventWorkflows` declined.\n */\n // The band is the WORKSPACE's (AGL-3472), so the count it is held to is\n // every site's runs, off the org's counter — see `run-meter.ts`.\n const meter: RunMeterScope = {\n firestore,\n hostRef,\n orgId: owner?.orgId,\n counter: 'workflowRuns',\n month: runMonthKey(),\n }\n const runLimit = resolveOrgEntitlements(org as any).workflowRunsPerMonth\n const runsUsed = await runsUsedThisMonth(meter)\n if (!(runsUsed + 1 <= runLimit)) {\n return res.status(402).json({\n error: `This workspace has used its ${runLimit} workflow runs for the month`,\n })\n }\n\n // Top-level primitives from the JSON body join the workflow scope.\n const body =\n typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n const scope: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(body)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n scope[key.slice(0, 64)] =\n typeof value === 'string' ? value.slice(0, 2000) : value\n }\n }\n\n const [functionDocs, variableDocs, workflowDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n hostRef.collection('workflows').limit(100).get(),\n ])\n const byName = <T extends { name?: string; deletedAt?: unknown }>(\n docs: FirebaseFirestore.QueryDocumentSnapshot[],\n ) => {\n const map: Record<string, T> = {}\n for (const doc of docs) {\n const data = doc.data() as T\n if (data?.name && !data.deletedAt) map[data.name] = data\n }\n return map\n }\n const workflows = byName<HostWorkflow>(workflowDocs.docs)\n const workflow = workflows[hook.workflowName?.trim() ?? '']\n if (!workflow) {\n return res.status(422).json({ error: 'No workflow bound to this hook' })\n }\n\n const hookEvent = `hook:${hook.name ?? hookId}`\n /*\n * A workflow of function calls is evaluated, as it always has been. One\n * with Actions steps is PERFORMED by the engine's executor — the posted\n * JSON is its payload — under the Actions tier gates, step by step, and\n * it is still one run on the meter below. Its working set is built from\n * the documents read above rather than read again.\n */\n const outcome: { error: string | null; value: unknown; summary: string } =\n workflowHasActionSteps(workflow)\n ? await (async () => {\n const workflowDoc = workflowDocs.docs.find(\n (doc) =>\n !doc.get('deletedAt') &&\n doc.get('name') === hook.workflowName?.trim(),\n )\n const execution = await executeWorkflow(\n automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth: 0,\n loadWorkflowContext: async () =>\n workflowContextFromDocs(\n functionDocs.docs,\n variableDocs.docs,\n workflowDocs.docs,\n ),\n }),\n {\n kind: 'workflow',\n id: workflowDoc?.id ?? '',\n name: workflow.name ?? '',\n },\n workflow as AutomationWorkflow,\n hookEvent,\n scope,\n )\n return {\n error: execution.errors.length\n ? execution.errors.join('; ')\n : null,\n value: execution.value,\n summary: execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : `Ran ${hook.workflowName}`,\n }\n })()\n : (() => {\n const run = runWorkflow(\n workflow,\n byName<HostFunction>(functionDocs.docs),\n byName<HostVariable>(variableDocs.docs),\n { event: hookEvent, ...scope },\n { workflows },\n )\n return run.ok === false\n ? { error: run.error, value: undefined, summary: '' }\n : {\n error: null,\n value: run.value,\n summary: `Ran ${hook.workflowName}`,\n }\n })()\n const failed = outcome.error !== null\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // An outside system called the hook's URL; the hook is who it was.\n triggeredBy: { kind: 'webhook', name: hook.workflowName },\n action: failed\n ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300)\n : `Inbound webhook ran \"${hook.workflowName}\"`,\n // The run-history shape (AGL-2222) — without `result` this execution\n // is invisible to the Runs table, which reads a verdict and not prose.\n result: failed ? 'failed' : 'succeeded',\n trigger: hookEvent,\n ...runSummaryFields(\n failed\n ? String(outcome.error).slice(0, 300)\n : outcome.summary.slice(0, 300),\n ),\n target: { type: 'workflow', id: hookId, name: hook.name ?? '' },\n // The site log's search finds a run by its hook's name (AGL-3321).\n searchTokens: activitySearchTokens({ target: { name: hook.name } }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n // The run happened, so it counts — including a FAILED one, exactly as\n // `runEventWorkflows` counts its failures. A run that executed and threw\n // spent the same compute as one that returned a value; only a run that\n // never started (the cap above, a missing workflow) must not count.\n await recordRuns({ ...meter, count: 1 })\n\n if (failed) return res.status(422).json({ error: outcome.error })\n return res.status(200).json({ ok: true, value: outcome.value })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Webhook failed' })\n }\n}\n\n/**\n * Registers the workflows plugin's public API routes (AGL-396), and the\n * engine's host-event listener beside them for a process whose boot did not\n * (see `declarations.server.ts`).\n */\nexport function registerWorkflowsApi(): void {\n registerWorkflowsServerDeclarations()\n registerPluginApiRoute('hooks/[hostId]/[hookId]', inboundHookHandler)\n}\n\n/**\n * The console half (AGL-2919), re-exported so one entry point serves both\n * surfaces: the manifest generator resolves every server surface through\n * `@aglyn/plugins-workflows/server`, and a second entry point would be a second\n * thing to keep in step with plugins.config.json.\n */\nexport { registerWorkflowsConsoleApi } from './server-console'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { workflowsPlanEntitlements } from './plan-entitlements'\n"],"names":["checkEntitlement","resolveOrgEntitlements","registerPluginApiRoute","registerPluginJob","activitySearchTokens","firebaseAdmin","getOrgForHost","timingSafeEqual","FieldValue","runSummaryFields","runWorkflow","eventRunSuspension","recordRuns","runMonthKey","runsUsedThisMonth","BUNDLE_ID","WORKFLOWS_BUNDLE_ID","registerWorkflowsServerDeclarations","automationRunEnv","executeWorkflow","runDueFlowEnrollments","workflowContextFromDocs","workflowHasActionSteps","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","recentByHook","Map","RATE_WINDOW_MS","RATE_MAX","rateLimited","key","now","Date","hits","get","filter","t","push","set","length","secretsMatch","expected","provided","Uint8Array","Buffer","from","inboundHookHandler","req","res","method","status","json","error","hostId","String","query","hookId","hook","firestore","app","hostRef","collection","doc","hookSnapshot","data","direction","enabled","headers","secret","owner","org","suspended","meter","orgId","counter","month","runLimit","workflowRunsPerMonth","runsUsed","body","JSON","parse","value","Object","entries","slice","functionDocs","variableDocs","workflowDocs","Promise","all","limit","byName","docs","map","deletedAt","workflows","workflow","workflowName","trim","hookEvent","outcome","workflowDoc","find","execution","depth","loadWorkflowContext","kind","id","errors","join","summary","outcomes","run","event","ok","undefined","failed","add","actorId","actorEmail","triggeredBy","action","result","trigger","target","type","searchTokens","createdAt","serverTimestamp","catch","count","console","registerWorkflowsApi","registerWorkflowsConsoleApi"],"mappings":"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"}