@aglyn/plugins-workflows 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/package.json +53 -0
  4. package/src/index.d.ts +18 -0
  5. package/src/index.js +19 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/automation-step-fields.component.d.ts +74 -0
  8. package/src/lib/components/automation-step-fields.component.js +1210 -0
  9. package/src/lib/components/automation-step-fields.component.js.map +1 -0
  10. package/src/lib/components/host-actions-card.component.d.ts +15 -0
  11. package/src/lib/components/host-actions-card.component.js +1229 -0
  12. package/src/lib/components/host-actions-card.component.js.map +1 -0
  13. package/src/lib/components/host-activity-card.component.d.ts +84 -0
  14. package/src/lib/components/host-activity-card.component.js +280 -0
  15. package/src/lib/components/host-activity-card.component.js.map +1 -0
  16. package/src/lib/components/host-run-history-card.component.d.ts +40 -0
  17. package/src/lib/components/host-run-history-card.component.js +284 -0
  18. package/src/lib/components/host-run-history-card.component.js.map +1 -0
  19. package/src/lib/components/host-webhooks-card.component.d.ts +15 -0
  20. package/src/lib/components/host-webhooks-card.component.js +421 -0
  21. package/src/lib/components/host-webhooks-card.component.js.map +1 -0
  22. package/src/lib/components/host-workflows-card.component.d.ts +18 -0
  23. package/src/lib/components/host-workflows-card.component.js +952 -0
  24. package/src/lib/components/host-workflows-card.component.js.map +1 -0
  25. package/src/lib/components/run-quota-line.component.d.ts +27 -0
  26. package/src/lib/components/run-quota-line.component.js +72 -0
  27. package/src/lib/components/run-quota-line.component.js.map +1 -0
  28. package/src/lib/components/use-automation-step-pickers.d.ts +27 -0
  29. package/src/lib/components/use-automation-step-pickers.js +205 -0
  30. package/src/lib/components/use-automation-step-pickers.js.map +1 -0
  31. package/src/lib/components/workflow-zones.d.ts +36 -0
  32. package/src/lib/components/workflow-zones.js +19 -0
  33. package/src/lib/components/workflow-zones.js.map +1 -0
  34. package/src/lib/components/workflows-console-page.d.ts +25 -0
  35. package/src/lib/components/workflows-console-page.js +112 -0
  36. package/src/lib/components/workflows-console-page.js.map +1 -0
  37. package/src/lib/components/workflows-console-sections.d.ts +34 -0
  38. package/src/lib/components/workflows-console-sections.js +50 -0
  39. package/src/lib/components/workflows-console-sections.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/declarations.server.d.ts +41 -0
  44. package/src/lib/declarations.server.js +54 -0
  45. package/src/lib/declarations.server.js.map +1 -0
  46. package/src/lib/engine/crm-action-steps.d.ts +129 -0
  47. package/src/lib/engine/crm-action-steps.js +432 -0
  48. package/src/lib/engine/crm-action-steps.js.map +1 -0
  49. package/src/lib/engine/flow-enrollments.d.ts +264 -0
  50. package/src/lib/engine/flow-enrollments.js +386 -0
  51. package/src/lib/engine/flow-enrollments.js.map +1 -0
  52. package/src/lib/engine/run-event-actions.d.ts +232 -0
  53. package/src/lib/engine/run-event-actions.js +1392 -0
  54. package/src/lib/engine/run-event-actions.js.map +1 -0
  55. package/src/lib/engine/run-event-automations.d.ts +28 -0
  56. package/src/lib/engine/run-event-automations.js +37 -0
  57. package/src/lib/engine/run-event-automations.js.map +1 -0
  58. package/src/lib/engine/run-event-workflows.d.ts +75 -0
  59. package/src/lib/engine/run-event-workflows.js +311 -0
  60. package/src/lib/engine/run-event-workflows.js.map +1 -0
  61. package/src/lib/engine/workflow-steps.d.ts +107 -0
  62. package/src/lib/engine/workflow-steps.js +124 -0
  63. package/src/lib/engine/workflow-steps.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +94 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server-automation-drafts.d.ts +90 -0
  68. package/src/lib/server-automation-drafts.js +429 -0
  69. package/src/lib/server-automation-drafts.js.map +1 -0
  70. package/src/lib/server-console.d.ts +27 -0
  71. package/src/lib/server-console.js +35 -0
  72. package/src/lib/server-console.js.map +1 -0
  73. package/src/lib/server.d.ts +29 -0
  74. package/src/lib/server.js +298 -0
  75. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Workflows feature plugin (AGL-395). Console-only — workflows, actions,
19
+ * and webhooks run server-side (event pipelines, `/api/hooks`), not through
20
+ * a canvas element, so there is no UI bundle. The console half declares the
21
+ * Automation nav + page through the ConsoleExtension registry, gated by the
22
+ * `workflows` entitlement (the Actions/Webhooks tabs run their own per-plan
23
+ * checks off the passed `org`). Where a workflow is used is asked of the
24
+ * platform's scan and drawn in a zone this plugin hosts.
25
+ *
26
+ * `pluginId`, `featureFlag` and `navTabId` all still read `workflows`: they
27
+ * are stored — an org's enabled-plugin ids, the plan's entitlement flags, the
28
+ * Remote Config release flag — and a section's name is not a reason to orphan
29
+ * them. Only what a reader sees carries the new name.
30
+ */
31
+ export declare function registerWorkflowsConsole(): void;
32
+ export { default as HostActivityCard } from './components/host-activity-card.component';
33
+ export type { HostActivityCardProps } from './components/host-activity-card.component';
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import * as Aglyn from "@aglyn/aglyn";
17
+ import { mdiSitemap } from "@aglyn/shared-data-mdi";
18
+ import { lazy } from "react";
19
+ import { HostActivityCard } from "./components/host-activity-card.component.js";
20
+ import { WORKFLOWS_CONSOLE_SECTIONS } from "./components/workflows-console-sections.js";
21
+ import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
22
+ import { WORKFLOW_USAGE_ZONE } from "./components/workflow-zones.js";
23
+ import { BUNDLE_ID } from "./constants/bundle-common.js";
24
+ /** Code-split: the Automation console page only loads when opened. */ const WorkflowsConsolePage = lazy(()=>import("./components/workflows-console-page.js"));
25
+ /**
26
+ * Workflows feature plugin (AGL-395). Console-only — workflows, actions,
27
+ * and webhooks run server-side (event pipelines, `/api/hooks`), not through
28
+ * a canvas element, so there is no UI bundle. The console half declares the
29
+ * Automation nav + page through the ConsoleExtension registry, gated by the
30
+ * `workflows` entitlement (the Actions/Webhooks tabs run their own per-plan
31
+ * checks off the passed `org`). Where a workflow is used is asked of the
32
+ * platform's scan and drawn in a zone this plugin hosts.
33
+ *
34
+ * `pluginId`, `featureFlag` and `navTabId` all still read `workflows`: they
35
+ * are stored — an org's enabled-plugin ids, the plan's entitlement flags, the
36
+ * Remote Config release flag — and a section's name is not a reason to orphan
37
+ * them. Only what a reader sees carries the new name.
38
+ */ export function registerWorkflowsConsole() {
39
+ registerPluginZone({
40
+ zone: WORKFLOW_USAGE_ZONE,
41
+ label: 'Where a workflow is used',
42
+ surface: 'console',
43
+ description: '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.'
44
+ }, // Named, because a spec calls this registrar without the loader.
45
+ {
46
+ pluginId: BUNDLE_ID
47
+ });
48
+ Aglyn.registerConsoleExtension({
49
+ // Host activity feed (AGL-419): rendered by the shell's
50
+ // 'hostActivity' widget slot (dashboard + editor view page).
51
+ widgets: [
52
+ {
53
+ slot: 'hostActivity',
54
+ widgetId: 'workflows-host-activity',
55
+ title: 'Recent Activity',
56
+ Component: HostActivityCard
57
+ }
58
+ ],
59
+ pluginId: BUNDLE_ID,
60
+ displayName: 'Automation',
61
+ featureFlag: 'workflows',
62
+ navItems: [
63
+ {
64
+ label: 'Automation',
65
+ // Workflows, Actions and Webhooks are the tabs INSIDE this section, so
66
+ // the section is named for what they have in common. `/workflows` —
67
+ // the address before the section had its own name — is redirected here
68
+ // by the console (see `Route.HOST_WORKFLOWS`).
69
+ href: '/automation',
70
+ // Sections as ROUTES (AGL-2501): each is a real URL the shell
71
+ // resolves and gates, so the page mounts the one being read.
72
+ sections: WORKFLOWS_CONSOLE_SECTIONS,
73
+ // Reuse the existing release-flag nav-tab so staff-preview gating is
74
+ // unchanged now that the tab comes from the plugin (AGL-395).
75
+ navTabId: 'nav-tab-workflows',
76
+ icon: {
77
+ path: mdiSitemap.path
78
+ },
79
+ header: {
80
+ title: 'Automation',
81
+ icon: {
82
+ path: mdiSitemap.path
83
+ },
84
+ docsTopic: 'workflows'
85
+ },
86
+ Component: WorkflowsConsolePage
87
+ }
88
+ ]
89
+ });
90
+ }
91
+ // Activity feed consumed directly by the app (dashboard + screen view).
92
+ export { default as HostActivityCard } from "./components/host-activity-card.component.js";
93
+
94
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +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 { WORKFLOWS_CONSOLE_SECTIONS } from './components/workflows-console-sections'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { WORKFLOW_USAGE_ZONE } from './components/workflow-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\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 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 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 ],\n pluginId: BUNDLE_ID,\n displayName: 'Automation',\n featureFlag: 'workflows',\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 the console (see `Route.HOST_WORKFLOWS`).\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}\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","WORKFLOWS_CONSOLE_SECTIONS","registerPluginZone","WORKFLOW_USAGE_ZONE","BUNDLE_ID","WorkflowsConsolePage","registerWorkflowsConsole","zone","label","surface","description","pluginId","registerConsoleExtension","widgets","slot","widgetId","title","Component","displayName","featureFlag","navItems","href","sections","navTabId","icon","path","header","docsTopic","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,UAAU,QAAQ,yBAAwB;AACnD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,gBAAgB,QAAQ,+CAA2C;AAC5E,SAASC,0BAA0B,QAAQ,6CAAyC;AACpF,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SAASC,mBAAmB,QAAQ,iCAA6B;AACjE,SAASC,SAAS,QAAQ,+BAA2B;AAErD,oEAAoE,GACpE,MAAMC,uBAAuBN,KAC3B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASO;IACdJ,mBACE;QACEK,MAAMJ;QACNK,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUP;IAAU;IAExBP,MAAMe,wBAAwB,CAAC;QAC7B,wDAAwD;QACxD,6DAA6D;QAC7DC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWjB;YACb;SACD;QACDW,UAAUP;QACVc,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEZ,OAAO;gBACP,uEAAuE;gBACvE,oEAAoE;gBACpE,uEAAuE;gBACvE,+CAA+C;gBAC/Ca,MAAM;gBACN,8DAA8D;gBAC9D,6DAA6D;gBAC7DC,UAAUrB;gBACV,qEAAqE;gBACrE,8DAA8D;gBAC9DsB,UAAU;gBACVC,MAAM;oBAAEC,MAAM3B,WAAW2B,IAAI;gBAAC;gBAC9BC,QAAQ;oBACNV,OAAO;oBACPQ,MAAM;wBAAEC,MAAM3B,WAAW2B,IAAI;oBAAC;oBAC9BE,WAAW;gBACb;gBACAV,WAAWZ;YACb;SACD;IACH;AACF;AAEA,wEAAwE;AACxE,SAASuB,WAAW5B,gBAAgB,QAAQ,+CAA2C"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type HostAction, type HostActionStepType } from '@aglyn/aglyn/app-utils/actions';
18
+ import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
19
+ /**
20
+ * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).
21
+ *
22
+ * The draft writer this plugin registers for the `automation` resource on the
23
+ * core's resource-drafts seam. A plugin that produces an automation — a
24
+ * generator working from a description, an importer bringing flows over from
25
+ * another tool — asks for the writer by name and gets this plugin's rules,
26
+ * never a copy of them:
27
+ *
28
+ * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the
29
+ * Actions editor saves (`hostActionDocument`), with only the fields each
30
+ * step type holds. It is listed on the Automation page's Actions, opens in
31
+ * that editor, and runs through the same executor as one a person made.
32
+ * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,
33
+ * so nothing it describes runs until a person switches it on.
34
+ * - THE SCHEMA is the editor's own validator, `validateHostAction`, which the
35
+ * executor's writers share. A placeholder the draft could not fill — a
36
+ * bracketed list name, a bracketed condition value — passes it, because it
37
+ * is a value a person replaces, and the facts name every one.
38
+ * - THE ROOM is the flat platform cap on live actions, counted inside the
39
+ * transaction with the arithmetic `/api/hosts/resources` uses.
40
+ * - THE ROLE is the resources route's: a member who may write the site's
41
+ * content. THE PLAN is the Actions editor's: the `actions` entitlement.
42
+ *
43
+ * Nothing is enabled, sent or run, and the writer touches the one new document.
44
+ */
45
+ type Firestore = FirebaseFirestore.Firestore;
46
+ /** The resource name this writer is registered under. */
47
+ export declare const AUTOMATION_DRAFT_RESOURCE = "automation";
48
+ /** The longest name the Actions editor stores. */
49
+ export declare const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
50
+ /** A draft's name when the caller asks for none. */
51
+ export declare const AUTOMATION_DRAFT_DEFAULT_NAME = "New automation";
52
+ /** The resources route's refusal for a member who may not write the site. */
53
+ export declare const AUTOMATION_DRAFT_ROLE_REFUSAL = "Editing requires the editor role";
54
+ /** What a person is told when their plan has no actions builder. It names the page that sells one. */
55
+ export declare const AUTOMATION_DRAFT_PLAN_REFUSAL = "Automations are not included on this workspace\u2019s plan. Upgrade in Billing to build them.";
56
+ /** The resources route's refusal at the cap. */
57
+ export declare const AUTOMATION_DRAFT_LIMIT_REFUSAL: string;
58
+ /**
59
+ * The fields each step type holds, besides `type` and the step's own `when`.
60
+ * Total over the step types, so a type the model gains is a compile error here
61
+ * until its fields are named, rather than a draft that silently drops them.
62
+ */
63
+ export declare const AUTOMATION_STEP_FIELDS: Readonly<Record<HostActionStepType, readonly string[]>>;
64
+ export type AutomationDraftContentRead = {
65
+ ok: true;
66
+ action: HostAction;
67
+ } | {
68
+ ok: false;
69
+ problems: string[];
70
+ };
71
+ /**
72
+ * What a caller sends as `content` — `{ action }` — read into the action this
73
+ * writer would store: the trigger's and each step's own fields and nothing
74
+ * else, named, OFF, and held to the editor's validator.
75
+ */
76
+ export declare function readAutomationDraftContent(content: Readonly<Record<string, unknown>>): AutomationDraftContentRead;
77
+ /** Whether content is an automation this site could store: well-formed, off, and valid. */
78
+ export declare function checkAutomationDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
79
+ export interface AutomationDraftWriterDeps {
80
+ /** The Admin SDK handle; specs hand in a double. */
81
+ firestore?: () => Firestore;
82
+ }
83
+ export declare function createAutomationDraftWriter(deps?: AutomationDraftWriterDeps): PluginResourceDraftWriter;
84
+ export declare const automationDraftWriter: PluginResourceDraftWriter;
85
+ /**
86
+ * Registers the writer; the console surface calls it, since only the console
87
+ * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
88
+ */
89
+ export declare function registerAutomationDraftWriter(): void;
90
+ export {};
@@ -0,0 +1,429 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { ACTIONS_MAX_PER_HOST, hostActionDocument, validateHostAction } from "@aglyn/aglyn/app-utils/actions";
18
+ import { automationPlaceholders } from "@aglyn/aglyn/app-utils/automation-placeholders";
19
+ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
20
+ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
+ import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
22
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
23
+ import { BUNDLE_ID } from "./constants/bundle-common.js";
24
+ /** The resource name this writer is registered under. */ export const AUTOMATION_DRAFT_RESOURCE = 'automation';
25
+ /** The longest name the Actions editor stores. */ export const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
26
+ /** A draft's name when the caller asks for none. */ export const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation';
27
+ /** The resources route's refusal for a member who may not write the site. */ export const AUTOMATION_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role';
28
+ /** What a person is told when their plan has no actions builder. It names the page that sells one. */ export const AUTOMATION_DRAFT_PLAN_REFUSAL = 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.';
29
+ /** The resources route's refusal at the cap. */ export const AUTOMATION_DRAFT_LIMIT_REFUSAL = `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` + 'delete some to make room';
30
+ /** A trigger bound to one element, which makes an action an interaction of its page. */ const LEAF_SELECTOR = /^\[data-aglyn="leaf:.+"\]$/;
31
+ /**
32
+ * The fields each step type holds, besides `type` and the step's own `when`.
33
+ * Total over the step types, so a type the model gains is a compile error here
34
+ * until its fields are named, rather than a draft that silently drops them.
35
+ */ export const AUTOMATION_STEP_FIELDS = {
36
+ runWorkflow: [
37
+ 'workflowId',
38
+ 'workflowName'
39
+ ],
40
+ siteAlert: [
41
+ 'message',
42
+ 'severity'
43
+ ],
44
+ customEvent: [
45
+ 'eventName'
46
+ ],
47
+ datasetAppend: [
48
+ 'datasetId',
49
+ 'datasetName'
50
+ ],
51
+ webhookPost: [
52
+ 'webhookId',
53
+ 'webhookName'
54
+ ],
55
+ showOverlay: [
56
+ 'overlayId',
57
+ 'overlayName'
58
+ ],
59
+ stickyNav: [
60
+ 'selector'
61
+ ],
62
+ addClass: [
63
+ 'selector',
64
+ 'className'
65
+ ],
66
+ removeClass: [
67
+ 'selector',
68
+ 'className'
69
+ ],
70
+ toggleClass: [
71
+ 'selector',
72
+ 'className'
73
+ ],
74
+ showElement: [
75
+ 'selector',
76
+ 'delayMs',
77
+ 'dismissOn'
78
+ ],
79
+ hideElement: [
80
+ 'selector',
81
+ 'delayMs'
82
+ ],
83
+ toggleElement: [
84
+ 'selector',
85
+ 'delayMs',
86
+ 'dismissOn'
87
+ ],
88
+ setAttribute: [
89
+ 'selector',
90
+ 'name',
91
+ 'value'
92
+ ],
93
+ removeAttribute: [
94
+ 'selector',
95
+ 'name'
96
+ ],
97
+ scrollTo: [
98
+ 'selector',
99
+ 'behavior',
100
+ 'offsetPx'
101
+ ],
102
+ playVideo: [
103
+ 'selector'
104
+ ],
105
+ openDrawer: [
106
+ 'drawerNodeId'
107
+ ],
108
+ closeDrawer: [
109
+ 'drawerNodeId'
110
+ ],
111
+ toggleDrawer: [
112
+ 'drawerNodeId'
113
+ ],
114
+ openMenu: [
115
+ 'menuNodeId'
116
+ ],
117
+ closeMenu: [
118
+ 'menuNodeId'
119
+ ],
120
+ toggleMenu: [
121
+ 'menuNodeId'
122
+ ],
123
+ showHtml: [
124
+ 'html'
125
+ ],
126
+ runJs: [
127
+ 'code'
128
+ ],
129
+ redirect: [
130
+ 'url',
131
+ 'screenId'
132
+ ],
133
+ trackGaEvent: [
134
+ 'eventName',
135
+ 'params'
136
+ ],
137
+ sendEmail: [
138
+ 'subject',
139
+ 'body',
140
+ 'toField',
141
+ 'topicId'
142
+ ],
143
+ notifyAdmins: [
144
+ 'title',
145
+ 'body'
146
+ ],
147
+ enrollList: [
148
+ 'listId',
149
+ 'listName'
150
+ ],
151
+ updateDataset: [
152
+ 'datasetId',
153
+ 'datasetName'
154
+ ],
155
+ assignCampaign: [
156
+ 'campaignId',
157
+ 'campaignName'
158
+ ],
159
+ wait: [
160
+ 'delayMinutes'
161
+ ],
162
+ waitForEvent: [
163
+ 'eventName',
164
+ 'timeoutMinutes'
165
+ ],
166
+ exitFlow: [],
167
+ setContactStage: [
168
+ 'lifecycleStage'
169
+ ],
170
+ addContactTag: [
171
+ 'tag'
172
+ ],
173
+ assignContactOwner: [
174
+ 'ownerUid',
175
+ 'ownerEmail',
176
+ 'roundRobin'
177
+ ],
178
+ createCrmTask: [
179
+ 'title',
180
+ 'kind',
181
+ 'dueInDays',
182
+ 'assigneeUid',
183
+ 'assigneeEmail'
184
+ ],
185
+ logCrmActivity: [
186
+ 'kind',
187
+ 'body'
188
+ ]
189
+ };
190
+ /** The trigger keys an automation stores. */ const TRIGGER_FIELDS = [
191
+ 'event',
192
+ 'filter',
193
+ 'conditions',
194
+ 'combinator',
195
+ 'selector',
196
+ 'threshold',
197
+ 'pathPattern',
198
+ 'oncePerVisitor',
199
+ 'oncePerSession',
200
+ 'cooldownMinutes',
201
+ 'everyTime'
202
+ ];
203
+ function isRecord(value) {
204
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
205
+ }
206
+ /** The keys of `source` that `keys` names and that hold something. */ function picked(source, keys) {
207
+ const out = {};
208
+ for (const key of keys){
209
+ if (source[key] !== undefined && source[key] !== null) out[key] = source[key];
210
+ }
211
+ return out;
212
+ }
213
+ function conditionOf(value) {
214
+ if (!isRecord(value)) return null;
215
+ return picked(value, [
216
+ 'field',
217
+ 'op',
218
+ 'value'
219
+ ]);
220
+ }
221
+ /**
222
+ * What a caller sends as `content` — `{ action }` — read into the action this
223
+ * writer would store: the trigger's and each step's own fields and nothing
224
+ * else, named, OFF, and held to the editor's validator.
225
+ */ export function readAutomationDraftContent(content) {
226
+ var _raw_name;
227
+ const raw = content['action'];
228
+ if (!isRecord(raw)) return {
229
+ ok: false,
230
+ problems: [
231
+ 'The draft holds no automation'
232
+ ]
233
+ };
234
+ const trigger = raw['trigger'];
235
+ const steps = raw['steps'];
236
+ if (!isRecord(trigger)) return {
237
+ ok: false,
238
+ problems: [
239
+ 'Pick a trigger event'
240
+ ]
241
+ };
242
+ if (!Array.isArray(steps)) return {
243
+ ok: false,
244
+ problems: [
245
+ 'Add at least one step'
246
+ ]
247
+ };
248
+ const problems = [];
249
+ const knownTypes = new Set(Object.keys(AUTOMATION_STEP_FIELDS));
250
+ const cleanSteps = [];
251
+ steps.forEach((step, index)=>{
252
+ const type = isRecord(step) ? step['type'] : undefined;
253
+ if (typeof type !== 'string' || !knownTypes.has(type)) {
254
+ problems.push(`Step ${index + 1}: not a step the Actions editor offers`);
255
+ return;
256
+ }
257
+ const source = step;
258
+ const when = isRecord(source['when']) ? _extends({
259
+ conditions: (Array.isArray(source['when']['conditions']) ? source['when']['conditions'] : []).map(conditionOf).filter((clause)=>clause !== null)
260
+ }, typeof source['when']['combinator'] === 'string' ? {
261
+ combinator: source['when']['combinator']
262
+ } : {}) : null;
263
+ cleanSteps.push(_extends({
264
+ type
265
+ }, picked(source, AUTOMATION_STEP_FIELDS[type]), when && when.conditions.length ? {
266
+ when
267
+ } : {}));
268
+ });
269
+ const cleanTrigger = picked(trigger, TRIGGER_FIELDS);
270
+ if (Array.isArray(cleanTrigger['conditions'])) {
271
+ cleanTrigger['conditions'] = cleanTrigger['conditions'].map(conditionOf).filter((clause)=>clause !== null);
272
+ }
273
+ // An element's own interaction belongs to the page that holds the element,
274
+ // and is authored there; the Actions list does not show one.
275
+ if (typeof cleanTrigger['selector'] === 'string' && LEAF_SELECTOR.test(cleanTrigger['selector'])) {
276
+ problems.push('An interaction on one element is set up on that element, not as an automation');
277
+ }
278
+ const name = String((_raw_name = raw['name']) != null ? _raw_name : '').replace(/\s+/g, ' ').trim().slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS);
279
+ const action = {
280
+ name: name || AUTOMATION_DRAFT_DEFAULT_NAME,
281
+ trigger: cleanTrigger,
282
+ steps: cleanSteps,
283
+ enabled: false
284
+ };
285
+ const invalid = validateHostAction(action);
286
+ if (invalid) problems.push(invalid);
287
+ return problems.length ? {
288
+ ok: false,
289
+ problems
290
+ } : {
291
+ ok: true,
292
+ action
293
+ };
294
+ }
295
+ /** What the writer reports about an automation beyond its identity. */ function factsOf(action) {
296
+ var _ref, _action_steps;
297
+ var _action_trigger;
298
+ return {
299
+ event: (_ref = (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event) != null ? _ref : '',
300
+ steps: ((_action_steps = action.steps) != null ? _action_steps : []).length,
301
+ enabled: action.enabled === true,
302
+ placeholders: automationPlaceholders(action).length
303
+ };
304
+ }
305
+ /** Whether content is an automation this site could store: well-formed, off, and valid. */ export function checkAutomationDraftContent(content) {
306
+ const read = readAutomationDraftContent(content);
307
+ return read.ok === false ? read : {
308
+ ok: true,
309
+ facts: factsOf(read.action)
310
+ };
311
+ }
312
+ function recordOf(snapshot) {
313
+ var _snapshot_data, _data_name;
314
+ const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
315
+ return {
316
+ id: snapshot.id,
317
+ name: String((_data_name = data.name) != null ? _data_name : ''),
318
+ versionId: null,
319
+ facts: factsOf(data)
320
+ };
321
+ }
322
+ function roleRefusal(host, uid) {
323
+ var _host_get;
324
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
325
+ return hostRoleCanWrite(role) ? null : {
326
+ status: 403,
327
+ error: AUTOMATION_DRAFT_ROLE_REFUSAL
328
+ };
329
+ }
330
+ function planRefusal(org) {
331
+ return checkEntitlement(org, 'actions') ? null : {
332
+ status: 403,
333
+ error: AUTOMATION_DRAFT_PLAN_REFUSAL
334
+ };
335
+ }
336
+ /** The live actions the site holds: a soft-deleted one frees its slot. */ function liveCount(rows) {
337
+ return rows.docs.filter((row)=>row.get('deletedAt') == null).length;
338
+ }
339
+ function roomRefusal(live) {
340
+ return live >= ACTIONS_MAX_PER_HOST ? {
341
+ status: 403,
342
+ error: AUTOMATION_DRAFT_LIMIT_REFUSAL
343
+ } : null;
344
+ }
345
+ export function createAutomationDraftWriter(deps = {}) {
346
+ var _deps_firestore;
347
+ const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
348
+ return {
349
+ refusal: async (context)=>{
350
+ var _ref, _roleRefusal;
351
+ const hostRef = firestore().collection('hosts').doc(context.hostId);
352
+ const host = await hostRef.get();
353
+ if (!host.exists) return {
354
+ status: 404,
355
+ error: 'Unknown site'
356
+ };
357
+ return (_ref = (_roleRefusal = roleRefusal(host, context.uid)) != null ? _roleRefusal : planRefusal(context.org)) != null ? _ref : roomRefusal(liveCount(await hostRef.collection('actions').select('deletedAt').get()));
358
+ },
359
+ check: (content)=>checkAutomationDraftContent(content),
360
+ read: async ({ hostId, id })=>{
361
+ const snapshot = await firestore().collection('hosts').doc(hostId).collection('actions').doc(id).get();
362
+ return snapshot.exists ? recordOf(snapshot) : null;
363
+ },
364
+ write: async (request)=>{
365
+ var _request_name;
366
+ const read = readAutomationDraftContent(request.content);
367
+ if (read.ok === false) return {
368
+ ok: false,
369
+ status: 400,
370
+ error: read.problems[0]
371
+ };
372
+ const db = firestore();
373
+ const hostRef = db.collection('hosts').doc(request.hostId);
374
+ const actionRef = hostRef.collection('actions').doc(request.id);
375
+ const name = String((_request_name = request.name) != null ? _request_name : '').replace(/\s+/g, ' ').trim().slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS) || read.action.name;
376
+ const action = _extends({}, read.action, {
377
+ name,
378
+ enabled: false,
379
+ recipe: null
380
+ });
381
+ return db.runTransaction(async (tx)=>{
382
+ var _ref, _roleRefusal;
383
+ // Every read before any write, which Firestore requires.
384
+ const [host, existing, rows] = await Promise.all([
385
+ tx.get(hostRef),
386
+ tx.get(actionRef),
387
+ tx.get(hostRef.collection('actions').select('deletedAt'))
388
+ ]);
389
+ if (!host.exists) return {
390
+ ok: false,
391
+ status: 404,
392
+ error: 'Unknown site'
393
+ };
394
+ if (existing.exists) return _extends({
395
+ ok: true,
396
+ replayed: true
397
+ }, recordOf(existing));
398
+ const refusal = (_ref = (_roleRefusal = roleRefusal(host, request.uid)) != null ? _roleRefusal : planRefusal(request.org)) != null ? _ref : roomRefusal(liveCount(rows));
399
+ if (refusal) return _extends({
400
+ ok: false
401
+ }, refusal);
402
+ tx.create(actionRef, _extends({}, hostActionDocument(action), {
403
+ createdAt: request.now,
404
+ updatedAt: request.now,
405
+ createdBy: request.uid
406
+ }));
407
+ return {
408
+ ok: true,
409
+ replayed: false,
410
+ id: request.id,
411
+ name,
412
+ versionId: null,
413
+ facts: factsOf(action)
414
+ };
415
+ });
416
+ }
417
+ };
418
+ }
419
+ export const automationDraftWriter = createAutomationDraftWriter();
420
+ /**
421
+ * Registers the writer; the console surface calls it, since only the console
422
+ * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
423
+ */ export function registerAutomationDraftWriter() {
424
+ registerPluginResourceDraftWriter(AUTOMATION_DRAFT_RESOURCE, automationDraftWriter, {
425
+ pluginId: BUNDLE_ID
426
+ });
427
+ }
428
+
429
+ //# sourceMappingURL=server-automation-drafts.js.map