@aglyn/plugins-workflows 1.0.0-beta.221 → 1.0.0-beta.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -14
- package/package.json +12 -12
- package/src/lib/components/automation-step-fields.component.d.ts +1 -15
- package/src/lib/components/automation-step-fields.component.js +93 -10
- package/src/lib/components/automation-step-fields.component.js.map +1 -1
- package/src/lib/components/automation-trigger-conditions.component.d.ts +2 -1
- package/src/lib/components/automation-trigger-conditions.component.js.map +1 -1
- package/src/lib/components/host-actions-card.component.js +85 -77
- package/src/lib/components/host-actions-card.component.js.map +1 -1
- package/src/lib/components/host-webhooks-card.component.js +2 -1
- package/src/lib/components/host-webhooks-card.component.js.map +1 -1
- package/src/lib/components/host-workflows-card.component.js.map +1 -1
- package/src/lib/components/org-automation-editor.component.d.ts +2 -1
- package/src/lib/components/org-automation-editor.component.js.map +1 -1
- package/src/lib/components/org-automations-card.component.js +2 -1
- package/src/lib/components/org-automations-card.component.js.map +1 -1
- package/src/lib/components/run-quota-line.component.d.ts +17 -4
- package/src/lib/components/run-quota-line.component.js +16 -7
- package/src/lib/components/run-quota-line.component.js.map +1 -1
- package/src/lib/components/site-org-automations-panel.component.js +2 -1
- package/src/lib/components/site-org-automations-panel.component.js.map +1 -1
- package/src/lib/components/workflows-console-page.js +18 -7
- package/src/lib/components/workflows-console-page.js.map +1 -1
- package/src/lib/declarations.d.ts +28 -0
- package/src/lib/declarations.js +37 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.js +17 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/engine/run-event-actions.d.ts +5 -10
- package/src/lib/engine/run-event-actions.js +103 -320
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-automations.d.ts +1 -1
- package/src/lib/engine/run-event-automations.js.map +1 -1
- package/src/lib/engine/run-event-workflows.d.ts +2 -1
- package/src/lib/engine/run-event-workflows.js +27 -22
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/run-meter.d.ts +98 -0
- package/src/lib/engine/run-meter.js +121 -0
- package/src/lib/engine/run-meter.js.map +1 -0
- package/src/lib/engine/run-org-automations.d.ts +3 -2
- package/src/lib/engine/run-org-automations.js +14 -11
- package/src/lib/engine/run-org-automations.js.map +1 -1
- package/src/lib/engine/workflow-steps.d.ts +1 -1
- package/src/lib/engine/workflow-steps.js +3 -2
- package/src/lib/engine/workflow-steps.js.map +1 -1
- package/src/lib/model/host-actions.d.ts +231 -0
- package/src/lib/model/host-actions.js +268 -0
- package/src/lib/model/host-actions.js.map +1 -0
- package/src/lib/model/org-automations.d.ts +2 -1
- package/src/lib/model/org-automations.js +6 -3
- package/src/lib/model/org-automations.js.map +1 -1
- package/src/lib/model/step-outcomes.d.ts +2 -2
- package/src/lib/model/step-outcomes.js +1 -1
- package/src/lib/model/step-outcomes.js.map +1 -1
- package/src/lib/plugin.js +10 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/action-test-run-route.d.ts +3 -2
- package/src/lib/server/action-test-run-route.js +3 -2
- package/src/lib/server/action-test-run-route.js.map +1 -1
- package/src/lib/server/org-automations-routes.d.ts +42 -1
- package/src/lib/server/org-automations-routes.js +120 -72
- package/src/lib/server/org-automations-routes.js.map +1 -1
- package/src/lib/server-automation-drafts.d.ts +2 -2
- package/src/lib/server-automation-drafts.js +9 -6
- package/src/lib/server-automation-drafts.js.map +1 -1
- package/src/lib/server.js +19 -13
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-crm-steps.d.ts +45 -0
- package/src/lib/testing/stand-in-crm-steps.js +97 -0
- package/src/lib/testing/stand-in-crm-steps.js.map +1 -0
- package/src/lib/testing/stand-in-dataset-list.js +11 -3
- package/src/lib/testing/stand-in-dataset-list.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-steps.d.ts +43 -0
- package/src/lib/testing/stand-in-dataset-steps.js +65 -0
- package/src/lib/testing/stand-in-dataset-steps.js.map +1 -0
- package/src/lib/testing/stand-in-person-records.d.ts +35 -0
- package/src/lib/testing/stand-in-person-records.js +54 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-recipes.d.ts +35 -0
- package/src/lib/testing/stand-in-recipes.js +201 -0
- package/src/lib/testing/stand-in-recipes.js.map +1 -0
- package/src/lib/transfer/org-automations-package.d.ts +54 -0
- package/src/lib/transfer/org-automations-package.js +167 -0
- package/src/lib/transfer/org-automations-package.js.map +1 -0
- package/src/lib/transfer/org-automations-package.server.d.ts +38 -0
- package/src/lib/transfer/org-automations-package.server.js +296 -0
- package/src/lib/transfer/org-automations-package.server.js.map +1 -0
- package/src/lib/usage-axes.js +5 -1
- package/src/lib/usage-axes.js.map +1 -1
- package/src/lib/engine/crm-action-steps.d.ts +0 -142
- package/src/lib/engine/crm-action-steps.js +0 -452
- package/src/lib/engine/crm-action-steps.js.map +0 -1
|
@@ -0,0 +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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"}
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type HostActionStep, type HostActionTriggerCondition, type TriggerCombinator } from '@aglyn/aglyn/app-utils/actions';
|
|
18
17
|
import { type ScopeToken } from '@aglyn/aglyn/app-utils/scope-tokens';
|
|
18
|
+
import { type TriggerCombinator } from '@aglyn/aglyn/app-utils/site-interactions';
|
|
19
|
+
import { type HostActionStep, type HostActionTriggerCondition } from './host-actions';
|
|
19
20
|
/**
|
|
20
21
|
* ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and
|
|
21
22
|
* places on the sites it chooses.
|
|
@@ -14,8 +14,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
18
|
-
import {
|
|
17
|
+
*/ import { normalizeVisibleTo, visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
18
|
+
import { ACTION_MAX_CONDITIONS, ACTION_MAX_STEPS, TRIGGER_COMBINATORS, TRIGGER_CONDITION_OPS } from "@aglyn/aglyn/app-utils/site-interactions";
|
|
19
|
+
import { HOST_ACTION_STEP_LABELS, validateHostAction } from "./host-actions.js";
|
|
19
20
|
/**
|
|
20
21
|
* ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and
|
|
21
22
|
* places on the sites it chooses.
|
|
@@ -210,13 +211,15 @@ const CONDITION_VALUE_MAX = 200;
|
|
|
210
211
|
createCrmTask: {
|
|
211
212
|
title: 200,
|
|
212
213
|
kind: 64,
|
|
214
|
+
priority: 16,
|
|
213
215
|
dueInDays: 'number',
|
|
214
216
|
assigneeUid: 128,
|
|
215
217
|
assigneeEmail: 320
|
|
216
218
|
},
|
|
217
219
|
logCrmActivity: {
|
|
218
220
|
kind: 64,
|
|
219
|
-
body: 2000
|
|
221
|
+
body: 2000,
|
|
222
|
+
direction: 16
|
|
220
223
|
},
|
|
221
224
|
customEvent: {
|
|
222
225
|
eventName: 64
|
|
@@ -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 ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n HOST_ACTION_STEP_LABELS,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n validateHostAction,\n} from '@aglyn/aglyn/app-utils/actions'\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'\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 dueInDays: 'number',\n assigneeUid: 128,\n assigneeEmail: 320,\n },\n logCrmActivity: { kind: 64, body: 2000 },\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":["ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","HOST_ACTION_STEP_LABELS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","validateHostAction","normalizeVisibleTo","visibleToHost","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","dueInDays","assigneeUid","assigneeEmail","logCrmActivity","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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,gBAAgB,EAChBC,uBAAuB,EAIvBC,mBAAmB,EACnBC,qBAAqB,EAErBC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,kBAAkB,EAElBC,aAAa,QACR,sCAAqC;AAG5C;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,+CAA+C,GAC/C,OAAO,MAAMC,6BAA6B,cAAa;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,4BAA4B;IACvCC,QAAQ;IACRC,OAAO;AACT,EAAU;AAEV;;;;;;;;;CASC,GACD,OAAO,MAAMC,sBAAsB,IAAG;AAEtC;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,GAAE;AAE/C;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,gBAAe;AAExD;;;;CAIC,GACD,OAAO,MAAMC,iCAAiC,uBAAsB;AAEpE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,gCAAgC;IAC3C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAA4C;AAK7C,MAAMC,kBAAuC,IAAIC,IAC/CF;AAGF,uDAAuD,GACvD,OAAO,SAASG,4BACdC,KAAc;IAEd,OAAO,OAAOA,UAAU,YAAYH,gBAAgBI,GAAG,CAACD;AAC1D;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAME,4BAA4B;IACvC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAiD;AAIlD,MAAMC,eAAoC,IAAIL,IAAII;AAElD,2DAA2D,GAC3D,OAAO,SAASE,wBACdC,IAAa;IAEb,OAAO,OAAOA,SAAS,YAAYF,aAAaF,GAAG,CAACI;AACtD;AAEA,8EAA8E,GAC9E,OAAO,MAAMC,4BAGRJ,0BAA0BK,GAAG,CAAC,CAACC,QAAW,CAAA;QAC7CA;QACAC,OAAO3B,uBAAuB,CAAC0B,MAAM;IACvC,CAAA,GAAG;AAEH;;;;;CAKC,GACD,OAAO,SAASE,yBAAyBC,IAExC;;IACC,IAAIP,wBAAwBO,wBAAAA,KAAMN,IAAI,GAAG,OAAO;IAChD,MAAMI,QAAQ3B,uBAAuB,CAAC6B,wBAAAA,KAAMN,IAAI,CAAuB;IACvE,IAAI,CAACI,OAAO,OAAO,CAAC,CAAC,EAAEG,eAAOD,wBAAAA,KAAMN,IAAI,mBAAI,IAAI,eAAe,CAAC;IAChE,OACE,CAAC,CAAC,EAAEI,MAAM,4DAA4D,CAAC,GACvE;AAEJ;AA+CA,wEAAwE,GACxE,OAAO,MAAMI,0BAA0B,GAAE;AAEzC,uCAAuC,GACvC,MAAMC,aAAa;AAEnB,iDAAiD,GACjD,MAAMC,sBAAsB;AAC5B,MAAMC,sBAAsB;AAE5B;;;;;;;;CAQC,GACD,MAAMC,cAGF;IACFC,WAAW;QAAEC,SAAS;QAAKC,MAAM;QAAMC,SAAS;QAAIC,SAAS;IAAI;IACjEC,cAAc;QAAEC,OAAO;QAAKJ,MAAM;IAAI;IACtCK,YAAY;QAAEC,QAAQ;QAAKC,UAAU;IAAI;IACzCC,gBAAgB;QAAEC,YAAY;QAAKC,cAAc;IAAI;IACrDC,eAAe;QAAEC,WAAW;QAAKC,aAAa;IAAI;IAClDC,eAAe;QAAEF,WAAW;QAAKC,aAAa;IAAI;IAClDE,iBAAiB;QAAEC,gBAAgB;IAAG;IACtCC,eAAe;QAAEC,KAAK;IAAI;IAC1BC,oBAAoB;QAClBC,UAAU;QACVC,YAAY;QACZC,YAAY;IACd;IACAC,eAAe;QACbnB,OAAO;QACPoB,MAAM;QACNC,WAAW;QACXC,aAAa;QACbC,eAAe;IACjB;IACAC,gBAAgB;QAAEJ,MAAM;QAAIxB,MAAM;IAAK;IACvC6B,aAAa;QAAEC,WAAW;IAAG;IAC7BC,MAAM;QAAEC,cAAc;IAAS;IAC/BC,cAAc;QAAEH,WAAW;QAAII,gBAAgB;IAAS;IACxDC,UAAU,CAAC;AACb;AAEA,MAAMC,WAAW,CAAChD,QAChBA,SAAS,OAAOA,UAAU,YAAY,CAACiD,MAAMC,OAAO,CAAClD,SAChDA,QACD;AAEN,oEAAoE,GACpE,SAASmD,cAAcC,GAAY;QAajBC,YAGMA;IAftB,MAAMA,MAAML,SAASI;IACrB,IAAI,CAACC,KAAK,OAAO;IACjB,MAAMC,KAAKD,GAAG,CAAC,KAAK;IACpB,IAAI,CAAC,AAAC7E,sBAA6C+E,QAAQ,CAACD,KAAK;YAI/CD;QAHhB,qEAAqE;QACrE,mCAAmC;QACnC,OAAO;YACLG,OAAOpD,QAAOiD,cAAAA,GAAG,CAAC,QAAQ,YAAZA,cAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGnD;YAClD+C,IAAIA;QACN;IACF;IACA,OAAO;QACLE,OAAOpD,QAAOiD,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGnD;QAClD+C,IAAIA;OACAA,OAAO,aACP;QAAEtD,OAAOI,QAAOiD,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAII,IAAI,GAAGC,KAAK,CAAC,GAAGlD;IAAqB,IACzE,CAAC;AAET;AAEA,SAASmD,eAAeP,GAAY;IAClC,OAAO,AAACH,CAAAA,MAAMC,OAAO,CAACE,OAAOA,MAAM,EAAE,AAAD,EACjCM,KAAK,CAAC,GAAGtF,wBAAwB,GACjC2B,GAAG,CAACoD,eACJS,MAAM,CAAC,CAACP,MAA2CA,QAAQ;AAChE;AAEA,SAASQ,eAAeT,GAAY;IAClC,OAAO,AAAC7E,oBAA2CgF,QAAQ,CAACH,OACvDA,MACD;AACN;AAEA,yEAAyE,GACzE,SAASU,SAASV,GAAY;QACbJ;IAAf,MAAMe,UAASf,YAAAA,SAASI,gBAATJ,YAAiB,CAAC;IACjC,MAAMnD,OAAOkE,MAAM,CAAC,OAAO;IAC3B,IAAI,CAACnE,wBAAwBC,OAAO,OAAO;QAAEA;IAAK;IAClD,MAAMM,OAAgC;QAAEN;IAAK;IAC7C,KAAK,MAAM,CAACmE,KAAK5B,KAAK,IAAI6B,OAAOC,OAAO,CAACzD,WAAW,CAACZ,KAAK,EAAG;QAC3D,MAAMG,QAAQ+D,MAAM,CAACC,IAAI;QACzB,IAAIhE,UAAUmE,aAAanE,UAAU,MAAM;QAC3C,IAAIoC,SAAS,WAAW;YACtB,IAAI,OAAOpC,UAAU,WAAWG,IAAI,CAAC6D,IAAI,GAAGhE;QAC9C,OAAO,IAAIoC,SAAS,UAAU;YAC5B,IAAI,OAAOpC,UAAU,YAAYoE,OAAOC,QAAQ,CAACrE,QAAQG,IAAI,CAAC6D,IAAI,GAAGhE;QACvE,OAAO,IAAI,OAAOA,UAAU,UAAU;YACpCG,IAAI,CAAC6D,IAAI,GAAGhE,MAAM0D,KAAK,CAAC,GAAGU,OAAOhC;QACpC;IACF;IACA,MAAMkC,OAAOtB,SAASe,MAAM,CAAC,OAAO;IACpC,IAAIO,MAAM;QACR,MAAMC,aAAaZ,eAAeW,IAAI,CAAC,aAAa;QACpD,IAAIC,WAAWC,MAAM,EAAE;gBAGPX;YAFd1D,IAAI,CAAC,OAAO,GAAG;gBACboE;gBACAE,UAAU,GAAEZ,kBAAAA,eAAeS,IAAI,CAAC,aAAa,aAAjCT,kBAAsC;YACpD;QACF;IACF;IACA,OAAO1D;AACT;AAKA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASuE,kBACdC,KAAc;QAED3B,WACOpC,YAIJoC,YACK4B,gBASCA;IAftB,MAAMhE,QAAOoC,YAAAA,SAAS2B,kBAAT3B,YAAmB,CAAC;IACjC,MAAM6B,OAAOzE,QAAOQ,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgB,IACjC6C,IAAI,GACJC,KAAK,CAAC,GAAGrD;IACZ,IAAI,CAACwE,MAAM,OAAO;QAAEC,IAAI;QAAOC,SAAS;IAAsB;IAC9D,MAAMH,WAAU5B,aAAAA,SAASpC,IAAI,CAAC,UAAU,aAAxBoC,aAA6B,CAAC;IAC9C,MAAMxD,QAAQY,QAAOwE,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB,IAAInB,IAAI;IACjD,IAAI,CAAClE,4BAA4BC,QAAQ;QACvC,OAAO;YACLsF,IAAI;YACJC,SACE,uEACA;QACJ;IACF;IACA,MAAMnB,SAASxD,QAAOwE,kBAAAA,OAAO,CAAC,SAAS,YAAjBA,kBAAqB,IACxCnB,IAAI,GACJC,KAAK,CAAC,GAAGpD;IACZ,MAAMiE,aAAaZ,eAAeiB,OAAO,CAAC,aAAa;IACvD,MAAMH,aAAaZ,eAAee,OAAO,CAAC,aAAa;IACvD,MAAMI,WAAW/B,MAAMC,OAAO,CAACtC,IAAI,CAAC,QAAQ,IAAIA,IAAI,CAAC,QAAQ,GAAG,EAAE;IAClE,IAAIoE,SAASR,MAAM,GAAGnG,kBAAkB;QACtC,OAAO;YACLyG,IAAI;YACJC,SAAS,CAAC,8BAA8B,EAAE1G,iBAAiB,MAAM,CAAC;QACpE;IACF;IACA,MAAM4G,QAAQD,SAASjF,GAAG,CAAC+D;IAC3B,KAAK,MAAM,CAACoB,OAAO/E,KAAK,IAAI8E,MAAMf,OAAO,GAAI;QAC3C,MAAMiB,UAAUjF,yBAAyBC;QACzC,IAAIgF,SAAS,OAAO;YAAEL,IAAI;YAAOC,SAAS,CAAC,KAAK,EAAEG,QAAQ,EAAE,EAAE,EAAEC,SAAS;QAAC;IAC5E;IACA,MAAMJ,UAAUtG,mBAAmB;QACjCoG;QACAD,SAAS;YACPpF;WACIoE,SAAS;YAAEA;QAAO,IAAI,CAAC,GACvBW,WAAWC,MAAM,GAAG;YAAED;QAAW,IAAI,CAAC,GACtCE,aAAa;YAAEA;QAAW,IAAI,CAAC;QAErCQ,OAAOA;IACT;IACA,IAAIF,SAAS,OAAO;QAAED,IAAI;QAAOC;IAAQ;IACzC,MAAMK,YAAY1G,mBAChBuE,MAAMC,OAAO,CAACtC,IAAI,CAAC,YAAY,IAC3B,AAACA,IAAI,CAAC,YAAY,CAAegD,MAAM,CACrC,CAACyB,QAA2B,OAAOA,UAAU,YAE/C,EAAE;IAER,IAAI,CAACD,WAAW;QACd,OAAO;YACLN,IAAI;YACJC,SAAS;QACX;IACF;IACA,OAAO;QACLD,IAAI;QACJ9E,OAAO;YACL6E;YACAD,SAAS;gBACPpF;eACIoE,SAAS;gBAAEA;YAAO,IAAI,CAAC;gBAC3BW,YAAYA,WAAWC,MAAM,GAAGD,aAAa;gBAC7CE,YAAYF,WAAWC,MAAM,GAAIC,qBAAAA,aAAc,QAAS;;YAE1DQ,OAAOA;YACPK,SAAS1E,IAAI,CAAC,UAAU,KAAK;YAC7BwE;QACF;IACF;AACF;AAEA,sDAAsD,GACtD,OAAO,SAASG,2BACdC,UAA0D;IAE1D,MAAMC,SAASD,8BAAAA,WAAYE,aAAa;IACxC,OAAOzC,MAAMC,OAAO,CAACuC,UACjBA,OAAO7B,MAAM,CAAC,CAAC+B,KAAqB,OAAOA,OAAO,YAClD,EAAE;AACR;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,wBACdJ,UAQa,EACbK,MAAc;IAEd,IAAI,CAACL,cAAcA,WAAWM,SAAS,EAAE;QACvC,OAAO;IACT;IACA,IAAIN,WAAWF,OAAO,KAAK,OAAO;QAChC,OAAO;IACT;IACA,IAAI,CAAC3G,cAAc6G,WAAWJ,SAAS,EAA0BS,SAAS;QACxE,OAAO;IACT;IACA,IAAIN,2BAA2BC,YAAYjC,QAAQ,CAACsC,SAAS;QAC3D,OAAO;IACT;IACA,OAAO;AACT;AAEA,uDAAuD,GACvD,OAAO,SAASE,wBACdP,UAAyD,EACzDK,MAAc;IAEd,OAAOD,wBAAwBJ,YAAYK,YAAY;AACzD;AAEA;;;;CAIC,GACD,OAAO,SAASG,mBACdN,aAAgC,EAChCN,SAA4B;IAE5B,OAAOM,cAAc9B,MAAM,CAAC,CAACiC,SAAWlH,cAAcyG,WAAWS;AACnE"}
|
|
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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"}
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { HostActionStepType } from '
|
|
17
|
+
import type { HostActionStepType } from './host-actions';
|
|
18
18
|
/**
|
|
19
19
|
* Past-tense phrases for a run summary (AGL-2171).
|
|
20
20
|
*
|
|
21
|
-
* `HOST_ACTION_STEP_LABELS` (
|
|
21
|
+
* `HOST_ACTION_STEP_LABELS` (`host-actions.ts`) names what a step WILL do, in the
|
|
22
22
|
* imperative, for a `Do` select — "Send an email". A run history is the
|
|
23
23
|
* opposite tense and a different audience: it says what already happened,
|
|
24
24
|
* on one line, several steps at a time. `/product/workflows` advertises
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/ /**
|
|
17
17
|
* Past-tense phrases for a run summary (AGL-2171).
|
|
18
18
|
*
|
|
19
|
-
* `HOST_ACTION_STEP_LABELS` (
|
|
19
|
+
* `HOST_ACTION_STEP_LABELS` (`host-actions.ts`) names what a step WILL do, in the
|
|
20
20
|
* imperative, for a `Do` select — "Send an email". A run history is the
|
|
21
21
|
* opposite tense and a different audience: it says what already happened,
|
|
22
22
|
* on one line, several steps at a time. `/product/workflows` advertises
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/step-outcomes.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 type { HostActionStepType } from '
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/step-outcomes.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 type { HostActionStepType } from './host-actions'\n\n/**\n * Past-tense phrases for a run summary (AGL-2171).\n *\n * `HOST_ACTION_STEP_LABELS` (`host-actions.ts`) names what a step WILL do, in the\n * imperative, for a `Do` select — \"Send an email\". A run history is the\n * opposite tense and a different audience: it says what already happened,\n * on one line, several steps at a time. `/product/workflows` advertises\n * exactly that shape — `Sent email · saved to Leads · webhook 200`.\n *\n * Two maps rather than one derived from the other, because \"Send a webhook\n * (Business)\" cannot be mechanically turned into \"webhook\" — the plan\n * suffix is part of the picker's label and has no business in a log line.\n * Only the steps a SERVER run can perform appear here; the client-side\n * steps run in the visitor's browser and never reach the activity write.\n */\nexport const HOST_ACTION_STEP_OUTCOMES: Partial<\n Record<HostActionStepType, string>\n> = {\n runWorkflow: 'ran workflow',\n siteAlert: 'showed an alert',\n customEvent: 'fired an event',\n datasetAppend: 'saved to dataset',\n updateDataset: 'updated dataset',\n webhookPost: 'webhook',\n sendEmail: 'sent email',\n notifyAdmins: 'notified admins',\n enrollList: 'enrolled in list',\n assignCampaign: 'assigned to campaign',\n wait: 'waiting',\n waitForEvent: 'waiting for',\n exitFlow: 'ended the flow',\n setContactStage: 'set stage',\n addContactTag: 'tagged',\n assignContactOwner: 'assigned owner',\n createCrmTask: 'created task',\n logCrmActivity: 'logged activity',\n}\n\n/**\n * One step's line in a run summary, with the detail that makes it useful.\n *\n * `saved to Leads` beats `saved to dataset`, and `webhook 200` beats\n * `webhook` — the status code is the entire reason anyone opens a run\n * history after a webhook, and it was being discarded on the line it\n * arrived.\n */\nexport function describeStepOutcome(\n type: HostActionStepType,\n detail?: string,\n): string {\n const base = HOST_ACTION_STEP_OUTCOMES[type] ?? String(type)\n const trimmed = detail?.trim()\n if (!trimmed) return base\n if (type === 'datasetAppend' || type === 'updateDataset') {\n // `saved to dataset` + `Leads` reads as `saved to Leads`, not\n // `saved to dataset Leads`.\n return `${base.replace(/ dataset$/, '')} ${trimmed}`\n }\n return `${base} ${trimmed}`\n}\n"],"names":["HOST_ACTION_STEP_OUTCOMES","runWorkflow","siteAlert","customEvent","datasetAppend","updateDataset","webhookPost","sendEmail","notifyAdmins","enrollList","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","describeStepOutcome","type","detail","base","String","trimmed","trim","replace"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,4BAET;IACFC,aAAa;IACbC,WAAW;IACXC,aAAa;IACbC,eAAe;IACfC,eAAe;IACfC,aAAa;IACbC,WAAW;IACXC,cAAc;IACdC,YAAY;IACZC,gBAAgB;IAChBC,MAAM;IACNC,cAAc;IACdC,UAAU;IACVC,iBAAiB;IACjBC,eAAe;IACfC,oBAAoB;IACpBC,eAAe;IACfC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,SAASC,oBACdC,IAAwB,EACxBC,MAAe;QAEFrB;IAAb,MAAMsB,QAAOtB,kCAAAA,yBAAyB,CAACoB,KAAK,YAA/BpB,kCAAmCuB,OAAOH;IACvD,MAAMI,UAAUH,0BAAAA,OAAQI,IAAI;IAC5B,IAAI,CAACD,SAAS,OAAOF;IACrB,IAAIF,SAAS,mBAAmBA,SAAS,iBAAiB;QACxD,8DAA8D;QAC9D,4BAA4B;QAC5B,OAAO,GAAGE,KAAKI,OAAO,CAAC,aAAa,IAAI,CAAC,EAAEF,SAAS;IACtD;IACA,OAAO,GAAGF,KAAK,CAAC,EAAEE,SAAS;AAC7B"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -20,6 +20,7 @@ import { HostActivityCard } from "./components/host-activity-card.component.js";
|
|
|
20
20
|
import { StaffOrgAutomationsCard, StaffSiteAutomationsCard } from "./components/staff-automations-card.component.js";
|
|
21
21
|
import { WORKFLOWS_CONSOLE_SECTIONS, WORKFLOWS_ORG_CONSOLE_SECTIONS } from "./components/workflows-console-sections.js";
|
|
22
22
|
import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
23
|
+
import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
|
|
23
24
|
import { AUTOMATION_EDITOR_ZONE, AUTOMATION_RUN_ZONE, HOST_AUTOMATIONS_ZONE, WORKFLOW_USAGE_ZONE } from "./components/workflow-zones.js";
|
|
24
25
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
25
26
|
import { registerWorkflowsRecordLists } from "./model/workflows-record-lists.js";
|
|
@@ -40,6 +41,15 @@ import { WORKFLOWS_SEARCH_SOURCES } from "./model/workflows-search-sources.js";
|
|
|
40
41
|
* Remote Config release flag — and a section's name is not a reason to orphan
|
|
41
42
|
* them. Only what a reader sees carries the new name.
|
|
42
43
|
*/ export function registerWorkflowsConsole() {
|
|
44
|
+
// How the Import & export hub names org automations in a workspace package (AGL-3535).
|
|
45
|
+
registerPluginTransferResourceUi('workflows.org-automations', {
|
|
46
|
+
label: 'Org automations',
|
|
47
|
+
icon: {
|
|
48
|
+
path: mdiSitemap.path
|
|
49
|
+
}
|
|
50
|
+
}, {
|
|
51
|
+
pluginId: BUNDLE_ID
|
|
52
|
+
});
|
|
43
53
|
registerWorkflowsRecordRoutes();
|
|
44
54
|
registerWorkflowsRecordLists();
|
|
45
55
|
registerPluginZone({
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiSitemap } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { HostActivityCard } from './components/host-activity-card.component'\nimport {\n StaffOrgAutomationsCard,\n StaffSiteAutomationsCard,\n} from './components/staff-automations-card.component'\nimport {\n WORKFLOWS_CONSOLE_SECTIONS,\n WORKFLOWS_ORG_CONSOLE_SECTIONS,\n} from './components/workflows-console-sections'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n AUTOMATION_EDITOR_ZONE,\n AUTOMATION_RUN_ZONE,\n HOST_AUTOMATIONS_ZONE,\n WORKFLOW_USAGE_ZONE,\n} from './components/workflow-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsRecordLists } from './model/workflows-record-lists'\nimport { registerWorkflowsRecordRoutes } from './model/workflows-record-routes'\nimport { WORKFLOWS_SEARCH_SOURCES } from './model/workflows-search-sources'\n\n/** Code-split: the Automation console page only loads when opened. */\nconst WorkflowsConsolePage = lazy(\n () => import('./components/workflows-console-page'),\n)\n\n/**\n * Workflows feature plugin (AGL-395). Console-only — workflows, actions,\n * and webhooks run server-side (event pipelines, `/api/hooks`), not through\n * a canvas element, so there is no UI bundle. The console half declares the\n * Automation nav + page through the ConsoleExtension registry, gated by the\n * `workflows` entitlement (the Actions/Webhooks tabs run their own per-plan\n * checks off the passed `org`). Where a workflow is used is asked of the\n * platform's scan and drawn in a zone this plugin hosts.\n *\n * `pluginId`, `featureFlag` and `navTabId` all still read `workflows`: they\n * are stored — an org's enabled-plugin ids, the plan's entitlement flags, the\n * Remote Config release flag — and a section's name is not a reason to orphan\n * them. Only what a reader sees carries the new name.\n */\nexport function registerWorkflowsConsole(): void {\n registerWorkflowsRecordRoutes()\n registerWorkflowsRecordLists()\n registerPluginZone(\n {\n zone: WORKFLOW_USAGE_ZONE,\n label: 'Where a workflow is used',\n surface: 'console',\n description:\n 'On the Automation page, after a reader asks where a workflow is used. A widget here lays out the scan the page already ran; it is handed the site, the answer and a way to close, and it writes nothing.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n // The zones the Automation page draws in its own layouts (AGL-2919): one\n // item in the Actions row, inside an open editor, on a failed run's row.\n registerPluginZone(\n {\n zone: HOST_AUTOMATIONS_ZONE,\n label: 'Automation page actions',\n surface: 'console',\n layout: 'bare',\n description:\n 'On the Automation page, beside Add action and Recipes: another way to start an automation. `openAction` opens a listed action in the Actions editor; a widget here writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_EDITOR_ZONE,\n label: 'An automation’s editor',\n surface: 'console',\n layout: 'bare',\n description:\n 'Inside the editor of one saved automation, an action or a workflow. A widget here reads the automation as it is stored and changes nothing in the editor.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_RUN_ZONE,\n label: 'A failed automation run',\n surface: 'console',\n layout: 'bare',\n description:\n 'On each failed run in an automation’s run history. A widget here reads the run as it was recorded and changes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Host activity feed (AGL-419): rendered by the shell's\n // 'hostActivity' widget slot (dashboard + editor view page).\n widgets: [\n {\n slot: 'hostActivity',\n widgetId: 'workflows-host-activity',\n title: 'Recent Activity',\n Component: HostActivityCard,\n },\n // The staff site and organization pages' automations (AGL-3379), read\n // only: staff see what runs on a site without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.staffSite,\n widgetId: 'workflows-staff-site-automations',\n title: 'Automations',\n Component: StaffSiteAutomationsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'workflows-staff-org-automations',\n title: 'Organization automations',\n Component: StaffOrgAutomationsCard,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Automation',\n featureFlag: 'workflows',\n searchSources: WORKFLOWS_SEARCH_SOURCES,\n navItems: [\n {\n label: 'Automation',\n // Workflows, Actions and Webhooks are the tabs INSIDE this section, so\n // the section is named for what they have in common. `/workflows` —\n // the address before the section had its own name — is redirected here\n // by this plugin's `consoleRedirects`.\n href: '/automation',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: WORKFLOWS_CONSOLE_SECTIONS,\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Automation hub (AGL-3302), at `/[orgSlug]/automation`:\n * the org automations placed across the organization's sites, and every\n * site's own workflows, actions and webhooks side by side. The same page\n * component, mounted with no site; the site tab's release flag gates both\n * levels, so one flag rolls the surface out at once.\n */\n orgNavItems: [\n {\n label: 'Automation',\n href: '/automation',\n sections: WORKFLOWS_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n })\n}\n\n// Activity feed consumed directly by the app (dashboard + screen view).\nexport { default as HostActivityCard } from './components/host-activity-card.component'\nexport type { HostActivityCardProps } from './components/host-activity-card.component'\n"],"names":["Aglyn","mdiSitemap","lazy","HostActivityCard","StaffOrgAutomationsCard","StaffSiteAutomationsCard","WORKFLOWS_CONSOLE_SECTIONS","WORKFLOWS_ORG_CONSOLE_SECTIONS","registerPluginZone","AUTOMATION_EDITOR_ZONE","AUTOMATION_RUN_ZONE","HOST_AUTOMATIONS_ZONE","WORKFLOW_USAGE_ZONE","BUNDLE_ID","registerWorkflowsRecordLists","registerWorkflowsRecordRoutes","WORKFLOWS_SEARCH_SOURCES","WorkflowsConsolePage","registerWorkflowsConsole","zone","label","surface","description","pluginId","layout","registerConsoleExtension","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","staffSite","adminOrgDetail","displayName","featureFlag","searchSources","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,UAAU,QAAQ,yBAAwB;AACnD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,gBAAgB,QAAQ,+CAA2C;AAC5E,SACEC,uBAAuB,EACvBC,wBAAwB,QACnB,mDAA+C;AACtD,SACEC,0BAA0B,EAC1BC,8BAA8B,QACzB,6CAAyC;AAChD,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,sBAAsB,EACtBC,mBAAmB,EACnBC,qBAAqB,EACrBC,mBAAmB,QACd,iCAA6B;AACpC,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,4BAA4B,QAAQ,oCAAgC;AAC7E,SAASC,6BAA6B,QAAQ,qCAAiC;AAC/E,SAASC,wBAAwB,QAAQ,sCAAkC;AAE3E,oEAAoE,GACpE,MAAMC,uBAAuBf,KAC3B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASgB;IACdH;IACAD;IACAN,mBACE;QACEW,MAAMP;QACNQ,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUV;IAAU;IAExB,yEAAyE;IACzE,yEAAyE;IACzEL,mBACE;QACEW,MAAMR;QACNS,OAAO;QACPC,SAAS;QACTG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUV;IAAU;IAExBL,mBACE;QACEW,MAAMV;QACNW,OAAO;QACPC,SAAS;QACTG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUV;IAAU;IAExBL,mBACE;QACEW,MAAMT;QACNU,OAAO;QACPC,SAAS;QACTG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUV;IAAU;IAExBb,MAAMyB,wBAAwB,CAAC;QAC7B,wDAAwD;QACxD,6DAA6D;QAC7DC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAW3B;YACb;YACA,sEAAsE;YACtE,0DAA0D;YAC1D;gBACEwB,MAAM3B,MAAM+B,oBAAoB,CAACC,SAAS;gBAC1CJ,UAAU;gBACVC,OAAO;gBACPC,WAAWzB;YACb;YACA;gBACEsB,MAAM3B,MAAM+B,oBAAoB,CAACE,cAAc;gBAC/CL,UAAU;gBACVC,OAAO;gBACPC,WAAW1B;YACb;SACD;QACDmB,UAAUV;QACVqB,aAAa;QACbC,aAAa;QACbC,eAAepB;QACfqB,UAAU;YACR;gBACEjB,OAAO;gBACP,uEAAuE;gBACvE,oEAAoE;gBACpE,uEAAuE;gBACvE,uCAAuC;gBACvCkB,MAAM;gBACN,8DAA8D;gBAC9D,6DAA6D;gBAC7DC,UAAUjC;gBACV,qEAAqE;gBACrE,8DAA8D;gBAC9DkC,UAAU;gBACVC,MAAM;oBAAEC,MAAMzC,WAAWyC,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNd,OAAO;oBACPY,MAAM;wBAAEC,MAAMzC,WAAWyC,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAd,WAAWb;YACb;SACD;QACD;;;;;;KAMC,GACD4B,aAAa;YACX;gBACEzB,OAAO;gBACPkB,MAAM;gBACNC,UAAUhC;gBACViC,UAAU;gBACVC,MAAM;oBAAEC,MAAMzC,WAAWyC,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNd,OAAO;oBACPY,MAAM;wBAAEC,MAAMzC,WAAWyC,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAd,WAAWb;YACb;SACD;IACH;AACF;AAEA,wEAAwE;AACxE,SAAS6B,WAAW3C,gBAAgB,QAAQ,+CAA2C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiSitemap } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { HostActivityCard } from './components/host-activity-card.component'\nimport {\n StaffOrgAutomationsCard,\n StaffSiteAutomationsCard,\n} from './components/staff-automations-card.component'\nimport {\n WORKFLOWS_CONSOLE_SECTIONS,\n WORKFLOWS_ORG_CONSOLE_SECTIONS,\n} from './components/workflows-console-sections'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n AUTOMATION_EDITOR_ZONE,\n AUTOMATION_RUN_ZONE,\n HOST_AUTOMATIONS_ZONE,\n WORKFLOW_USAGE_ZONE,\n} from './components/workflow-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsRecordLists } from './model/workflows-record-lists'\nimport { registerWorkflowsRecordRoutes } from './model/workflows-record-routes'\nimport { WORKFLOWS_SEARCH_SOURCES } from './model/workflows-search-sources'\n\n/** Code-split: the Automation console page only loads when opened. */\nconst WorkflowsConsolePage = lazy(\n () => import('./components/workflows-console-page'),\n)\n\n/**\n * Workflows feature plugin (AGL-395). Console-only — workflows, actions,\n * and webhooks run server-side (event pipelines, `/api/hooks`), not through\n * a canvas element, so there is no UI bundle. The console half declares the\n * Automation nav + page through the ConsoleExtension registry, gated by the\n * `workflows` entitlement (the Actions/Webhooks tabs run their own per-plan\n * checks off the passed `org`). Where a workflow is used is asked of the\n * platform's scan and drawn in a zone this plugin hosts.\n *\n * `pluginId`, `featureFlag` and `navTabId` all still read `workflows`: they\n * are stored — an org's enabled-plugin ids, the plan's entitlement flags, the\n * Remote Config release flag — and a section's name is not a reason to orphan\n * them. Only what a reader sees carries the new name.\n */\nexport function registerWorkflowsConsole(): void {\n // How the Import & export hub names org automations in a workspace package (AGL-3535).\n registerPluginTransferResourceUi(\n 'workflows.org-automations',\n { label: 'Org automations', icon: { path: mdiSitemap.path } },\n { pluginId: BUNDLE_ID },\n )\n registerWorkflowsRecordRoutes()\n registerWorkflowsRecordLists()\n registerPluginZone(\n {\n zone: WORKFLOW_USAGE_ZONE,\n label: 'Where a workflow is used',\n surface: 'console',\n description:\n 'On the Automation page, after a reader asks where a workflow is used. A widget here lays out the scan the page already ran; it is handed the site, the answer and a way to close, and it writes nothing.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n // The zones the Automation page draws in its own layouts (AGL-2919): one\n // item in the Actions row, inside an open editor, on a failed run's row.\n registerPluginZone(\n {\n zone: HOST_AUTOMATIONS_ZONE,\n label: 'Automation page actions',\n surface: 'console',\n layout: 'bare',\n description:\n 'On the Automation page, beside Add action and Recipes: another way to start an automation. `openAction` opens a listed action in the Actions editor; a widget here writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_EDITOR_ZONE,\n label: 'An automation’s editor',\n surface: 'console',\n layout: 'bare',\n description:\n 'Inside the editor of one saved automation, an action or a workflow. A widget here reads the automation as it is stored and changes nothing in the editor.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: AUTOMATION_RUN_ZONE,\n label: 'A failed automation run',\n surface: 'console',\n layout: 'bare',\n description:\n 'On each failed run in an automation’s run history. A widget here reads the run as it was recorded and changes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Host activity feed (AGL-419): rendered by the shell's\n // 'hostActivity' widget slot (dashboard + editor view page).\n widgets: [\n {\n slot: 'hostActivity',\n widgetId: 'workflows-host-activity',\n title: 'Recent Activity',\n Component: HostActivityCard,\n },\n // The staff site and organization pages' automations (AGL-3379), read\n // only: staff see what runs on a site without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.staffSite,\n widgetId: 'workflows-staff-site-automations',\n title: 'Automations',\n Component: StaffSiteAutomationsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'workflows-staff-org-automations',\n title: 'Organization automations',\n Component: StaffOrgAutomationsCard,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Automation',\n featureFlag: 'workflows',\n searchSources: WORKFLOWS_SEARCH_SOURCES,\n navItems: [\n {\n label: 'Automation',\n // Workflows, Actions and Webhooks are the tabs INSIDE this section, so\n // the section is named for what they have in common. `/workflows` —\n // the address before the section had its own name — is redirected here\n // by this plugin's `consoleRedirects`.\n href: '/automation',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: WORKFLOWS_CONSOLE_SECTIONS,\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Automation hub (AGL-3302), at `/[orgSlug]/automation`:\n * the org automations placed across the organization's sites, and every\n * site's own workflows, actions and webhooks side by side. The same page\n * component, mounted with no site; the site tab's release flag gates both\n * levels, so one flag rolls the surface out at once.\n */\n orgNavItems: [\n {\n label: 'Automation',\n href: '/automation',\n sections: WORKFLOWS_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-workflows',\n icon: { path: mdiSitemap.path },\n header: {\n title: 'Automation',\n icon: { path: mdiSitemap.path },\n docsTopic: 'workflows',\n },\n Component: WorkflowsConsolePage,\n },\n ],\n })\n}\n\n// Activity feed consumed directly by the app (dashboard + screen view).\nexport { default as HostActivityCard } from './components/host-activity-card.component'\nexport type { HostActivityCardProps } from './components/host-activity-card.component'\n"],"names":["Aglyn","mdiSitemap","lazy","HostActivityCard","StaffOrgAutomationsCard","StaffSiteAutomationsCard","WORKFLOWS_CONSOLE_SECTIONS","WORKFLOWS_ORG_CONSOLE_SECTIONS","registerPluginZone","registerPluginTransferResourceUi","AUTOMATION_EDITOR_ZONE","AUTOMATION_RUN_ZONE","HOST_AUTOMATIONS_ZONE","WORKFLOW_USAGE_ZONE","BUNDLE_ID","registerWorkflowsRecordLists","registerWorkflowsRecordRoutes","WORKFLOWS_SEARCH_SOURCES","WorkflowsConsolePage","registerWorkflowsConsole","label","icon","path","pluginId","zone","surface","description","layout","registerConsoleExtension","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","staffSite","adminOrgDetail","displayName","featureFlag","searchSources","navItems","href","sections","navTabId","header","docsTopic","orgNavItems","default"],"mappings":"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"}
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
* The stored action, through `runSingleActionOutcome` — the single-action path
|
|
43
43
|
* a page's dispatch takes — so the switch, the conditions, the plan gate and
|
|
44
44
|
* the month's run allowance are the ones a real run meets, the run is metered
|
|
45
|
-
* on the site's `actionRuns`, and it lands in the action's
|
|
46
|
-
* any other. It is marked rather than simulated: see
|
|
45
|
+
* on the site's and the workspace's `actionRuns`, and it lands in the action's
|
|
46
|
+
* Runs history like any other. It is marked rather than simulated: see
|
|
47
|
+
* `actionTestRunPayload`.
|
|
47
48
|
* When a gate stops it, the refusal names the gate, because a test that ran
|
|
48
49
|
* nothing must not read as one that ran.
|
|
49
50
|
*
|
|
@@ -42,8 +42,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
42
42
|
* The stored action, through `runSingleActionOutcome` — the single-action path
|
|
43
43
|
* a page's dispatch takes — so the switch, the conditions, the plan gate and
|
|
44
44
|
* the month's run allowance are the ones a real run meets, the run is metered
|
|
45
|
-
* on the site's `actionRuns`, and it lands in the action's
|
|
46
|
-
* any other. It is marked rather than simulated: see
|
|
45
|
+
* on the site's and the workspace's `actionRuns`, and it lands in the action's
|
|
46
|
+
* Runs history like any other. It is marked rather than simulated: see
|
|
47
|
+
* `actionTestRunPayload`.
|
|
47
48
|
* When a gate stops it, the refusal names the gate, because a test that ran
|
|
48
49
|
* nothing must not read as one that ran.
|
|
49
50
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/action-test-run-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).\n *\n * The Actions card's Test button runs an action with an in-page trigger as\n * though the trigger had fired on the site, and says what came back. A\n * published page reaches the same engine path through `events/dispatch`,\n * which is a TENANT route — served on the site's own origin, to anonymous\n * visitors — and the console is another app on another origin whose\n * dispatcher never loads it. This is the console's door to that path, and\n * unlike the page's it knows who is asking.\n *\n * ## Who may\n *\n * A site admin or editor, read off the host's `memberRoles`, or staff. The\n * publish roles rather than the content roles, because a test run is a LIVE\n * run: its emails are sent, its webhooks posted and its rows written, as a\n * visitor's would be. An author edits and makes nothing live, and so does not\n * run automations either.\n *\n * The site's plugin switch is the dispatcher's: the request names its site as\n * a top-level `hostId`, so a site with Automation switched off has no such\n * door.\n *\n * ## What runs\n *\n * The stored action, through `runSingleActionOutcome` — the single-action path\n * a page's dispatch takes — so the switch, the conditions, the plan gate and\n * the month's run allowance are the ones a real run meets, the run is metered\n * on the site's `actionRuns`, and it lands in the action's Runs history like\n * any other. It is marked rather than simulated: see `actionTestRunPayload`.\n * When a gate stops it, the refusal names the gate, because a test that ran\n * nothing must not read as one that ran.\n *\n * Only an in-page trigger: a server event (a form submitted, an order placed)\n * carries fields no sample stands in for, and its runs come from the doors\n * that raise it.\n */\n\nimport {\n type HostAction,\n hostRoleCanPublish,\n isSiteEventType,\n type PluginApiHandler,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel, as for the org automation doors beside this\n * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { consumeRateLimit } from '@aglyn/tenant-data-admin/server/rate-limit-store'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport type {\n SingleActionOutcome,\n SingleActionSkip,\n} from '../engine/run-event-actions'\nimport { actionTestRunPayload } from '../model/action-test-run'\n\nexport { ACTION_TEST_RUN_API_ROUTE } from '../model/action-test-run'\n\n/**\n * Test runs one person may start on one site in a minute.\n *\n * Every test is a real run with real sends, so this is the feature's own\n * bucket beneath the console dispatcher's general one: a person iterating on\n * an action tests every few seconds at most, and past this is a script.\n */\nexport const ACTION_TEST_RUNS_PER_MINUTE = 10\n\nconst ACTION_TEST_RUN_WINDOW_MS = 60_000\n\n/** What the door reaches outside this module; specs hand in their own. */\nexport interface ActionTestRunDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; staff?: unknown }>\n consumeRateLimit(\n key: string,\n options: { limit: number; windowMs: number },\n ): Promise<{ allowed: boolean; resetMs: number }>\n runAction(\n hostId: string,\n actionId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<SingleActionOutcome>\n now(): number\n}\n\nfunction defaultDeps(): ActionTestRunDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, staff: decoded['staff'] }\n },\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n // Imported by the first test run rather than with the console surface,\n // as the host-event listener imports it.\n runAction: async (hostId, actionId, event, payload) => {\n const { runSingleActionOutcome } = await import('../engine/run-event-actions')\n return await runSingleActionOutcome(hostId, actionId, event, payload)\n },\n now: () => Date.now(),\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\n/** The status and words for each gate that can stop a test run. */\nfunction refusalFor(\n skipped: SingleActionSkip,\n outcome: SingleActionOutcome,\n payload: HostEventPayload,\n): { status: number; error: string } {\n switch (skipped) {\n case 'missing':\n return { status: 404, error: 'That action no longer exists' }\n case 'disabled':\n return { status: 409, error: 'Switch the action on to test it' }\n case 'event':\n return {\n status: 409,\n error: 'The action changed while it was being tested — try again',\n }\n case 'conditions':\n return {\n status: 422,\n error:\n `Nothing ran: a test run on ${String(payload['path'])} does not ` +\n 'meet this action’s conditions',\n }\n case 'plan':\n return {\n status: 403,\n error:\n `Actions need the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade',\n }\n case 'allowance':\n return {\n status: 402,\n error: `This site has used its ${outcome.limit} action runs for the month`,\n }\n default:\n return { status: 500, error: 'The test run could not be completed. Try again.' }\n }\n}\n\n/**\n * `automations/actions/test-run`: run one of a site's actions now.\n *\n * Body `{ hostId, actionId, payload? }`. Answers `{ ok: true, alerts }` when\n * the action's steps ran — the site alerts they produced, for the card to show\n * — and otherwise an `error` saying what stopped it.\n */\nexport function createActionTestRunHandler(\n overrides: Partial<ActionTestRunDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const actionId = String(req.body?.actionId ?? '')\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(actionId)) {\n return res.status(400).json({ error: 'Invalid actionId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n let caller: { uid: string; staff?: unknown }\n try {\n caller = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[workflows] test run could not verify the caller', error)\n return res\n .status(500)\n .json({ error: 'The sign-in could not be checked. Try again.' })\n }\n\n try {\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[caller.uid]\n if (caller.staff !== true && !hostRoleCanPublish(role)) {\n return res\n .status(403)\n .json({ error: 'Only a site admin or editor can test an action' })\n }\n\n const rate = await deps.consumeRateLimit(\n `workflows-test-run:${hostId}:${caller.uid}`,\n { limit: ACTION_TEST_RUNS_PER_MINUTE, windowMs: ACTION_TEST_RUN_WINDOW_MS },\n )\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))),\n )\n return res\n .status(429)\n .json({ error: 'Too many test runs — wait a minute and try again' })\n }\n\n const stored = await hostRef.collection('actions').doc(actionId).get()\n if (!stored.exists || stored.get('deletedAt')) {\n return res.status(404).json({ error: 'That action no longer exists' })\n }\n const event = String(\n (stored.data() as HostAction | undefined)?.trigger?.event ?? '',\n )\n if (!isSiteEventType(event)) {\n return res.status(400).json({\n error: 'Only an action with an in-page trigger can be tested here',\n })\n }\n\n const payload = actionTestRunPayload(req.body?.payload)\n const outcome = await deps.runAction(hostId, actionId, event, payload)\n if (outcome.ran) {\n return res.status(200).json({ ok: true, alerts: outcome.alerts })\n }\n const refusal = refusalFor(outcome.skipped ?? 'failed', outcome, payload)\n return res.status(refusal.status).json({ error: refusal.error })\n } catch (error) {\n console.error('[workflows] test run failed', hostId, actionId, error)\n return res\n .status(500)\n .json({ error: 'The test run could not be completed. Try again.' })\n }\n }\n}\n\nexport const actionTestRunHandler = createActionTestRunHandler()\n"],"names":["hostRoleCanPublish","isSiteEventType","planLabelGrantingFeature","isDocumentId","firebaseAdmin","isRefusedIdToken","consumeRateLimit","actionTestRunPayload","ACTION_TEST_RUN_API_ROUTE","ACTION_TEST_RUNS_PER_MINUTE","ACTION_TEST_RUN_WINDOW_MS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","staff","key","options","runAction","hostId","actionId","event","payload","runSingleActionOutcome","now","Date","bearerToken","headers","authorization","String","startsWith","slice","length","refusalFor","skipped","outcome","status","error","limit","createActionTestRunHandler","overrides","req","res","deps","method","json","body","caller","console","host","stored","hostRef","collection","doc","get","exists","role","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/action-test-run-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).\n *\n * The Actions card's Test button runs an action with an in-page trigger as\n * though the trigger had fired on the site, and says what came back. A\n * published page reaches the same engine path through `events/dispatch`,\n * which is a TENANT route — served on the site's own origin, to anonymous\n * visitors — and the console is another app on another origin whose\n * dispatcher never loads it. This is the console's door to that path, and\n * unlike the page's it knows who is asking.\n *\n * ## Who may\n *\n * A site admin or editor, read off the host's `memberRoles`, or staff. The\n * publish roles rather than the content roles, because a test run is a LIVE\n * run: its emails are sent, its webhooks posted and its rows written, as a\n * visitor's would be. An author edits and makes nothing live, and so does not\n * run automations either.\n *\n * The site's plugin switch is the dispatcher's: the request names its site as\n * a top-level `hostId`, so a site with Automation switched off has no such\n * door.\n *\n * ## What runs\n *\n * The stored action, through `runSingleActionOutcome` — the single-action path\n * a page's dispatch takes — so the switch, the conditions, the plan gate and\n * the month's run allowance are the ones a real run meets, the run is metered\n * on the site's and the workspace's `actionRuns`, and it lands in the action's\n * Runs history like any other. It is marked rather than simulated: see\n * `actionTestRunPayload`.\n * When a gate stops it, the refusal names the gate, because a test that ran\n * nothing must not read as one that ran.\n *\n * Only an in-page trigger: a server event (a form submitted, an order placed)\n * carries fields no sample stands in for, and its runs come from the doors\n * that raise it.\n */\n\nimport {\n hostRoleCanPublish,\n isSiteEventType,\n type PluginApiHandler,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel, as for the org automation doors beside this\n * one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { consumeRateLimit } from '@aglyn/tenant-data-admin/server/rate-limit-store'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport type {\n SingleActionOutcome,\n SingleActionSkip,\n} from '../engine/run-event-actions'\nimport { actionTestRunPayload } from '../model/action-test-run'\nimport type { HostAction } from '../model/host-actions'\n\nexport { ACTION_TEST_RUN_API_ROUTE } from '../model/action-test-run'\n\n/**\n * Test runs one person may start on one site in a minute.\n *\n * Every test is a real run with real sends, so this is the feature's own\n * bucket beneath the console dispatcher's general one: a person iterating on\n * an action tests every few seconds at most, and past this is a script.\n */\nexport const ACTION_TEST_RUNS_PER_MINUTE = 10\n\nconst ACTION_TEST_RUN_WINDOW_MS = 60_000\n\n/** What the door reaches outside this module; specs hand in their own. */\nexport interface ActionTestRunDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; staff?: unknown }>\n consumeRateLimit(\n key: string,\n options: { limit: number; windowMs: number },\n ): Promise<{ allowed: boolean; resetMs: number }>\n runAction(\n hostId: string,\n actionId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<SingleActionOutcome>\n now(): number\n}\n\nfunction defaultDeps(): ActionTestRunDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, staff: decoded['staff'] }\n },\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n // Imported by the first test run rather than with the console surface,\n // as the host-event listener imports it.\n runAction: async (hostId, actionId, event, payload) => {\n const { runSingleActionOutcome } = await import('../engine/run-event-actions')\n return await runSingleActionOutcome(hostId, actionId, event, payload)\n },\n now: () => Date.now(),\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\n/** The status and words for each gate that can stop a test run. */\nfunction refusalFor(\n skipped: SingleActionSkip,\n outcome: SingleActionOutcome,\n payload: HostEventPayload,\n): { status: number; error: string } {\n switch (skipped) {\n case 'missing':\n return { status: 404, error: 'That action no longer exists' }\n case 'disabled':\n return { status: 409, error: 'Switch the action on to test it' }\n case 'event':\n return {\n status: 409,\n error: 'The action changed while it was being tested — try again',\n }\n case 'conditions':\n return {\n status: 422,\n error:\n `Nothing ran: a test run on ${String(payload['path'])} does not ` +\n 'meet this action’s conditions',\n }\n case 'plan':\n return {\n status: 403,\n error:\n `Actions need the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade',\n }\n case 'allowance':\n return {\n status: 402,\n error: `This site has used its ${outcome.limit} action runs for the month`,\n }\n default:\n return { status: 500, error: 'The test run could not be completed. Try again.' }\n }\n}\n\n/**\n * `automations/actions/test-run`: run one of a site's actions now.\n *\n * Body `{ hostId, actionId, payload? }`. Answers `{ ok: true, alerts }` when\n * the action's steps ran — the site alerts they produced, for the card to show\n * — and otherwise an `error` saying what stopped it.\n */\nexport function createActionTestRunHandler(\n overrides: Partial<ActionTestRunDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const actionId = String(req.body?.actionId ?? '')\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(actionId)) {\n return res.status(400).json({ error: 'Invalid actionId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n let caller: { uid: string; staff?: unknown }\n try {\n caller = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[workflows] test run could not verify the caller', error)\n return res\n .status(500)\n .json({ error: 'The sign-in could not be checked. Try again.' })\n }\n\n try {\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[caller.uid]\n if (caller.staff !== true && !hostRoleCanPublish(role)) {\n return res\n .status(403)\n .json({ error: 'Only a site admin or editor can test an action' })\n }\n\n const rate = await deps.consumeRateLimit(\n `workflows-test-run:${hostId}:${caller.uid}`,\n { limit: ACTION_TEST_RUNS_PER_MINUTE, windowMs: ACTION_TEST_RUN_WINDOW_MS },\n )\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))),\n )\n return res\n .status(429)\n .json({ error: 'Too many test runs — wait a minute and try again' })\n }\n\n const stored = await hostRef.collection('actions').doc(actionId).get()\n if (!stored.exists || stored.get('deletedAt')) {\n return res.status(404).json({ error: 'That action no longer exists' })\n }\n const event = String(\n (stored.data() as HostAction | undefined)?.trigger?.event ?? '',\n )\n if (!isSiteEventType(event)) {\n return res.status(400).json({\n error: 'Only an action with an in-page trigger can be tested here',\n })\n }\n\n const payload = actionTestRunPayload(req.body?.payload)\n const outcome = await deps.runAction(hostId, actionId, event, payload)\n if (outcome.ran) {\n return res.status(200).json({ ok: true, alerts: outcome.alerts })\n }\n const refusal = refusalFor(outcome.skipped ?? 'failed', outcome, payload)\n return res.status(refusal.status).json({ error: refusal.error })\n } catch (error) {\n console.error('[workflows] test run failed', hostId, actionId, error)\n return res\n .status(500)\n .json({ error: 'The test run could not be completed. Try again.' })\n }\n }\n}\n\nexport const actionTestRunHandler = createActionTestRunHandler()\n"],"names":["hostRoleCanPublish","isSiteEventType","planLabelGrantingFeature","isDocumentId","firebaseAdmin","isRefusedIdToken","consumeRateLimit","actionTestRunPayload","ACTION_TEST_RUN_API_ROUTE","ACTION_TEST_RUNS_PER_MINUTE","ACTION_TEST_RUN_WINDOW_MS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","staff","key","options","runAction","hostId","actionId","event","payload","runSingleActionOutcome","now","Date","bearerToken","headers","authorization","String","startsWith","slice","length","refusalFor","skipped","outcome","status","error","limit","createActionTestRunHandler","overrides","req","res","deps","method","json","body","caller","console","host","stored","hostRef","collection","doc","get","exists","role","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type AglynOrgMember, type PluginApiHandler, type PluginApiRequestSubject } from '@aglyn/aglyn/server';
|
|
18
|
-
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATION_API_ROUTES } from '../model/org-automations';
|
|
18
|
+
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATION_API_ROUTES, type OrgAutomationFields } from '../model/org-automations';
|
|
19
19
|
export { ORG_AUTOMATION_API_ROUTES };
|
|
20
20
|
/** What `automations/manage` may be asked to do. */
|
|
21
21
|
export declare const ORG_AUTOMATION_MANAGE_ACTIONS: readonly ["create", "update", "setEnabled", "delete"];
|
|
@@ -57,6 +57,47 @@ export interface OrgAutomationRouteDeps {
|
|
|
57
57
|
* `hostId` is: the handler refuses anyone who is not an org-wide member.
|
|
58
58
|
*/
|
|
59
59
|
export declare function orgAutomationsManageSubject(request: Request): Promise<PluginApiRequestSubject | null>;
|
|
60
|
+
/** What writing an org automation came to: its id, or the refusal the route answers with. */
|
|
61
|
+
export type OrgAutomationWrite = {
|
|
62
|
+
ok: true;
|
|
63
|
+
id: string;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
status: number;
|
|
67
|
+
error: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Creates an org automation as `automations/manage` does: its sites must be
|
|
71
|
+
* the organization's, and the organization holds at most
|
|
72
|
+
* {@link ORG_AUTOMATIONS_MAX} live ones. The route and the package import
|
|
73
|
+
* (AGL-3535) both create through here.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createOrgAutomationRecord(firestore: FirebaseFirestore.Firestore, input: {
|
|
76
|
+
orgId: string;
|
|
77
|
+
id: string;
|
|
78
|
+
fields: OrgAutomationFields;
|
|
79
|
+
actorUid: string;
|
|
80
|
+
}): Promise<OrgAutomationWrite>;
|
|
81
|
+
/** Rewrites a live org automation as `automations/manage` does; a site it no longer runs on leaves its pause list. */
|
|
82
|
+
export declare function updateOrgAutomationRecord(firestore: FirebaseFirestore.Firestore, input: {
|
|
83
|
+
orgId: string;
|
|
84
|
+
id: string;
|
|
85
|
+
fields: OrgAutomationFields;
|
|
86
|
+
actorUid: string;
|
|
87
|
+
}): Promise<OrgAutomationWrite>;
|
|
88
|
+
/**
|
|
89
|
+
* Deletes an org automation as `automations/manage` does — softly, switched
|
|
90
|
+
* off (see the handler). `null` for one that does not exist; `again` for
|
|
91
|
+
* one already deleted.
|
|
92
|
+
*/
|
|
93
|
+
export declare function deleteOrgAutomationRecord(firestore: FirebaseFirestore.Firestore, input: {
|
|
94
|
+
orgId: string;
|
|
95
|
+
id: string;
|
|
96
|
+
actorUid: string;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
name: string;
|
|
99
|
+
again: boolean;
|
|
100
|
+
} | null>;
|
|
60
101
|
/**
|
|
61
102
|
* `automations/manage`: create, edit, switch on or off, and delete one of the
|
|
62
103
|
* organization's automations.
|