@aglyn/plugins-workflows 1.0.0-beta.179 → 1.0.0-beta.181

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 (52) hide show
  1. package/package.json +12 -11
  2. package/src/lib/components/automation-trigger-conditions.component.d.ts +43 -0
  3. package/src/lib/components/automation-trigger-conditions.component.js +189 -0
  4. package/src/lib/components/automation-trigger-conditions.component.js.map +1 -0
  5. package/src/lib/components/host-actions-card.component.js +27 -176
  6. package/src/lib/components/host-actions-card.component.js.map +1 -1
  7. package/src/lib/components/org-automation-editor.component.d.ts +51 -0
  8. package/src/lib/components/org-automation-editor.component.js +310 -0
  9. package/src/lib/components/org-automation-editor.component.js.map +1 -0
  10. package/src/lib/components/org-automations-card.component.d.ts +28 -0
  11. package/src/lib/components/org-automations-card.component.js +373 -0
  12. package/src/lib/components/org-automations-card.component.js.map +1 -0
  13. package/src/lib/components/org-site-automation-list.component.d.ts +43 -0
  14. package/src/lib/components/org-site-automation-list.component.js +289 -0
  15. package/src/lib/components/org-site-automation-list.component.js.map +1 -0
  16. package/src/lib/components/site-org-automations-panel.component.d.ts +22 -0
  17. package/src/lib/components/site-org-automations-panel.component.js +201 -0
  18. package/src/lib/components/site-org-automations-panel.component.js.map +1 -0
  19. package/src/lib/components/use-org-automation-step-pickers.d.ts +20 -0
  20. package/src/lib/components/use-org-automation-step-pickers.js +108 -0
  21. package/src/lib/components/use-org-automation-step-pickers.js.map +1 -0
  22. package/src/lib/components/use-org-automations-api.d.ts +16 -0
  23. package/src/lib/components/use-org-automations-api.js +55 -0
  24. package/src/lib/components/use-org-automations-api.js.map +1 -0
  25. package/src/lib/components/workflows-console-page.js +63 -1
  26. package/src/lib/components/workflows-console-page.js.map +1 -1
  27. package/src/lib/components/workflows-console-sections.d.ts +19 -1
  28. package/src/lib/components/workflows-console-sections.js +37 -2
  29. package/src/lib/components/workflows-console-sections.js.map +1 -1
  30. package/src/lib/components/workflows-org-mount.d.ts +52 -0
  31. package/src/lib/components/workflows-org-mount.js +49 -0
  32. package/src/lib/components/workflows-org-mount.js.map +1 -0
  33. package/src/lib/engine/flow-enrollments.d.ts +26 -10
  34. package/src/lib/engine/flow-enrollments.js +14 -7
  35. package/src/lib/engine/flow-enrollments.js.map +1 -1
  36. package/src/lib/engine/run-event-actions.d.ts +57 -4
  37. package/src/lib/engine/run-event-actions.js +119 -19
  38. package/src/lib/engine/run-event-actions.js.map +1 -1
  39. package/src/lib/engine/run-org-automations.d.ts +75 -0
  40. package/src/lib/engine/run-org-automations.js +154 -0
  41. package/src/lib/engine/run-org-automations.js.map +1 -0
  42. package/src/lib/model/org-automations.d.ts +232 -0
  43. package/src/lib/model/org-automations.js +423 -0
  44. package/src/lib/model/org-automations.js.map +1 -0
  45. package/src/lib/plugin.js +26 -1
  46. package/src/lib/plugin.js.map +1 -1
  47. package/src/lib/server/org-automations-routes.d.ts +88 -0
  48. package/src/lib/server/org-automations-routes.js +474 -0
  49. package/src/lib/server/org-automations-routes.js.map +1 -0
  50. package/src/lib/server-console.d.ts +2 -1
  51. package/src/lib/server-console.js +12 -2
  52. package/src/lib/server-console.js.map +1 -1
@@ -0,0 +1,75 @@
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 FlowEnrollment } from './flow-enrollments';
18
+ /**
19
+ * THE ORGANIZATION'S AUTOMATIONS, as the engine meets them (AGL-3302).
20
+ *
21
+ * Two doors lead here. An event arrives and the site's own actions are
22
+ * joined by the org automations placed on the site — `runEventActions` asks
23
+ * {@link findOrgAutomationsForEvent} and runs what it answers beside them. A
24
+ * person waiting inside an org automation comes due, or the event they were
25
+ * waiting for arrives, and {@link resumeOrgAutomationEnrollment} continues
26
+ * them against the organization's document.
27
+ *
28
+ * Both run on the event's SITE. The run environment is the site's, so the
29
+ * sending identity, the suppression lists, the consent group, the email meter
30
+ * and the run allowance are all the site's — an automation placed on five
31
+ * sites is five sites running it for themselves.
32
+ */
33
+ /** The org automations one event runs on one site, and whose they are. */
34
+ export interface PlacedOrgAutomations {
35
+ orgId: string;
36
+ docs: FirebaseFirestore.QueryDocumentSnapshot[];
37
+ }
38
+ /** Test seam: forget every remembered organization. */
39
+ export declare function resetOrgAutomationLookupsForTests(): void;
40
+ /**
41
+ * The org automations this event runs on this site, or null when there are
42
+ * none — including when the event is not one an org automation can start on,
43
+ * which is answered before any read.
44
+ *
45
+ * One query: the event, switched on, and placed on this site — by the org
46
+ * token or the site's own — capped at {@link MAX_TRIGGERED_ORG_AUTOMATIONS}.
47
+ * `enabled` is in the query rather than filtered after it so a switched-off
48
+ * or deleted automation (deletion switches it off too) can never take one of
49
+ * the capped places; a PAUSE is per site and cannot be asked of an array in
50
+ * the same query, so a paused automation is dropped here, after it.
51
+ *
52
+ * Served by the composite index on `automations` — `visibleTo` CONTAINS,
53
+ * `trigger.event`, `enabled` — in `cloud/firebase-firestore.indexes.json`.
54
+ *
55
+ * Never throws: an organization that cannot be read runs no org automation,
56
+ * and the site's own actions run exactly as they would have.
57
+ */
58
+ export declare function findOrgAutomationsForEvent(hostId: string, event: string): Promise<PlacedOrgAutomations | null>;
59
+ /**
60
+ * Continues one person waiting inside an org automation, on the site they
61
+ * were enrolled on — the org twin of the site action's resume, asking the
62
+ * same questions of the organization's document instead of the site's.
63
+ *
64
+ * - The kill switch is `orgs/{orgId}/automations/{id}`: deleted, switched
65
+ * off, taken off this site, or paused on this site, and the person stops.
66
+ * - The site must still belong to that organization, and its plan must
67
+ * still carry the actions builder.
68
+ * - The snapshot runs, never the automation's current steps.
69
+ * - Counted on this site's action-run meter, never refused by it: the person
70
+ * is already inside, and the gate belongs at enrollment.
71
+ */
72
+ export declare function resumeOrgAutomationEnrollment(enrollment: FlowEnrollment, ref: FirebaseFirestore.DocumentReference, options: {
73
+ timedOut?: boolean;
74
+ nowMs: number;
75
+ }): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'>;
@@ -0,0 +1,154 @@
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 { checkEntitlement, FLOW_TIMED_OUT_FIELD } from "@aglyn/aglyn/server";
18
+ import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
19
+ import { firebaseAdmin, getOrgForHost, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
20
+ import { FieldValue } from "firebase-admin/firestore";
21
+ import { isOrgAutomationTriggerEvent, MAX_TRIGGERED_ORG_AUTOMATIONS, ORG_AUTOMATIONS_COLLECTION, orgAutomationRunsOnHost, orgAutomationStopReason } from "../model/org-automations.js";
22
+ import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
23
+ import { executeAction, FLOW_CLAIM_RETRY_MS, makeWorkflowContextLoader, stopFlowEnrollment } from "./run-event-actions.js";
24
+ /**
25
+ * How long a site's organization is remembered between events.
26
+ *
27
+ * The `hostIndex` read is the price of asking the organization at all, and it
28
+ * repeats on every server event of a busy site and again for each event a
29
+ * run's own steps raise. Remembered briefly rather than forever: a site that
30
+ * changes hands is picked up within the window, and the engine refuses to run
31
+ * a remembered organization's automations for a site whose current owner —
32
+ * read fresh by the entitlement gate before anything runs — is another.
33
+ */ const ORG_LOOKUP_TTL_MS = 60000;
34
+ /** Entries kept before the memo starts over; far past one instance's working set. */ const ORG_LOOKUP_MAX_ENTRIES = 1000;
35
+ const orgLookups = new Map();
36
+ /** Test seam: forget every remembered organization. */ export function resetOrgAutomationLookupsForTests() {
37
+ orgLookups.clear();
38
+ }
39
+ async function orgIdForHost(hostId) {
40
+ const nowMs = Date.now();
41
+ const known = orgLookups.get(hostId);
42
+ if (known && nowMs - known.atMs < ORG_LOOKUP_TTL_MS) return known.orgId;
43
+ const orgId = await resolveOrgIdForHost(hostId);
44
+ if (orgLookups.size >= ORG_LOOKUP_MAX_ENTRIES) orgLookups.clear();
45
+ orgLookups.set(hostId, {
46
+ orgId,
47
+ atMs: nowMs
48
+ });
49
+ return orgId;
50
+ }
51
+ /**
52
+ * The org automations this event runs on this site, or null when there are
53
+ * none — including when the event is not one an org automation can start on,
54
+ * which is answered before any read.
55
+ *
56
+ * One query: the event, switched on, and placed on this site — by the org
57
+ * token or the site's own — capped at {@link MAX_TRIGGERED_ORG_AUTOMATIONS}.
58
+ * `enabled` is in the query rather than filtered after it so a switched-off
59
+ * or deleted automation (deletion switches it off too) can never take one of
60
+ * the capped places; a PAUSE is per site and cannot be asked of an array in
61
+ * the same query, so a paused automation is dropped here, after it.
62
+ *
63
+ * Served by the composite index on `automations` — `visibleTo` CONTAINS,
64
+ * `trigger.event`, `enabled` — in `cloud/firebase-firestore.indexes.json`.
65
+ *
66
+ * Never throws: an organization that cannot be read runs no org automation,
67
+ * and the site's own actions run exactly as they would have.
68
+ */ export async function findOrgAutomationsForEvent(hostId, event) {
69
+ if (!isOrgAutomationTriggerEvent(event)) return null;
70
+ try {
71
+ const orgId = await orgIdForHost(hostId);
72
+ if (!orgId) return null;
73
+ const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection(ORG_AUTOMATIONS_COLLECTION).where('trigger.event', '==', event).where('enabled', '==', true).where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)).limit(MAX_TRIGGERED_ORG_AUTOMATIONS).get();
74
+ const docs = snapshot.docs.filter((doc)=>orgAutomationRunsOnHost(doc.data(), hostId));
75
+ return docs.length ? {
76
+ orgId,
77
+ docs
78
+ } : null;
79
+ } catch (error) {
80
+ console.error('[org automations] lookup failed', hostId, event, error);
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Continues one person waiting inside an org automation, on the site they
86
+ * were enrolled on — the org twin of the site action's resume, asking the
87
+ * same questions of the organization's document instead of the site's.
88
+ *
89
+ * - The kill switch is `orgs/{orgId}/automations/{id}`: deleted, switched
90
+ * off, taken off this site, or paused on this site, and the person stops.
91
+ * - The site must still belong to that organization, and its plan must
92
+ * still carry the actions builder.
93
+ * - The snapshot runs, never the automation's current steps.
94
+ * - Counted on this site's action-run meter, never refused by it: the person
95
+ * is already inside, and the gate belongs at enrollment.
96
+ */ export async function resumeOrgAutomationEnrollment(enrollment, ref, options) {
97
+ var _enrollment_orgId, _owner_org, _enrollment_payload;
98
+ const { nowMs } = options;
99
+ const hostId = enrollment.hostId;
100
+ const firestore = firebaseAdmin.app().firestore();
101
+ const hostRef = firestore.collection('hosts').doc(hostId);
102
+ const stop = (reason)=>stopFlowEnrollment(enrollment, ref, reason);
103
+ const orgId = String((_enrollment_orgId = enrollment.orgId) != null ? _enrollment_orgId : '');
104
+ const doc = orgId ? await firestore.collection('orgs').doc(orgId).collection(ORG_AUTOMATIONS_COLLECTION).doc(enrollment.actionId).get().catch(()=>null) : null;
105
+ const automation = (doc == null ? void 0 : doc.exists) ? doc.data() : null;
106
+ if (!automation) return await stop('the org automation was deleted');
107
+ const refusal = orgAutomationStopReason(automation, hostId);
108
+ if (refusal) return await stop(refusal);
109
+ const owner = await getOrgForHost(hostId).catch(()=>null);
110
+ if (!owner || owner.orgId !== orgId) {
111
+ return await stop('this site is no longer part of the organization that runs the org automation');
112
+ }
113
+ if (!checkEntitlement(owner.org, 'actions')) {
114
+ return await stop('this site’s plan no longer includes automations');
115
+ }
116
+ const env = {
117
+ hostId,
118
+ hostRef,
119
+ alerts: [],
120
+ // Admitted by the `actions` gate above.
121
+ actionsAllowed: true,
122
+ webhooksAllowed: checkEntitlement(owner.org, 'webhooks'),
123
+ crmAllowed: checkEntitlement(owner.org, 'crm'),
124
+ depth: 0,
125
+ org: (_owner_org = owner.org) != null ? _owner_org : null,
126
+ orgId: owner.orgId,
127
+ loadWorkflowContext: makeWorkflowContextLoader(hostRef)
128
+ };
129
+ const payload = _extends({}, (_enrollment_payload = enrollment.payload) != null ? _enrollment_payload : {}, options.timedOut ? {
130
+ [FLOW_TIMED_OUT_FIELD]: true
131
+ } : {});
132
+ const ending = await executeAction(env, enrollment.actionId, automation, enrollment.event, payload, {
133
+ startIndex: enrollment.nextStepIndex,
134
+ steps: enrollment.steps,
135
+ enrollmentRef: ref,
136
+ orgAutomation: {
137
+ orgId
138
+ }
139
+ });
140
+ if (ending === 'deferred') {
141
+ await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs);
142
+ return ending;
143
+ }
144
+ if (ending !== 'waiting') await endFlowEnrollment(ref);
145
+ const monthKey = new Date(nowMs).toISOString().slice(0, 7);
146
+ await hostRef.collection('counters').doc('actionRuns').set({
147
+ [monthKey]: FieldValue.increment(1)
148
+ }, {
149
+ merge: true
150
+ }).catch(()=>undefined);
151
+ return ending;
152
+ }
153
+
154
+ //# sourceMappingURL=run-org-automations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-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 checkEntitlement,\n FLOW_TIMED_OUT_FIELD,\n type HostActionStep,\n} from '@aglyn/aglyn/server'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n firebaseAdmin,\n getOrgForHost,\n resolveOrgIdForHost,\n} from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n isOrgAutomationTriggerEvent,\n MAX_TRIGGERED_ORG_AUTOMATIONS,\n ORG_AUTOMATIONS_COLLECTION,\n type OrgAutomation,\n orgAutomationRunsOnHost,\n orgAutomationStopReason,\n} from '../model/org-automations'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n type ActionRunEnv,\n executeAction,\n FLOW_CLAIM_RETRY_MS,\n makeWorkflowContextLoader,\n stopFlowEnrollment,\n} from './run-event-actions'\n\n/**\n * THE ORGANIZATION'S AUTOMATIONS, as the engine meets them (AGL-3302).\n *\n * Two doors lead here. An event arrives and the site's own actions are\n * joined by the org automations placed on the site — `runEventActions` asks\n * {@link findOrgAutomationsForEvent} and runs what it answers beside them. A\n * person waiting inside an org automation comes due, or the event they were\n * waiting for arrives, and {@link resumeOrgAutomationEnrollment} continues\n * them against the organization's document.\n *\n * Both run on the event's SITE. The run environment is the site's, so the\n * sending identity, the suppression lists, the consent group, the email meter\n * and the run allowance are all the site's — an automation placed on five\n * sites is five sites running it for themselves.\n */\n\n/** The org automations one event runs on one site, and whose they are. */\nexport interface PlacedOrgAutomations {\n orgId: string\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n}\n\n/**\n * How long a site's organization is remembered between events.\n *\n * The `hostIndex` read is the price of asking the organization at all, and it\n * repeats on every server event of a busy site and again for each event a\n * run's own steps raise. Remembered briefly rather than forever: a site that\n * changes hands is picked up within the window, and the engine refuses to run\n * a remembered organization's automations for a site whose current owner —\n * read fresh by the entitlement gate before anything runs — is another.\n */\nconst ORG_LOOKUP_TTL_MS = 60_000\n\n/** Entries kept before the memo starts over; far past one instance's working set. */\nconst ORG_LOOKUP_MAX_ENTRIES = 1_000\n\nconst orgLookups = new Map<string, { orgId: string | null; atMs: number }>()\n\n/** Test seam: forget every remembered organization. */\nexport function resetOrgAutomationLookupsForTests(): void {\n orgLookups.clear()\n}\n\nasync function orgIdForHost(hostId: string): Promise<string | null> {\n const nowMs = Date.now()\n const known = orgLookups.get(hostId)\n if (known && nowMs - known.atMs < ORG_LOOKUP_TTL_MS) return known.orgId\n const orgId = await resolveOrgIdForHost(hostId)\n if (orgLookups.size >= ORG_LOOKUP_MAX_ENTRIES) orgLookups.clear()\n orgLookups.set(hostId, { orgId, atMs: nowMs })\n return orgId\n}\n\n/**\n * The org automations this event runs on this site, or null when there are\n * none — including when the event is not one an org automation can start on,\n * which is answered before any read.\n *\n * One query: the event, switched on, and placed on this site — by the org\n * token or the site's own — capped at {@link MAX_TRIGGERED_ORG_AUTOMATIONS}.\n * `enabled` is in the query rather than filtered after it so a switched-off\n * or deleted automation (deletion switches it off too) can never take one of\n * the capped places; a PAUSE is per site and cannot be asked of an array in\n * the same query, so a paused automation is dropped here, after it.\n *\n * Served by the composite index on `automations` — `visibleTo` CONTAINS,\n * `trigger.event`, `enabled` — in `cloud/firebase-firestore.indexes.json`.\n *\n * Never throws: an organization that cannot be read runs no org automation,\n * and the site's own actions run exactly as they would have.\n */\nexport async function findOrgAutomationsForEvent(\n hostId: string,\n event: string,\n): Promise<PlacedOrgAutomations | null> {\n if (!isOrgAutomationTriggerEvent(event)) return null\n try {\n const orgId = await orgIdForHost(hostId)\n if (!orgId) return null\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n .where('trigger.event', '==', event)\n .where('enabled', '==', true)\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId))\n .limit(MAX_TRIGGERED_ORG_AUTOMATIONS)\n .get()\n const docs = snapshot.docs.filter((doc) =>\n orgAutomationRunsOnHost(doc.data() as OrgAutomation, hostId),\n )\n return docs.length ? { orgId, docs } : null\n } catch (error) {\n console.error('[org automations] lookup failed', hostId, event, error)\n return null\n }\n}\n\n/**\n * Continues one person waiting inside an org automation, on the site they\n * were enrolled on — the org twin of the site action's resume, asking the\n * same questions of the organization's document instead of the site's.\n *\n * - The kill switch is `orgs/{orgId}/automations/{id}`: deleted, switched\n * off, taken off this site, or paused on this site, and the person stops.\n * - The site must still belong to that organization, and its plan must\n * still carry the actions builder.\n * - The snapshot runs, never the automation's current steps.\n * - Counted on this site's action-run meter, never refused by it: the person\n * is already inside, and the gate belongs at enrollment.\n */\nexport async function resumeOrgAutomationEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const stop = (reason: string) => stopFlowEnrollment(enrollment, ref, reason)\n\n const orgId = String(enrollment.orgId ?? '')\n const doc = orgId\n ? await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n : null\n const automation = doc?.exists ? (doc.data() as OrgAutomation) : null\n if (!automation) return await stop('the org automation was deleted')\n const refusal = orgAutomationStopReason(automation, hostId)\n if (refusal) return await stop(refusal)\n\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!owner || owner.orgId !== orgId) {\n return await stop(\n 'this site is no longer part of the organization that runs the org automation',\n )\n }\n if (!checkEntitlement(owner.org as any, 'actions')) {\n return await stop('this site’s plan no longer includes automations')\n }\n\n const env: ActionRunEnv = {\n hostId,\n hostRef,\n alerts: [],\n // Admitted by the `actions` gate above.\n actionsAllowed: true,\n webhooksAllowed: checkEntitlement(owner.org as any, 'webhooks'),\n crmAllowed: checkEntitlement(owner.org as any, 'crm'),\n depth: 0,\n org: owner.org ?? null,\n orgId: owner.orgId,\n loadWorkflowContext: makeWorkflowContextLoader(hostRef),\n }\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on a site action's resume.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const ending = await executeAction(\n env,\n enrollment.actionId,\n automation,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps as HostActionStep[],\n enrollmentRef: ref,\n orgAutomation: { orgId },\n },\n )\n if (ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return ending\n }\n if (ending !== 'waiting') await endFlowEnrollment(ref)\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('actionRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return ending\n}\n"],"names":["checkEntitlement","FLOW_TIMED_OUT_FIELD","scopeTokensForHost","firebaseAdmin","getOrgForHost","resolveOrgIdForHost","FieldValue","isOrgAutomationTriggerEvent","MAX_TRIGGERED_ORG_AUTOMATIONS","ORG_AUTOMATIONS_COLLECTION","orgAutomationRunsOnHost","orgAutomationStopReason","deferFlowEnrollment","endFlowEnrollment","executeAction","FLOW_CLAIM_RETRY_MS","makeWorkflowContextLoader","stopFlowEnrollment","ORG_LOOKUP_TTL_MS","ORG_LOOKUP_MAX_ENTRIES","orgLookups","Map","resetOrgAutomationLookupsForTests","clear","orgIdForHost","hostId","nowMs","Date","now","known","get","atMs","orgId","size","set","findOrgAutomationsForEvent","event","snapshot","app","firestore","collection","doc","where","limit","docs","filter","data","length","error","console","resumeOrgAutomationEnrollment","enrollment","ref","options","owner","hostRef","stop","reason","String","actionId","catch","automation","exists","refusal","org","env","alerts","actionsAllowed","webhooksAllowed","crmAllowed","depth","loadWorkflowContext","payload","timedOut","ending","startIndex","nextStepIndex","steps","enrollmentRef","orgAutomation","monthKey","toISOString","slice","increment","merge","undefined"],"mappings":"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"}
@@ -0,0 +1,232 @@
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 HostActionStep, type HostActionTriggerCondition, type TriggerCombinator } from '@aglyn/aglyn/app-utils/actions';
18
+ import { type ScopeToken } from '@aglyn/aglyn/app-utils/scope-tokens';
19
+ /**
20
+ * ORG AUTOMATIONS (AGL-3302): one automation the organization writes once and
21
+ * places on the sites it chooses.
22
+ *
23
+ * A site's own actions stay where they are, at `hosts/{hostId}/actions`. They
24
+ * are not moved here, and the reasons are the four things a site action can
25
+ * do that an org automation must not:
26
+ *
27
+ * - call the site's functions and variables through a workflow step — the
28
+ * working set a computed variable binds to, which is one site's;
29
+ * - run the steps only a visitor's page can run (overlays, drawers, class
30
+ * toggles, a redirect to a screen), which the `actions` collection also
31
+ * holds for element interactions;
32
+ * - be fired by `/api/events/dispatch`, which is unauthenticated and ties an
33
+ * action id to a site only by its path;
34
+ * - wait, with the resume beat reading a missing document as the kill
35
+ * switch, so a migration would stop every person already inside a flow.
36
+ *
37
+ * An org automation is therefore its own document with a narrower
38
+ * vocabulary: triggers only the server raises, and steps that mean the same
39
+ * thing on whichever site they run. Each run executes AS the site the event
40
+ * happened on — that site's sender, suppressions, consent group, meters and
41
+ * run allowance — so placing one automation on five sites is five sites each
42
+ * running it for themselves, never one site acting for the others.
43
+ *
44
+ * Pure and client-safe: the console's editor, the server routes and the
45
+ * engine read the same definitions from here.
46
+ */
47
+ /** `orgs/{orgId}/automations/{automationId}`. */
48
+ export declare const ORG_AUTOMATIONS_COLLECTION = "automations";
49
+ /**
50
+ * The two server doors onto the collection, under the plugin's `automations`
51
+ * API prefix: the organization's (`manage`) and a site's own pause
52
+ * (`pause`). Named here so the console's caller and the server's registration
53
+ * read one spelling.
54
+ */
55
+ export declare const ORG_AUTOMATION_API_ROUTES: {
56
+ readonly manage: "automations/manage";
57
+ readonly pause: "automations/pause";
58
+ };
59
+ /**
60
+ * How many live org automations one organization may hold.
61
+ *
62
+ * A flat platform cap in the shape `ACTIONS_MAX_PER_HOST` takes, and smaller,
63
+ * because the two are different things: a site action is authored per site
64
+ * and per element, where an org automation is a shared building block placed
65
+ * on many sites at once. A hundred is far past what an organization writes by
66
+ * hand and keeps the org hub's list — which reads every live one — a single
67
+ * bounded window. Soft-deleted documents do not count.
68
+ */
69
+ export declare const ORG_AUTOMATIONS_MAX = 100;
70
+ /**
71
+ * How many org automations one event may run on one site: the same fan-out
72
+ * bound the site's own actions take (`MAX_TRIGGERED_ACTIONS`), counted
73
+ * separately so the two cannot crowd each other out of the query.
74
+ */
75
+ export declare const MAX_TRIGGERED_ORG_AUTOMATIONS = 10;
76
+ /**
77
+ * The run history's target type for a run of an org automation, on the
78
+ * activity feed of the site it ran on.
79
+ */
80
+ export declare const ORG_AUTOMATION_RUN_TARGET = "orgAutomation";
81
+ /**
82
+ * The org activity target an org automation's own lifecycle rows are filed
83
+ * under: the plugin's namespaced resource (`pluginId:noun`), which the org's
84
+ * feed reads as "Automation" — core's target list names only core's resources.
85
+ */
86
+ export declare const ORG_AUTOMATION_ACTIVITY_TARGET = "workflows:automation";
87
+ /**
88
+ * The events an org automation may start on: every host event the SERVER
89
+ * raises and a person can be the subject of.
90
+ *
91
+ * Listed rather than derived from `HOST_EVENT_TYPES`, deliberately. The engine
92
+ * queries the organization's automations on every event in this list, so a
93
+ * new host event must not become an org trigger — and a new read on every
94
+ * occurrence of it — by being added to the platform's list. Two are left out
95
+ * on purpose: `pageView` fires on every request to every published page, and
96
+ * an org query on it would be a read per visitor; `memberSignOut` is raised
97
+ * by no door today.
98
+ */
99
+ export declare const ORG_AUTOMATION_TRIGGER_EVENTS: readonly ["formSubmission", "lead", "contactCreated", "contactStageChanged", "booking", "memberSignUp", "memberSignIn", "dealStageChanged", "dealWon", "dealLost", "taskCompleted"];
100
+ export type OrgAutomationTriggerEvent = (typeof ORG_AUTOMATION_TRIGGER_EVENTS)[number];
101
+ /** Whether an org automation may start on this event. */
102
+ export declare function isOrgAutomationTriggerEvent(event: unknown): event is OrgAutomationTriggerEvent;
103
+ /**
104
+ * The steps an org automation may hold: the server steps that read the same
105
+ * on every site.
106
+ *
107
+ * What is left out, and why:
108
+ * - every step the visitor's page runs — overlays, drawers, menus, class
109
+ * toggles, custom HTML and JavaScript, analytics, redirects, the site
110
+ * alert — because an org trigger is a server event with no page to act on;
111
+ * - `runWorkflow`, because a workflow is one site's and calls that site's
112
+ * functions and variables;
113
+ * - `webhookPost`, because a webhook's address and secret are one site's.
114
+ *
115
+ * `customEvent` stays: it fires a custom event ON the site the run is on,
116
+ * which that site's own actions may listen for — the documented way for an
117
+ * org automation to hand over to something site-specific.
118
+ */
119
+ export declare const ORG_AUTOMATION_STEP_TYPES: readonly ["sendEmail", "notifyAdmins", "enrollList", "assignCampaign", "datasetAppend", "updateDataset", "setContactStage", "addContactTag", "assignContactOwner", "createCrmTask", "logCrmActivity", "customEvent", "wait", "waitForEvent", "exitFlow"];
120
+ export type OrgAutomationStepType = (typeof ORG_AUTOMATION_STEP_TYPES)[number];
121
+ /** Whether an org automation may run a step of this type. */
122
+ export declare function isOrgAutomationStepType(type: unknown): type is OrgAutomationStepType;
123
+ /** The org editor's "Do" picker: each allowed step with its label, in order. */
124
+ export declare const ORG_AUTOMATION_STEP_KINDS: ReadonlyArray<{
125
+ value: OrgAutomationStepType;
126
+ label: string;
127
+ }>;
128
+ /**
129
+ * Why an org automation cannot hold this step, or null when it can.
130
+ *
131
+ * Shared by the save route, which refuses it, and by the engine, which
132
+ * refuses to run one a writer stored anyway.
133
+ */
134
+ export declare function orgAutomationStepRefusal(step: {
135
+ type?: unknown;
136
+ }): string | null;
137
+ /** The trigger an org automation starts on, as it is stored. */
138
+ export interface OrgAutomationTrigger {
139
+ event: OrgAutomationTriggerEvent;
140
+ /** Optional expression over the payload; runs only when truthy. */
141
+ filter?: string;
142
+ conditions: HostActionTriggerCondition[] | null;
143
+ combinator: TriggerCombinator | null;
144
+ }
145
+ /**
146
+ * The fields an org automation is written with — by the save route, and by
147
+ * nothing else: the collection is closed to client writes.
148
+ */
149
+ export interface OrgAutomationFields {
150
+ name: string;
151
+ trigger: OrgAutomationTrigger;
152
+ steps: HostActionStep[];
153
+ enabled: boolean;
154
+ /**
155
+ * The sites it runs on, as scope tokens: `['org']` is every site of the
156
+ * organization, now and later; `['host:{id}', …]` names up to thirty.
157
+ */
158
+ visibleTo: ScopeToken[];
159
+ }
160
+ /** `orgs/{orgId}/automations/{id}`, as it is stored. */
161
+ export interface OrgAutomation extends OrgAutomationFields {
162
+ /**
163
+ * The sites that have paused it for themselves — each a site id, never a
164
+ * scope token. The "host level control": a site's admin or editor adds or
165
+ * removes their own site here, and only their own.
166
+ */
167
+ pausedHostIds: string[];
168
+ createdBy?: string;
169
+ updatedBy?: string;
170
+ /**
171
+ * Set when it was deleted. Always written — `null` while live — so the
172
+ * org hub's list can ask for the live ones by equality.
173
+ */
174
+ deletedAt: unknown;
175
+ }
176
+ /** A stored org automation with its id, as the console lists it. */
177
+ export type OrgAutomationRow = OrgAutomation & {
178
+ $id: string;
179
+ };
180
+ /** Longest name an org automation may carry: the actions editor's cap. */
181
+ export declare const ORG_AUTOMATION_NAME_MAX = 60;
182
+ /** What a caller sends to create or replace an org automation. */
183
+ export type OrgAutomationInput = Record<string, unknown>;
184
+ /**
185
+ * Reads an org automation out of an untrusted request body: the fields it may
186
+ * hold, in the shape they are stored, or the first reason it cannot be saved.
187
+ *
188
+ * The rules are the actions builder's own — `validateHostAction` checks the
189
+ * name, the trigger's conditions and every step's fields — and on top of them
190
+ * the two this vocabulary adds: the trigger must be an org trigger and every
191
+ * step an org step. The placement must be a scope a save can store:
192
+ * `normalizeVisibleTo` refuses an empty one and one over thirty sites rather
193
+ * than guessing between sharing wider and taking access away.
194
+ *
195
+ * Whether each named site belongs to the organization is the route's to ask,
196
+ * because it takes a read.
197
+ */
198
+ export declare function readOrgAutomation(input: unknown): {
199
+ ok: true;
200
+ value: OrgAutomationFields;
201
+ } | {
202
+ ok: false;
203
+ problem: string;
204
+ };
205
+ /** A stored document's pause list, read defensively. */
206
+ export declare function orgAutomationPausedHostIds(automation: {
207
+ pausedHostIds?: unknown;
208
+ } | null | undefined): string[];
209
+ /**
210
+ * Why a stored org automation will not run on this site, or null when it
211
+ * will — the one answer the engine asks when an event arrives and again when
212
+ * a person waiting inside the automation is due.
213
+ *
214
+ * Deleted, switched off, taken off the site and paused there are the four
215
+ * ways an organization or a site says "not here", and every one of them
216
+ * stops the people already waiting on this site as well as the next run: the
217
+ * kill switch a site action's resume applies, for the same reason.
218
+ */
219
+ export declare function orgAutomationStopReason(automation: {
220
+ deletedAt?: unknown;
221
+ enabled?: unknown;
222
+ visibleTo?: unknown;
223
+ pausedHostIds?: unknown;
224
+ } | null | undefined, hostId: string): string | null;
225
+ /** Whether a stored org automation runs on this site. */
226
+ export declare function orgAutomationRunsOnHost(automation: Parameters<typeof orgAutomationStopReason>[0], hostId: string): boolean;
227
+ /**
228
+ * A pause list after the placement changed: a site the automation no longer
229
+ * runs on has nothing left to pause, so its entry goes with it, and a site
230
+ * placed on it again later starts unpaused.
231
+ */
232
+ export declare function prunePausedHostIds(pausedHostIds: readonly string[], visibleTo: readonly string[]): string[];