@aglyn/plugins-workflows 1.0.0-beta.222 → 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.
- package/package.json +12 -12
- package/src/lib/components/automation-step-fields.component.d.ts +16 -2
- package/src/lib/components/automation-step-fields.component.js +53 -2
- package/src/lib/components/automation-step-fields.component.js.map +1 -1
- package/src/lib/components/automation-trigger-conditions.component.d.ts +23 -2
- package/src/lib/components/automation-trigger-conditions.component.js +89 -10
- package/src/lib/components/automation-trigger-conditions.component.js.map +1 -1
- package/src/lib/components/host-actions-card.component.js +64 -30
- package/src/lib/components/host-actions-card.component.js.map +1 -1
- package/src/lib/components/host-workflows-card.component.js +31 -10
- package/src/lib/components/host-workflows-card.component.js.map +1 -1
- package/src/lib/components/org-automation-editor.component.d.ts +1 -1
- package/src/lib/components/org-automation-editor.component.js +20 -11
- package/src/lib/components/org-automation-editor.component.js.map +1 -1
- package/src/lib/engine/email-merge.d.ts +69 -0
- package/src/lib/engine/email-merge.js +98 -0
- package/src/lib/engine/email-merge.js.map +1 -0
- package/src/lib/engine/run-event-actions.d.ts +30 -0
- package/src/lib/engine/run-event-actions.js +158 -54
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-workflows.js +44 -8
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/run-org-automations.js +3 -1
- package/src/lib/engine/run-org-automations.js.map +1 -1
- package/src/lib/engine/triggered-docs.d.ts +39 -0
- package/src/lib/engine/triggered-docs.js +47 -0
- package/src/lib/engine/triggered-docs.js.map +1 -0
- package/src/lib/model/host-actions.d.ts +53 -0
- package/src/lib/model/host-actions.js +50 -2
- package/src/lib/model/host-actions.js.map +1 -1
- package/src/lib/model/org-automations.d.ts +6 -4
- package/src/lib/model/org-automations.js +8 -5
- package/src/lib/model/org-automations.js.map +1 -1
- package/src/lib/server-automation-drafts.js +2 -1
- package/src/lib/server-automation-drafts.js.map +1 -1
- package/src/lib/testing/stand-in-crm-steps.js +1 -1
- package/src/lib/testing/stand-in-crm-steps.js.map +1 -1
- package/src/lib/testing/stand-in-recipes.d.ts +3 -2
- package/src/lib/testing/stand-in-recipes.js +17 -12
- package/src/lib/testing/stand-in-recipes.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/host-actions.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 CONTACT_TAG_MAX_LENGTH,\n type ContactLifecycleStage,\n CRM_TASK_MAX_DUE_DAYS,\n type CrmActivityKind,\n type CrmTaskKind,\n isContactLifecycleStage,\n isCrmActivityKind,\n isCrmTaskKind,\n} from '@aglyn/aglyn/app-utils/crm-kinds'\nimport type { CrmActivityDirection, CrmTaskPriority } from '@aglyn/aglyn/app-utils/crm'\nimport { HOST_EVENT_TYPES } from '@aglyn/aglyn/app-utils/host-events'\nimport {\n CLIENT_ACTION_STEP_TYPES,\n type ClientInteractionStep,\n CUSTOM_EVENT_PATTERN,\n type InteractionCondition,\n type InteractionStepBase,\n type InteractionStepGuard,\n interactionStepLabel,\n type InteractionTrigger,\n type SiteAlert,\n type SiteInteraction,\n validateInteraction,\n} from '@aglyn/aglyn/app-utils/site-interactions'\n\n/** A task's priorities, by meaning (AGL-3517) — the `taskPriority` picklist's meanings. */\nconst TASK_PRIORITIES: readonly (CrmTaskPriority | undefined)[] = ['low', 'normal', 'high']\n\n/** The directions a logged call or email takes (AGL-3517) — `CRM_ACTIVITY_DIRECTIONS`. */\nconst ACTIVITY_DIRECTIONS: Partial<Record<CrmActivityKind, readonly (CrmActivityDirection | undefined)[]>> = {\n call: ['outbound', 'inbound', 'internal'],\n email: ['outbound', 'inbound'],\n}\n/**\n * THE AUTOMATION VOCABULARY (AGL-148): HubSpot-style event → action\n * automation on top of the AGL-128 event triggers. An action listens for a\n * host event (built-in or a custom name fired by another action), optionally\n * filters on the payload, and runs an ordered step list. Pure types and\n * validation; the engine that runs them is this plugin's (`engine/`).\n *\n * An action is a SITE INTERACTION (core `site-interactions.ts`) with server\n * steps added: its trigger, its conditions, its client steps, its stored shape\n * (`siteInteractionDocument`), the slice a visitor's page receives\n * (`interactionStepsForClient`) and its recipe stamp are the platform's. What\n * is declared below is the automation's own — the server steps' shapes, the\n * flow's bounds and each server step's checks. How each step is named, which\n * steps hold a run and which fields a person types words into are declared in\n * `plugins.config.json` (`interactionSteps`), so a page, a drafter and a\n * validator read them without loading this plugin; the checks are registered\n * from this plugin's declarations (`interaction-step-checks`).\n */\n\n/** The automation's names for the platform's interaction shapes. */\nexport type HostActionTriggerCondition = InteractionCondition\nexport type HostActionTrigger = InteractionTrigger\nexport type HostActionStepGuard = InteractionStepGuard\n/** Alert produced by a `siteAlert` step, surfaced to the emitting client. */\nexport type HostActionAlert = SiteAlert\n\n/**\n * The scope key a resumed flow carries to say the wait ended on the CLOCK\n * rather than on the event it was watching for.\n *\n * This is how a `waitForEvent` gets its timeout branch without a nested step\n * list: the flow resumes either way, and the step after it carries a `when`\n * naming this field. Underscored because it shares a namespace with the\n * event payload's own fields, which are merchant-authored form field names.\n * Declared as the step's `holds.timeoutField` for readers that do not load\n * this plugin; this plugin's spec holds the two equal.\n */\nexport const FLOW_TIMED_OUT_FIELD = '_waitTimedOut'\n\n/** The steps an automation adds to the platform's client steps: the server's, run by the workflows plugin. */\ntype ServerActionStep = (\n // Entity references carry a doc id (AGL-261, rename-safe) with the name\n // kept as a display hint; pre-AGL-261 docs have only the name and the\n // executor resolves either.\n | { type: 'runWorkflow'; workflowId?: string; workflowName?: string }\n | { type: 'customEvent'; eventName: string }\n | { type: 'datasetAppend'; datasetId?: string; datasetName?: string }\n | { type: 'webhookPost'; webhookId?: string; webhookName?: string }\n // Server-side steps (AGL-257).\n | {\n type: 'sendEmail'\n subject: string\n body: string\n toField?: string\n /**\n * The stream this message belongs to, so a recipient who left it is not\n * mailed. Absent on every step authored before topics reached the\n * actions editor, which the executor resolves to the default topic.\n */\n topicId?: string\n }\n | { type: 'notifyAdmins'; title: string; body?: string }\n | { type: 'enrollList'; listId?: string; listName?: string }\n | { type: 'updateDataset'; datasetId?: string; datasetName?: string }\n | { type: 'assignCampaign'; campaignId?: string; campaignName?: string }\n /*\n * THE THREE FLOW STEPS.\n *\n * `wait` is the one that matters: without a durable delay an automation is\n * always trigger → immediate actions, so no welcome series, win-back or\n * post-purchase follow-up can exist at all. The other two are what make a\n * delay useful — something to end the flow early, and a wait that ends on\n * an event instead of on the clock.\n *\n * All three are SERVER steps. A delay outlives the page view that started\n * it by days, so the browser that fired the trigger is long gone by the time\n * the flow continues; `interactionStepsForClient` truncates the client’s copy\n * of the step list at the first of these for that reason.\n */\n | {\n type: 'wait'\n /** Whole minutes to hold before the next step. */\n delayMinutes: number\n }\n | {\n type: 'waitForEvent'\n /** The host or custom event that resumes this person's flow. */\n eventName: string\n /**\n * Whole minutes after which the flow continues anyway, with\n * {@link FLOW_TIMED_OUT_FIELD} true in scope. There is always a\n * deadline: a wait with no timeout is an enrollment that lives forever.\n */\n timeoutMinutes: number\n }\n /** Ends the enrollment here. Paired with a `when`, this is the exit branch. */\n | { type: 'exitFlow' }\n /*\n * THE CRM STEPS (AGL-2605).\n *\n * Server steps, every one, because each writes a record only the Admin\n * SDK may write on a visitor's behalf. Each acts on ONE person — the\n * contact the event names, resolved by `contactId` when the payload\n * carries one and by `email` otherwise — and each writes inside the\n * site's own facet or stamps the site's own scope, for the reason\n * `assignCampaign` gives: a contact row is shared by every site in the\n * org, and a stage or a tag is one holder's business record. A step whose\n * event names nobody the site can see does nothing and says so in the run.\n *\n * None of them carries a doc-id reference for the reference audit to\n * check: a stage and a kind are fixed vocabularies, a tag is free text,\n * and an owner is a member who is resolved at run time.\n */\n | { type: 'setContactStage'; lifecycleStage: ContactLifecycleStage }\n | { type: 'addContactTag'; tag: string }\n /**\n * The owner by uid when a member id was typed or a picker wrote the step,\n * by email when an address was typed; the executor resolves either against\n * the org's roster, and only the roster (AGL-2614).\n * Or `roundRobin`, and the owner is the next member of the pool the CRM's\n * settings keep (AGL-2618) — a step that names nobody and hands the\n * choice to the rotation, so a stage change can spread its follow-ups\n * across a team rather than pile them on one rep. The two are exclusive:\n * a rotation step carries no member, and the validator refuses one that\n * names both.\n */\n | {\n type: 'assignContactOwner'\n ownerUid?: string\n ownerEmail?: string\n roundRobin?: boolean\n }\n | {\n type: 'createCrmTask'\n title: string\n /** The task's Type, by meaning; the run stores the org's label for it (AGL-3517). */\n kind: CrmTaskKind\n /** Its Priority, by meaning — `normal` when absent. */\n priority?: CrmTaskPriority\n /** Days from the run to the due date; `0` is due today. */\n dueInDays: number\n /**\n * Who gets it — by uid or by an address, both resolved against the\n * roster the way the owner step's are. Neither named, the task goes\n * to the contact's owner, then to nobody.\n */\n assigneeUid?: string\n assigneeEmail?: string\n }\n | {\n type: 'logCrmActivity'\n kind: CrmActivityKind\n body: string\n /** Which way a call or an email went (AGL-3517); no other kind takes one. */\n direction?: CrmActivityDirection\n }\n) & {\n /** See {@link InteractionStepGuard}. Absent means the step always runs. */\n when?: InteractionStepGuard | null\n}\n\n/** A step of an automation: one of the platform's client steps, or one of its server steps. */\nexport type HostActionStep = ClientInteractionStep | ServerActionStep\n\n/**\n * The steps that suspend a run and continue it later, from a job beat.\n *\n * Named as a set rather than checked inline because three surfaces have to\n * agree on it: the executor stops here and writes an enrollment, the client\n * payload is truncated here, and the validator refuses a flow that waits\n * without a person to wait for. A visitor's page and a drafter read the same\n * fact from each step's declaration (`holds`, `interactionSteps` in\n * `plugins.config.json`), which this plugin's spec holds to this set.\n */\nexport const FLOW_SUSPENDING_STEP_TYPES: ReadonlySet<HostActionStepType> =\n new Set(['wait', 'waitForEvent'] as const)\n\nexport function isFlowSuspendingStep(step: HostActionStep): boolean {\n return FLOW_SUSPENDING_STEP_TYPES.has(step.type)\n}\n\nexport type HostActionStepType = HostActionStep['type']\n\n/**\n * `hosts/{hostId}/actions/{id}` doc: a site interaction whose steps may be\n * the automation's server steps as well as the platform's client steps.\n */\nexport type HostAction = SiteInteraction<HostActionStep>\n\n/**\n * The shortest and longest a flow may wait.\n *\n * The floor is a minute because the resume beat runs on a minute, so anything\n * under one is a delay the scheduler cannot honor and would only read as\n * imprecision. The ceiling is ninety days: long enough for the win-back that\n * is the longest sequence anybody writes, and short enough that an enrollment\n * is not an unbounded lease on a document. A person waiting inside a flow is\n * storage the merchant is not looking at, and a wait measured in years is\n * indistinguishable from one nobody will ever collect. Declared as the band\n * each waiting step `holds`; this plugin's spec holds the two equal.\n */\nexport const FLOW_WAIT_MIN_MINUTES = 1\nexport const FLOW_WAIT_MAX_MINUTES = 90 * 24 * 60\n\n/**\n * Every step an action may hold, in the order the editor's \"Do\" picker offers\n * them. Total over the step types: a type the vocabulary gains is a compile\n * error here until it is placed.\n */\nconst STEP_ORDER: Readonly<Record<HostActionStepType, number>> = {\n runWorkflow: 0,\n siteAlert: 1,\n customEvent: 2,\n datasetAppend: 3,\n webhookPost: 4,\n showOverlay: 5,\n stickyNav: 6,\n addClass: 7,\n toggleClass: 8,\n removeClass: 9,\n showElement: 10,\n hideElement: 11,\n toggleElement: 12,\n openDrawer: 13,\n closeDrawer: 14,\n toggleDrawer: 15,\n openMenu: 16,\n closeMenu: 17,\n toggleMenu: 18,\n setAttribute: 19,\n removeAttribute: 20,\n scrollTo: 21,\n playVideo: 22,\n showHtml: 23,\n runJs: 24,\n redirect: 25,\n trackGaEvent: 26,\n sendEmail: 27,\n notifyAdmins: 28,\n enrollList: 29,\n updateDataset: 30,\n assignCampaign: 31,\n wait: 32,\n waitForEvent: 33,\n exitFlow: 34,\n setContactStage: 35,\n addContactTag: 36,\n assignContactOwner: 37,\n createCrmTask: 38,\n logCrmActivity: 39,\n}\n\n/** Every step type an action may hold, in the picker's order. */\nexport const HOST_ACTION_STEP_TYPES: readonly HostActionStepType[] = (\n Object.keys(STEP_ORDER) as HostActionStepType[]\n).sort((a, b) => STEP_ORDER[a] - STEP_ORDER[b])\n\n/** The server steps among them: every type that is not one of the platform's client steps. */\nexport const SERVER_ACTION_STEP_TYPES: readonly HostActionStepType[] = HOST_ACTION_STEP_TYPES.filter(\n (type) => !CLIENT_ACTION_STEP_TYPES.has(type),\n)\n\n/**\n * How each step reads in the editor's picker, a run history and a sentence:\n * the platform's label for a client step, and the label its declaration\n * gives every other (`interactionSteps` in `plugins.config.json`), so a page,\n * a drafter and this editor name a step alike.\n */\nexport const HOST_ACTION_STEP_LABELS: Readonly<Record<HostActionStepType, string>> =\n Object.fromEntries(\n HOST_ACTION_STEP_TYPES.map((type) => [type, interactionStepLabel(type) ?? type]),\n ) as Record<HostActionStepType, string>\n\n/** A whole number of minutes inside the wait band. */\nexport function isFlowWaitMinutes(value: unknown): boolean {\n return (\n Number.isInteger(value) &&\n (value as number) >= FLOW_WAIT_MIN_MINUTES &&\n (value as number) <= FLOW_WAIT_MAX_MINUTES\n )\n}\n\n/** True for a custom (non-built-in) event name an action may fire. */\nexport function isCustomEventName(event: string): boolean {\n return (\n !HOST_EVENT_TYPES.includes(event as any) &&\n CUSTOM_EVENT_PATTERN.test(event)\n )\n}\n\n/**\n * Validates an action doc shape; returns a human-readable error or null.\n * Server and console share this so bad steps never persist or run.\n */\nexport function validateHostAction(action: HostAction): string | null {\n // The name, the recipe stamp, the trigger, the step guards, the client\n // steps and every declared step's pick are the platform's to check; the\n // server steps are this module's.\n return validateInteraction(action, {\n validateStep: (step, label) => hostActionStepProblem(step, label),\n })\n}\n\n/**\n * A server step's own complaint, or null. A `runWorkflow` step's pick is\n * checked with every other declared step's (`interactionSteps` in\n * `plugins.config.json`), by `validateInteraction`.\n *\n * Registered from this plugin's declarations as the check of every server\n * step (`interaction-step-checks`), so a plugin that writes an automation it\n * does not edit — a recipe installed into a site — refuses what this editor\n * would.\n */\nexport function hostActionStepProblem(candidate: InteractionStepBase, label: string): string | null {\n const step = candidate as HostActionStep\n if (step.type === 'wait' && !isFlowWaitMinutes(step.delayMinutes)) {\n return `${label}: wait between ${FLOW_WAIT_MIN_MINUTES} minute and ${FLOW_WAIT_MAX_MINUTES} minutes`\n }\n if (step.type === 'waitForEvent') {\n const waited = step.eventName?.trim() ?? ''\n if (\n !waited ||\n (!HOST_EVENT_TYPES.includes(waited as any) && !isCustomEventName(waited))\n ) {\n return `${label}: pick the event to wait for`\n }\n if (!isFlowWaitMinutes(step.timeoutMinutes)) {\n return `${label}: give up after ${FLOW_WAIT_MIN_MINUTES}–${FLOW_WAIT_MAX_MINUTES} minutes`\n }\n }\n if (step.type === 'customEvent') {\n if (!isCustomEventName(step.eventName?.trim() ?? '')) {\n return `${label}: custom event names are 2–40 letters, digits, dashes`\n }\n }\n if (\n step.type === 'datasetAppend' &&\n !step.datasetId?.trim() &&\n !step.datasetName?.trim()\n ) {\n return `${label}: pick a dataset`\n }\n if (\n step.type === 'webhookPost' &&\n !step.webhookId?.trim() &&\n !step.webhookName?.trim()\n ) {\n return `${label}: pick a webhook`\n }\n if (step.type === 'sendEmail') {\n if (!step.subject?.trim()) return `${label}: enter the subject`\n if (!step.body?.trim()) return `${label}: enter the email body`\n }\n if (step.type === 'notifyAdmins' && !step.title?.trim()) {\n return `${label}: enter the notification title`\n }\n if (\n step.type === 'enrollList' &&\n !step.listId?.trim() &&\n !step.listName?.trim()\n ) {\n return `${label}: pick a list`\n }\n if (\n step.type === 'updateDataset' &&\n !step.datasetId?.trim() &&\n !step.datasetName?.trim()\n ) {\n return `${label}: pick a dataset`\n }\n if (\n step.type === 'assignCampaign' &&\n !step.campaignId?.trim() &&\n !step.campaignName?.trim()\n ) {\n return `${label}: pick a campaign`\n }\n // CRM steps (AGL-2605). A stage or a kind outside its vocabulary is\n // refused here rather than stored: the executor treats the value as\n // trusted and would write it into a facet every stage report counts.\n if (\n step.type === 'setContactStage' &&\n !isContactLifecycleStage(step.lifecycleStage)\n ) {\n return `${label}: pick a lifecycle stage`\n }\n if (step.type === 'addContactTag') {\n const tag = step.tag?.trim() ?? ''\n if (!tag) return `${label}: enter the tag`\n if (tag.length > CONTACT_TAG_MAX_LENGTH) {\n return `${label}: tags are at most ${CONTACT_TAG_MAX_LENGTH} characters`\n }\n }\n if (step.type === 'assignContactOwner') {\n const named = Boolean(step.ownerUid?.trim() || step.ownerEmail?.trim())\n if (step.roundRobin === true && named) {\n return `${label}: pick round robin or a member, not both`\n }\n if (\n step.roundRobin !== true &&\n !step.ownerUid?.trim() &&\n !step.ownerEmail?.trim().includes('@')\n ) {\n return `${label}: enter the owner’s email address`\n }\n }\n if (step.type === 'createCrmTask') {\n if (!step.title?.trim()) return `${label}: give the task a title`\n if (!isCrmTaskKind(step.kind)) return `${label}: pick the type of task`\n if (step.priority !== undefined && !TASK_PRIORITIES.includes(step.priority)) {\n return `${label}: pick the task’s priority`\n }\n if (\n !Number.isInteger(step.dueInDays) ||\n step.dueInDays < 0 ||\n step.dueInDays > CRM_TASK_MAX_DUE_DAYS\n ) {\n return `${label}: due in 0–${CRM_TASK_MAX_DUE_DAYS} days`\n }\n // Optional — but an address that is not one would resolve to nobody at\n // run time, and the task would land unassigned with no word why.\n const assigneeEmail = step.assigneeEmail?.trim() ?? ''\n if (assigneeEmail && !assigneeEmail.includes('@')) {\n return `${label}: enter the assignee’s email address`\n }\n }\n if (step.type === 'logCrmActivity') {\n if (!isCrmActivityKind(step.kind)) {\n return `${label}: pick the kind of activity`\n }\n if (step.direction !== undefined) {\n const directions = ACTIVITY_DIRECTIONS[step.kind] ?? []\n if (!directions.includes(step.direction)) {\n return directions.length\n ? `${label}: pick which way the ${step.kind} went`\n : `${label}: only a call or an email takes a direction`\n }\n }\n if (!step.body?.trim()) return `${label}: write what happened`\n }\n return null\n}\n"],"names":["CONTACT_TAG_MAX_LENGTH","CRM_TASK_MAX_DUE_DAYS","isContactLifecycleStage","isCrmActivityKind","isCrmTaskKind","HOST_EVENT_TYPES","CLIENT_ACTION_STEP_TYPES","CUSTOM_EVENT_PATTERN","interactionStepLabel","validateInteraction","TASK_PRIORITIES","ACTIVITY_DIRECTIONS","call","email","FLOW_TIMED_OUT_FIELD","FLOW_SUSPENDING_STEP_TYPES","Set","isFlowSuspendingStep","step","has","type","FLOW_WAIT_MIN_MINUTES","FLOW_WAIT_MAX_MINUTES","STEP_ORDER","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","toggleClass","removeClass","showElement","hideElement","toggleElement","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","setAttribute","removeAttribute","scrollTo","playVideo","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","HOST_ACTION_STEP_TYPES","Object","keys","sort","a","b","SERVER_ACTION_STEP_TYPES","filter","HOST_ACTION_STEP_LABELS","fromEntries","map","isFlowWaitMinutes","value","Number","isInteger","isCustomEventName","event","includes","test","validateHostAction","action","validateStep","label","hostActionStepProblem","candidate","delayMinutes","waited","eventName","trim","timeoutMinutes","datasetId","datasetName","webhookId","webhookName","subject","body","title","listId","listName","campaignId","campaignName","lifecycleStage","tag","length","named","Boolean","ownerUid","ownerEmail","roundRobin","kind","priority","undefined","dueInDays","assigneeEmail","direction","directions"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/host-actions.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 CONTACT_TAG_MAX_LENGTH,\n type ContactLifecycleStage,\n CRM_TASK_MAX_DUE_DAYS,\n type CrmActivityKind,\n type CrmTaskKind,\n isContactLifecycleStage,\n isCrmActivityKind,\n isCrmTaskKind,\n} from '@aglyn/aglyn/app-utils/crm-kinds'\nimport type { CrmActivityDirection, CrmTaskPriority } from '@aglyn/aglyn/app-utils/crm'\nimport { HOST_EVENT_TYPES, hostEventRecipientActed } from '@aglyn/aglyn/app-utils/host-events'\nimport {\n CLIENT_ACTION_STEP_TYPES,\n type ClientInteractionStep,\n CUSTOM_EVENT_PATTERN,\n type InteractionCondition,\n type InteractionStepBase,\n type InteractionStepGuard,\n interactionStepLabel,\n type InteractionTrigger,\n type SiteAlert,\n type SiteInteraction,\n validateInteraction,\n} from '@aglyn/aglyn/app-utils/site-interactions'\n\n/** A task's priorities, by meaning (AGL-3517) — the `taskPriority` picklist's meanings. */\nconst TASK_PRIORITIES: readonly (CrmTaskPriority | undefined)[] = ['low', 'normal', 'high']\n\n/** The directions a logged call or email takes (AGL-3517) — `CRM_ACTIVITY_DIRECTIONS`. */\nconst ACTIVITY_DIRECTIONS: Partial<Record<CrmActivityKind, readonly (CrmActivityDirection | undefined)[]>> = {\n call: ['outbound', 'inbound', 'internal'],\n email: ['outbound', 'inbound'],\n}\n/**\n * THE AUTOMATION VOCABULARY (AGL-148): HubSpot-style event → action\n * automation on top of the AGL-128 event triggers. An action listens for a\n * host event (built-in or a custom name fired by another action), optionally\n * filters on the payload, and runs an ordered step list. Pure types and\n * validation; the engine that runs them is this plugin's (`engine/`).\n *\n * An action is a SITE INTERACTION (core `site-interactions.ts`) with server\n * steps added: its trigger, its conditions, its client steps, its stored shape\n * (`siteInteractionDocument`), the slice a visitor's page receives\n * (`interactionStepsForClient`) and its recipe stamp are the platform's. What\n * is declared below is the automation's own — the server steps' shapes, the\n * flow's bounds and each server step's checks. How each step is named, which\n * steps hold a run and which fields a person types words into are declared in\n * `plugins.config.json` (`interactionSteps`), so a page, a drafter and a\n * validator read them without loading this plugin; the checks are registered\n * from this plugin's declarations (`interaction-step-checks`).\n */\n\n/** The automation's names for the platform's interaction shapes. */\nexport type HostActionTriggerCondition = InteractionCondition\nexport type HostActionTrigger = InteractionTrigger\nexport type HostActionStepGuard = InteractionStepGuard\n/** Alert produced by a `siteAlert` step, surfaced to the emitting client. */\nexport type HostActionAlert = SiteAlert\n\n/**\n * The scope key a resumed flow carries to say the wait ended on the CLOCK\n * rather than on the event it was watching for.\n *\n * This is how a `waitForEvent` gets its timeout branch without a nested step\n * list: the flow resumes either way, and the step after it carries a `when`\n * naming this field. Underscored because it shares a namespace with the\n * event payload's own fields, which are merchant-authored form field names.\n * Declared as the step's `holds.timeoutField` for readers that do not load\n * this plugin; this plugin's spec holds the two equal.\n */\nexport const FLOW_TIMED_OUT_FIELD = '_waitTimedOut'\n\n/** The steps an automation adds to the platform's client steps: the server's, run by the workflows plugin. */\ntype ServerActionStep = (\n // Entity references carry a doc id (AGL-261, rename-safe) with the name\n // kept as a display hint; pre-AGL-261 docs have only the name and the\n // executor resolves either.\n | { type: 'runWorkflow'; workflowId?: string; workflowName?: string }\n | { type: 'customEvent'; eventName: string }\n | { type: 'datasetAppend'; datasetId?: string; datasetName?: string }\n | { type: 'webhookPost'; webhookId?: string; webhookName?: string }\n // Server-side steps (AGL-257).\n | {\n type: 'sendEmail'\n subject: string\n body: string\n toField?: string\n /**\n * The stream this message belongs to, so a recipient who left it is not\n * mailed. Absent on every step authored before topics reached the\n * actions editor, which the executor resolves to the default topic.\n */\n topicId?: string\n /**\n * A TRANSACTIONAL REPLY (AGL-3458): the message answers what the\n * recipient just did — submitted a form, booked, signed up — so it goes\n * out with no unsubscribe header and no unsubscribe link, the way the\n * Inbox's reply to a submission does. Only a step that qualifies can be\n * one ({@link sendEmailReplyIneligibility}); absent reads as on for a\n * qualifying step, and `false` sends it as a mailing anyway.\n */\n transactional?: boolean\n }\n | { type: 'notifyAdmins'; title: string; body?: string }\n | { type: 'enrollList'; listId?: string; listName?: string }\n | { type: 'updateDataset'; datasetId?: string; datasetName?: string }\n | { type: 'assignCampaign'; campaignId?: string; campaignName?: string }\n /*\n * THE THREE FLOW STEPS.\n *\n * `wait` is the one that matters: without a durable delay an automation is\n * always trigger → immediate actions, so no welcome series, win-back or\n * post-purchase follow-up can exist at all. The other two are what make a\n * delay useful — something to end the flow early, and a wait that ends on\n * an event instead of on the clock.\n *\n * All three are SERVER steps. A delay outlives the page view that started\n * it by days, so the browser that fired the trigger is long gone by the time\n * the flow continues; `interactionStepsForClient` truncates the client’s copy\n * of the step list at the first of these for that reason.\n */\n | {\n type: 'wait'\n /** Whole minutes to hold before the next step. */\n delayMinutes: number\n }\n | {\n type: 'waitForEvent'\n /** The host or custom event that resumes this person's flow. */\n eventName: string\n /**\n * Whole minutes after which the flow continues anyway, with\n * {@link FLOW_TIMED_OUT_FIELD} true in scope. There is always a\n * deadline: a wait with no timeout is an enrollment that lives forever.\n */\n timeoutMinutes: number\n }\n /** Ends the enrollment here. Paired with a `when`, this is the exit branch. */\n | { type: 'exitFlow' }\n /*\n * THE CRM STEPS (AGL-2605).\n *\n * Server steps, every one, because each writes a record only the Admin\n * SDK may write on a visitor's behalf. Each acts on ONE person — the\n * contact the event names, resolved by `contactId` when the payload\n * carries one and by `email` otherwise — and each writes inside the\n * site's own facet or stamps the site's own scope, for the reason\n * `assignCampaign` gives: a contact row is shared by every site in the\n * org, and a stage or a tag is one holder's business record. A step whose\n * event names nobody the site can see does nothing and says so in the run.\n *\n * None of them carries a doc-id reference for the reference audit to\n * check: a stage and a kind are fixed vocabularies, a tag is free text,\n * and an owner is a member who is resolved at run time.\n */\n | { type: 'setContactStage'; lifecycleStage: ContactLifecycleStage }\n | { type: 'addContactTag'; tag: string }\n /**\n * The owner by uid when a member id was typed or a picker wrote the step,\n * by email when an address was typed; the executor resolves either against\n * the org's roster, and only the roster (AGL-2614).\n * Or `roundRobin`, and the owner is the next member of the pool the CRM's\n * settings keep (AGL-2618) — a step that names nobody and hands the\n * choice to the rotation, so a stage change can spread its follow-ups\n * across a team rather than pile them on one rep. The two are exclusive:\n * a rotation step carries no member, and the validator refuses one that\n * names both.\n */\n | {\n type: 'assignContactOwner'\n ownerUid?: string\n ownerEmail?: string\n roundRobin?: boolean\n }\n | {\n type: 'createCrmTask'\n title: string\n /** The task's Type, by meaning; the run stores the org's label for it (AGL-3517). */\n kind: CrmTaskKind\n /** Its Priority, by meaning — `normal` when absent. */\n priority?: CrmTaskPriority\n /** Days from the run to the due date; `0` is due today. */\n dueInDays: number\n /**\n * Who gets it — by uid or by an address, both resolved against the\n * roster the way the owner step's are. Neither named, the task goes\n * to the contact's owner, then to nobody.\n */\n assigneeUid?: string\n assigneeEmail?: string\n }\n | {\n type: 'logCrmActivity'\n kind: CrmActivityKind\n body: string\n /** Which way a call or an email went (AGL-3517); no other kind takes one. */\n direction?: CrmActivityDirection\n }\n) & {\n /** See {@link InteractionStepGuard}. Absent means the step always runs. */\n when?: InteractionStepGuard | null\n}\n\n/** A step of an automation: one of the platform's client steps, or one of its server steps. */\nexport type HostActionStep = ClientInteractionStep | ServerActionStep\n\n/**\n * The steps that suspend a run and continue it later, from a job beat.\n *\n * Named as a set rather than checked inline because three surfaces have to\n * agree on it: the executor stops here and writes an enrollment, the client\n * payload is truncated here, and the validator refuses a flow that waits\n * without a person to wait for. A visitor's page and a drafter read the same\n * fact from each step's declaration (`holds`, `interactionSteps` in\n * `plugins.config.json`), which this plugin's spec holds to this set.\n */\nexport const FLOW_SUSPENDING_STEP_TYPES: ReadonlySet<HostActionStepType> =\n new Set(['wait', 'waitForEvent'] as const)\n\nexport function isFlowSuspendingStep(step: HostActionStep): boolean {\n return FLOW_SUSPENDING_STEP_TYPES.has(step.type)\n}\n\n/**\n * Whether the step at `index` runs after a wait — a `wait` or a\n * `waitForEvent` earlier in the list — and so on the business's schedule\n * rather than as the immediate response to the event.\n */\nexport function stepRunsAfterWait(\n steps: readonly HostActionStep[] | null | undefined,\n index: number,\n): boolean {\n return (steps ?? []).slice(0, Math.max(0, index)).some(isFlowSuspendingStep)\n}\n\n/**\n * Why a `sendEmail` step cannot be a TRANSACTIONAL REPLY (AGL-3458), or\n * `null` when it can.\n *\n * A reply answers what the recipient just did, so all four have to hold:\n *\n * - `event` — the trigger is the recipient's own action: a form submitted,\n * a booking, a sign-up, a new lead (`recipientActed` on the event's\n * declaration). A stage change or a won deal is the business acting.\n * - `wait` — the step runs at once. After a wait it goes out on the\n * business's schedule, to somebody who did one thing once, which is a\n * mailing (`marketing-send.ts`).\n * - `topic` — the step names no email topic. A topic is a stream somebody\n * can leave, which only a mailing belongs to.\n * - `recipient` — it goes to the address the event carries, the person who\n * acted, rather than through `toField` to somebody else.\n */\nexport type SendEmailReplyIneligibility = 'event' | 'wait' | 'topic' | 'recipient'\n\nexport function sendEmailReplyIneligibility(\n step: Pick<Extract<HostActionStep, { type: 'sendEmail' }>, 'type' | 'topicId' | 'toField'>,\n context: { event: string | null | undefined; afterWait: boolean },\n): SendEmailReplyIneligibility | null {\n if (!hostEventRecipientActed(context.event)) return 'event'\n if (context.afterWait) return 'wait'\n if (String(step.topicId ?? '').trim()) return 'topic'\n const toField = String(step.toField ?? '').trim()\n if (toField && toField !== 'email') return 'recipient'\n return null\n}\n\n/** What the editor and the validator say about each — see {@link sendEmailReplyIneligibility}. */\nexport const SEND_EMAIL_REPLY_INELIGIBLE_REASONS: Record<SendEmailReplyIneligibility, string> = {\n event:\n 'only a reply to the person’s own form submission, booking or sign-up can be ' +\n 'transactional',\n wait: 'an email after a wait is a mailing, so it keeps its unsubscribe link',\n topic: 'an email in a topic is a mailing, so it keeps its unsubscribe link',\n recipient: 'only an email to the person who acted can be a transactional reply',\n}\n\n/**\n * Whether a `sendEmail` step goes out as a transactional reply: it qualifies,\n * and its author did not switch the reply off. Absent is ON for a step that\n * qualifies, so the auto-reply a site already sends to a form becomes one\n * without anybody editing it.\n */\nexport function sendEmailIsTransactionalReply(\n step: Pick<\n Extract<HostActionStep, { type: 'sendEmail' }>,\n 'type' | 'topicId' | 'toField' | 'transactional'\n >,\n context: { event: string | null | undefined; afterWait: boolean },\n): boolean {\n return step.transactional !== false && sendEmailReplyIneligibility(step, context) === null\n}\n\nexport type HostActionStepType = HostActionStep['type']\n\n/**\n * `hosts/{hostId}/actions/{id}` doc: a site interaction whose steps may be\n * the automation's server steps as well as the platform's client steps.\n */\nexport type HostAction = SiteInteraction<HostActionStep>\n\n/**\n * The shortest and longest a flow may wait.\n *\n * The floor is a minute because the resume beat runs on a minute, so anything\n * under one is a delay the scheduler cannot honor and would only read as\n * imprecision. The ceiling is ninety days: long enough for the win-back that\n * is the longest sequence anybody writes, and short enough that an enrollment\n * is not an unbounded lease on a document. A person waiting inside a flow is\n * storage the merchant is not looking at, and a wait measured in years is\n * indistinguishable from one nobody will ever collect. Declared as the band\n * each waiting step `holds`; this plugin's spec holds the two equal.\n */\nexport const FLOW_WAIT_MIN_MINUTES = 1\nexport const FLOW_WAIT_MAX_MINUTES = 90 * 24 * 60\n\n/**\n * Every step an action may hold, in the order the editor's \"Do\" picker offers\n * them. Total over the step types: a type the vocabulary gains is a compile\n * error here until it is placed.\n */\nconst STEP_ORDER: Readonly<Record<HostActionStepType, number>> = {\n runWorkflow: 0,\n siteAlert: 1,\n customEvent: 2,\n datasetAppend: 3,\n webhookPost: 4,\n showOverlay: 5,\n stickyNav: 6,\n addClass: 7,\n toggleClass: 8,\n removeClass: 9,\n showElement: 10,\n hideElement: 11,\n toggleElement: 12,\n openDrawer: 13,\n closeDrawer: 14,\n toggleDrawer: 15,\n openMenu: 16,\n closeMenu: 17,\n toggleMenu: 18,\n setAttribute: 19,\n removeAttribute: 20,\n scrollTo: 21,\n playVideo: 22,\n showHtml: 23,\n runJs: 24,\n redirect: 25,\n trackGaEvent: 26,\n sendEmail: 27,\n notifyAdmins: 28,\n enrollList: 29,\n updateDataset: 30,\n assignCampaign: 31,\n wait: 32,\n waitForEvent: 33,\n exitFlow: 34,\n setContactStage: 35,\n addContactTag: 36,\n assignContactOwner: 37,\n createCrmTask: 38,\n logCrmActivity: 39,\n}\n\n/** Every step type an action may hold, in the picker's order. */\nexport const HOST_ACTION_STEP_TYPES: readonly HostActionStepType[] = (\n Object.keys(STEP_ORDER) as HostActionStepType[]\n).sort((a, b) => STEP_ORDER[a] - STEP_ORDER[b])\n\n/** The server steps among them: every type that is not one of the platform's client steps. */\nexport const SERVER_ACTION_STEP_TYPES: readonly HostActionStepType[] = HOST_ACTION_STEP_TYPES.filter(\n (type) => !CLIENT_ACTION_STEP_TYPES.has(type),\n)\n\n/**\n * How each step reads in the editor's picker, a run history and a sentence:\n * the platform's label for a client step, and the label its declaration\n * gives every other (`interactionSteps` in `plugins.config.json`), so a page,\n * a drafter and this editor name a step alike.\n */\nexport const HOST_ACTION_STEP_LABELS: Readonly<Record<HostActionStepType, string>> =\n Object.fromEntries(\n HOST_ACTION_STEP_TYPES.map((type) => [type, interactionStepLabel(type) ?? type]),\n ) as Record<HostActionStepType, string>\n\n/** A whole number of minutes inside the wait band. */\nexport function isFlowWaitMinutes(value: unknown): boolean {\n return (\n Number.isInteger(value) &&\n (value as number) >= FLOW_WAIT_MIN_MINUTES &&\n (value as number) <= FLOW_WAIT_MAX_MINUTES\n )\n}\n\n/** True for a custom (non-built-in) event name an action may fire. */\nexport function isCustomEventName(event: string): boolean {\n return (\n !HOST_EVENT_TYPES.includes(event as any) &&\n CUSTOM_EVENT_PATTERN.test(event)\n )\n}\n\n/**\n * Validates an action doc shape; returns a human-readable error or null.\n * Server and console share this so bad steps never persist or run.\n */\nexport function validateHostAction(action: HostAction): string | null {\n // The name, the recipe stamp, the trigger, the step guards, the client\n // steps and every declared step's pick are the platform's to check; the\n // server steps are this module's.\n const problem = validateInteraction(action, {\n validateStep: (step, label) => hostActionStepProblem(step, label),\n })\n if (problem) return problem\n /*\n * A step SWITCHED to a transactional reply has to be one (AGL-3458). The\n * executor would send it as a mailing anyway — the unsubscribe is never\n * dropped from mail that is not a reply — so saving the switch would show\n * an author a promise the run does not keep.\n */\n const steps = action.steps ?? []\n for (const [index, step] of steps.entries()) {\n if (step.type !== 'sendEmail' || step.transactional !== true) continue\n const why = sendEmailReplyIneligibility(step, {\n event: action.trigger?.event,\n afterWait: stepRunsAfterWait(steps, index),\n })\n if (why) return `Step ${index + 1}: ${SEND_EMAIL_REPLY_INELIGIBLE_REASONS[why]}`\n }\n return null\n}\n\n/**\n * A server step's own complaint, or null. A `runWorkflow` step's pick is\n * checked with every other declared step's (`interactionSteps` in\n * `plugins.config.json`), by `validateInteraction`.\n *\n * Registered from this plugin's declarations as the check of every server\n * step (`interaction-step-checks`), so a plugin that writes an automation it\n * does not edit — a recipe installed into a site — refuses what this editor\n * would.\n */\nexport function hostActionStepProblem(candidate: InteractionStepBase, label: string): string | null {\n const step = candidate as HostActionStep\n if (step.type === 'wait' && !isFlowWaitMinutes(step.delayMinutes)) {\n return `${label}: wait between ${FLOW_WAIT_MIN_MINUTES} minute and ${FLOW_WAIT_MAX_MINUTES} minutes`\n }\n if (step.type === 'waitForEvent') {\n const waited = step.eventName?.trim() ?? ''\n if (\n !waited ||\n (!HOST_EVENT_TYPES.includes(waited as any) && !isCustomEventName(waited))\n ) {\n return `${label}: pick the event to wait for`\n }\n if (!isFlowWaitMinutes(step.timeoutMinutes)) {\n return `${label}: give up after ${FLOW_WAIT_MIN_MINUTES}–${FLOW_WAIT_MAX_MINUTES} minutes`\n }\n }\n if (step.type === 'customEvent') {\n if (!isCustomEventName(step.eventName?.trim() ?? '')) {\n return `${label}: custom event names are 2–40 letters, digits, dashes`\n }\n }\n if (\n step.type === 'datasetAppend' &&\n !step.datasetId?.trim() &&\n !step.datasetName?.trim()\n ) {\n return `${label}: pick a dataset`\n }\n if (\n step.type === 'webhookPost' &&\n !step.webhookId?.trim() &&\n !step.webhookName?.trim()\n ) {\n return `${label}: pick a webhook`\n }\n if (step.type === 'sendEmail') {\n if (!step.subject?.trim()) return `${label}: enter the subject`\n if (!step.body?.trim()) return `${label}: enter the email body`\n }\n if (step.type === 'notifyAdmins' && !step.title?.trim()) {\n return `${label}: enter the notification title`\n }\n if (\n step.type === 'enrollList' &&\n !step.listId?.trim() &&\n !step.listName?.trim()\n ) {\n return `${label}: pick a list`\n }\n if (\n step.type === 'updateDataset' &&\n !step.datasetId?.trim() &&\n !step.datasetName?.trim()\n ) {\n return `${label}: pick a dataset`\n }\n if (\n step.type === 'assignCampaign' &&\n !step.campaignId?.trim() &&\n !step.campaignName?.trim()\n ) {\n return `${label}: pick a campaign`\n }\n // CRM steps (AGL-2605). A stage or a kind outside its vocabulary is\n // refused here rather than stored: the executor treats the value as\n // trusted and would write it into a facet every stage report counts.\n if (\n step.type === 'setContactStage' &&\n !isContactLifecycleStage(step.lifecycleStage)\n ) {\n return `${label}: pick a lifecycle stage`\n }\n if (step.type === 'addContactTag') {\n const tag = step.tag?.trim() ?? ''\n if (!tag) return `${label}: enter the tag`\n if (tag.length > CONTACT_TAG_MAX_LENGTH) {\n return `${label}: tags are at most ${CONTACT_TAG_MAX_LENGTH} characters`\n }\n }\n if (step.type === 'assignContactOwner') {\n const named = Boolean(step.ownerUid?.trim() || step.ownerEmail?.trim())\n if (step.roundRobin === true && named) {\n return `${label}: pick round robin or a member, not both`\n }\n if (\n step.roundRobin !== true &&\n !step.ownerUid?.trim() &&\n !step.ownerEmail?.trim().includes('@')\n ) {\n return `${label}: enter the owner’s email address`\n }\n }\n if (step.type === 'createCrmTask') {\n if (!step.title?.trim()) return `${label}: give the task a title`\n if (!isCrmTaskKind(step.kind)) return `${label}: pick the type of task`\n if (step.priority !== undefined && !TASK_PRIORITIES.includes(step.priority)) {\n return `${label}: pick the task’s priority`\n }\n if (\n !Number.isInteger(step.dueInDays) ||\n step.dueInDays < 0 ||\n step.dueInDays > CRM_TASK_MAX_DUE_DAYS\n ) {\n return `${label}: due in 0–${CRM_TASK_MAX_DUE_DAYS} days`\n }\n // Optional — but an address that is not one would resolve to nobody at\n // run time, and the task would land unassigned with no word why.\n const assigneeEmail = step.assigneeEmail?.trim() ?? ''\n if (assigneeEmail && !assigneeEmail.includes('@')) {\n return `${label}: enter the assignee’s email address`\n }\n }\n if (step.type === 'logCrmActivity') {\n if (!isCrmActivityKind(step.kind)) {\n return `${label}: pick the kind of activity`\n }\n if (step.direction !== undefined) {\n const directions = ACTIVITY_DIRECTIONS[step.kind] ?? []\n if (!directions.includes(step.direction)) {\n return directions.length\n ? `${label}: pick which way the ${step.kind} went`\n : `${label}: only a call or an email takes a direction`\n }\n }\n if (!step.body?.trim()) return `${label}: write what happened`\n }\n return null\n}\n"],"names":["CONTACT_TAG_MAX_LENGTH","CRM_TASK_MAX_DUE_DAYS","isContactLifecycleStage","isCrmActivityKind","isCrmTaskKind","HOST_EVENT_TYPES","hostEventRecipientActed","CLIENT_ACTION_STEP_TYPES","CUSTOM_EVENT_PATTERN","interactionStepLabel","validateInteraction","TASK_PRIORITIES","ACTIVITY_DIRECTIONS","call","email","FLOW_TIMED_OUT_FIELD","FLOW_SUSPENDING_STEP_TYPES","Set","isFlowSuspendingStep","step","has","type","stepRunsAfterWait","steps","index","slice","Math","max","some","sendEmailReplyIneligibility","context","event","afterWait","String","topicId","trim","toField","SEND_EMAIL_REPLY_INELIGIBLE_REASONS","wait","topic","recipient","sendEmailIsTransactionalReply","transactional","FLOW_WAIT_MIN_MINUTES","FLOW_WAIT_MAX_MINUTES","STEP_ORDER","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","toggleClass","removeClass","showElement","hideElement","toggleElement","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","setAttribute","removeAttribute","scrollTo","playVideo","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","HOST_ACTION_STEP_TYPES","Object","keys","sort","a","b","SERVER_ACTION_STEP_TYPES","filter","HOST_ACTION_STEP_LABELS","fromEntries","map","isFlowWaitMinutes","value","Number","isInteger","isCustomEventName","includes","test","validateHostAction","action","problem","validateStep","label","hostActionStepProblem","entries","why","trigger","candidate","delayMinutes","waited","eventName","timeoutMinutes","datasetId","datasetName","webhookId","webhookName","subject","body","title","listId","listName","campaignId","campaignName","lifecycleStage","tag","length","named","Boolean","ownerUid","ownerEmail","roundRobin","kind","priority","undefined","dueInDays","assigneeEmail","direction","directions"],"mappings":"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"}
|
|
@@ -69,11 +69,13 @@ export declare const ORG_AUTOMATION_API_ROUTES: {
|
|
|
69
69
|
*/
|
|
70
70
|
export declare const ORG_AUTOMATIONS_MAX = 100;
|
|
71
71
|
/**
|
|
72
|
-
* How many org automations one event may run on one site:
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* How many org automations one event may run on one site: every one the
|
|
73
|
+
* organization may hold (AGL-3458). The query asks for switched-on ones only
|
|
74
|
+
* — a deletion switches an automation off — so nothing that cannot run takes
|
|
75
|
+
* a place, and a site's own actions are read by a query of their own, so the
|
|
76
|
+
* two cannot crowd each other out.
|
|
75
77
|
*/
|
|
76
|
-
export declare const MAX_TRIGGERED_ORG_AUTOMATIONS =
|
|
78
|
+
export declare const MAX_TRIGGERED_ORG_AUTOMATIONS = 100;
|
|
77
79
|
/**
|
|
78
80
|
* The run history's target type for a run of an org automation, on the
|
|
79
81
|
* activity feed of the site it ran on.
|
|
@@ -65,10 +65,12 @@ import { HOST_ACTION_STEP_LABELS, validateHostAction } from "./host-actions.js";
|
|
|
65
65
|
* bounded window. Soft-deleted documents do not count.
|
|
66
66
|
*/ export const ORG_AUTOMATIONS_MAX = 100;
|
|
67
67
|
/**
|
|
68
|
-
* How many org automations one event may run on one site:
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
68
|
+
* How many org automations one event may run on one site: every one the
|
|
69
|
+
* organization may hold (AGL-3458). The query asks for switched-on ones only
|
|
70
|
+
* — a deletion switches an automation off — so nothing that cannot run takes
|
|
71
|
+
* a place, and a site's own actions are read by a query of their own, so the
|
|
72
|
+
* two cannot crowd each other out.
|
|
73
|
+
*/ export const MAX_TRIGGERED_ORG_AUTOMATIONS = ORG_AUTOMATIONS_MAX;
|
|
72
74
|
/**
|
|
73
75
|
* The run history's target type for a run of an org automation, on the
|
|
74
76
|
* activity feed of the site it ran on.
|
|
@@ -175,7 +177,8 @@ const CONDITION_VALUE_MAX = 200;
|
|
|
175
177
|
subject: 200,
|
|
176
178
|
body: 5000,
|
|
177
179
|
toField: 64,
|
|
178
|
-
topicId: 128
|
|
180
|
+
topicId: 128,
|
|
181
|
+
transactional: 'boolean'
|
|
179
182
|
},
|
|
180
183
|
notifyAdmins: {
|
|
181
184
|
title: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/org-automations.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 normalizeVisibleTo,\n type ScopeToken,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './host-actions'\n\n/**\n * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and\n * places on the sites it chooses.\n *\n * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They\n * are not moved here, and the reasons are the four things a site action can\n * do that an org automation must not:\n *\n * - call the site's functions and variables through a workflow step — the\n * working set a computed variable binds to, which is one site's;\n * - run the steps only a visitor's page can run (overlays, drawers, class\n * toggles, a redirect to a screen), which the `actions` collection also\n * holds for element interactions;\n * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an\n * action id to a site only by its path;\n * - wait, with the resume beat reading a missing document as the kill\n * switch, so a migration would stop every person already inside a flow.\n *\n * An org automation is therefore its own document with a narrower\n * vocabulary: triggers only the server raises, and steps that mean the same\n * thing on whichever site they run. Each run executes AS the site the event\n * happened on — that site's sender, suppressions, consent group, meters and\n * run allowance — so placing one automation on five sites is five sites each\n * running it for themselves, never one site acting for the others.\n *\n * Pure and client-safe: the console's editor, the server routes and the\n * engine read the same definitions from here.\n */\n\n/** `orgs/{orgId}/automations/{automationId}`. */\nexport const ORG_AUTOMATIONS_COLLECTION = 'automations'\n\n/**\n * The two server doors onto the collection, under the plugin's `automations`\n * API prefix: the organization's (`manage`) and a site's own pause\n * (`pause`). Named here so the console's caller and the server's registration\n * read one spelling.\n */\nexport const ORG_AUTOMATION_API_ROUTES = {\n manage: 'automations/manage',\n pause: 'automations/pause',\n} as const\n\n/**\n * How many live org automations one organization may hold.\n *\n * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,\n * because the two are different things: a site action is authored per site\n * and per element, where an org automation is a shared building block placed\n * on many sites at once. A hundred is far past what an organization writes by\n * hand and keeps the org hub's list — which reads every live one — a single\n * bounded window. Soft-deleted documents do not count.\n */\nexport const ORG_AUTOMATIONS_MAX = 100\n\n/**\n * How many org automations one event may run on one site: the same fan-out\n * bound the site's own actions take (`MAX_TRIGGERED_ACTIONS`), counted\n * separately so the two cannot crowd each other out of the query.\n */\nexport const MAX_TRIGGERED_ORG_AUTOMATIONS = 10\n\n/**\n * The run history's target type for a run of an org automation, on the\n * activity feed of the site it ran on.\n */\nexport const ORG_AUTOMATION_RUN_TARGET = 'orgAutomation'\n\n/**\n * The org activity target an org automation's own lifecycle rows are filed\n * under: the plugin's namespaced resource (`pluginId:noun`), which the org's\n * feed reads as \"Automation\" — core's target list names only core's resources.\n */\nexport const ORG_AUTOMATION_ACTIVITY_TARGET = 'workflows:automation'\n\n/**\n * The events an org automation may start on: every host event the SERVER\n * raises and a person can be the subject of.\n *\n * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine\n * queries the organization's automations on every event in this list, so a\n * new host event must not become an org trigger — and a new read on every\n * occurrence of it — by being added to the platform's list. Two are left out\n * on purpose: `pageView` fires on every request to every published page, and\n * an org query on it would be a read per visitor; `memberSignOut` is raised\n * by no door today.\n */\nexport const ORG_AUTOMATION_TRIGGER_EVENTS = [\n 'formSubmission',\n 'lead',\n 'contactCreated',\n 'contactStageChanged',\n 'booking',\n 'memberSignUp',\n 'memberSignIn',\n 'dealStageChanged',\n 'dealWon',\n 'dealLost',\n 'taskCompleted',\n] as const satisfies readonly HostEventType[]\n\nexport type OrgAutomationTriggerEvent =\n (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number]\n\nconst ORG_TRIGGER_SET: ReadonlySet<string> = new Set(\n ORG_AUTOMATION_TRIGGER_EVENTS,\n)\n\n/** Whether an org automation may start on this event. */\nexport function isOrgAutomationTriggerEvent(\n event: unknown,\n): event is OrgAutomationTriggerEvent {\n return typeof event === 'string' && ORG_TRIGGER_SET.has(event)\n}\n\n/**\n * The steps an org automation may hold: the server steps that read the same\n * on every site.\n *\n * What is left out, and why:\n * - every step the visitor's page runs — overlays, drawers, menus, class\n * toggles, custom HTML and JavaScript, analytics, redirects, the site\n * alert — because an org trigger is a server event with no page to act on;\n * - `runWorkflow`, because a workflow is one site's and calls that site's\n * functions and variables;\n * - `webhookPost`, because a webhook's address and secret are one site's.\n *\n * `customEvent` stays: it fires a custom event ON the site the run is on,\n * which that site's own actions may listen for — the documented way for an\n * org automation to hand over to something site-specific.\n */\nexport const ORG_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'datasetAppend',\n 'updateDataset',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n 'customEvent',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n] as const satisfies readonly HostActionStepType[]\n\nexport type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number]\n\nconst ORG_STEP_SET: ReadonlySet<string> = new Set(ORG_AUTOMATION_STEP_TYPES)\n\n/** Whether an org automation may run a step of this type. */\nexport function isOrgAutomationStepType(\n type: unknown,\n): type is OrgAutomationStepType {\n return typeof type === 'string' && ORG_STEP_SET.has(type)\n}\n\n/** The org editor's \"Do\" picker: each allowed step with its label, in order. */\nexport const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{\n value: OrgAutomationStepType\n label: string\n}> = ORG_AUTOMATION_STEP_TYPES.map((value) => ({\n value,\n label: HOST_ACTION_STEP_LABELS[value],\n}))\n\n/**\n * Why an org automation cannot hold this step, or null when it can.\n *\n * Shared by the save route, which refuses it, and by the engine, which\n * refuses to run one a writer stored anyway.\n */\nexport function orgAutomationStepRefusal(step: {\n type?: unknown\n}): string | null {\n if (isOrgAutomationStepType(step?.type)) return null\n const label = HOST_ACTION_STEP_LABELS[step?.type as HostActionStepType]\n if (!label) return `“${String(step?.type ?? '')}” is not a step`\n return (\n `“${label}” belongs to one site, so an org automation cannot run it — ` +\n 'build it as an action on that site'\n )\n}\n\n/** The trigger an org automation starts on, as it is stored. */\nexport interface OrgAutomationTrigger {\n event: OrgAutomationTriggerEvent\n /** Optional expression over the payload; runs only when truthy. */\n filter?: string\n conditions: HostActionTriggerCondition[] | null\n combinator: TriggerCombinator | null\n}\n\n/**\n * The fields an org automation is written with — by the save route, and by\n * nothing else: the collection is closed to client writes.\n */\nexport interface OrgAutomationFields {\n name: string\n trigger: OrgAutomationTrigger\n steps: HostActionStep[]\n enabled: boolean\n /**\n * The sites it runs on, as scope tokens: `['org']` is every site of the\n * organization, now and later; `['host:{id}', …]` names up to thirty.\n */\n visibleTo: ScopeToken[]\n}\n\n/** `orgs/{orgId}/automations/{id}`, as it is stored. */\nexport interface OrgAutomation extends OrgAutomationFields {\n /**\n * The sites that have paused it for themselves — each a site id, never a\n * scope token. The \"host level control\": a site's admin or editor adds or\n * removes their own site here, and only their own.\n */\n pausedHostIds: string[]\n createdBy?: string\n updatedBy?: string\n /**\n * Set when it was deleted. Always written — `null` while live — so the\n * org hub's list can ask for the live ones by equality.\n */\n deletedAt: unknown\n}\n\n/** A stored org automation with its id, as the console lists it. */\nexport type OrgAutomationRow = OrgAutomation & { $id: string }\n\n/** Longest name an org automation may carry: the actions editor's cap. */\nexport const ORG_AUTOMATION_NAME_MAX = 60\n\n/** Longest trigger filter expression. */\nconst FILTER_MAX = 500\n\n/** Longest a condition's field or value may be. */\nconst CONDITION_FIELD_MAX = 64\nconst CONDITION_VALUE_MAX = 200\n\n/**\n * Every field a step of each type may carry beyond `type` and `when`, with\n * the longest a string in it may be. What the save route stores is exactly\n * this: a key a step's type does not list is dropped, so the document holds\n * the vocabulary and nothing a caller made up.\n *\n * The caps follow what the engine reads — it cuts a subject at 200 and a body\n * at 5,000 before sending — so nothing is stored that could not be run.\n */\nconst STEP_FIELDS: Record<\n OrgAutomationStepType,\n Readonly<Record<string, 'string' | 'number' | 'boolean' | number>>\n> = {\n sendEmail: { subject: 200, body: 5000, toField: 64, topicId: 128 },\n notifyAdmins: { title: 200, body: 500 },\n enrollList: { listId: 128, listName: 200 },\n assignCampaign: { campaignId: 128, campaignName: 200 },\n datasetAppend: { datasetId: 128, datasetName: 200 },\n updateDataset: { datasetId: 128, datasetName: 200 },\n setContactStage: { lifecycleStage: 64 },\n addContactTag: { tag: 200 },\n assignContactOwner: {\n ownerUid: 128,\n ownerEmail: 320,\n roundRobin: 'boolean',\n },\n createCrmTask: {\n title: 200,\n kind: 64,\n priority: 16,\n dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000, direction: 16 },\n customEvent: { eventName: 64 },\n wait: { delayMinutes: 'number' },\n waitForEvent: { eventName: 64, timeoutMinutes: 'number' },\n exitFlow: {},\n}\n\nconst asObject = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\n/** A condition as it is stored, or null for a row that is not one. */\nfunction readCondition(raw: unknown): HostActionTriggerCondition | null {\n const row = asObject(raw)\n if (!row) return null\n const op = row['op']\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n // Kept as written so the validator names the missing operator rather\n // than the row silently vanishing.\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as never,\n }\n }\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as HostActionTriggerCondition['op'],\n ...(op !== 'notEmpty'\n ? { value: String(row['value'] ?? '').trim().slice(0, CONDITION_VALUE_MAX) }\n : {}),\n }\n}\n\nfunction readConditions(raw: unknown): HostActionTriggerCondition[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, ACTION_MAX_CONDITIONS + 1)\n .map(readCondition)\n .filter((row): row is HostActionTriggerCondition => row !== null)\n}\n\nfunction readCombinator(raw: unknown): TriggerCombinator | null {\n return (TRIGGER_COMBINATORS as readonly unknown[]).includes(raw)\n ? (raw as TriggerCombinator)\n : null\n}\n\n/** One step as it would be stored: its own fields only, strings capped. */\nfunction readStep(raw: unknown): HostActionStep | { type: unknown } {\n const source = asObject(raw) ?? {}\n const type = source['type']\n if (!isOrgAutomationStepType(type)) return { type }\n const step: Record<string, unknown> = { type }\n for (const [key, kind] of Object.entries(STEP_FIELDS[type])) {\n const value = source[key]\n if (value === undefined || value === null) continue\n if (kind === 'boolean') {\n if (typeof value === 'boolean') step[key] = value\n } else if (kind === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) step[key] = value\n } else if (typeof value === 'string') {\n step[key] = value.slice(0, Number(kind))\n }\n }\n const when = asObject(source['when'])\n if (when) {\n const conditions = readConditions(when['conditions'])\n if (conditions.length) {\n step['when'] = {\n conditions,\n combinator: readCombinator(when['combinator']) ?? 'and',\n }\n }\n }\n return step as HostActionStep\n}\n\n/** What a caller sends to create or replace an org automation. */\nexport type OrgAutomationInput = Record<string, unknown>\n\n/**\n * Reads an org automation out of an untrusted request body: the fields it may\n * hold, in the shape they are stored, or the first reason it cannot be saved.\n *\n * The rules are the actions builder's own — `validateHostAction` checks the\n * name, the trigger's conditions and every step's fields — and on top of them\n * the two this vocabulary adds: the trigger must be an org trigger and every\n * step an org step. The placement must be a scope a save can store:\n * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather\n * than guessing between sharing wider and taking access away.\n *\n * Whether each named site belongs to the organization is the route's to ask,\n * because it takes a read.\n */\nexport function readOrgAutomation(\n input: unknown,\n): { ok: true; value: OrgAutomationFields } | { ok: false; problem: string } {\n const body = asObject(input) ?? {}\n const name = String(body['name'] ?? '')\n .trim()\n .slice(0, ORG_AUTOMATION_NAME_MAX)\n if (!name) return { ok: false, problem: 'Name the automation' }\n const trigger = asObject(body['trigger']) ?? {}\n const event = String(trigger['event'] ?? '').trim()\n if (!isOrgAutomationTriggerEvent(event)) {\n return {\n ok: false,\n problem:\n 'Pick a trigger an org automation can start on — a form, a lead, a ' +\n 'booking, a member or a CRM event',\n }\n }\n const filter = String(trigger['filter'] ?? '')\n .trim()\n .slice(0, FILTER_MAX)\n const conditions = readConditions(trigger['conditions'])\n const combinator = readCombinator(trigger['combinator'])\n const rawSteps = Array.isArray(body['steps']) ? body['steps'] : []\n if (rawSteps.length > ACTION_MAX_STEPS) {\n return {\n ok: false,\n problem: `Org automations are capped at ${ACTION_MAX_STEPS} steps`,\n }\n }\n const steps = rawSteps.map(readStep)\n for (const [index, step] of steps.entries()) {\n const refusal = orgAutomationStepRefusal(step)\n if (refusal) return { ok: false, problem: `Step ${index + 1}: ${refusal}` }\n }\n const problem = validateHostAction({\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n ...(conditions.length ? { conditions } : {}),\n ...(combinator ? { combinator } : {}),\n },\n steps: steps as HostActionStep[],\n })\n if (problem) return { ok: false, problem }\n const visibleTo = normalizeVisibleTo(\n Array.isArray(body['visibleTo'])\n ? (body['visibleTo'] as unknown[]).filter(\n (token): token is string => typeof token === 'string',\n )\n : [],\n )\n if (!visibleTo) {\n return {\n ok: false,\n problem: 'Choose every site, or up to 30 sites, for it to run on',\n }\n }\n return {\n ok: true,\n value: {\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n conditions: conditions.length ? conditions : null,\n combinator: conditions.length ? (combinator ?? 'and') : null,\n },\n steps: steps as HostActionStep[],\n enabled: body['enabled'] !== false,\n visibleTo,\n },\n }\n}\n\n/** A stored document's pause list, read defensively. */\nexport function orgAutomationPausedHostIds(\n automation: { pausedHostIds?: unknown } | null | undefined,\n): string[] {\n const paused = automation?.pausedHostIds\n return Array.isArray(paused)\n ? paused.filter((id): id is string => typeof id === 'string')\n : []\n}\n\n/**\n * Why a stored org automation will not run on this site, or null when it\n * will — the one answer the engine asks when an event arrives and again when\n * a person waiting inside the automation is due.\n *\n * Deleted, switched off, taken off the site and paused there are the four\n * ways an organization or a site says \"not here\", and every one of them\n * stops the people already waiting on this site as well as the next run: the\n * kill switch a site action's resume applies, for the same reason.\n */\nexport function orgAutomationStopReason(\n automation:\n | {\n deletedAt?: unknown\n enabled?: unknown\n visibleTo?: unknown\n pausedHostIds?: unknown\n }\n | null\n | undefined,\n hostId: string,\n): string | null {\n if (!automation || automation.deletedAt) {\n return 'the org automation was deleted'\n }\n if (automation.enabled === false) {\n return 'the org automation was switched off'\n }\n if (!visibleToHost(automation.visibleTo as string[] | undefined, hostId)) {\n return 'the org automation no longer runs on this site'\n }\n if (orgAutomationPausedHostIds(automation).includes(hostId)) {\n return 'the org automation is paused on this site'\n }\n return null\n}\n\n/** Whether a stored org automation runs on this site. */\nexport function orgAutomationRunsOnHost(\n automation: Parameters<typeof orgAutomationStopReason>[0],\n hostId: string,\n): boolean {\n return orgAutomationStopReason(automation, hostId) === null\n}\n\n/**\n * A pause list after the placement changed: a site the automation no longer\n * runs on has nothing left to pause, so its entry goes with it, and a site\n * placed on it again later starts unpaused.\n */\nexport function prunePausedHostIds(\n pausedHostIds: readonly string[],\n visibleTo: readonly string[],\n): string[] {\n return pausedHostIds.filter((hostId) => visibleToHost(visibleTo, hostId))\n}\n"],"names":["normalizeVisibleTo","visibleToHost","ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","HOST_ACTION_STEP_LABELS","validateHostAction","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATION_API_ROUTES","manage","pause","ORG_AUTOMATIONS_MAX","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATION_RUN_TARGET","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_TRIGGER_SET","Set","isOrgAutomationTriggerEvent","event","has","ORG_AUTOMATION_STEP_TYPES","ORG_STEP_SET","isOrgAutomationStepType","type","ORG_AUTOMATION_STEP_KINDS","map","value","label","orgAutomationStepRefusal","step","String","ORG_AUTOMATION_NAME_MAX","FILTER_MAX","CONDITION_FIELD_MAX","CONDITION_VALUE_MAX","STEP_FIELDS","sendEmail","subject","body","toField","topicId","notifyAdmins","title","enrollList","listId","listName","assignCampaign","campaignId","campaignName","datasetAppend","datasetId","datasetName","updateDataset","setContactStage","lifecycleStage","addContactTag","tag","assignContactOwner","ownerUid","ownerEmail","roundRobin","createCrmTask","kind","priority","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","direction","customEvent","eventName","wait","delayMinutes","waitForEvent","timeoutMinutes","exitFlow","asObject","Array","isArray","readCondition","raw","row","op","includes","field","trim","slice","readConditions","filter","readCombinator","readStep","source","key","Object","entries","undefined","Number","isFinite","when","conditions","length","combinator","readOrgAutomation","input","trigger","name","ok","problem","rawSteps","steps","index","refusal","visibleTo","token","enabled","orgAutomationPausedHostIds","automation","paused","pausedHostIds","id","orgAutomationStopReason","hostId","deletedAt","orgAutomationRunsOnHost","prunePausedHostIds"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/org-automations.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 normalizeVisibleTo,\n type ScopeToken,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { HostEventType } from '@aglyn/aglyn/app-utils/host-events'\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './host-actions'\n\n/**\n * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and\n * places on the sites it chooses.\n *\n * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They\n * are not moved here, and the reasons are the four things a site action can\n * do that an org automation must not:\n *\n * - call the site's functions and variables through a workflow step — the\n * working set a computed variable binds to, which is one site's;\n * - run the steps only a visitor's page can run (overlays, drawers, class\n * toggles, a redirect to a screen), which the `actions` collection also\n * holds for element interactions;\n * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an\n * action id to a site only by its path;\n * - wait, with the resume beat reading a missing document as the kill\n * switch, so a migration would stop every person already inside a flow.\n *\n * An org automation is therefore its own document with a narrower\n * vocabulary: triggers only the server raises, and steps that mean the same\n * thing on whichever site they run. Each run executes AS the site the event\n * happened on — that site's sender, suppressions, consent group, meters and\n * run allowance — so placing one automation on five sites is five sites each\n * running it for themselves, never one site acting for the others.\n *\n * Pure and client-safe: the console's editor, the server routes and the\n * engine read the same definitions from here.\n */\n\n/** `orgs/{orgId}/automations/{automationId}`. */\nexport const ORG_AUTOMATIONS_COLLECTION = 'automations'\n\n/**\n * The two server doors onto the collection, under the plugin's `automations`\n * API prefix: the organization's (`manage`) and a site's own pause\n * (`pause`). Named here so the console's caller and the server's registration\n * read one spelling.\n */\nexport const ORG_AUTOMATION_API_ROUTES = {\n manage: 'automations/manage',\n pause: 'automations/pause',\n} as const\n\n/**\n * How many live org automations one organization may hold.\n *\n * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,\n * because the two are different things: a site action is authored per site\n * and per element, where an org automation is a shared building block placed\n * on many sites at once. A hundred is far past what an organization writes by\n * hand and keeps the org hub's list — which reads every live one — a single\n * bounded window. Soft-deleted documents do not count.\n */\nexport const ORG_AUTOMATIONS_MAX = 100\n\n/**\n * How many org automations one event may run on one site: every one the\n * organization may hold (AGL-3458). The query asks for switched-on ones only\n * — a deletion switches an automation off — so nothing that cannot run takes\n * a place, and a site's own actions are read by a query of their own, so the\n * two cannot crowd each other out.\n */\nexport const MAX_TRIGGERED_ORG_AUTOMATIONS = ORG_AUTOMATIONS_MAX\n\n/**\n * The run history's target type for a run of an org automation, on the\n * activity feed of the site it ran on.\n */\nexport const ORG_AUTOMATION_RUN_TARGET = 'orgAutomation'\n\n/**\n * The org activity target an org automation's own lifecycle rows are filed\n * under: the plugin's namespaced resource (`pluginId:noun`), which the org's\n * feed reads as \"Automation\" — core's target list names only core's resources.\n */\nexport const ORG_AUTOMATION_ACTIVITY_TARGET = 'workflows:automation'\n\n/**\n * The events an org automation may start on: every host event the SERVER\n * raises and a person can be the subject of.\n *\n * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine\n * queries the organization's automations on every event in this list, so a\n * new host event must not become an org trigger — and a new read on every\n * occurrence of it — by being added to the platform's list. Two are left out\n * on purpose: `pageView` fires on every request to every published page, and\n * an org query on it would be a read per visitor; `memberSignOut` is raised\n * by no door today.\n */\nexport const ORG_AUTOMATION_TRIGGER_EVENTS = [\n 'formSubmission',\n 'lead',\n 'contactCreated',\n 'contactStageChanged',\n 'booking',\n 'memberSignUp',\n 'memberSignIn',\n 'dealStageChanged',\n 'dealWon',\n 'dealLost',\n 'taskCompleted',\n] as const satisfies readonly HostEventType[]\n\nexport type OrgAutomationTriggerEvent =\n (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number]\n\nconst ORG_TRIGGER_SET: ReadonlySet<string> = new Set(\n ORG_AUTOMATION_TRIGGER_EVENTS,\n)\n\n/** Whether an org automation may start on this event. */\nexport function isOrgAutomationTriggerEvent(\n event: unknown,\n): event is OrgAutomationTriggerEvent {\n return typeof event === 'string' && ORG_TRIGGER_SET.has(event)\n}\n\n/**\n * The steps an org automation may hold: the server steps that read the same\n * on every site.\n *\n * What is left out, and why:\n * - every step the visitor's page runs — overlays, drawers, menus, class\n * toggles, custom HTML and JavaScript, analytics, redirects, the site\n * alert — because an org trigger is a server event with no page to act on;\n * - `runWorkflow`, because a workflow is one site's and calls that site's\n * functions and variables;\n * - `webhookPost`, because a webhook's address and secret are one site's.\n *\n * `customEvent` stays: it fires a custom event ON the site the run is on,\n * which that site's own actions may listen for — the documented way for an\n * org automation to hand over to something site-specific.\n */\nexport const ORG_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'datasetAppend',\n 'updateDataset',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n 'customEvent',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n] as const satisfies readonly HostActionStepType[]\n\nexport type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number]\n\nconst ORG_STEP_SET: ReadonlySet<string> = new Set(ORG_AUTOMATION_STEP_TYPES)\n\n/** Whether an org automation may run a step of this type. */\nexport function isOrgAutomationStepType(\n type: unknown,\n): type is OrgAutomationStepType {\n return typeof type === 'string' && ORG_STEP_SET.has(type)\n}\n\n/** The org editor's \"Do\" picker: each allowed step with its label, in order. */\nexport const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{\n value: OrgAutomationStepType\n label: string\n}> = ORG_AUTOMATION_STEP_TYPES.map((value) => ({\n value,\n label: HOST_ACTION_STEP_LABELS[value],\n}))\n\n/**\n * Why an org automation cannot hold this step, or null when it can.\n *\n * Shared by the save route, which refuses it, and by the engine, which\n * refuses to run one a writer stored anyway.\n */\nexport function orgAutomationStepRefusal(step: {\n type?: unknown\n}): string | null {\n if (isOrgAutomationStepType(step?.type)) return null\n const label = HOST_ACTION_STEP_LABELS[step?.type as HostActionStepType]\n if (!label) return `“${String(step?.type ?? '')}” is not a step`\n return (\n `“${label}” belongs to one site, so an org automation cannot run it — ` +\n 'build it as an action on that site'\n )\n}\n\n/** The trigger an org automation starts on, as it is stored. */\nexport interface OrgAutomationTrigger {\n event: OrgAutomationTriggerEvent\n /** Optional expression over the payload; runs only when truthy. */\n filter?: string\n conditions: HostActionTriggerCondition[] | null\n combinator: TriggerCombinator | null\n}\n\n/**\n * The fields an org automation is written with — by the save route, and by\n * nothing else: the collection is closed to client writes.\n */\nexport interface OrgAutomationFields {\n name: string\n trigger: OrgAutomationTrigger\n steps: HostActionStep[]\n enabled: boolean\n /**\n * The sites it runs on, as scope tokens: `['org']` is every site of the\n * organization, now and later; `['host:{id}', …]` names up to thirty.\n */\n visibleTo: ScopeToken[]\n}\n\n/** `orgs/{orgId}/automations/{id}`, as it is stored. */\nexport interface OrgAutomation extends OrgAutomationFields {\n /**\n * The sites that have paused it for themselves — each a site id, never a\n * scope token. The \"host level control\": a site's admin or editor adds or\n * removes their own site here, and only their own.\n */\n pausedHostIds: string[]\n createdBy?: string\n updatedBy?: string\n /**\n * Set when it was deleted. Always written — `null` while live — so the\n * org hub's list can ask for the live ones by equality.\n */\n deletedAt: unknown\n}\n\n/** A stored org automation with its id, as the console lists it. */\nexport type OrgAutomationRow = OrgAutomation & { $id: string }\n\n/** Longest name an org automation may carry: the actions editor's cap. */\nexport const ORG_AUTOMATION_NAME_MAX = 60\n\n/** Longest trigger filter expression. */\nconst FILTER_MAX = 500\n\n/** Longest a condition's field or value may be. */\nconst CONDITION_FIELD_MAX = 64\nconst CONDITION_VALUE_MAX = 200\n\n/**\n * Every field a step of each type may carry beyond `type` and `when`, with\n * the longest a string in it may be. What the save route stores is exactly\n * this: a key a step's type does not list is dropped, so the document holds\n * the vocabulary and nothing a caller made up.\n *\n * The caps follow what the engine reads — it cuts a subject at 200 and a body\n * at 5,000 before sending — so nothing is stored that could not be run.\n */\nconst STEP_FIELDS: Record<\n OrgAutomationStepType,\n Readonly<Record<string, 'string' | 'number' | 'boolean' | number>>\n> = {\n sendEmail: { subject: 200, body: 5000, toField: 64, topicId: 128, transactional: 'boolean' },\n notifyAdmins: { title: 200, body: 500 },\n enrollList: { listId: 128, listName: 200 },\n assignCampaign: { campaignId: 128, campaignName: 200 },\n datasetAppend: { datasetId: 128, datasetName: 200 },\n updateDataset: { datasetId: 128, datasetName: 200 },\n setContactStage: { lifecycleStage: 64 },\n addContactTag: { tag: 200 },\n assignContactOwner: {\n ownerUid: 128,\n ownerEmail: 320,\n roundRobin: 'boolean',\n },\n createCrmTask: {\n title: 200,\n kind: 64,\n priority: 16,\n dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000, direction: 16 },\n customEvent: { eventName: 64 },\n wait: { delayMinutes: 'number' },\n waitForEvent: { eventName: 64, timeoutMinutes: 'number' },\n exitFlow: {},\n}\n\nconst asObject = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\n/** A condition as it is stored, or null for a row that is not one. */\nfunction readCondition(raw: unknown): HostActionTriggerCondition | null {\n const row = asObject(raw)\n if (!row) return null\n const op = row['op']\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n // Kept as written so the validator names the missing operator rather\n // than the row silently vanishing.\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as never,\n }\n }\n return {\n field: String(row['field'] ?? '').trim().slice(0, CONDITION_FIELD_MAX),\n op: op as HostActionTriggerCondition['op'],\n ...(op !== 'notEmpty'\n ? { value: String(row['value'] ?? '').trim().slice(0, CONDITION_VALUE_MAX) }\n : {}),\n }\n}\n\nfunction readConditions(raw: unknown): HostActionTriggerCondition[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, ACTION_MAX_CONDITIONS + 1)\n .map(readCondition)\n .filter((row): row is HostActionTriggerCondition => row !== null)\n}\n\nfunction readCombinator(raw: unknown): TriggerCombinator | null {\n return (TRIGGER_COMBINATORS as readonly unknown[]).includes(raw)\n ? (raw as TriggerCombinator)\n : null\n}\n\n/** One step as it would be stored: its own fields only, strings capped. */\nfunction readStep(raw: unknown): HostActionStep | { type: unknown } {\n const source = asObject(raw) ?? {}\n const type = source['type']\n if (!isOrgAutomationStepType(type)) return { type }\n const step: Record<string, unknown> = { type }\n for (const [key, kind] of Object.entries(STEP_FIELDS[type])) {\n const value = source[key]\n if (value === undefined || value === null) continue\n if (kind === 'boolean') {\n if (typeof value === 'boolean') step[key] = value\n } else if (kind === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) step[key] = value\n } else if (typeof value === 'string') {\n step[key] = value.slice(0, Number(kind))\n }\n }\n const when = asObject(source['when'])\n if (when) {\n const conditions = readConditions(when['conditions'])\n if (conditions.length) {\n step['when'] = {\n conditions,\n combinator: readCombinator(when['combinator']) ?? 'and',\n }\n }\n }\n return step as HostActionStep\n}\n\n/** What a caller sends to create or replace an org automation. */\nexport type OrgAutomationInput = Record<string, unknown>\n\n/**\n * Reads an org automation out of an untrusted request body: the fields it may\n * hold, in the shape they are stored, or the first reason it cannot be saved.\n *\n * The rules are the actions builder's own — `validateHostAction` checks the\n * name, the trigger's conditions and every step's fields — and on top of them\n * the two this vocabulary adds: the trigger must be an org trigger and every\n * step an org step. The placement must be a scope a save can store:\n * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather\n * than guessing between sharing wider and taking access away.\n *\n * Whether each named site belongs to the organization is the route's to ask,\n * because it takes a read.\n */\nexport function readOrgAutomation(\n input: unknown,\n): { ok: true; value: OrgAutomationFields } | { ok: false; problem: string } {\n const body = asObject(input) ?? {}\n const name = String(body['name'] ?? '')\n .trim()\n .slice(0, ORG_AUTOMATION_NAME_MAX)\n if (!name) return { ok: false, problem: 'Name the automation' }\n const trigger = asObject(body['trigger']) ?? {}\n const event = String(trigger['event'] ?? '').trim()\n if (!isOrgAutomationTriggerEvent(event)) {\n return {\n ok: false,\n problem:\n 'Pick a trigger an org automation can start on — a form, a lead, a ' +\n 'booking, a member or a CRM event',\n }\n }\n const filter = String(trigger['filter'] ?? '')\n .trim()\n .slice(0, FILTER_MAX)\n const conditions = readConditions(trigger['conditions'])\n const combinator = readCombinator(trigger['combinator'])\n const rawSteps = Array.isArray(body['steps']) ? body['steps'] : []\n if (rawSteps.length > ACTION_MAX_STEPS) {\n return {\n ok: false,\n problem: `Org automations are capped at ${ACTION_MAX_STEPS} steps`,\n }\n }\n const steps = rawSteps.map(readStep)\n for (const [index, step] of steps.entries()) {\n const refusal = orgAutomationStepRefusal(step)\n if (refusal) return { ok: false, problem: `Step ${index + 1}: ${refusal}` }\n }\n const problem = validateHostAction({\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n ...(conditions.length ? { conditions } : {}),\n ...(combinator ? { combinator } : {}),\n },\n steps: steps as HostActionStep[],\n })\n if (problem) return { ok: false, problem }\n const visibleTo = normalizeVisibleTo(\n Array.isArray(body['visibleTo'])\n ? (body['visibleTo'] as unknown[]).filter(\n (token): token is string => typeof token === 'string',\n )\n : [],\n )\n if (!visibleTo) {\n return {\n ok: false,\n problem: 'Choose every site, or up to 30 sites, for it to run on',\n }\n }\n return {\n ok: true,\n value: {\n name,\n trigger: {\n event,\n ...(filter ? { filter } : {}),\n conditions: conditions.length ? conditions : null,\n combinator: conditions.length ? (combinator ?? 'and') : null,\n },\n steps: steps as HostActionStep[],\n enabled: body['enabled'] !== false,\n visibleTo,\n },\n }\n}\n\n/** A stored document's pause list, read defensively. */\nexport function orgAutomationPausedHostIds(\n automation: { pausedHostIds?: unknown } | null | undefined,\n): string[] {\n const paused = automation?.pausedHostIds\n return Array.isArray(paused)\n ? paused.filter((id): id is string => typeof id === 'string')\n : []\n}\n\n/**\n * Why a stored org automation will not run on this site, or null when it\n * will — the one answer the engine asks when an event arrives and again when\n * a person waiting inside the automation is due.\n *\n * Deleted, switched off, taken off the site and paused there are the four\n * ways an organization or a site says \"not here\", and every one of them\n * stops the people already waiting on this site as well as the next run: the\n * kill switch a site action's resume applies, for the same reason.\n */\nexport function orgAutomationStopReason(\n automation:\n | {\n deletedAt?: unknown\n enabled?: unknown\n visibleTo?: unknown\n pausedHostIds?: unknown\n }\n | null\n | undefined,\n hostId: string,\n): string | null {\n if (!automation || automation.deletedAt) {\n return 'the org automation was deleted'\n }\n if (automation.enabled === false) {\n return 'the org automation was switched off'\n }\n if (!visibleToHost(automation.visibleTo as string[] | undefined, hostId)) {\n return 'the org automation no longer runs on this site'\n }\n if (orgAutomationPausedHostIds(automation).includes(hostId)) {\n return 'the org automation is paused on this site'\n }\n return null\n}\n\n/** Whether a stored org automation runs on this site. */\nexport function orgAutomationRunsOnHost(\n automation: Parameters<typeof orgAutomationStopReason>[0],\n hostId: string,\n): boolean {\n return orgAutomationStopReason(automation, hostId) === null\n}\n\n/**\n * A pause list after the placement changed: a site the automation no longer\n * runs on has nothing left to pause, so its entry goes with it, and a site\n * placed on it again later starts unpaused.\n */\nexport function prunePausedHostIds(\n pausedHostIds: readonly string[],\n visibleTo: readonly string[],\n): string[] {\n return pausedHostIds.filter((hostId) => visibleToHost(visibleTo, hostId))\n}\n"],"names":["normalizeVisibleTo","visibleToHost","ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","HOST_ACTION_STEP_LABELS","validateHostAction","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATION_API_ROUTES","manage","pause","ORG_AUTOMATIONS_MAX","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATION_RUN_TARGET","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_TRIGGER_SET","Set","isOrgAutomationTriggerEvent","event","has","ORG_AUTOMATION_STEP_TYPES","ORG_STEP_SET","isOrgAutomationStepType","type","ORG_AUTOMATION_STEP_KINDS","map","value","label","orgAutomationStepRefusal","step","String","ORG_AUTOMATION_NAME_MAX","FILTER_MAX","CONDITION_FIELD_MAX","CONDITION_VALUE_MAX","STEP_FIELDS","sendEmail","subject","body","toField","topicId","transactional","notifyAdmins","title","enrollList","listId","listName","assignCampaign","campaignId","campaignName","datasetAppend","datasetId","datasetName","updateDataset","setContactStage","lifecycleStage","addContactTag","tag","assignContactOwner","ownerUid","ownerEmail","roundRobin","createCrmTask","kind","priority","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","direction","customEvent","eventName","wait","delayMinutes","waitForEvent","timeoutMinutes","exitFlow","asObject","Array","isArray","readCondition","raw","row","op","includes","field","trim","slice","readConditions","filter","readCombinator","readStep","source","key","Object","entries","undefined","Number","isFinite","when","conditions","length","combinator","readOrgAutomation","input","trigger","name","ok","problem","rawSteps","steps","index","refusal","visibleTo","token","enabled","orgAutomationPausedHostIds","automation","paused","pausedHostIds","id","orgAutomationStopReason","hostId","deletedAt","orgAutomationRunsOnHost","prunePausedHostIds"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server-automation-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n ACTIONS_MAX_PER_HOST,\n siteInteractionDocument,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './model/host-actions'\nimport { interactionPlaceholders } from '@aglyn/aglyn/app-utils/draft-placeholders'\n\n/**\n * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).\n *\n * The draft writer this plugin registers for the `automation` resource on the\n * core's resource-drafts seam. A plugin that produces an automation — a\n * generator working from a description, an importer bringing flows over from\n * another tool — asks for the writer by name and gets this plugin's rules,\n * never a copy of them:\n *\n * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the\n * Actions editor saves (`siteInteractionDocument`), with only the fields each\n * step type holds. It is listed on the Automation page's Actions, opens in\n * that editor, and runs through the same executor as one a person made.\n * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,\n * so nothing it describes runs until a person switches it on.\n * - THE SCHEMA is the editor's own validator, `validateHostAction`, which the\n * executor's writers share. A placeholder the draft could not fill — a\n * bracketed list name, a bracketed condition value — passes it, because it\n * is a value a person replaces, and the facts name every one.\n * - THE ROOM is the flat platform cap on live actions, counted inside the\n * transaction with the arithmetic `/api/hosts/resources` uses.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content. THE PLAN is the Actions editor's: the `actions` entitlement.\n *\n * Nothing is enabled, sent or run, and the writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const AUTOMATION_DRAFT_RESOURCE = 'automation'\n\n/** The longest name the Actions editor stores. */\nexport const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60\n\n/** A draft's name when the caller asks for none. */\nexport const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation'\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const AUTOMATION_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** What a person is told when their plan has no actions builder. It names the page that sells one. */\nexport const AUTOMATION_DRAFT_PLAN_REFUSAL =\n 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.'\n\n/** The resources route's refusal at the cap. */\nexport const AUTOMATION_DRAFT_LIMIT_REFUSAL =\n `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` +\n 'delete some to make room'\n\n/** A trigger bound to one element, which makes an action an interaction of its page. */\nconst LEAF_SELECTOR = /^\\[data-aglyn=\"leaf:.+\"\\]$/\n\n/**\n * The fields each step type holds, besides `type` and the step's own `when`.\n * Total over the step types, so a type the model gains is a compile error here\n * until its fields are named, rather than a draft that silently drops them.\n */\nexport const AUTOMATION_STEP_FIELDS: Readonly<Record<HostActionStepType, readonly string[]>> = {\n runWorkflow: ['workflowId', 'workflowName'],\n siteAlert: ['message', 'severity'],\n customEvent: ['eventName'],\n datasetAppend: ['datasetId', 'datasetName'],\n webhookPost: ['webhookId', 'webhookName'],\n showOverlay: ['overlayId', 'overlayName'],\n stickyNav: ['selector'],\n addClass: ['selector', 'className'],\n removeClass: ['selector', 'className'],\n toggleClass: ['selector', 'className'],\n showElement: ['selector', 'delayMs', 'dismissOn'],\n hideElement: ['selector', 'delayMs'],\n toggleElement: ['selector', 'delayMs', 'dismissOn'],\n setAttribute: ['selector', 'name', 'value'],\n removeAttribute: ['selector', 'name'],\n scrollTo: ['selector', 'behavior', 'offsetPx'],\n playVideo: ['selector'],\n openDrawer: ['drawerNodeId'],\n closeDrawer: ['drawerNodeId'],\n toggleDrawer: ['drawerNodeId'],\n openMenu: ['menuNodeId'],\n closeMenu: ['menuNodeId'],\n toggleMenu: ['menuNodeId'],\n showHtml: ['html'],\n runJs: ['code'],\n redirect: ['url', 'screenId'],\n trackGaEvent: ['eventName', 'params'],\n sendEmail: ['subject', 'body', 'toField', 'topicId'],\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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,gBAAgB,QAAQ,2CAA0C;AAE3E,SACEC,iCAAiC,QAM5B,qDAAoD;AAC3D,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,2CAA0C;AACjD,SAKEC,kBAAkB,QACb,0BAAsB;AAC7B,SAASC,uBAAuB,QAAQ,4CAA2C;AA+BnF,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,EAAET,qBAAqB,YAAY,CAAC,GAC5E,2BAA0B;AAE5B,sFAAsF,GACtF,MAAMU,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;QAAY;QAAa;QAAe;KAAgB;IACzFC,gBAAgB;QAAC;QAAQ;QAAQ;KAAY;AAC/C,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,UAAU/F,mBAAmB6F;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,cAAcjG,wBAAwB4F,QAAQR,MAAM;IACtD;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,OAAOpH,iBAAiBqH,QAAQ,OAAO;QAAEE,QAAQ;QAAKC,OAAO5G;IAA8B;AAC7F;AAEA,SAAS6G,YAAYC,GAA6C;IAChE,OAAOzH,iBAAiByH,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,QAAQ3H,uBAAuB;QAAEkH,QAAQ;QAAKC,OAAO1G;IAA+B,IAAI;AACjG;AAOA,OAAO,SAASmH,4BACdC,OAAkC,CAAC,CAAC;QAElBA;IAAlB,MAAMC,aAAYD,kBAAAA,KAAKC,SAAS,YAAdD,kBAAmB,IAAM/H,cAAciI,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,aAChB3I,wBAAwB8F;oBAC3BsD,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;IACd7J,kCAAkCO,2BAA2BqJ,uBAAuB;QAClFE,UAAU5J;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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"}
|
|
@@ -44,7 +44,7 @@ const capitalized = (word)=>word.charAt(0).toUpperCase() + word.slice(1);
|
|
|
44
44
|
var _ref, _request_payload_contactId;
|
|
45
45
|
const named = String((_ref = (_request_payload_contactId = request.payload['contactId']) != null ? _request_payload_contactId : request.payload['email']) != null ? _ref : '');
|
|
46
46
|
return {
|
|
47
|
-
error: `no contact this site can see for ${named}`
|
|
47
|
+
error: `no contact or lead this site can see for ${named}`
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
const facet = (field)=>`facets.${double.groupId}.${field}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/testing/stand-in-crm-steps.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 registerServerStepExecutor,\n type ServerStepRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** The contact an event names, as the spec's own Firestore double holds it. */\nexport interface CrmContactDouble {\n id: string\n update(patch: Record<string, unknown>): Promise<unknown>\n}\n\n/** Where the stand-in finds the person and which site facet it writes. */\nexport interface CrmStepsDouble {\n /** The contact the event names, or `null` for nobody the site can see. */\n contact(request: ServerStepRequest): Promise<CrmContactDouble | null>\n /** The consent group whose facet the site writes. */\n groupId: string\n}\n\n/** The step types the CRM declares under `serverSteps`. */\nexport const STAND_IN_CRM_STEP_TYPES = [\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n] as const\n\nconst capitalized = (word: string) => word.charAt(0).toUpperCase() + word.slice(1)\n\n/**\n * The CRM steps the CRM runs for this engine (AGL-3080), stood in for this\n * plugin's specs — this plugin may not load the CRM. Over the spec's own\n * Firestore double it does what the owner's executor does for the shapes these\n * specs store: it finds the contact the event names, writes a stage or a tag\n * inside the site's facet, answers the run history's detail, and answers a\n * stage that moved as the `contactStageChanged` event for the engine to raise.\n * An owner, a task and an activity are answered by their detail alone.\n *\n * Owners, tasks, scopes, the plan gate and the activity ceiling are the CRM's\n * own spec's (`automation-steps.spec.ts`), and the real pair runs together in\n * `apps/console/specs/crm-automation-steps.spec.ts`.\n */\nexport function standInCrmSteps(double: CrmStepsDouble): () => void {\n return registerServerStepExecutor(\n STAND_IN_CRM_STEP_TYPES,\n async (request) => {\n const step = request.step as Record<string, unknown> & { type: string }\n const contact = await double.contact(request)\n if (!contact) {\n const named = String(request.payload['contactId'] ?? request.payload['email'] ?? '')\n return { error: `no contact this site can see for ${named}` }\n }\n const facet = (field: string) => `facets.${double.groupId}.${field}`\n if (step.type === 'setContactStage') {\n const stage = String(step['lifecycleStage'] ?? '')\n await contact.update({\n [facet('lifecycleStage')]: stage,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return {\n detail: capitalized(stage),\n emit: {\n event: 'contactStageChanged',\n payload: { contactId: contact.id, lifecycleStage: stage },\n },\n }\n }\n if (step.type === 'addContactTag') {\n const tag = String(step['tag'] ?? '').trim()\n await contact.update({\n [facet('tags')]: FieldValue.arrayUnion(tag),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { detail: tag }\n }\n if (step.type === 'assignContactOwner') {\n return { detail: step['roundRobin'] === true ? 'round robin' : String(step['ownerEmail'] ?? step['ownerUid'] ?? '') }\n }\n if (step.type === 'createCrmTask') return { detail: String(step['title'] ?? '').slice(0, 60) }\n return { detail: String(step['kind'] ?? '') }\n },\n { pluginId: 'crm' },\n )\n}\n"],"names":["registerServerStepExecutor","FieldValue","STAND_IN_CRM_STEP_TYPES","capitalized","word","charAt","toUpperCase","slice","standInCrmSteps","double","request","step","contact","named","String","payload","error","facet","field","groupId","type","stage","update","updatedAt","serverTimestamp","detail","emit","event","contactId","id","lifecycleStage","tag","trim","arrayUnion","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,QAErB,kDAAiD;AACxD,SAASC,UAAU,QAAQ,2BAA0B;AAgBrD,yDAAyD,GACzD,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,MAAMC,cAAc,CAACC,OAAiBA,KAAKC,MAAM,CAAC,GAAGC,WAAW,KAAKF,KAAKG,KAAK,CAAC;AAEhF;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBAAgBC,MAAsB;IACpD,OAAOT,2BACLE,yBACA,OAAOQ;YAiCsDC,aACnCA;QAjCxB,MAAMA,OAAOD,QAAQC,IAAI;QACzB,MAAMC,UAAU,MAAMH,OAAOG,OAAO,CAACF;QACrC,IAAI,CAACE,SAAS;gBACSF,MAAAA;YAArB,MAAMG,QAAQC,QAAOJ,QAAAA,6BAAAA,QAAQK,OAAO,CAAC,YAAY,YAA5BL,6BAAgCA,QAAQK,OAAO,CAAC,QAAQ,YAAxDL,OAA4D;YACjF,OAAO;gBAAEM,OAAO,CAAC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/testing/stand-in-crm-steps.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 registerServerStepExecutor,\n type ServerStepRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** The contact an event names, as the spec's own Firestore double holds it. */\nexport interface CrmContactDouble {\n id: string\n update(patch: Record<string, unknown>): Promise<unknown>\n}\n\n/** Where the stand-in finds the person and which site facet it writes. */\nexport interface CrmStepsDouble {\n /** The contact the event names, or `null` for nobody the site can see. */\n contact(request: ServerStepRequest): Promise<CrmContactDouble | null>\n /** The consent group whose facet the site writes. */\n groupId: string\n}\n\n/** The step types the CRM declares under `serverSteps`. */\nexport const STAND_IN_CRM_STEP_TYPES = [\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n] as const\n\nconst capitalized = (word: string) => word.charAt(0).toUpperCase() + word.slice(1)\n\n/**\n * The CRM steps the CRM runs for this engine (AGL-3080), stood in for this\n * plugin's specs — this plugin may not load the CRM. Over the spec's own\n * Firestore double it does what the owner's executor does for the shapes these\n * specs store: it finds the contact the event names, writes a stage or a tag\n * inside the site's facet, answers the run history's detail, and answers a\n * stage that moved as the `contactStageChanged` event for the engine to raise.\n * An owner, a task and an activity are answered by their detail alone.\n *\n * Owners, tasks, scopes, the plan gate and the activity ceiling are the CRM's\n * own spec's (`automation-steps.spec.ts`), and the real pair runs together in\n * `apps/console/specs/crm-automation-steps.spec.ts`.\n */\nexport function standInCrmSteps(double: CrmStepsDouble): () => void {\n return registerServerStepExecutor(\n STAND_IN_CRM_STEP_TYPES,\n async (request) => {\n const step = request.step as Record<string, unknown> & { type: string }\n const contact = await double.contact(request)\n if (!contact) {\n const named = String(request.payload['contactId'] ?? request.payload['email'] ?? '')\n return { error: `no contact or lead this site can see for ${named}` }\n }\n const facet = (field: string) => `facets.${double.groupId}.${field}`\n if (step.type === 'setContactStage') {\n const stage = String(step['lifecycleStage'] ?? '')\n await contact.update({\n [facet('lifecycleStage')]: stage,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return {\n detail: capitalized(stage),\n emit: {\n event: 'contactStageChanged',\n payload: { contactId: contact.id, lifecycleStage: stage },\n },\n }\n }\n if (step.type === 'addContactTag') {\n const tag = String(step['tag'] ?? '').trim()\n await contact.update({\n [facet('tags')]: FieldValue.arrayUnion(tag),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { detail: tag }\n }\n if (step.type === 'assignContactOwner') {\n return { detail: step['roundRobin'] === true ? 'round robin' : String(step['ownerEmail'] ?? step['ownerUid'] ?? '') }\n }\n if (step.type === 'createCrmTask') return { detail: String(step['title'] ?? '').slice(0, 60) }\n return { detail: String(step['kind'] ?? '') }\n },\n { pluginId: 'crm' },\n )\n}\n"],"names":["registerServerStepExecutor","FieldValue","STAND_IN_CRM_STEP_TYPES","capitalized","word","charAt","toUpperCase","slice","standInCrmSteps","double","request","step","contact","named","String","payload","error","facet","field","groupId","type","stage","update","updatedAt","serverTimestamp","detail","emit","event","contactId","id","lifecycleStage","tag","trim","arrayUnion","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,QAErB,kDAAiD;AACxD,SAASC,UAAU,QAAQ,2BAA0B;AAgBrD,yDAAyD,GACzD,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,MAAMC,cAAc,CAACC,OAAiBA,KAAKC,MAAM,CAAC,GAAGC,WAAW,KAAKF,KAAKG,KAAK,CAAC;AAEhF;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBAAgBC,MAAsB;IACpD,OAAOT,2BACLE,yBACA,OAAOQ;YAiCsDC,aACnCA;QAjCxB,MAAMA,OAAOD,QAAQC,IAAI;QACzB,MAAMC,UAAU,MAAMH,OAAOG,OAAO,CAACF;QACrC,IAAI,CAACE,SAAS;gBACSF,MAAAA;YAArB,MAAMG,QAAQC,QAAOJ,QAAAA,6BAAAA,QAAQK,OAAO,CAAC,YAAY,YAA5BL,6BAAgCA,QAAQK,OAAO,CAAC,QAAQ,YAAxDL,OAA4D;YACjF,OAAO;gBAAEM,OAAO,CAAC,yCAAyC,EAAEH,OAAO;YAAC;QACtE;QACA,MAAMI,QAAQ,CAACC,QAAkB,CAAC,OAAO,EAAET,OAAOU,OAAO,CAAC,CAAC,EAAED,OAAO;QACpE,IAAIP,KAAKS,IAAI,KAAK,mBAAmB;gBACdT;YAArB,MAAMU,QAAQP,QAAOH,uBAAAA,IAAI,CAAC,iBAAiB,YAAtBA,uBAA0B;YAC/C,MAAMC,QAAQU,MAAM,CAAC;gBACnB,CAACL,MAAM,kBAAkB,EAAEI;gBAC3BE,WAAWtB,WAAWuB,eAAe;YACvC;YACA,OAAO;gBACLC,QAAQtB,YAAYkB;gBACpBK,MAAM;oBACJC,OAAO;oBACPZ,SAAS;wBAAEa,WAAWhB,QAAQiB,EAAE;wBAAEC,gBAAgBT;oBAAM;gBAC1D;YACF;QACF;QACA,IAAIV,KAAKS,IAAI,KAAK,iBAAiB;gBACdT;YAAnB,MAAMoB,MAAMjB,QAAOH,YAAAA,IAAI,CAAC,MAAM,YAAXA,YAAe,IAAIqB,IAAI;YAC1C,MAAMpB,QAAQU,MAAM,CAAC;gBACnB,CAACL,MAAM,QAAQ,EAAEhB,WAAWgC,UAAU,CAACF;gBACvCR,WAAWtB,WAAWuB,eAAe;YACvC;YACA,OAAO;gBAAEC,QAAQM;YAAI;QACvB;QACA,IAAIpB,KAAKS,IAAI,KAAK,sBAAsB;gBACgCT,OAAAA;YAAtE,OAAO;gBAAEc,QAAQd,IAAI,CAAC,aAAa,KAAK,OAAO,gBAAgBG,QAAOH,SAAAA,mBAAAA,IAAI,CAAC,aAAa,YAAlBA,mBAAsBA,IAAI,CAAC,WAAW,YAAtCA,QAA0C;YAAI;QACtH;QACA,IAAIA,KAAKS,IAAI,KAAK,iBAAiB,OAAO;YAAEK,QAAQX,QAAOH,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB,IAAIJ,KAAK,CAAC,GAAG;QAAI;QAC7F,OAAO;YAAEkB,QAAQX,QAAOH,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgB;QAAI;IAC9C,GACA;QAAEuB,UAAU;IAAM;AAEtB"}
|
|
@@ -29,7 +29,8 @@ export declare function standInRecipes(): void;
|
|
|
29
29
|
/**
|
|
30
30
|
* The `form` list source the Forms plugin publishes (AGL-3080), stood in: a
|
|
31
31
|
* site's forms by document id, an archived one left out, a form named by its
|
|
32
|
-
* display name or its id
|
|
33
|
-
*
|
|
32
|
+
* display name or its id, and whether it files leads shared as its
|
|
33
|
+
* `routesLeads` fact. What the Forms plugin itself answers is held in its own
|
|
34
|
+
* spec.
|
|
34
35
|
*/
|
|
35
36
|
export declare function standInFormList(): void;
|