@aglyn/plugins-workflows 1.0.0-beta.285 → 1.0.0-beta.288
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -12
- package/src/lib/declarations.js +8 -0
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/model/workflows-permissions.d.ts +26 -0
- package/src/lib/model/workflows-permissions.js +60 -0
- package/src/lib/model/workflows-permissions.js.map +1 -0
- package/src/lib/server/action-test-run-route.d.ts +0 -38
- package/src/lib/server/action-test-run-route.js +5 -5
- package/src/lib/server/action-test-run-route.js.map +1 -1
- package/src/lib/server/org-automations-routes.d.ts +3 -0
- package/src/lib/server/org-automations-routes.js +17 -19
- package/src/lib/server/org-automations-routes.js.map +1 -1
- package/src/lib/server-automation-drafts.d.ts +2 -1
- package/src/lib/server-automation-drafts.js +4 -6
- package/src/lib/server-automation-drafts.js.map +1 -1
- package/src/lib/transfer/org-automations-package.server.d.ts +2 -0
- package/src/lib/transfer/org-automations-package.server.js +7 -8
- package/src/lib/transfer/org-automations-package.server.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-workflows",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.288",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.288",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.288",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.288",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.288",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.288",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.288",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.288",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.288",
|
|
36
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.288",
|
|
37
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.288",
|
|
38
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.288",
|
|
39
39
|
"@swc/helpers": "0.5.23"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
package/src/lib/declarations.js
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
// this at boot, and the barrel reaches the client contexts.
|
|
18
18
|
import { registerInteractionStepChecks } from "@aglyn/aglyn/plugin-manager/interaction-step-checks";
|
|
19
19
|
import { registerPluginActivityActions } from "@aglyn/aglyn/plugin-manager/plugin-activity-actions";
|
|
20
|
+
import { registerPluginEntitlements } from "@aglyn/aglyn/plugin-manager/plugin-entitlements";
|
|
20
21
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
21
22
|
import { hostActionStepProblem, SERVER_ACTION_STEP_TYPES } from "./model/host-actions.js";
|
|
22
23
|
import { describeAutomationRunAction } from "./model/run-activity-words.js";
|
|
24
|
+
import { WORKFLOWS_PERMISSIONS } from "./model/workflows-permissions.js";
|
|
23
25
|
/**
|
|
24
26
|
* The automation editor's DECLARATIONS: what another surface reads of this
|
|
25
27
|
* plugin before any of its own surfaces load, in both apps and in the
|
|
@@ -31,6 +33,12 @@ import { describeAutomationRunAction } from "./model/run-activity-words.js";
|
|
|
31
33
|
* this editor would (`validateStoredInteraction`). Light by construction: the
|
|
32
34
|
* checks are pure, and nothing here reaches Firestore or React.
|
|
33
35
|
*/ export function registerWorkflowsDeclarations() {
|
|
36
|
+
// The org automations' keys (AGL-4047): the one permission catalog lists
|
|
37
|
+
// them beside the core keys.
|
|
38
|
+
registerPluginEntitlements({
|
|
39
|
+
pluginId: BUNDLE_ID,
|
|
40
|
+
orgPermissions: WORKFLOWS_PERMISSIONS
|
|
41
|
+
});
|
|
34
42
|
registerInteractionStepChecks(SERVER_ACTION_STEP_TYPES, hostActionStepProblem, {
|
|
35
43
|
pluginId: BUNDLE_ID
|
|
36
44
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/declarations.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// The seam from its own module, not the plugin-manager barrel: both apps load\n// this at boot, and the barrel reaches the client contexts.\nimport { registerInteractionStepChecks } from '@aglyn/aglyn/plugin-manager/interaction-step-checks'\nimport { registerPluginActivityActions } from '@aglyn/aglyn/plugin-manager/plugin-activity-actions'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { hostActionStepProblem, SERVER_ACTION_STEP_TYPES } from './model/host-actions'\nimport { describeAutomationRunAction } from './model/run-activity-words'\n\n/**\n * The automation editor's DECLARATIONS: what another surface reads of this\n * plugin before any of its own surfaces load, in both apps and in the\n * console's browser.\n *\n * The checks of the server steps an automation holds (AGL-3080), so a plugin\n * that writes an automation it does not edit — a recipe installed into a\n * site, a drafted automation graded before it is handed over — refuses what\n * this editor would (`validateStoredInteraction`). Light by construction: the\n * checks are pure, and nothing here reaches Firestore or React.\n */\nexport function registerWorkflowsDeclarations(): void {\n registerInteractionStepChecks(SERVER_ACTION_STEP_TYPES, hostActionStepProblem, { pluginId: BUNDLE_ID })\n // The run lines the engine files in a site's activity read in words in every feed and the apps' activity rows (AGL-3892).\n registerPluginActivityActions({\n pluginId: BUNDLE_ID,\n group: { id: 'automation', label: 'Automation' },\n actions: [],\n describeAction: describeAutomationRunAction,\n })\n}\n"],"names":["registerInteractionStepChecks","registerPluginActivityActions","BUNDLE_ID","hostActionStepProblem","SERVER_ACTION_STEP_TYPES","describeAutomationRunAction","registerWorkflowsDeclarations","pluginId","group","id","label","actions","describeAction"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,4DAA4D;AAC5D,SAASA,6BAA6B,QAAQ,sDAAqD;AACnG,SAASC,6BAA6B,QAAQ,sDAAqD;AACnG,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,qBAAqB,EAAEC,wBAAwB,QAAQ,0BAAsB;AACtF,SAASC,2BAA2B,QAAQ,gCAA4B;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/declarations.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// The seam from its own module, not the plugin-manager barrel: both apps load\n// this at boot, and the barrel reaches the client contexts.\nimport { registerInteractionStepChecks } from '@aglyn/aglyn/plugin-manager/interaction-step-checks'\nimport { registerPluginActivityActions } from '@aglyn/aglyn/plugin-manager/plugin-activity-actions'\nimport { registerPluginEntitlements } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { hostActionStepProblem, SERVER_ACTION_STEP_TYPES } from './model/host-actions'\nimport { describeAutomationRunAction } from './model/run-activity-words'\nimport { WORKFLOWS_PERMISSIONS } from './model/workflows-permissions'\n\n/**\n * The automation editor's DECLARATIONS: what another surface reads of this\n * plugin before any of its own surfaces load, in both apps and in the\n * console's browser.\n *\n * The checks of the server steps an automation holds (AGL-3080), so a plugin\n * that writes an automation it does not edit — a recipe installed into a\n * site, a drafted automation graded before it is handed over — refuses what\n * this editor would (`validateStoredInteraction`). Light by construction: the\n * checks are pure, and nothing here reaches Firestore or React.\n */\nexport function registerWorkflowsDeclarations(): void {\n // The org automations' keys (AGL-4047): the one permission catalog lists\n // them beside the core keys.\n registerPluginEntitlements({ pluginId: BUNDLE_ID, orgPermissions: WORKFLOWS_PERMISSIONS })\n registerInteractionStepChecks(SERVER_ACTION_STEP_TYPES, hostActionStepProblem, { pluginId: BUNDLE_ID })\n // The run lines the engine files in a site's activity read in words in every feed and the apps' activity rows (AGL-3892).\n registerPluginActivityActions({\n pluginId: BUNDLE_ID,\n group: { id: 'automation', label: 'Automation' },\n actions: [],\n describeAction: describeAutomationRunAction,\n })\n}\n"],"names":["registerInteractionStepChecks","registerPluginActivityActions","registerPluginEntitlements","BUNDLE_ID","hostActionStepProblem","SERVER_ACTION_STEP_TYPES","describeAutomationRunAction","WORKFLOWS_PERMISSIONS","registerWorkflowsDeclarations","pluginId","orgPermissions","group","id","label","actions","describeAction"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,4DAA4D;AAC5D,SAASA,6BAA6B,QAAQ,sDAAqD;AACnG,SAASC,6BAA6B,QAAQ,sDAAqD;AACnG,SAASC,0BAA0B,QAAQ,kDAAiD;AAC5F,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,qBAAqB,EAAEC,wBAAwB,QAAQ,0BAAsB;AACtF,SAASC,2BAA2B,QAAQ,gCAA4B;AACxE,SAASC,qBAAqB,QAAQ,mCAA+B;AAErE;;;;;;;;;;CAUC,GACD,OAAO,SAASC;IACd,yEAAyE;IACzE,6BAA6B;IAC7BN,2BAA2B;QAAEO,UAAUN;QAAWO,gBAAgBH;IAAsB;IACxFP,8BAA8BK,0BAA0BD,uBAAuB;QAAEK,UAAUN;IAAU;IACrG,0HAA0H;IAC1HF,8BAA8B;QAC5BQ,UAAUN;QACVQ,OAAO;YAAEC,IAAI;YAAcC,OAAO;QAAa;QAC/CC,SAAS,EAAE;QACXC,gBAAgBT;IAClB;AACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
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 { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types';
|
|
18
|
+
import type { PluginOrgPermissionDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-entitlements';
|
|
19
|
+
/** Workflows' keys. */
|
|
20
|
+
export type WorkflowsPermission = 'workflows.orgAutomations' | 'workflows.sitePause';
|
|
21
|
+
/** Who paused an org automation on a site before it was a key: the site's Admin and Editor. */
|
|
22
|
+
export declare const WORKFLOWS_SITE_PAUSE_PRESET: Readonly<Record<HostAccessRole, boolean>>;
|
|
23
|
+
/** The keys as the plugin declares them into the org permission catalog. */
|
|
24
|
+
export declare const WORKFLOWS_PERMISSIONS: readonly PluginOrgPermissionDeclaration[];
|
|
25
|
+
/** The refusal a door answers without one: names the permission, never a role. */
|
|
26
|
+
export declare function workflowsPermissionRefusal(key: WorkflowsPermission): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
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 _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
/** Who paused an org automation on a site before it was a key: the site's Admin and Editor. */ export const WORKFLOWS_SITE_PAUSE_PRESET = {
|
|
18
|
+
admin: true,
|
|
19
|
+
editor: true,
|
|
20
|
+
author: false,
|
|
21
|
+
viewer: false
|
|
22
|
+
};
|
|
23
|
+
/** The keys as the plugin declares them into the org permission catalog. */ export const WORKFLOWS_PERMISSIONS = [
|
|
24
|
+
{
|
|
25
|
+
key: 'workflows.orgAutomations',
|
|
26
|
+
label: 'Manage org automations',
|
|
27
|
+
description: 'Create, change, switch on and off, remove and import the organization’s automations. Members of the whole workspace only.',
|
|
28
|
+
roleDefaults: {
|
|
29
|
+
owner: true,
|
|
30
|
+
admin: true,
|
|
31
|
+
editor: true,
|
|
32
|
+
viewer: false
|
|
33
|
+
},
|
|
34
|
+
group: 'workflows',
|
|
35
|
+
groupLabel: 'Workflows'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'workflows.sitePause',
|
|
39
|
+
label: 'Pause org automations on a site',
|
|
40
|
+
description: 'Pause or resume one of the organization’s automations on a site. Follows the site role unless a role or member sets it; "Manage org automations" also allows it.',
|
|
41
|
+
roleDefaults: {
|
|
42
|
+
owner: true,
|
|
43
|
+
admin: true,
|
|
44
|
+
editor: true,
|
|
45
|
+
viewer: false
|
|
46
|
+
},
|
|
47
|
+
hostRoleDefaults: _extends({}, WORKFLOWS_SITE_PAUSE_PRESET),
|
|
48
|
+
group: 'workflows',
|
|
49
|
+
groupLabel: 'Workflows',
|
|
50
|
+
presetFromSiteRole: true
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
/** The refusal a door answers without one: names the permission, never a role. */ export function workflowsPermissionRefusal(key) {
|
|
54
|
+
var _ref;
|
|
55
|
+
var _WORKFLOWS_PERMISSIONS_find;
|
|
56
|
+
const label = (_ref = (_WORKFLOWS_PERMISSIONS_find = WORKFLOWS_PERMISSIONS.find((entry)=>entry.key === key)) == null ? void 0 : _WORKFLOWS_PERMISSIONS_find.label) != null ? _ref : key;
|
|
57
|
+
return `Your role does not include "${label}" — ask an organization admin`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=workflows-permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/workflows-permissions.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 { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport type { PluginOrgPermissionDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\n\n/*\n * WORKFLOWS' KEYS (AGL-4047).\n *\n * The organization's automations were written by an owner, admin or editor\n * of the whole workspace, and paused on one site by that site's Admin or\n * Editor (or by one of the workspace's managers). Those are now keys:\n * \"Manage org automations\", whose default is the workspace role, and \"Pause\n * org automations on a site\", whose preset is the site role\n * (`presetFromSiteRole`). Every member keeps exactly the access they had,\n * and a custom role, a member's own permissions or a site's toggle can grant\n * or refuse either one.\n */\n\n/** Workflows' keys. */\nexport type WorkflowsPermission = 'workflows.orgAutomations' | 'workflows.sitePause'\n\n/** Who paused an org automation on a site before it was a key: the site's Admin and Editor. */\nexport const WORKFLOWS_SITE_PAUSE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: true,\n author: false,\n viewer: false,\n}\n\n/** The keys as the plugin declares them into the org permission catalog. */\nexport const WORKFLOWS_PERMISSIONS: readonly PluginOrgPermissionDeclaration[] = [\n {\n key: 'workflows.orgAutomations',\n label: 'Manage org automations',\n description:\n 'Create, change, switch on and off, remove and import the organization’s automations. Members of the whole workspace only.',\n roleDefaults: { owner: true, admin: true, editor: true, viewer: false },\n group: 'workflows',\n groupLabel: 'Workflows',\n },\n {\n key: 'workflows.sitePause',\n label: 'Pause org automations on a site',\n description:\n 'Pause or resume one of the organization’s automations on a site. Follows the site role unless a role or member sets it; \"Manage org automations\" also allows it.',\n roleDefaults: { owner: true, admin: true, editor: true, viewer: false },\n hostRoleDefaults: { ...WORKFLOWS_SITE_PAUSE_PRESET },\n group: 'workflows',\n groupLabel: 'Workflows',\n presetFromSiteRole: true,\n },\n]\n\n/** The refusal a door answers without one: names the permission, never a role. */\nexport function workflowsPermissionRefusal(key: WorkflowsPermission): string {\n const label = WORKFLOWS_PERMISSIONS.find((entry) => entry.key === key)?.label ?? key\n return `Your role does not include \"${label}\" — ask an organization admin`\n}\n"],"names":["WORKFLOWS_SITE_PAUSE_PRESET","admin","editor","author","viewer","WORKFLOWS_PERMISSIONS","key","label","description","roleDefaults","owner","group","groupLabel","hostRoleDefaults","presetFromSiteRole","workflowsPermissionRefusal","find","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAqBD,6FAA6F,GAC7F,OAAO,MAAMA,8BAAyE;IACpFC,OAAO;IACPC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;AACV,EAAC;AAED,0EAA0E,GAC1E,OAAO,MAAMC,wBAAmE;IAC9E;QACEC,KAAK;QACLC,OAAO;QACPC,aACE;QACFC,cAAc;YAAEC,OAAO;YAAMT,OAAO;YAAMC,QAAQ;YAAME,QAAQ;QAAM;QACtEO,OAAO;QACPC,YAAY;IACd;IACA;QACEN,KAAK;QACLC,OAAO;QACPC,aACE;QACFC,cAAc;YAAEC,OAAO;YAAMT,OAAO;YAAMC,QAAQ;YAAME,QAAQ;QAAM;QACtES,kBAAkB,aAAKb;QACvBW,OAAO;QACPC,YAAY;QACZE,oBAAoB;IACtB;CACD,CAAA;AAED,gFAAgF,GAChF,OAAO,SAASC,2BAA2BT,GAAwB;;QACnDD;IAAd,MAAME,iBAAQF,8BAAAA,sBAAsBW,IAAI,CAAC,CAACC,QAAUA,MAAMX,GAAG,KAAKA,yBAApDD,4BAA0DE,KAAK,mBAAID;IACjF,OAAO,CAAC,4BAA4B,EAAEC,MAAM,6BAA6B,CAAC;AAC5E"}
|
|
@@ -14,44 +14,6 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
/**
|
|
18
|
-
* TESTING ONE OF A SITE'S ACTIONS FROM THE CONSOLE (AGL-3309).
|
|
19
|
-
*
|
|
20
|
-
* The Actions card's Test button runs an action with an in-page trigger as
|
|
21
|
-
* though the trigger had fired on the site, and says what came back. A
|
|
22
|
-
* published page reaches the same engine path through `events/dispatch`,
|
|
23
|
-
* which is a TENANT route — served on the site's own origin, to anonymous
|
|
24
|
-
* visitors — and the console is another app on another origin whose
|
|
25
|
-
* dispatcher never loads it. This is the console's door to that path, and
|
|
26
|
-
* unlike the page's it knows who is asking.
|
|
27
|
-
*
|
|
28
|
-
* ## Who may
|
|
29
|
-
*
|
|
30
|
-
* A site admin or editor, read off the host's `memberRoles`, or staff. The
|
|
31
|
-
* publish roles rather than the content roles, because a test run is a LIVE
|
|
32
|
-
* run: its emails are sent, its webhooks posted and its rows written, as a
|
|
33
|
-
* visitor's would be. An author edits and makes nothing live, and so does not
|
|
34
|
-
* run automations either.
|
|
35
|
-
*
|
|
36
|
-
* The site's plugin switch is the dispatcher's: the request names its site as
|
|
37
|
-
* a top-level `hostId`, so a site with Automation switched off has no such
|
|
38
|
-
* door.
|
|
39
|
-
*
|
|
40
|
-
* ## What runs
|
|
41
|
-
*
|
|
42
|
-
* The stored action, through `runSingleActionOutcome` — the single-action path
|
|
43
|
-
* a page's dispatch takes — so the switch, the conditions, the plan gate and
|
|
44
|
-
* the month's run allowance are the ones a real run meets, the run is metered
|
|
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`.
|
|
48
|
-
* When a gate stops it, the refusal names the gate, because a test that ran
|
|
49
|
-
* nothing must not read as one that ran.
|
|
50
|
-
*
|
|
51
|
-
* Only an in-page trigger: a server event (a form submitted, an order placed)
|
|
52
|
-
* carries fields no sample stands in for, and its runs come from the doors
|
|
53
|
-
* that raise it.
|
|
54
|
-
*/
|
|
55
17
|
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
56
18
|
import type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
|
|
57
19
|
import type { SingleActionOutcome } from '../engine/run-event-actions';
|
|
@@ -51,7 +51,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
51
51
|
* Only an in-page trigger: a server event (a form submitted, an order placed)
|
|
52
52
|
* carries fields no sample stands in for, and its runs come from the doors
|
|
53
53
|
* that raise it.
|
|
54
|
-
*/ import {
|
|
54
|
+
*/ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
55
|
+
import { isSiteEventType, planLabelGrantingFeature } from "@aglyn/aglyn/server";
|
|
55
56
|
/*
|
|
56
57
|
* The MODULES, not the barrel, as for the org automation doors beside this
|
|
57
58
|
* one: `@aglyn/tenant-data-admin`'s index reaches the Next render cache.
|
|
@@ -183,7 +184,7 @@ function defaultDeps() {
|
|
|
183
184
|
});
|
|
184
185
|
}
|
|
185
186
|
try {
|
|
186
|
-
var
|
|
187
|
+
var _ref2, _outcome_skipped;
|
|
187
188
|
var _stored_data_trigger, _stored_data, _req_body2;
|
|
188
189
|
const firestore = deps.firestore();
|
|
189
190
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
@@ -191,10 +192,9 @@ function defaultDeps() {
|
|
|
191
192
|
if (!host.exists) return res.status(404).json({
|
|
192
193
|
error: 'Unknown site'
|
|
193
194
|
});
|
|
194
|
-
|
|
195
|
-
if (caller.staff !== true && !hostRoleCanPublish(role)) {
|
|
195
|
+
if (caller.staff !== true && !holdsSiteKey(host, caller.uid, 'site.publish')) {
|
|
196
196
|
return res.status(403).json({
|
|
197
|
-
error: '
|
|
197
|
+
error: siteKeyRefusal('site.publish')
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
const rate = await deps.consumeRateLimit(`workflows-test-run:${hostId}:${caller.uid}`, {
|
|
@@ -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 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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"}
|
|
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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport {\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 if (caller.staff !== true && !holdsSiteKey(host, caller.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\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":["holdsSiteKey","siteKeyRefusal","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","stored","hostRef","collection","doc","host","get","exists","rate","windowMs","allowed","setHeader","Math","max","ceil","resetMs","data","trigger","ran","ok","alerts","refusal","actionTestRunHandler"],"mappings":"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"}
|
|
@@ -15,6 +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 { type WorkflowsPermission } from '../model/workflows-permissions';
|
|
18
19
|
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATION_API_ROUTES, type OrgAutomationFields } from '../model/org-automations';
|
|
19
20
|
export { ORG_AUTOMATION_API_ROUTES };
|
|
20
21
|
/** What `automations/manage` may be asked to do. */
|
|
@@ -32,6 +33,8 @@ export interface OrgAutomationRouteDeps {
|
|
|
32
33
|
} | null>;
|
|
33
34
|
getOrgDoc(orgId: string): Promise<Record<string, unknown> | null>;
|
|
34
35
|
resolveOrgIdForHost(hostId: string): Promise<string | null>;
|
|
36
|
+
/** Whether a member holds a workspace key with no site named (AGL-4047): `memberHasOrgPermission`. */
|
|
37
|
+
holdsOrgKey(orgId: string, member: Partial<AglynOrgMember>, key: WorkflowsPermission): Promise<boolean>;
|
|
35
38
|
logOrgActivity(orgId: string, actor: {
|
|
36
39
|
uid: string;
|
|
37
40
|
email?: string | null;
|
|
@@ -37,6 +37,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
37
37
|
* THIS site. The site's own admins and editors may use it, and so may an
|
|
38
38
|
* org-wide editor; nobody may pause a site through it but the one named.
|
|
39
39
|
*/ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
40
|
+
import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
40
41
|
import { hostIdsFromScope, visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
41
42
|
import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
42
43
|
import { checkEntitlement, isOrgWideMember, planLabelGrantingFeature } from "@aglyn/aglyn/server";
|
|
@@ -46,9 +47,10 @@ import { checkEntitlement, isOrgWideMember, planLabelGrantingFeature } from "@ag
|
|
|
46
47
|
* the id predicate and the org reads.
|
|
47
48
|
*/ import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
|
|
48
49
|
import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
49
|
-
import { getOrgDoc, logHostActivity, logOrgActivity, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
|
|
50
|
+
import { getOrgDoc, logHostActivity, logOrgActivity, memberHasOrgPermission, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
|
|
50
51
|
import { FieldValue } from "firebase-admin/firestore";
|
|
51
52
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
53
|
+
import { WORKFLOWS_SITE_PAUSE_PRESET, workflowsPermissionRefusal } from "../model/workflows-permissions.js";
|
|
52
54
|
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATION_API_ROUTES, ORG_AUTOMATIONS_COLLECTION, ORG_AUTOMATIONS_MAX, orgAutomationPausedHostIds, prunePausedHostIds, readOrgAutomation } from "../model/org-automations.js";
|
|
53
55
|
export { ORG_AUTOMATION_API_ROUTES };
|
|
54
56
|
/** What `automations/manage` may be asked to do. */ export const ORG_AUTOMATION_MANAGE_ACTIONS = [
|
|
@@ -57,15 +59,6 @@ export { ORG_AUTOMATION_API_ROUTES };
|
|
|
57
59
|
'setEnabled',
|
|
58
60
|
'delete'
|
|
59
61
|
];
|
|
60
|
-
/** The org roles that may write the organization's automations. */ const ORG_MANAGE_ROLES = [
|
|
61
|
-
'owner',
|
|
62
|
-
'admin',
|
|
63
|
-
'editor'
|
|
64
|
-
];
|
|
65
|
-
/** The site roles that may pause an org automation on their own site. */ const SITE_PAUSE_ROLES = [
|
|
66
|
-
'admin',
|
|
67
|
-
'editor'
|
|
68
|
-
];
|
|
69
62
|
function defaultDeps() {
|
|
70
63
|
return {
|
|
71
64
|
firestore: ()=>firebaseAdmin.app().firestore(),
|
|
@@ -83,6 +76,7 @@ function defaultDeps() {
|
|
|
83
76
|
return (_ref = await getOrgDoc(orgId)) != null ? _ref : null;
|
|
84
77
|
},
|
|
85
78
|
resolveOrgIdForHost,
|
|
79
|
+
holdsOrgKey: (orgId, member, key)=>memberHasOrgPermission(orgId, member, key),
|
|
86
80
|
logOrgActivity: (orgId, actor, action, target)=>logOrgActivity(orgId, actor, action, target),
|
|
87
81
|
logHostActivity: (hostId, actor, action, target)=>logHostActivity(hostId, actor, action, target),
|
|
88
82
|
newId: createResourceUid
|
|
@@ -264,7 +258,7 @@ function automationsOf(firestore, orgId) {
|
|
|
264
258
|
error: 'Unauthenticated'
|
|
265
259
|
});
|
|
266
260
|
try {
|
|
267
|
-
var
|
|
261
|
+
var _caller_email;
|
|
268
262
|
var _req_body3;
|
|
269
263
|
const caller = await deps.verifyIdToken(token).catch(()=>null);
|
|
270
264
|
if (!caller) return res.status(401).json({
|
|
@@ -272,9 +266,12 @@ function automationsOf(firestore, orgId) {
|
|
|
272
266
|
});
|
|
273
267
|
const membership = await deps.resolveOrgMembership(caller.uid, orgId);
|
|
274
268
|
const member = membership == null ? void 0 : membership.member;
|
|
275
|
-
|
|
269
|
+
// "Manage org automations" (AGL-4047), for members of the whole
|
|
270
|
+
// workspace: its default is the owner, admins and editors this asked
|
|
271
|
+
// for before, then their custom role and their own permissions.
|
|
272
|
+
if (!member || !isOrgWideMember(member) || !await deps.holdsOrgKey(orgId, member, 'workflows.orgAutomations')) {
|
|
276
273
|
return res.status(403).json({
|
|
277
|
-
error: '
|
|
274
|
+
error: workflowsPermissionRefusal('workflows.orgAutomations')
|
|
278
275
|
});
|
|
279
276
|
}
|
|
280
277
|
/*
|
|
@@ -437,7 +434,7 @@ function automationsOf(firestore, orgId) {
|
|
|
437
434
|
error: 'Unauthenticated'
|
|
438
435
|
});
|
|
439
436
|
try {
|
|
440
|
-
var _ref2,
|
|
437
|
+
var _ref2, _caller_email;
|
|
441
438
|
var _req_body3;
|
|
442
439
|
const caller = await deps.verifyIdToken(token).catch(()=>null);
|
|
443
440
|
if (!caller) return res.status(401).json({
|
|
@@ -465,17 +462,18 @@ function automationsOf(firestore, orgId) {
|
|
|
465
462
|
* projection — the same answer every site door asks — or an org-wide
|
|
466
463
|
* editor, asked of the membership itself so a projection that has not
|
|
467
464
|
* caught up cannot refuse the organization's own managers.
|
|
468
|
-
*/
|
|
469
|
-
|
|
465
|
+
*/ // "Pause org automations on a site" (AGL-4047): the site's stamp, else
|
|
466
|
+
// the preset of the member's site role, the Admin and Editor this asked
|
|
467
|
+
// for; or "Manage org automations" for a member of the whole workspace.
|
|
468
|
+
let allowed = holdsStampedSiteKey(host, caller.uid, 'workflows.sitePause', WORKFLOWS_SITE_PAUSE_PRESET);
|
|
470
469
|
if (!allowed) {
|
|
471
|
-
var _ref3;
|
|
472
470
|
const membership = await deps.resolveOrgMembership(caller.uid, orgId);
|
|
473
471
|
const member = membership == null ? void 0 : membership.member;
|
|
474
|
-
allowed = Boolean(member) && isOrgWideMember(member) &&
|
|
472
|
+
allowed = Boolean(member) && isOrgWideMember(member) && await deps.holdsOrgKey(orgId, member, 'workflows.orgAutomations');
|
|
475
473
|
}
|
|
476
474
|
if (!allowed) {
|
|
477
475
|
return res.status(403).json({
|
|
478
|
-
error: '
|
|
476
|
+
error: workflowsPermissionRefusal('workflows.sitePause')
|
|
479
477
|
});
|
|
480
478
|
}
|
|
481
479
|
const name = await firestore.runTransaction(async (transaction)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/org-automations-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WRITING AN ORG AUTOMATION, and pausing one on a site (AGL-3302).\n *\n * `orgs/{orgId}/automations` is closed to every client write, the way the\n * outreach collections are: an org automation runs on every site it is\n * placed on, as that site, so its trigger, its steps and its placement are\n * decisions the server checks — the vocabulary, the organization's sites, the\n * plan — and nothing a browser may write around.\n *\n * ## Two doors\n *\n * `automations/manage` is the organization's: it creates, edits, switches on\n * and off, and deletes. It names the org and NO site, so it declares a\n * subject resolver for the dispatchers' release gate and asks the org's\n * plugin switch itself; the caller must be an org-wide owner, admin or\n * editor.\n *\n * `automations/pause` is the SITE's — the host level control. It names the\n * site, so the dispatcher applies that site's plugin switch as for any site\n * request, and it changes one thing: whether this automation is paused on\n * THIS site. The site's own admins and editors may use it, and so may an\n * org-wide editor; nobody may pause a site through it but the one named.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostIdsFromScope, visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n type AglynOrgMember,\n checkEntitlement,\n isOrgWideMember,\n type PluginApiHandler,\n type PluginApiRequestSubject,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel: `@aglyn/tenant-data-admin`'s index reaches the\n * Next render cache, and everything these doors need is the Admin SDK handle,\n * the id predicate and the org reads.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n getOrgDoc,\n logHostActivity,\n logOrgActivity,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n ORG_AUTOMATION_ACTIVITY_TARGET,\n ORG_AUTOMATION_API_ROUTES,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n orgAutomationPausedHostIds,\n prunePausedHostIds,\n readOrgAutomation,\n type OrgAutomationFields,\n} from '../model/org-automations'\n\nexport { ORG_AUTOMATION_API_ROUTES }\n\n/** What `automations/manage` may be asked to do. */\nexport const ORG_AUTOMATION_MANAGE_ACTIONS = [\n 'create',\n 'update',\n 'setEnabled',\n 'delete',\n] as const\n\ntype ManageAction = (typeof ORG_AUTOMATION_MANAGE_ACTIONS)[number]\n\n/** The org roles that may write the organization's automations. */\nconst ORG_MANAGE_ROLES = ['owner', 'admin', 'editor'] as const\n\n/** The site roles that may pause an org automation on their own site. */\nconst SITE_PAUSE_ROLES = ['admin', 'editor'] as const\n\n/** What the doors reach outside this module; specs hand in their own. */\nexport interface OrgAutomationRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; email?: string | null }>\n resolveOrgMembership(\n uid: string,\n orgId: string,\n ): Promise<{ orgId: string; member: Partial<AglynOrgMember> } | null>\n getOrgDoc(orgId: string): Promise<Record<string, unknown> | null>\n resolveOrgIdForHost(hostId: string): Promise<string | null>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof ORG_AUTOMATION_ACTIVITY_TARGET; id: string; name?: string },\n ): Promise<void>\n logHostActivity(\n hostId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: 'workflow'; id: string; name?: string },\n ): Promise<void>\n newId(): string\n}\n\nfunction defaultDeps(): OrgAutomationRouteDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, email: decoded.email ?? null }\n },\n resolveOrgMembership: async (uid, orgId) =>\n (await resolveOrgMembership(uid, orgId)) as never,\n getOrgDoc: async (orgId) =>\n ((await getOrgDoc(orgId)) as Record<string, unknown> | null) ?? null,\n resolveOrgIdForHost,\n logOrgActivity: (orgId, actor, action, target) =>\n logOrgActivity(orgId, actor, action, target),\n logHostActivity: (hostId, actor, action, target) =>\n logHostActivity(hostId, actor, action, target),\n newId: createResourceUid,\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\nfunction automationsOf(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n}\n\n/**\n * Whether every site a placement names belongs to this organization — asked\n * of `hostIndex`, the mirror `resolveOrgIdForHost` reads, in one round trip.\n * `['org']` names no site and always answers yes.\n */\nasync function placementIsTheOrgs(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n visibleTo: readonly string[],\n): Promise<boolean> {\n const hostIds = hostIdsFromScope(visibleTo)\n if (!hostIds.length) return true\n if (!hostIds.every(isDocumentId)) return false\n const entries = await firestore.getAll(\n ...hostIds.map((hostId) => firestore.collection('hostIndex').doc(hostId)),\n )\n return entries.every((entry) => entry.exists && entry.get('orgId') === orgId)\n}\n\n/**\n * The organization a `automations/manage` request is for, read off its JSON\n * body, so the dispatchers' release gate asks about the right organization.\n * Consulted only for a request that names no site. Unverified, exactly as a\n * `hostId` is: the handler refuses anyone who is not an org-wide member.\n */\nexport async function orgAutomationsManageSubject(\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n if (request.method !== 'POST') return null\n const body = (await request.json().catch(() => null)) as {\n orgId?: unknown\n } | null\n const orgId = typeof body?.orgId === 'string' ? body.orgId : ''\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/** What writing an org automation came to: its id, or the refusal the route answers with. */\nexport type OrgAutomationWrite = { ok: true; id: string } | { ok: false; status: number; error: string }\n\n/**\n * Creates an org automation as `automations/manage` does: its sites must be\n * the organization's, and the organization holds at most\n * {@link ORG_AUTOMATIONS_MAX} live ones. The route and the package import\n * (AGL-3535) both create through here.\n */\nexport async function createOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; fields: OrgAutomationFields; actorUid: string },\n): Promise<OrgAutomationWrite> {\n if (!(await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo))) {\n return { ok: false, status: 400, error: 'A site you chose is not in this organization' }\n }\n const collection = automationsOf(firestore, input.orgId)\n /*\n * Counted before the write rather than inside a transaction. The\n * cap bounds storage, not money, so the overshoot two simultaneous\n * saves can buy is one document — the trade the site actions cap\n * makes for the same reason.\n */\n const live = (await collection.where('deletedAt', '==', null).count().get()).data().count\n if (live >= ORG_AUTOMATIONS_MAX) {\n return {\n ok: false,\n status: 409,\n error: `An organization holds at most ${ORG_AUTOMATIONS_MAX} org automations — delete one you no longer use first`,\n }\n }\n await collection.doc(input.id).create({\n ...input.fields,\n pausedHostIds: [],\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n createdBy: input.actorUid,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return { ok: true, id: input.id }\n}\n\n/** Rewrites a live org automation as `automations/manage` does; a site it no longer runs on leaves its pause list. */\nexport async function updateOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; fields: OrgAutomationFields; actorUid: string },\n): Promise<OrgAutomationWrite> {\n if (!(await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo))) {\n return { ok: false, status: 400, error: 'A site you chose is not in this organization' }\n }\n const collection = automationsOf(firestore, input.orgId)\n const outcome = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(input.id)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return 'missing'\n transaction.update(ref, {\n ...input.fields,\n // A site the automation no longer runs on has nothing left to\n // pause: its entry goes, so a later placement starts unpaused.\n pausedHostIds: prunePausedHostIds(orgAutomationPausedHostIds(current.data()), input.fields.visibleTo),\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return 'updated'\n })\n return outcome === 'missing' ? { ok: false, status: 404, error: 'Unknown org automation' } : { ok: true, id: input.id }\n}\n\n/**\n * Deletes an org automation as `automations/manage` does — softly, switched\n * off (see the handler). `null` for one that does not exist; `again` for\n * one already deleted.\n */\nexport async function deleteOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; actorUid: string },\n): Promise<{ name: string; again: boolean } | null> {\n return firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, input.orgId).doc(input.id)\n const current = await transaction.get(ref)\n if (!current.exists) return null\n // Deleting twice is the same request answered again.\n if (current.get('deletedAt')) return { name: '', again: true }\n transaction.update(ref, {\n deletedAt: FieldValue.serverTimestamp(),\n deletedBy: input.actorUid,\n // Off as well, so the engine's query — which asks for switched-on\n // automations only — never spends one of its places on it.\n enabled: false,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return { name: String(current.get('name') ?? ''), again: false }\n })\n}\n\n/**\n * `automations/manage`: create, edit, switch on or off, and delete one of the\n * organization's automations.\n *\n * A DELETE is soft — `deletedAt` stamped and the automation switched off —\n * because the id is what every run history row and every waiting person\n * names: the rows keep a name to show, and the people waiting inside it are\n * stopped by the resume's kill switch on their next beat. Deleting and\n * switching off are never refused for want of a plan, so an organization\n * that downgraded can always stop what it built.\n */\nexport function createOrgAutomationsManageHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const orgId = String(req.body?.orgId ?? '')\n const action = String(req.body?.action ?? '') as ManageAction\n if (!isDocumentId(orgId)) {\n return res.status(400).json({ error: 'Invalid orgId' })\n }\n if (!(ORG_AUTOMATION_MANAGE_ACTIONS as readonly string[]).includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const automationId = String(req.body?.automationId ?? '')\n if (action !== 'create' && !isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n if (\n !member ||\n !isOrgWideMember(member) ||\n !(ORG_MANAGE_ROLES as readonly string[]).includes(String(member.role ?? ''))\n ) {\n return res\n .status(403)\n .json({ error: 'Not an organization owner, admin or editor' })\n }\n /*\n * A request naming no site reaches here past only the release gate —\n * the dispatcher's per-site switch needs a site — so the organization's\n * own switch is asked here.\n */\n const org = await deps.getOrgDoc(orgId)\n if (!org || !isPluginEnabled(org as never, BUNDLE_ID)) {\n return res.status(404).json({ error: 'Not found' })\n }\n const enabling =\n action === 'create' ||\n action === 'update' ||\n (action === 'setEnabled' && req.body?.enabled === true)\n if (enabling && !checkEntitlement(org as never, 'actions')) {\n return res.status(403).json({\n error: `Org automations need the ${planLabelGrantingFeature('actions') ?? 'Pro'} plan`,\n })\n }\n\n const firestore = deps.firestore()\n const collection = automationsOf(firestore, orgId)\n const actor = { uid: caller.uid, email: caller.email ?? null }\n const logged = (verb: string, id: string, name: string) =>\n deps.logOrgActivity(orgId, actor, verb, {\n type: ORG_AUTOMATION_ACTIVITY_TARGET,\n id,\n name,\n })\n\n if (action === 'create' || action === 'update') {\n const read = readOrgAutomation(req.body?.automation)\n if (read.ok === false) {\n return res.status(400).json({ error: read.problem })\n }\n const written =\n action === 'create'\n ? await createOrgAutomationRecord(firestore, { orgId, id: deps.newId(), fields: read.value, actorUid: caller.uid })\n : await updateOrgAutomationRecord(firestore, { orgId, id: automationId, fields: read.value, actorUid: caller.uid })\n if (written.ok === false) {\n return res.status(written.status).json({ error: written.error })\n }\n await logged(action === 'create' ? 'Created an org automation' : 'Edited an org automation', written.id, read.value.name)\n return res.status(200).json({ automationId: written.id })\n }\n\n if (action === 'setEnabled') {\n if (typeof req.body?.enabled !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is enabled' })\n }\n const enabled = req.body.enabled as boolean\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(automationId)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return null\n transaction.update(ref, {\n enabled,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n await logged(\n enabled ? 'Switched on an org automation' : 'Switched off an org automation',\n automationId,\n name,\n )\n return res.status(200).json({ automationId, enabled })\n }\n\n // delete\n const deleted = await deleteOrgAutomationRecord(firestore, { orgId, id: automationId, actorUid: caller.uid })\n if (!deleted) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n if (!deleted.again) {\n await logged('Deleted an org automation', automationId, deleted.name)\n }\n return res.status(200).json({ automationId, deleted: true })\n } catch (error) {\n console.error('[org automations] manage failed', orgId, action, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\n/**\n * `automations/pause`: pause or resume one org automation on ONE site.\n *\n * `pausedHostIds` is changed with `arrayUnion`/`arrayRemove` of the named\n * site alone, inside a transaction that first confirms the automation runs\n * there — so a site's editor can take their own site out of an org\n * automation and put it back, and can never touch another site's entry or\n * the automation itself. Pausing stops the people already waiting inside it\n * on this site as well, on their next beat: the resume reads a pause as the\n * kill switch.\n *\n * Never refused for want of a plan: a pause is how a site stops something,\n * and stopping is always allowed.\n */\nexport function createOrgAutomationPauseHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const automationId = String(req.body?.automationId ?? '')\n const paused = req.body?.paused\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n if (typeof paused !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is paused' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const firestore = deps.firestore()\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const orgId = await deps.resolveOrgIdForHost(hostId)\n if (!orgId) {\n return res.status(409).json({\n error: 'This site is not part of an organization, so it runs no org automations.',\n })\n }\n const requestedOrgId = String(req.body?.orgId ?? '')\n if (requestedOrgId && requestedOrgId !== orgId) {\n return res.status(400).json({ error: 'That site is not in this organization' })\n }\n /*\n * The site's own admins and editors, read off the host's `memberRoles`\n * projection — the same answer every site door asks — or an org-wide\n * editor, asked of the membership itself so a projection that has not\n * caught up cannot refuse the organization's own managers.\n */\n const siteRole = String((host.get('memberRoles') ?? {})[caller.uid] ?? '')\n let allowed = (SITE_PAUSE_ROLES as readonly string[]).includes(siteRole)\n if (!allowed) {\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n allowed =\n Boolean(member) &&\n isOrgWideMember(member) &&\n (ORG_MANAGE_ROLES as readonly string[]).includes(String(member?.role ?? ''))\n }\n if (!allowed) {\n return res\n .status(403)\n .json({ error: 'Not an admin or editor of this site' })\n }\n\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, orgId).doc(automationId)\n const current = await transaction.get(ref)\n if (\n !current.exists ||\n current.get('deletedAt') ||\n !visibleToHost(current.get('visibleTo') as string[] | undefined, hostId)\n ) {\n return null\n }\n transaction.update(ref, {\n pausedHostIds: paused\n ? FieldValue.arrayUnion(hostId)\n : FieldValue.arrayRemove(hostId),\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res\n .status(404)\n .json({ error: 'No org automation by that id runs on this site' })\n }\n await deps.logHostActivity(\n hostId,\n { uid: caller.uid, email: caller.email ?? null },\n paused\n ? 'Paused an org automation on this site'\n : 'Resumed an org automation on this site',\n { type: 'workflow', id: automationId, name },\n )\n return res.status(200).json({ automationId, hostId, paused })\n } catch (error) {\n console.error('[org automations] pause failed', hostId, automationId, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\nexport const orgAutomationsManageHandler = createOrgAutomationsManageHandler()\nexport const orgAutomationPauseHandler = createOrgAutomationPauseHandler()\n"],"names":["createResourceUid","hostIdsFromScope","visibleToHost","isPluginEnabled","checkEntitlement","isOrgWideMember","planLabelGrantingFeature","isDocumentId","firebaseAdmin","getOrgDoc","logHostActivity","logOrgActivity","resolveOrgIdForHost","resolveOrgMembership","FieldValue","BUNDLE_ID","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_API_ROUTES","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","orgAutomationPausedHostIds","prunePausedHostIds","readOrgAutomation","ORG_AUTOMATION_MANAGE_ACTIONS","ORG_MANAGE_ROLES","SITE_PAUSE_ROLES","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","email","orgId","actor","action","target","hostId","newId","bearerToken","headers","authorization","String","startsWith","slice","length","automationsOf","collection","doc","placementIsTheOrgs","visibleTo","hostIds","every","entries","getAll","map","entry","exists","get","orgAutomationsManageSubject","request","method","body","json","catch","createOrgAutomationRecord","input","fields","ok","status","error","live","where","count","data","id","create","pausedHostIds","deletedAt","createdAt","serverTimestamp","createdBy","actorUid","updatedAt","updatedBy","updateOrgAutomationRecord","outcome","runTransaction","transaction","ref","current","update","deleteOrgAutomationRecord","name","again","deletedBy","enabled","createOrgAutomationsManageHandler","overrides","req","res","deps","includes","automationId","member","caller","membership","role","org","enabling","logged","verb","type","read","automation","problem","written","value","deleted","console","createOrgAutomationPauseHandler","paused","host","requestedOrgId","siteRole","allowed","Boolean","arrayUnion","arrayRemove","orgAutomationsManageHandler","orgAutomationPauseHandler"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/server/org-automations-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WRITING AN ORG AUTOMATION, and pausing one on a site (AGL-3302).\n *\n * `orgs/{orgId}/automations` is closed to every client write, the way the\n * outreach collections are: an org automation runs on every site it is\n * placed on, as that site, so its trigger, its steps and its placement are\n * decisions the server checks — the vocabulary, the organization's sites, the\n * plan — and nothing a browser may write around.\n *\n * ## Two doors\n *\n * `automations/manage` is the organization's: it creates, edits, switches on\n * and off, and deletes. It names the org and NO site, so it declares a\n * subject resolver for the dispatchers' release gate and asks the org's\n * plugin switch itself; the caller must be an org-wide owner, admin or\n * editor.\n *\n * `automations/pause` is the SITE's — the host level control. It names the\n * site, so the dispatcher applies that site's plugin switch as for any site\n * request, and it changes one thing: whether this automation is paused on\n * THIS site. The site's own admins and editors may use it, and so may an\n * org-wide editor; nobody may pause a site through it but the one named.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { hostIdsFromScope, visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n type AglynOrgMember,\n checkEntitlement,\n isOrgWideMember,\n type PluginApiHandler,\n type PluginApiRequestSubject,\n planLabelGrantingFeature,\n} from '@aglyn/aglyn/server'\n/*\n * The MODULES, not the barrel: `@aglyn/tenant-data-admin`'s index reaches the\n * Next render cache, and everything these doors need is the Admin SDK handle,\n * the id predicate and the org reads.\n */\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n getOrgDoc,\n logHostActivity,\n logOrgActivity,\n memberHasOrgPermission,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n WORKFLOWS_SITE_PAUSE_PRESET,\n workflowsPermissionRefusal,\n type WorkflowsPermission,\n} from '../model/workflows-permissions'\nimport {\n ORG_AUTOMATION_ACTIVITY_TARGET,\n ORG_AUTOMATION_API_ROUTES,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n orgAutomationPausedHostIds,\n prunePausedHostIds,\n readOrgAutomation,\n type OrgAutomationFields,\n} from '../model/org-automations'\n\nexport { ORG_AUTOMATION_API_ROUTES }\n\n/** What `automations/manage` may be asked to do. */\nexport const ORG_AUTOMATION_MANAGE_ACTIONS = [\n 'create',\n 'update',\n 'setEnabled',\n 'delete',\n] as const\n\ntype ManageAction = (typeof ORG_AUTOMATION_MANAGE_ACTIONS)[number]\n\n\n/** What the doors reach outside this module; specs hand in their own. */\nexport interface OrgAutomationRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string; email?: string | null }>\n resolveOrgMembership(\n uid: string,\n orgId: string,\n ): Promise<{ orgId: string; member: Partial<AglynOrgMember> } | null>\n getOrgDoc(orgId: string): Promise<Record<string, unknown> | null>\n resolveOrgIdForHost(hostId: string): Promise<string | null>\n /** Whether a member holds a workspace key with no site named (AGL-4047): `memberHasOrgPermission`. */\n holdsOrgKey(orgId: string, member: Partial<AglynOrgMember>, key: WorkflowsPermission): Promise<boolean>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof ORG_AUTOMATION_ACTIVITY_TARGET; id: string; name?: string },\n ): Promise<void>\n logHostActivity(\n hostId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: 'workflow'; id: string; name?: string },\n ): Promise<void>\n newId(): string\n}\n\nfunction defaultDeps(): OrgAutomationRouteDeps {\n return {\n firestore: () =>\n firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore,\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid, email: decoded.email ?? null }\n },\n resolveOrgMembership: async (uid, orgId) =>\n (await resolveOrgMembership(uid, orgId)) as never,\n getOrgDoc: async (orgId) =>\n ((await getOrgDoc(orgId)) as Record<string, unknown> | null) ?? null,\n resolveOrgIdForHost,\n holdsOrgKey: (orgId, member, key) => memberHasOrgPermission(orgId, member, key as never),\n logOrgActivity: (orgId, actor, action, target) =>\n logOrgActivity(orgId, actor, action, target),\n logHostActivity: (hostId, actor, action, target) =>\n logHostActivity(hostId, actor, action, target),\n newId: createResourceUid,\n }\n}\n\n/** The bearer token a request carries, or null. */\nfunction bearerToken(headers: Partial<Record<string, string | string[]>>): string | null {\n const authorization = String(headers['authorization'] ?? '')\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length) || null\n : null\n}\n\nfunction automationsOf(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(ORG_AUTOMATIONS_COLLECTION)\n}\n\n/**\n * Whether every site a placement names belongs to this organization — asked\n * of `hostIndex`, the mirror `resolveOrgIdForHost` reads, in one round trip.\n * `['org']` names no site and always answers yes.\n */\nasync function placementIsTheOrgs(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n visibleTo: readonly string[],\n): Promise<boolean> {\n const hostIds = hostIdsFromScope(visibleTo)\n if (!hostIds.length) return true\n if (!hostIds.every(isDocumentId)) return false\n const entries = await firestore.getAll(\n ...hostIds.map((hostId) => firestore.collection('hostIndex').doc(hostId)),\n )\n return entries.every((entry) => entry.exists && entry.get('orgId') === orgId)\n}\n\n/**\n * The organization a `automations/manage` request is for, read off its JSON\n * body, so the dispatchers' release gate asks about the right organization.\n * Consulted only for a request that names no site. Unverified, exactly as a\n * `hostId` is: the handler refuses anyone who is not an org-wide member.\n */\nexport async function orgAutomationsManageSubject(\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n if (request.method !== 'POST') return null\n const body = (await request.json().catch(() => null)) as {\n orgId?: unknown\n } | null\n const orgId = typeof body?.orgId === 'string' ? body.orgId : ''\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/** What writing an org automation came to: its id, or the refusal the route answers with. */\nexport type OrgAutomationWrite = { ok: true; id: string } | { ok: false; status: number; error: string }\n\n/**\n * Creates an org automation as `automations/manage` does: its sites must be\n * the organization's, and the organization holds at most\n * {@link ORG_AUTOMATIONS_MAX} live ones. The route and the package import\n * (AGL-3535) both create through here.\n */\nexport async function createOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; fields: OrgAutomationFields; actorUid: string },\n): Promise<OrgAutomationWrite> {\n if (!(await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo))) {\n return { ok: false, status: 400, error: 'A site you chose is not in this organization' }\n }\n const collection = automationsOf(firestore, input.orgId)\n /*\n * Counted before the write rather than inside a transaction. The\n * cap bounds storage, not money, so the overshoot two simultaneous\n * saves can buy is one document — the trade the site actions cap\n * makes for the same reason.\n */\n const live = (await collection.where('deletedAt', '==', null).count().get()).data().count\n if (live >= ORG_AUTOMATIONS_MAX) {\n return {\n ok: false,\n status: 409,\n error: `An organization holds at most ${ORG_AUTOMATIONS_MAX} org automations — delete one you no longer use first`,\n }\n }\n await collection.doc(input.id).create({\n ...input.fields,\n pausedHostIds: [],\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n createdBy: input.actorUid,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return { ok: true, id: input.id }\n}\n\n/** Rewrites a live org automation as `automations/manage` does; a site it no longer runs on leaves its pause list. */\nexport async function updateOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; fields: OrgAutomationFields; actorUid: string },\n): Promise<OrgAutomationWrite> {\n if (!(await placementIsTheOrgs(firestore, input.orgId, input.fields.visibleTo))) {\n return { ok: false, status: 400, error: 'A site you chose is not in this organization' }\n }\n const collection = automationsOf(firestore, input.orgId)\n const outcome = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(input.id)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return 'missing'\n transaction.update(ref, {\n ...input.fields,\n // A site the automation no longer runs on has nothing left to\n // pause: its entry goes, so a later placement starts unpaused.\n pausedHostIds: prunePausedHostIds(orgAutomationPausedHostIds(current.data()), input.fields.visibleTo),\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return 'updated'\n })\n return outcome === 'missing' ? { ok: false, status: 404, error: 'Unknown org automation' } : { ok: true, id: input.id }\n}\n\n/**\n * Deletes an org automation as `automations/manage` does — softly, switched\n * off (see the handler). `null` for one that does not exist; `again` for\n * one already deleted.\n */\nexport async function deleteOrgAutomationRecord(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; id: string; actorUid: string },\n): Promise<{ name: string; again: boolean } | null> {\n return firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, input.orgId).doc(input.id)\n const current = await transaction.get(ref)\n if (!current.exists) return null\n // Deleting twice is the same request answered again.\n if (current.get('deletedAt')) return { name: '', again: true }\n transaction.update(ref, {\n deletedAt: FieldValue.serverTimestamp(),\n deletedBy: input.actorUid,\n // Off as well, so the engine's query — which asks for switched-on\n // automations only — never spends one of its places on it.\n enabled: false,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: input.actorUid,\n })\n return { name: String(current.get('name') ?? ''), again: false }\n })\n}\n\n/**\n * `automations/manage`: create, edit, switch on or off, and delete one of the\n * organization's automations.\n *\n * A DELETE is soft — `deletedAt` stamped and the automation switched off —\n * because the id is what every run history row and every waiting person\n * names: the rows keep a name to show, and the people waiting inside it are\n * stopped by the resume's kill switch on their next beat. Deleting and\n * switching off are never refused for want of a plan, so an organization\n * that downgraded can always stop what it built.\n */\nexport function createOrgAutomationsManageHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const orgId = String(req.body?.orgId ?? '')\n const action = String(req.body?.action ?? '') as ManageAction\n if (!isDocumentId(orgId)) {\n return res.status(400).json({ error: 'Invalid orgId' })\n }\n if (!(ORG_AUTOMATION_MANAGE_ACTIONS as readonly string[]).includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const automationId = String(req.body?.automationId ?? '')\n if (action !== 'create' && !isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n // \"Manage org automations\" (AGL-4047), for members of the whole\n // workspace: its default is the owner, admins and editors this asked\n // for before, then their custom role and their own permissions.\n if (\n !member ||\n !isOrgWideMember(member) ||\n !(await deps.holdsOrgKey(orgId, member, 'workflows.orgAutomations'))\n ) {\n return res.status(403).json({ error: workflowsPermissionRefusal('workflows.orgAutomations') })\n }\n /*\n * A request naming no site reaches here past only the release gate —\n * the dispatcher's per-site switch needs a site — so the organization's\n * own switch is asked here.\n */\n const org = await deps.getOrgDoc(orgId)\n if (!org || !isPluginEnabled(org as never, BUNDLE_ID)) {\n return res.status(404).json({ error: 'Not found' })\n }\n const enabling =\n action === 'create' ||\n action === 'update' ||\n (action === 'setEnabled' && req.body?.enabled === true)\n if (enabling && !checkEntitlement(org as never, 'actions')) {\n return res.status(403).json({\n error: `Org automations need the ${planLabelGrantingFeature('actions') ?? 'Pro'} plan`,\n })\n }\n\n const firestore = deps.firestore()\n const collection = automationsOf(firestore, orgId)\n const actor = { uid: caller.uid, email: caller.email ?? null }\n const logged = (verb: string, id: string, name: string) =>\n deps.logOrgActivity(orgId, actor, verb, {\n type: ORG_AUTOMATION_ACTIVITY_TARGET,\n id,\n name,\n })\n\n if (action === 'create' || action === 'update') {\n const read = readOrgAutomation(req.body?.automation)\n if (read.ok === false) {\n return res.status(400).json({ error: read.problem })\n }\n const written =\n action === 'create'\n ? await createOrgAutomationRecord(firestore, { orgId, id: deps.newId(), fields: read.value, actorUid: caller.uid })\n : await updateOrgAutomationRecord(firestore, { orgId, id: automationId, fields: read.value, actorUid: caller.uid })\n if (written.ok === false) {\n return res.status(written.status).json({ error: written.error })\n }\n await logged(action === 'create' ? 'Created an org automation' : 'Edited an org automation', written.id, read.value.name)\n return res.status(200).json({ automationId: written.id })\n }\n\n if (action === 'setEnabled') {\n if (typeof req.body?.enabled !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is enabled' })\n }\n const enabled = req.body.enabled as boolean\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = collection.doc(automationId)\n const current = await transaction.get(ref)\n if (!current.exists || current.get('deletedAt')) return null\n transaction.update(ref, {\n enabled,\n updatedAt: FieldValue.serverTimestamp(),\n updatedBy: caller.uid,\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n await logged(\n enabled ? 'Switched on an org automation' : 'Switched off an org automation',\n automationId,\n name,\n )\n return res.status(200).json({ automationId, enabled })\n }\n\n // delete\n const deleted = await deleteOrgAutomationRecord(firestore, { orgId, id: automationId, actorUid: caller.uid })\n if (!deleted) {\n return res.status(404).json({ error: 'Unknown org automation' })\n }\n if (!deleted.again) {\n await logged('Deleted an org automation', automationId, deleted.name)\n }\n return res.status(200).json({ automationId, deleted: true })\n } catch (error) {\n console.error('[org automations] manage failed', orgId, action, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\n/**\n * `automations/pause`: pause or resume one org automation on ONE site.\n *\n * `pausedHostIds` is changed with `arrayUnion`/`arrayRemove` of the named\n * site alone, inside a transaction that first confirms the automation runs\n * there — so a site's editor can take their own site out of an org\n * automation and put it back, and can never touch another site's entry or\n * the automation itself. Pausing stops the people already waiting inside it\n * on this site as well, on their next beat: the resume reads a pause as the\n * kill switch.\n *\n * Never refused for want of a plan: a pause is how a site stops something,\n * and stopping is always allowed.\n */\nexport function createOrgAutomationPauseHandler(\n overrides: Partial<OrgAutomationRouteDeps> = {},\n): PluginApiHandler {\n return async (req, res) => {\n const deps = { ...defaultDeps(), ...overrides }\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const automationId = String(req.body?.automationId ?? '')\n const paused = req.body?.paused\n if (!isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (!isDocumentId(automationId)) {\n return res.status(400).json({ error: 'Invalid automationId' })\n }\n if (typeof paused !== 'boolean') {\n return res.status(400).json({ error: 'Say whether it is paused' })\n }\n const token = bearerToken(req.headers)\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const caller = await deps.verifyIdToken(token).catch(() => null)\n if (!caller) return res.status(401).json({ error: 'Unauthenticated' })\n const firestore = deps.firestore()\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const orgId = await deps.resolveOrgIdForHost(hostId)\n if (!orgId) {\n return res.status(409).json({\n error: 'This site is not part of an organization, so it runs no org automations.',\n })\n }\n const requestedOrgId = String(req.body?.orgId ?? '')\n if (requestedOrgId && requestedOrgId !== orgId) {\n return res.status(400).json({ error: 'That site is not in this organization' })\n }\n /*\n * The site's own admins and editors, read off the host's `memberRoles`\n * projection — the same answer every site door asks — or an org-wide\n * editor, asked of the membership itself so a projection that has not\n * caught up cannot refuse the organization's own managers.\n */\n // \"Pause org automations on a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked\n // for; or \"Manage org automations\" for a member of the whole workspace.\n let allowed = holdsStampedSiteKey(host, caller.uid, 'workflows.sitePause', WORKFLOWS_SITE_PAUSE_PRESET)\n if (!allowed) {\n const membership = await deps.resolveOrgMembership(caller.uid, orgId)\n const member = membership?.member\n allowed =\n Boolean(member) &&\n isOrgWideMember(member) &&\n (await deps.holdsOrgKey(orgId, member as Partial<AglynOrgMember>, 'workflows.orgAutomations'))\n }\n if (!allowed) {\n return res.status(403).json({ error: workflowsPermissionRefusal('workflows.sitePause') })\n }\n\n const name = await firestore.runTransaction(async (transaction) => {\n const ref = automationsOf(firestore, orgId).doc(automationId)\n const current = await transaction.get(ref)\n if (\n !current.exists ||\n current.get('deletedAt') ||\n !visibleToHost(current.get('visibleTo') as string[] | undefined, hostId)\n ) {\n return null\n }\n transaction.update(ref, {\n pausedHostIds: paused\n ? FieldValue.arrayUnion(hostId)\n : FieldValue.arrayRemove(hostId),\n })\n return String(current.get('name') ?? '')\n })\n if (name === null) {\n return res\n .status(404)\n .json({ error: 'No org automation by that id runs on this site' })\n }\n await deps.logHostActivity(\n hostId,\n { uid: caller.uid, email: caller.email ?? null },\n paused\n ? 'Paused an org automation on this site'\n : 'Resumed an org automation on this site',\n { type: 'workflow', id: automationId, name },\n )\n return res.status(200).json({ automationId, hostId, paused })\n } catch (error) {\n console.error('[org automations] pause failed', hostId, automationId, error)\n return res\n .status(500)\n .json({ error: 'The request could not be completed' })\n }\n }\n}\n\nexport const orgAutomationsManageHandler = createOrgAutomationsManageHandler()\nexport const orgAutomationPauseHandler = createOrgAutomationPauseHandler()\n"],"names":["createResourceUid","holdsStampedSiteKey","hostIdsFromScope","visibleToHost","isPluginEnabled","checkEntitlement","isOrgWideMember","planLabelGrantingFeature","isDocumentId","firebaseAdmin","getOrgDoc","logHostActivity","logOrgActivity","memberHasOrgPermission","resolveOrgIdForHost","resolveOrgMembership","FieldValue","BUNDLE_ID","WORKFLOWS_SITE_PAUSE_PRESET","workflowsPermissionRefusal","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATION_API_ROUTES","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","orgAutomationPausedHostIds","prunePausedHostIds","readOrgAutomation","ORG_AUTOMATION_MANAGE_ACTIONS","defaultDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","email","orgId","holdsOrgKey","member","key","actor","action","target","hostId","newId","bearerToken","headers","authorization","String","startsWith","slice","length","automationsOf","collection","doc","placementIsTheOrgs","visibleTo","hostIds","every","entries","getAll","map","entry","exists","get","orgAutomationsManageSubject","request","method","body","json","catch","createOrgAutomationRecord","input","fields","ok","status","error","live","where","count","data","id","create","pausedHostIds","deletedAt","createdAt","serverTimestamp","createdBy","actorUid","updatedAt","updatedBy","updateOrgAutomationRecord","outcome","runTransaction","transaction","ref","current","update","deleteOrgAutomationRecord","name","again","deletedBy","enabled","createOrgAutomationsManageHandler","overrides","req","res","deps","includes","automationId","caller","membership","org","enabling","logged","verb","type","read","automation","problem","written","value","deleted","console","createOrgAutomationPauseHandler","paused","host","requestedOrgId","allowed","Boolean","arrayUnion","arrayRemove","orgAutomationsManageHandler","orgAutomationPauseHandler"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,SAASA,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,mBAAmB,QAAQ,0CAAyC;AAC7E,SAASC,gBAAgB,EAAEC,aAAa,QAAQ,sCAAqC;AACrF,SAASC,eAAe,QAAQ,8CAA6C;AAC7E,SAEEC,gBAAgB,EAChBC,eAAe,EAGfC,wBAAwB,QACnB,sBAAqB;AAC5B;;;;CAIC,GACD,SAASC,YAAY,QAAQ,8CAA6C;AAC1E,OAAOC,mBAAmB,iDAAgD;AAC1E,SACEC,SAAS,EACTC,eAAe,EACfC,cAAc,EACdC,sBAAsB,EACtBC,mBAAmB,EACnBC,oBAAoB,QACf,gDAA+C;AACtD,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SACEC,2BAA2B,EAC3BC,0BAA0B,QAErB,oCAAgC;AACvC,SACEC,8BAA8B,EAC9BC,yBAAyB,EACzBC,0BAA0B,EAC1BC,mBAAmB,EACnBC,0BAA0B,EAC1BC,kBAAkB,EAClBC,iBAAiB,QAEZ,8BAA0B;AAEjC,SAASL,yBAAyB,GAAE;AAEpC,kDAAkD,GAClD,OAAO,MAAMM,gCAAgC;IAC3C;IACA;IACA;IACA;CACD,CAAS;AAgCV,SAASC;IACP,OAAO;QACLC,WAAW,IACTpB,cAAcqB,GAAG,GAAGD,SAAS;QAC/BE,eAAe,OAAOC;gBAEcC;YADlC,MAAMA,UAAU,MAAMxB,cAAcqB,GAAG,GAAGI,IAAI,GAAGH,aAAa,CAACC;YAC/D,OAAO;gBAAEG,KAAKF,QAAQE,GAAG;gBAAEC,KAAK,GAAEH,iBAAAA,QAAQG,KAAK,YAAbH,iBAAiB;YAAK;QAC1D;QACAlB,sBAAsB,OAAOoB,KAAKE,QAC/B,MAAMtB,qBAAqBoB,KAAKE;QACnC3B,WAAW,OAAO2B;gBACd;oBAAA,OAAA,MAAM3B,UAAU2B,kBAAhB,OAA8D;;QAClEvB;QACAwB,aAAa,CAACD,OAAOE,QAAQC,MAAQ3B,uBAAuBwB,OAAOE,QAAQC;QAC3E5B,gBAAgB,CAACyB,OAAOI,OAAOC,QAAQC,SACrC/B,eAAeyB,OAAOI,OAAOC,QAAQC;QACvChC,iBAAiB,CAACiC,QAAQH,OAAOC,QAAQC,SACvChC,gBAAgBiC,QAAQH,OAAOC,QAAQC;QACzCE,OAAO7C;IACT;AACF;AAEA,iDAAiD,GACjD,SAAS8C,YAAYC,OAAmD;QACzCA;IAA7B,MAAMC,gBAAgBC,QAAOF,yBAAAA,OAAO,CAAC,gBAAgB,YAAxBA,yBAA4B;IACzD,OAAOC,cAAcE,UAAU,CAAC,aAC5BF,cAAcG,KAAK,CAAC,UAAUC,MAAM,KAAK,OACzC;AACN;AAEA,SAASC,cACPxB,SAAsC,EACtCQ,KAAa;IAEb,OAAOR,UACJyB,UAAU,CAAC,QACXC,GAAG,CAAClB,OACJiB,UAAU,CAAChC;AAChB;AAEA;;;;CAIC,GACD,eAAekC,mBACb3B,SAAsC,EACtCQ,KAAa,EACboB,SAA4B;IAE5B,MAAMC,UAAUxD,iBAAiBuD;IACjC,IAAI,CAACC,QAAQN,MAAM,EAAE,OAAO;IAC5B,IAAI,CAACM,QAAQC,KAAK,CAACnD,eAAe,OAAO;IACzC,MAAMoD,UAAU,MAAM/B,UAAUgC,MAAM,IACjCH,QAAQI,GAAG,CAAC,CAAClB,SAAWf,UAAUyB,UAAU,CAAC,aAAaC,GAAG,CAACX;IAEnE,OAAOgB,QAAQD,KAAK,CAAC,CAACI,QAAUA,MAAMC,MAAM,IAAID,MAAME,GAAG,CAAC,aAAa5B;AACzE;AAEA;;;;;CAKC,GACD,OAAO,eAAe6B,4BACpBC,OAAgB;IAEhB,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAO;IACtC,MAAMC,OAAQ,MAAMF,QAAQG,IAAI,GAAGC,KAAK,CAAC,IAAM;IAG/C,MAAMlC,QAAQ,QAAOgC,wBAAAA,KAAMhC,KAAK,MAAK,WAAWgC,KAAKhC,KAAK,GAAG;IAC7D,OAAO7B,aAAa6B,SAAS;QAAEA;IAAM,IAAI;AAC3C;AAKA;;;;;CAKC,GACD,OAAO,eAAemC,0BACpB3C,SAAsC,EACtC4C,KAAmF;IAEnF,IAAI,CAAE,MAAMjB,mBAAmB3B,WAAW4C,MAAMpC,KAAK,EAAEoC,MAAMC,MAAM,CAACjB,SAAS,GAAI;QAC/E,OAAO;YAAEkB,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAA+C;IACzF;IACA,MAAMvB,aAAaD,cAAcxB,WAAW4C,MAAMpC,KAAK;IACvD;;;;;GAKC,GACD,MAAMyC,OAAO,AAAC,CAAA,MAAMxB,WAAWyB,KAAK,CAAC,aAAa,MAAM,MAAMC,KAAK,GAAGf,GAAG,EAAC,EAAGgB,IAAI,GAAGD,KAAK;IACzF,IAAIF,QAAQvD,qBAAqB;QAC/B,OAAO;YACLoD,IAAI;YACJC,QAAQ;YACRC,OAAO,CAAC,8BAA8B,EAAEtD,oBAAoB,qDAAqD,CAAC;QACpH;IACF;IACA,MAAM+B,WAAWC,GAAG,CAACkB,MAAMS,EAAE,EAAEC,MAAM,CAAC,aACjCV,MAAMC,MAAM;QACfU,eAAe,EAAE;QACjBC,WAAW;QACXC,WAAWtE,WAAWuE,eAAe;QACrCC,WAAWf,MAAMgB,QAAQ;QACzBC,WAAW1E,WAAWuE,eAAe;QACrCI,WAAWlB,MAAMgB,QAAQ;;IAE3B,OAAO;QAAEd,IAAI;QAAMO,IAAIT,MAAMS,EAAE;IAAC;AAClC;AAEA,oHAAoH,GACpH,OAAO,eAAeU,0BACpB/D,SAAsC,EACtC4C,KAAmF;IAEnF,IAAI,CAAE,MAAMjB,mBAAmB3B,WAAW4C,MAAMpC,KAAK,EAAEoC,MAAMC,MAAM,CAACjB,SAAS,GAAI;QAC/E,OAAO;YAAEkB,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAA+C;IACzF;IACA,MAAMvB,aAAaD,cAAcxB,WAAW4C,MAAMpC,KAAK;IACvD,MAAMwD,UAAU,MAAMhE,UAAUiE,cAAc,CAAC,OAAOC;QACpD,MAAMC,MAAM1C,WAAWC,GAAG,CAACkB,MAAMS,EAAE;QACnC,MAAMe,UAAU,MAAMF,YAAY9B,GAAG,CAAC+B;QACtC,IAAI,CAACC,QAAQjC,MAAM,IAAIiC,QAAQhC,GAAG,CAAC,cAAc,OAAO;QACxD8B,YAAYG,MAAM,CAACF,KAAK,aACnBvB,MAAMC,MAAM;YACf,8DAA8D;YAC9D,+DAA+D;YAC/DU,eAAe3D,mBAAmBD,2BAA2ByE,QAAQhB,IAAI,KAAKR,MAAMC,MAAM,CAACjB,SAAS;YACpGiC,WAAW1E,WAAWuE,eAAe;YACrCI,WAAWlB,MAAMgB,QAAQ;;QAE3B,OAAO;IACT;IACA,OAAOI,YAAY,YAAY;QAAElB,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAyB,IAAI;QAAEF,IAAI;QAAMO,IAAIT,MAAMS,EAAE;IAAC;AACxH;AAEA;;;;CAIC,GACD,OAAO,eAAeiB,0BACpBtE,SAAsC,EACtC4C,KAAsD;IAEtD,OAAO5C,UAAUiE,cAAc,CAAC,OAAOC;YAefE;QAdtB,MAAMD,MAAM3C,cAAcxB,WAAW4C,MAAMpC,KAAK,EAAEkB,GAAG,CAACkB,MAAMS,EAAE;QAC9D,MAAMe,UAAU,MAAMF,YAAY9B,GAAG,CAAC+B;QACtC,IAAI,CAACC,QAAQjC,MAAM,EAAE,OAAO;QAC5B,qDAAqD;QACrD,IAAIiC,QAAQhC,GAAG,CAAC,cAAc,OAAO;YAAEmC,MAAM;YAAIC,OAAO;QAAK;QAC7DN,YAAYG,MAAM,CAACF,KAAK;YACtBX,WAAWrE,WAAWuE,eAAe;YACrCe,WAAW7B,MAAMgB,QAAQ;YACzB,kEAAkE;YAClE,2DAA2D;YAC3Dc,SAAS;YACTb,WAAW1E,WAAWuE,eAAe;YACrCI,WAAWlB,MAAMgB,QAAQ;QAC3B;QACA,OAAO;YAAEW,MAAMnD,QAAOgD,eAAAA,QAAQhC,GAAG,CAAC,mBAAZgC,eAAuB;YAAKI,OAAO;QAAM;IACjE;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASG,kCACdC,YAA6C,CAAC,CAAC;IAE/C,OAAO,OAAOC,KAAKC;;YAKID,WACCA,YAOMA;QAZ5B,MAAME,OAAO,aAAKhF,eAAkB6E;QACpC,IAAIC,IAAItC,MAAM,KAAK,QAAQ;YACzB,OAAOuC,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAqB;QAC5D;QACA,MAAMxC,QAAQY,gBAAOyD,YAAAA,IAAIrC,IAAI,qBAARqC,UAAUrE,KAAK,mBAAI;QACxC,MAAMK,SAASO,iBAAOyD,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUhE,MAAM,oBAAI;QAC1C,IAAI,CAAClC,aAAa6B,QAAQ;YACxB,OAAOsE,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAgB;QACvD;QACA,IAAI,CAAC,AAAClD,8BAAoDkF,QAAQ,CAACnE,SAAS;YAC1E,OAAOiE,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAiB;QACxD;QACA,MAAMiC,eAAe7D,iBAAOyD,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUI,YAAY,oBAAI;QACtD,IAAIpE,WAAW,YAAY,CAAClC,aAAasG,eAAe;YACtD,OAAOH,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAuB;QAC9D;QACA,MAAM7C,QAAQc,YAAY4D,IAAI3D,OAAO;QACrC,IAAI,CAACf,OAAO,OAAO2E,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;YAAEO,OAAO;QAAkB;QAEnE,IAAI;gBAoCsCkC;gBATVL;YA1B9B,MAAMK,SAAS,MAAMH,KAAK7E,aAAa,CAACC,OAAOuC,KAAK,CAAC,IAAM;YAC3D,IAAI,CAACwC,QAAQ,OAAOJ,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAkB;YACpE,MAAMmC,aAAa,MAAMJ,KAAK7F,oBAAoB,CAACgG,OAAO5E,GAAG,EAAEE;YAC/D,MAAME,SAASyE,8BAAAA,WAAYzE,MAAM;YACjC,gEAAgE;YAChE,qEAAqE;YACrE,gEAAgE;YAChE,IACE,CAACA,UACD,CAACjC,gBAAgBiC,WACjB,CAAE,MAAMqE,KAAKtE,WAAW,CAACD,OAAOE,QAAQ,6BACxC;gBACA,OAAOoE,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEO,OAAO1D,2BAA2B;gBAA4B;YAC9F;YACA;;;;OAIC,GACD,MAAM8F,MAAM,MAAML,KAAKlG,SAAS,CAAC2B;YACjC,IAAI,CAAC4E,OAAO,CAAC7G,gBAAgB6G,KAAchG,YAAY;gBACrD,OAAO0F,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEO,OAAO;gBAAY;YACnD;YACA,MAAMqC,WACJxE,WAAW,YACXA,WAAW,YACVA,WAAW,gBAAgBgE,EAAAA,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUH,OAAO,MAAK;YACpD,IAAIW,YAAY,CAAC7G,iBAAiB4G,KAAc,YAAY;oBAErB1G;gBADrC,OAAOoG,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAC1BO,OAAO,CAAC,yBAAyB,GAAEtE,4BAAAA,yBAAyB,sBAAzBA,4BAAuC,MAAM,KAAK,CAAC;gBACxF;YACF;YAEA,MAAMsB,YAAY+E,KAAK/E,SAAS;YAChC,MAAMyB,aAAaD,cAAcxB,WAAWQ;YAC5C,MAAMI,QAAQ;gBAAEN,KAAK4E,OAAO5E,GAAG;gBAAEC,KAAK,GAAE2E,gBAAAA,OAAO3E,KAAK,YAAZ2E,gBAAgB;YAAK;YAC7D,MAAMI,SAAS,CAACC,MAAclC,IAAYkB,OACxCQ,KAAKhG,cAAc,CAACyB,OAAOI,OAAO2E,MAAM;oBACtCC,MAAMjG;oBACN8D;oBACAkB;gBACF;YAEF,IAAI1D,WAAW,YAAYA,WAAW,UAAU;oBACfgE;gBAA/B,MAAMY,OAAO5F,mBAAkBgF,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUa,UAAU;gBACnD,IAAID,KAAK3C,EAAE,KAAK,OAAO;oBACrB,OAAOgC,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;wBAAEO,OAAOyC,KAAKE,OAAO;oBAAC;gBACpD;gBACA,MAAMC,UACJ/E,WAAW,WACP,MAAM8B,0BAA0B3C,WAAW;oBAAEQ;oBAAO6C,IAAI0B,KAAK/D,KAAK;oBAAI6B,QAAQ4C,KAAKI,KAAK;oBAAEjC,UAAUsB,OAAO5E,GAAG;gBAAC,KAC/G,MAAMyD,0BAA0B/D,WAAW;oBAAEQ;oBAAO6C,IAAI4B;oBAAcpC,QAAQ4C,KAAKI,KAAK;oBAAEjC,UAAUsB,OAAO5E,GAAG;gBAAC;gBACrH,IAAIsF,QAAQ9C,EAAE,KAAK,OAAO;oBACxB,OAAOgC,IAAI/B,MAAM,CAAC6C,QAAQ7C,MAAM,EAAEN,IAAI,CAAC;wBAAEO,OAAO4C,QAAQ5C,KAAK;oBAAC;gBAChE;gBACA,MAAMsC,OAAOzE,WAAW,WAAW,8BAA8B,4BAA4B+E,QAAQvC,EAAE,EAAEoC,KAAKI,KAAK,CAACtB,IAAI;gBACxH,OAAOO,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEwC,cAAcW,QAAQvC,EAAE;gBAAC;YACzD;YAEA,IAAIxC,WAAW,cAAc;oBAChBgE;gBAAX,IAAI,SAAOA,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUH,OAAO,MAAK,WAAW;oBAC1C,OAAOI,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;wBAAEO,OAAO;oBAA4B;gBACnE;gBACA,MAAM0B,UAAUG,IAAIrC,IAAI,CAACkC,OAAO;gBAChC,MAAMH,OAAO,MAAMvE,UAAUiE,cAAc,CAAC,OAAOC;wBASnCE;oBARd,MAAMD,MAAM1C,WAAWC,GAAG,CAACuD;oBAC3B,MAAMb,UAAU,MAAMF,YAAY9B,GAAG,CAAC+B;oBACtC,IAAI,CAACC,QAAQjC,MAAM,IAAIiC,QAAQhC,GAAG,CAAC,cAAc,OAAO;oBACxD8B,YAAYG,MAAM,CAACF,KAAK;wBACtBO;wBACAb,WAAW1E,WAAWuE,eAAe;wBACrCI,WAAWoB,OAAO5E,GAAG;oBACvB;oBACA,OAAOc,QAAOgD,eAAAA,QAAQhC,GAAG,CAAC,mBAAZgC,eAAuB;gBACvC;gBACA,IAAIG,SAAS,MAAM;oBACjB,OAAOO,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;wBAAEO,OAAO;oBAAyB;gBAChE;gBACA,MAAMsC,OACJZ,UAAU,kCAAkC,kCAC5CO,cACAV;gBAEF,OAAOO,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEwC;oBAAcP;gBAAQ;YACtD;YAEA,SAAS;YACT,MAAMoB,UAAU,MAAMxB,0BAA0BtE,WAAW;gBAAEQ;gBAAO6C,IAAI4B;gBAAcrB,UAAUsB,OAAO5E,GAAG;YAAC;YAC3G,IAAI,CAACwF,SAAS;gBACZ,OAAOhB,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEO,OAAO;gBAAyB;YAChE;YACA,IAAI,CAAC8C,QAAQtB,KAAK,EAAE;gBAClB,MAAMc,OAAO,6BAA6BL,cAAca,QAAQvB,IAAI;YACtE;YACA,OAAOO,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEwC;gBAAca,SAAS;YAAK;QAC5D,EAAE,OAAO9C,OAAO;YACd+C,QAAQ/C,KAAK,CAAC,mCAAmCxC,OAAOK,QAAQmC;YAChE,OAAO8B,IACJ/B,MAAM,CAAC,KACPN,IAAI,CAAC;gBAAEO,OAAO;YAAqC;QACxD;IACF;AACF;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASgD,gCACdpB,YAA6C,CAAC,CAAC;IAE/C,OAAO,OAAOC,KAAKC;;YAKKD,WACMA,YACbA;QANf,MAAME,OAAO,aAAKhF,eAAkB6E;QACpC,IAAIC,IAAItC,MAAM,KAAK,QAAQ;YACzB,OAAOuC,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAqB;QAC5D;QACA,MAAMjC,SAASK,gBAAOyD,YAAAA,IAAIrC,IAAI,qBAARqC,UAAU9D,MAAM,mBAAI;QAC1C,MAAMkE,eAAe7D,iBAAOyD,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUI,YAAY,oBAAI;QACtD,MAAMgB,UAASpB,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUoB,MAAM;QAC/B,IAAI,CAACtH,aAAaoC,SAAS;YACzB,OAAO+D,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAiB;QACxD;QACA,IAAI,CAACrE,aAAasG,eAAe;YAC/B,OAAOH,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAuB;QAC9D;QACA,IAAI,OAAOiD,WAAW,WAAW;YAC/B,OAAOnB,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAA2B;QAClE;QACA,MAAM7C,QAAQc,YAAY4D,IAAI3D,OAAO;QACrC,IAAI,CAACf,OAAO,OAAO2E,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;YAAEO,OAAO;QAAkB;QAEnE,IAAI;uBA8D0BkC;gBAlDEL;YAX9B,MAAMK,SAAS,MAAMH,KAAK7E,aAAa,CAACC,OAAOuC,KAAK,CAAC,IAAM;YAC3D,IAAI,CAACwC,QAAQ,OAAOJ,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAkB;YACpE,MAAMhD,YAAY+E,KAAK/E,SAAS;YAChC,MAAMkG,OAAO,MAAMlG,UAAUyB,UAAU,CAAC,SAASC,GAAG,CAACX,QAAQqB,GAAG;YAChE,IAAI,CAAC8D,KAAK/D,MAAM,EAAE,OAAO2C,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEO,OAAO;YAAe;YACtE,MAAMxC,QAAQ,MAAMuE,KAAK9F,mBAAmB,CAAC8B;YAC7C,IAAI,CAACP,OAAO;gBACV,OAAOsE,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAC1BO,OAAO;gBACT;YACF;YACA,MAAMmD,iBAAiB/E,iBAAOyD,aAAAA,IAAIrC,IAAI,qBAARqC,WAAUrE,KAAK,oBAAI;YACjD,IAAI2F,kBAAkBA,mBAAmB3F,OAAO;gBAC9C,OAAOsE,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEO,OAAO;gBAAwC;YAC/E;YACA;;;;;OAKC,GACD,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,IAAIoD,UAAUhI,oBAAoB8H,MAAMhB,OAAO5E,GAAG,EAAE,uBAAuBjB;YAC3E,IAAI,CAAC+G,SAAS;gBACZ,MAAMjB,aAAa,MAAMJ,KAAK7F,oBAAoB,CAACgG,OAAO5E,GAAG,EAAEE;gBAC/D,MAAME,SAASyE,8BAAAA,WAAYzE,MAAM;gBACjC0F,UACEC,QAAQ3F,WACRjC,gBAAgBiC,WACf,MAAMqE,KAAKtE,WAAW,CAACD,OAAOE,QAAmC;YACtE;YACA,IAAI,CAAC0F,SAAS;gBACZ,OAAOtB,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;oBAAEO,OAAO1D,2BAA2B;gBAAuB;YACzF;YAEA,MAAMiF,OAAO,MAAMvE,UAAUiE,cAAc,CAAC,OAAOC;oBAenCE;gBAdd,MAAMD,MAAM3C,cAAcxB,WAAWQ,OAAOkB,GAAG,CAACuD;gBAChD,MAAMb,UAAU,MAAMF,YAAY9B,GAAG,CAAC+B;gBACtC,IACE,CAACC,QAAQjC,MAAM,IACfiC,QAAQhC,GAAG,CAAC,gBACZ,CAAC9D,cAAc8F,QAAQhC,GAAG,CAAC,cAAsCrB,SACjE;oBACA,OAAO;gBACT;gBACAmD,YAAYG,MAAM,CAACF,KAAK;oBACtBZ,eAAe0C,SACX9G,WAAWmH,UAAU,CAACvF,UACtB5B,WAAWoH,WAAW,CAACxF;gBAC7B;gBACA,OAAOK,QAAOgD,eAAAA,QAAQhC,GAAG,CAAC,mBAAZgC,eAAuB;YACvC;YACA,IAAIG,SAAS,MAAM;gBACjB,OAAOO,IACJ/B,MAAM,CAAC,KACPN,IAAI,CAAC;oBAAEO,OAAO;gBAAiD;YACpE;YACA,MAAM+B,KAAKjG,eAAe,CACxBiC,QACA;gBAAET,KAAK4E,OAAO5E,GAAG;gBAAEC,KAAK,GAAE2E,gBAAAA,OAAO3E,KAAK,YAAZ2E,gBAAgB;YAAK,GAC/Ce,SACI,0CACA,0CACJ;gBAAET,MAAM;gBAAYnC,IAAI4B;gBAAcV;YAAK;YAE7C,OAAOO,IAAI/B,MAAM,CAAC,KAAKN,IAAI,CAAC;gBAAEwC;gBAAclE;gBAAQkF;YAAO;QAC7D,EAAE,OAAOjD,OAAO;YACd+C,QAAQ/C,KAAK,CAAC,kCAAkCjC,QAAQkE,cAAcjC;YACtE,OAAO8B,IACJ/B,MAAM,CAAC,KACPN,IAAI,CAAC;gBAAEO,OAAO;YAAqC;QACxD;IACF;AACF;AAEA,OAAO,MAAMwD,8BAA8B7B,oCAAmC;AAC9E,OAAO,MAAM8B,4BAA4BT,kCAAiC"}
|
|
@@ -50,7 +50,8 @@ export declare const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
|
|
|
50
50
|
/** A draft's name when the caller asks for none. */
|
|
51
51
|
export declare const AUTOMATION_DRAFT_DEFAULT_NAME = "New automation";
|
|
52
52
|
/** The resources route's refusal for a member who may not write the site. */
|
|
53
|
-
|
|
53
|
+
/** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */
|
|
54
|
+
export declare const AUTOMATION_DRAFT_ROLE_REFUSAL: string;
|
|
54
55
|
/** What a person is told when their plan has no actions builder. It names the page that sells one. */
|
|
55
56
|
export declare const AUTOMATION_DRAFT_PLAN_REFUSAL = "Automations are not included on this workspace\u2019s plan. Upgrade in Billing to build them.";
|
|
56
57
|
/** The resources route's refusal at the cap. */
|
|
@@ -14,8 +14,8 @@ 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 { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
19
19
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
20
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
21
21
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
@@ -25,7 +25,7 @@ import { interactionPlaceholders } from "@aglyn/aglyn/app-utils/draft-placeholde
|
|
|
25
25
|
/** The resource name this writer is registered under. */ export const AUTOMATION_DRAFT_RESOURCE = 'automation';
|
|
26
26
|
/** The longest name the Actions editor stores. */ export const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60;
|
|
27
27
|
/** A draft's name when the caller asks for none. */ export const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation';
|
|
28
|
-
/** The resources route's refusal for a member who may not write the site. */
|
|
28
|
+
/** The resources route's refusal for a member who may not write the site. */ /** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */ export const AUTOMATION_DRAFT_ROLE_REFUSAL = siteKeyRefusal('site.edit');
|
|
29
29
|
/** 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.';
|
|
30
30
|
/** 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';
|
|
31
31
|
/** A trigger bound to one element, which makes an action an interaction of its page. */ const LEAF_SELECTOR = /^\[data-aglyn="leaf:.+"\]$/;
|
|
@@ -324,9 +324,7 @@ function recordOf(snapshot) {
|
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
326
|
function roleRefusal(host, uid) {
|
|
327
|
-
|
|
328
|
-
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
329
|
-
return hostRoleCanWrite(role) ? null : {
|
|
327
|
+
return holdsSiteKey(host, uid, 'site.edit') ? null : {
|
|
330
328
|
status: 403,
|
|
331
329
|
error: AUTOMATION_DRAFT_ROLE_REFUSAL
|
|
332
330
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server-automation-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n ACTIONS_MAX_PER_HOST,\n siteInteractionDocument,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './model/host-actions'\nimport { interactionPlaceholders } from '@aglyn/aglyn/app-utils/draft-placeholders'\n\n/**\n * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).\n *\n * The draft writer this plugin registers for the `automation` resource on the\n * core's resource-drafts seam. A plugin that produces an automation — a\n * generator working from a description, an importer bringing flows over from\n * another tool — asks for the writer by name and gets this plugin's rules,\n * never a copy of them:\n *\n * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the\n * Actions editor saves (`siteInteractionDocument`), with only the fields each\n * step type holds. It is listed on the Automation page's Actions, opens in\n * that editor, and runs through the same executor as one a person made.\n * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,\n * so nothing it describes runs until a person switches it on.\n * - THE SCHEMA is the editor's own validator, `validateHostAction`, which the\n * executor's writers share. A placeholder the draft could not fill — a\n * bracketed list name, a bracketed condition value — passes it, because it\n * is a value a person replaces, and the facts name every one.\n * - THE ROOM is the flat platform cap on live actions, counted inside the\n * transaction with the arithmetic `/api/hosts/resources` uses.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content. THE PLAN is the Actions editor's: the `actions` entitlement.\n *\n * Nothing is enabled, sent or run, and the writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const AUTOMATION_DRAFT_RESOURCE = 'automation'\n\n/** The longest name the Actions editor stores. */\nexport const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60\n\n/** A draft's name when the caller asks for none. */\nexport const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation'\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const AUTOMATION_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** What a person is told when their plan has no actions builder. It names the page that sells one. */\nexport const AUTOMATION_DRAFT_PLAN_REFUSAL =\n 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.'\n\n/** The resources route's refusal at the cap. */\nexport const AUTOMATION_DRAFT_LIMIT_REFUSAL =\n `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` +\n 'delete some to make room'\n\n/** A trigger bound to one element, which makes an action an interaction of its page. */\nconst LEAF_SELECTOR = /^\\[data-aglyn=\"leaf:.+\"\\]$/\n\n/**\n * The fields each step type holds, besides `type` and the step's own `when`.\n * Total over the step types, so a type the model gains is a compile error here\n * until its fields are named, rather than a draft that silently drops them.\n */\nexport const AUTOMATION_STEP_FIELDS: Readonly<Record<HostActionStepType, readonly string[]>> = {\n runWorkflow: ['workflowId', 'workflowName'],\n siteAlert: ['message', 'severity'],\n customEvent: ['eventName'],\n datasetAppend: ['datasetId', 'datasetName'],\n webhookPost: ['webhookId', 'webhookName'],\n showOverlay: ['overlayId', 'overlayName'],\n stickyNav: ['selector'],\n addClass: ['selector', 'className'],\n removeClass: ['selector', 'className'],\n toggleClass: ['selector', 'className'],\n showElement: ['selector', 'delayMs', 'dismissOn'],\n hideElement: ['selector', 'delayMs'],\n toggleElement: ['selector', 'delayMs', 'dismissOn'],\n setAttribute: ['selector', 'name', 'value'],\n removeAttribute: ['selector', 'name'],\n scrollTo: ['selector', 'behavior', 'offsetPx'],\n playVideo: ['selector'],\n openDrawer: ['drawerNodeId'],\n closeDrawer: ['drawerNodeId'],\n toggleDrawer: ['drawerNodeId'],\n openMenu: ['menuNodeId'],\n closeMenu: ['menuNodeId'],\n toggleMenu: ['menuNodeId'],\n showHtml: ['html'],\n runJs: ['code'],\n redirect: ['url', 'screenId'],\n trackGaEvent: ['eventName', 'params'],\n sendEmail: ['subject', 'body', 'toField', 'topicId', 'transactional'],\n notifyAdmins: ['title', 'body'],\n enrollList: ['listId', 'listName'],\n updateDataset: ['datasetId', 'datasetName'],\n assignCampaign: ['campaignId', 'campaignName'],\n wait: ['delayMinutes'],\n waitForEvent: ['eventName', 'timeoutMinutes'],\n exitFlow: [],\n setContactStage: ['lifecycleStage'],\n addContactTag: ['tag'],\n assignContactOwner: ['ownerUid', 'ownerEmail', 'roundRobin'],\n createCrmTask: ['title', 'kind', 'priority', 'dueInDays', 'assigneeUid', 'assigneeEmail'],\n logCrmActivity: ['kind', 'body', 'direction'],\n}\n\n/** The trigger keys an automation stores. */\nconst TRIGGER_FIELDS = [\n 'event',\n 'filter',\n 'conditions',\n 'combinator',\n 'selector',\n 'threshold',\n 'pathPattern',\n 'oncePerVisitor',\n 'oncePerSession',\n 'cooldownMinutes',\n 'everyTime',\n] as const\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The keys of `source` that `keys` names and that hold something. */\nfunction picked(source: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n if (source[key] !== undefined && source[key] !== null) out[key] = source[key]\n }\n return out\n}\n\nfunction conditionOf(value: unknown): HostActionTriggerCondition | null {\n if (!isRecord(value)) return null\n return picked(value, ['field', 'op', 'value']) as unknown as HostActionTriggerCondition\n}\n\nexport type AutomationDraftContentRead =\n | { ok: true; action: HostAction }\n | { ok: false; problems: string[] }\n\n/**\n * What a caller sends as `content` — `{ action }` — read into the action this\n * writer would store: the trigger's and each step's own fields and nothing\n * else, named, OFF, and held to the editor's validator.\n */\nexport function readAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): AutomationDraftContentRead {\n const raw = content['action']\n if (!isRecord(raw)) return { ok: false, problems: ['The draft holds no automation'] }\n const trigger = raw['trigger']\n const steps = raw['steps']\n if (!isRecord(trigger)) return { ok: false, problems: ['Pick a trigger event'] }\n if (!Array.isArray(steps)) return { ok: false, problems: ['Add at least one step'] }\n const problems: string[] = []\n const knownTypes = new Set(Object.keys(AUTOMATION_STEP_FIELDS))\n const cleanSteps: HostActionStep[] = []\n steps.forEach((step, index) => {\n const type = isRecord(step) ? step['type'] : undefined\n if (typeof type !== 'string' || !knownTypes.has(type)) {\n problems.push(`Step ${index + 1}: not a step the Actions editor offers`)\n return\n }\n const source = step as Record<string, unknown>\n const when = isRecord(source['when'])\n ? {\n conditions: (Array.isArray(source['when']['conditions']) ? source['when']['conditions'] : [])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null),\n ...(typeof source['when']['combinator'] === 'string'\n ? { combinator: source['when']['combinator'] }\n : {}),\n }\n : null\n cleanSteps.push({\n type,\n ...picked(source, AUTOMATION_STEP_FIELDS[type as HostActionStepType]),\n ...(when && when.conditions.length ? { when } : {}),\n } as HostActionStep)\n })\n const cleanTrigger = picked(trigger, TRIGGER_FIELDS)\n if (Array.isArray(cleanTrigger['conditions'])) {\n cleanTrigger['conditions'] = (cleanTrigger['conditions'] as unknown[])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null)\n }\n // An element's own interaction belongs to the page that holds the element,\n // and is authored there; the Actions list does not show one.\n if (typeof cleanTrigger['selector'] === 'string' && LEAF_SELECTOR.test(cleanTrigger['selector'])) {\n problems.push('An interaction on one element is set up on that element, not as an automation')\n }\n const name = String(raw['name'] ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS)\n const action: HostAction = {\n name: name || AUTOMATION_DRAFT_DEFAULT_NAME,\n trigger: cleanTrigger as unknown as HostAction['trigger'],\n steps: cleanSteps,\n enabled: false,\n }\n const invalid = validateHostAction(action)\n if (invalid) problems.push(invalid)\n return problems.length ? { ok: false, problems } : { ok: true, action }\n}\n\n/** What the writer reports about an automation beyond its identity. */\nfunction factsOf(action: Pick<HostAction, 'trigger' | 'steps' | 'enabled'>): Record<string, unknown> {\n return {\n event: action.trigger?.event ?? '',\n steps: (action.steps ?? []).length,\n enabled: action.enabled === true,\n placeholders: interactionPlaceholders(action).length,\n }\n}\n\n/** Whether content is an automation this site could store: well-formed, off, and valid. */\nexport function checkAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): PluginDraftCheck {\n const read = readAutomationDraftContent(content)\n return read.ok === false ? read : { ok: true, facts: factsOf(read.action) }\n}\n\nfunction recordOf(snapshot: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (snapshot.data() ?? {}) as HostAction\n return {\n id: snapshot.id,\n name: String(data.name ?? ''),\n versionId: null,\n facts: factsOf(data),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: AUTOMATION_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: Readonly<Record<string, unknown>> | null): PluginDraftRefusal | null {\n return checkEntitlement(org as Partial<AglynOrgBilling> | null, 'actions')\n ? null\n : { status: 403, error: AUTOMATION_DRAFT_PLAN_REFUSAL }\n}\n\n/** The live actions the site holds: a soft-deleted one frees its slot. */\nfunction liveCount(rows: FirebaseFirestore.QuerySnapshot): number {\n return rows.docs.filter((row) => row.get('deletedAt') == null).length\n}\n\nfunction roomRefusal(live: number): PluginDraftRefusal | null {\n return live >= ACTIONS_MAX_PER_HOST ? { status: 403, error: AUTOMATION_DRAFT_LIMIT_REFUSAL } : null\n}\n\nexport interface AutomationDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createAutomationDraftWriter(\n deps: AutomationDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n planRefusal(context.org) ??\n roomRefusal(liveCount(await hostRef.collection('actions').select('deletedAt').get()))\n )\n },\n\n check: (content) => checkAutomationDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const snapshot = await firestore().collection('hosts').doc(hostId).collection('actions').doc(id).get()\n return snapshot.exists ? recordOf(snapshot) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readAutomationDraftContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const actionRef = hostRef.collection('actions').doc(request.id)\n const name =\n String(request.name ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS) || read.action.name\n const action: HostAction = { ...read.action, name, enabled: false, recipe: null }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(actionRef),\n tx.get(hostRef.collection('actions').select('deletedAt')),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const refusal =\n roleRefusal(host, request.uid) ?? planRefusal(request.org) ?? roomRefusal(liveCount(rows))\n if (refusal) return { ok: false, ...refusal }\n tx.create(actionRef, {\n ...siteInteractionDocument(action),\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return { ok: true, replayed: false, id: request.id, name, versionId: null, facts: factsOf(action) }\n })\n },\n }\n}\n\nexport const automationDraftWriter = createAutomationDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerAutomationDraftWriter(): void {\n registerPluginResourceDraftWriter(AUTOMATION_DRAFT_RESOURCE, automationDraftWriter, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","ACTIONS_MAX_PER_HOST","siteInteractionDocument","validateHostAction","interactionPlaceholders","AUTOMATION_DRAFT_RESOURCE","AUTOMATION_DRAFT_NAME_MAX_CHARS","AUTOMATION_DRAFT_DEFAULT_NAME","AUTOMATION_DRAFT_ROLE_REFUSAL","AUTOMATION_DRAFT_PLAN_REFUSAL","AUTOMATION_DRAFT_LIMIT_REFUSAL","LEAF_SELECTOR","AUTOMATION_STEP_FIELDS","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","removeClass","toggleClass","showElement","hideElement","toggleElement","setAttribute","removeAttribute","scrollTo","playVideo","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","TRIGGER_FIELDS","isRecord","value","Array","isArray","picked","source","keys","out","key","undefined","conditionOf","readAutomationDraftContent","content","raw","ok","problems","trigger","steps","knownTypes","Set","Object","cleanSteps","forEach","step","index","type","has","push","when","conditions","map","filter","clause","combinator","length","cleanTrigger","test","name","String","replace","trim","slice","action","enabled","invalid","factsOf","event","placeholders","checkAutomationDraftContent","read","facts","recordOf","snapshot","data","id","versionId","roleRefusal","host","uid","role","get","status","error","planRefusal","org","liveCount","rows","docs","row","roomRefusal","live","createAutomationDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","doc","hostId","exists","select","check","write","request","db","actionRef","recipe","runTransaction","tx","existing","Promise","all","replayed","create","createdAt","now","updatedAt","createdBy","automationDraftWriter","registerAutomationDraftWriter","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/server-automation-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n ACTIONS_MAX_PER_HOST,\n siteInteractionDocument,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport {\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n validateHostAction,\n} from './model/host-actions'\nimport { interactionPlaceholders } from '@aglyn/aglyn/app-utils/draft-placeholders'\n\n/**\n * AN AUTOMATION ANOTHER PLUGIN ASKS FOR (AGL-2919).\n *\n * The draft writer this plugin registers for the `automation` resource on the\n * core's resource-drafts seam. A plugin that produces an automation — a\n * generator working from a description, an importer bringing flows over from\n * another tool — asks for the writer by name and gets this plugin's rules,\n * never a copy of them:\n *\n * - THE DOCUMENT is `hosts/{hostId}/actions/{id}` in the stored shape the\n * Actions editor saves (`siteInteractionDocument`), with only the fields each\n * step type holds. It is listed on the Automation page's Actions, opens in\n * that editor, and runs through the same executor as one a person made.\n * - IT IS OFF. A draft is written `enabled: false` whatever the caller sent,\n * so nothing it describes runs until a person switches it on.\n * - THE SCHEMA is the editor's own validator, `validateHostAction`, which the\n * executor's writers share. A placeholder the draft could not fill — a\n * bracketed list name, a bracketed condition value — passes it, because it\n * is a value a person replaces, and the facts name every one.\n * - THE ROOM is the flat platform cap on live actions, counted inside the\n * transaction with the arithmetic `/api/hosts/resources` uses.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content. THE PLAN is the Actions editor's: the `actions` entitlement.\n *\n * Nothing is enabled, sent or run, and the writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const AUTOMATION_DRAFT_RESOURCE = 'automation'\n\n/** The longest name the Actions editor stores. */\nexport const AUTOMATION_DRAFT_NAME_MAX_CHARS = 60\n\n/** A draft's name when the caller asks for none. */\nexport const AUTOMATION_DRAFT_DEFAULT_NAME = 'New automation'\n\n/** The resources route's refusal for a member who may not write the site. */\n/** Names \"Edit site content\" (`site.edit`, AGL-4047), never a role. */\nexport const AUTOMATION_DRAFT_ROLE_REFUSAL = siteKeyRefusal('site.edit')\n\n/** What a person is told when their plan has no actions builder. It names the page that sells one. */\nexport const AUTOMATION_DRAFT_PLAN_REFUSAL =\n 'Automations are not included on this workspace’s plan. Upgrade in Billing to build them.'\n\n/** The resources route's refusal at the cap. */\nexport const AUTOMATION_DRAFT_LIMIT_REFUSAL =\n `interactions and actions are capped at ${ACTIONS_MAX_PER_HOST} per site — ` +\n 'delete some to make room'\n\n/** A trigger bound to one element, which makes an action an interaction of its page. */\nconst LEAF_SELECTOR = /^\\[data-aglyn=\"leaf:.+\"\\]$/\n\n/**\n * The fields each step type holds, besides `type` and the step's own `when`.\n * Total over the step types, so a type the model gains is a compile error here\n * until its fields are named, rather than a draft that silently drops them.\n */\nexport const AUTOMATION_STEP_FIELDS: Readonly<Record<HostActionStepType, readonly string[]>> = {\n runWorkflow: ['workflowId', 'workflowName'],\n siteAlert: ['message', 'severity'],\n customEvent: ['eventName'],\n datasetAppend: ['datasetId', 'datasetName'],\n webhookPost: ['webhookId', 'webhookName'],\n showOverlay: ['overlayId', 'overlayName'],\n stickyNav: ['selector'],\n addClass: ['selector', 'className'],\n removeClass: ['selector', 'className'],\n toggleClass: ['selector', 'className'],\n showElement: ['selector', 'delayMs', 'dismissOn'],\n hideElement: ['selector', 'delayMs'],\n toggleElement: ['selector', 'delayMs', 'dismissOn'],\n setAttribute: ['selector', 'name', 'value'],\n removeAttribute: ['selector', 'name'],\n scrollTo: ['selector', 'behavior', 'offsetPx'],\n playVideo: ['selector'],\n openDrawer: ['drawerNodeId'],\n closeDrawer: ['drawerNodeId'],\n toggleDrawer: ['drawerNodeId'],\n openMenu: ['menuNodeId'],\n closeMenu: ['menuNodeId'],\n toggleMenu: ['menuNodeId'],\n showHtml: ['html'],\n runJs: ['code'],\n redirect: ['url', 'screenId'],\n trackGaEvent: ['eventName', 'params'],\n sendEmail: ['subject', 'body', 'toField', 'topicId', 'transactional'],\n notifyAdmins: ['title', 'body'],\n enrollList: ['listId', 'listName'],\n updateDataset: ['datasetId', 'datasetName'],\n assignCampaign: ['campaignId', 'campaignName'],\n wait: ['delayMinutes'],\n waitForEvent: ['eventName', 'timeoutMinutes'],\n exitFlow: [],\n setContactStage: ['lifecycleStage'],\n addContactTag: ['tag'],\n assignContactOwner: ['ownerUid', 'ownerEmail', 'roundRobin'],\n createCrmTask: ['title', 'kind', 'priority', 'dueInDays', 'assigneeUid', 'assigneeEmail'],\n logCrmActivity: ['kind', 'body', 'direction'],\n}\n\n/** The trigger keys an automation stores. */\nconst TRIGGER_FIELDS = [\n 'event',\n 'filter',\n 'conditions',\n 'combinator',\n 'selector',\n 'threshold',\n 'pathPattern',\n 'oncePerVisitor',\n 'oncePerSession',\n 'cooldownMinutes',\n 'everyTime',\n] as const\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The keys of `source` that `keys` names and that hold something. */\nfunction picked(source: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n if (source[key] !== undefined && source[key] !== null) out[key] = source[key]\n }\n return out\n}\n\nfunction conditionOf(value: unknown): HostActionTriggerCondition | null {\n if (!isRecord(value)) return null\n return picked(value, ['field', 'op', 'value']) as unknown as HostActionTriggerCondition\n}\n\nexport type AutomationDraftContentRead =\n | { ok: true; action: HostAction }\n | { ok: false; problems: string[] }\n\n/**\n * What a caller sends as `content` — `{ action }` — read into the action this\n * writer would store: the trigger's and each step's own fields and nothing\n * else, named, OFF, and held to the editor's validator.\n */\nexport function readAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): AutomationDraftContentRead {\n const raw = content['action']\n if (!isRecord(raw)) return { ok: false, problems: ['The draft holds no automation'] }\n const trigger = raw['trigger']\n const steps = raw['steps']\n if (!isRecord(trigger)) return { ok: false, problems: ['Pick a trigger event'] }\n if (!Array.isArray(steps)) return { ok: false, problems: ['Add at least one step'] }\n const problems: string[] = []\n const knownTypes = new Set(Object.keys(AUTOMATION_STEP_FIELDS))\n const cleanSteps: HostActionStep[] = []\n steps.forEach((step, index) => {\n const type = isRecord(step) ? step['type'] : undefined\n if (typeof type !== 'string' || !knownTypes.has(type)) {\n problems.push(`Step ${index + 1}: not a step the Actions editor offers`)\n return\n }\n const source = step as Record<string, unknown>\n const when = isRecord(source['when'])\n ? {\n conditions: (Array.isArray(source['when']['conditions']) ? source['when']['conditions'] : [])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null),\n ...(typeof source['when']['combinator'] === 'string'\n ? { combinator: source['when']['combinator'] }\n : {}),\n }\n : null\n cleanSteps.push({\n type,\n ...picked(source, AUTOMATION_STEP_FIELDS[type as HostActionStepType]),\n ...(when && when.conditions.length ? { when } : {}),\n } as HostActionStep)\n })\n const cleanTrigger = picked(trigger, TRIGGER_FIELDS)\n if (Array.isArray(cleanTrigger['conditions'])) {\n cleanTrigger['conditions'] = (cleanTrigger['conditions'] as unknown[])\n .map(conditionOf)\n .filter((clause): clause is HostActionTriggerCondition => clause !== null)\n }\n // An element's own interaction belongs to the page that holds the element,\n // and is authored there; the Actions list does not show one.\n if (typeof cleanTrigger['selector'] === 'string' && LEAF_SELECTOR.test(cleanTrigger['selector'])) {\n problems.push('An interaction on one element is set up on that element, not as an automation')\n }\n const name = String(raw['name'] ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS)\n const action: HostAction = {\n name: name || AUTOMATION_DRAFT_DEFAULT_NAME,\n trigger: cleanTrigger as unknown as HostAction['trigger'],\n steps: cleanSteps,\n enabled: false,\n }\n const invalid = validateHostAction(action)\n if (invalid) problems.push(invalid)\n return problems.length ? { ok: false, problems } : { ok: true, action }\n}\n\n/** What the writer reports about an automation beyond its identity. */\nfunction factsOf(action: Pick<HostAction, 'trigger' | 'steps' | 'enabled'>): Record<string, unknown> {\n return {\n event: action.trigger?.event ?? '',\n steps: (action.steps ?? []).length,\n enabled: action.enabled === true,\n placeholders: interactionPlaceholders(action).length,\n }\n}\n\n/** Whether content is an automation this site could store: well-formed, off, and valid. */\nexport function checkAutomationDraftContent(\n content: Readonly<Record<string, unknown>>,\n): PluginDraftCheck {\n const read = readAutomationDraftContent(content)\n return read.ok === false ? read : { ok: true, facts: factsOf(read.action) }\n}\n\nfunction recordOf(snapshot: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (snapshot.data() ?? {}) as HostAction\n return {\n id: snapshot.id,\n name: String(data.name ?? ''),\n versionId: null,\n facts: factsOf(data),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n return holdsSiteKey(host, uid, 'site.edit') ? null : { status: 403, error: AUTOMATION_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: Readonly<Record<string, unknown>> | null): PluginDraftRefusal | null {\n return checkEntitlement(org as Partial<AglynOrgBilling> | null, 'actions')\n ? null\n : { status: 403, error: AUTOMATION_DRAFT_PLAN_REFUSAL }\n}\n\n/** The live actions the site holds: a soft-deleted one frees its slot. */\nfunction liveCount(rows: FirebaseFirestore.QuerySnapshot): number {\n return rows.docs.filter((row) => row.get('deletedAt') == null).length\n}\n\nfunction roomRefusal(live: number): PluginDraftRefusal | null {\n return live >= ACTIONS_MAX_PER_HOST ? { status: 403, error: AUTOMATION_DRAFT_LIMIT_REFUSAL } : null\n}\n\nexport interface AutomationDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createAutomationDraftWriter(\n deps: AutomationDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n planRefusal(context.org) ??\n roomRefusal(liveCount(await hostRef.collection('actions').select('deletedAt').get()))\n )\n },\n\n check: (content) => checkAutomationDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const snapshot = await firestore().collection('hosts').doc(hostId).collection('actions').doc(id).get()\n return snapshot.exists ? recordOf(snapshot) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readAutomationDraftContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const actionRef = hostRef.collection('actions').doc(request.id)\n const name =\n String(request.name ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, AUTOMATION_DRAFT_NAME_MAX_CHARS) || read.action.name\n const action: HostAction = { ...read.action, name, enabled: false, recipe: null }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(actionRef),\n tx.get(hostRef.collection('actions').select('deletedAt')),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const refusal =\n roleRefusal(host, request.uid) ?? planRefusal(request.org) ?? roomRefusal(liveCount(rows))\n if (refusal) return { ok: false, ...refusal }\n tx.create(actionRef, {\n ...siteInteractionDocument(action),\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return { ok: true, replayed: false, id: request.id, name, versionId: null, facts: factsOf(action) }\n })\n },\n }\n}\n\nexport const automationDraftWriter = createAutomationDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerAutomationDraftWriter(): void {\n registerPluginResourceDraftWriter(AUTOMATION_DRAFT_RESOURCE, automationDraftWriter, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["checkEntitlement","holdsSiteKey","siteKeyRefusal","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","ACTIONS_MAX_PER_HOST","siteInteractionDocument","validateHostAction","interactionPlaceholders","AUTOMATION_DRAFT_RESOURCE","AUTOMATION_DRAFT_NAME_MAX_CHARS","AUTOMATION_DRAFT_DEFAULT_NAME","AUTOMATION_DRAFT_ROLE_REFUSAL","AUTOMATION_DRAFT_PLAN_REFUSAL","AUTOMATION_DRAFT_LIMIT_REFUSAL","LEAF_SELECTOR","AUTOMATION_STEP_FIELDS","runWorkflow","siteAlert","customEvent","datasetAppend","webhookPost","showOverlay","stickyNav","addClass","removeClass","toggleClass","showElement","hideElement","toggleElement","setAttribute","removeAttribute","scrollTo","playVideo","openDrawer","closeDrawer","toggleDrawer","openMenu","closeMenu","toggleMenu","showHtml","runJs","redirect","trackGaEvent","sendEmail","notifyAdmins","enrollList","updateDataset","assignCampaign","wait","waitForEvent","exitFlow","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","TRIGGER_FIELDS","isRecord","value","Array","isArray","picked","source","keys","out","key","undefined","conditionOf","readAutomationDraftContent","content","raw","ok","problems","trigger","steps","knownTypes","Set","Object","cleanSteps","forEach","step","index","type","has","push","when","conditions","map","filter","clause","combinator","length","cleanTrigger","test","name","String","replace","trim","slice","action","enabled","invalid","factsOf","event","placeholders","checkAutomationDraftContent","read","facts","recordOf","snapshot","data","id","versionId","roleRefusal","host","uid","status","error","planRefusal","org","liveCount","rows","docs","row","get","roomRefusal","live","createAutomationDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","doc","hostId","exists","select","check","write","request","db","actionRef","recipe","runTransaction","tx","existing","Promise","all","replayed","create","createdAt","now","updatedAt","createdBy","automationDraftWriter","registerAutomationDraftWriter","pluginId"],"mappings":"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"}
|
|
@@ -22,6 +22,8 @@ export interface OrgAutomationsPackageDeps {
|
|
|
22
22
|
firestore(): Firestore;
|
|
23
23
|
readOrg(orgId: string): Promise<Record<string, unknown> | null>;
|
|
24
24
|
memberOf(uid: string, orgId: string): Promise<Record<string, unknown> | null>;
|
|
25
|
+
/** Whether a member holds "Manage org automations" (AGL-4047): `memberHasOrgPermission`. */
|
|
26
|
+
holdsOrgAutomations(orgId: string, member: Record<string, unknown>): Promise<boolean>;
|
|
25
27
|
logOrgActivity(orgId: string, uid: string, action: string, target: {
|
|
26
28
|
id: string;
|
|
27
29
|
name: string;
|
|
@@ -30,7 +30,8 @@ import { existingPackageItemsOf } from "@aglyn/aglyn/data-transfer";
|
|
|
30
30
|
import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
31
31
|
import { checkEntitlement, isOrgWideMember, planLabelGrantingFeature } from "@aglyn/aglyn/server";
|
|
32
32
|
import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
33
|
-
import { getOrgDoc, logOrgActivity, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
|
|
33
|
+
import { getOrgDoc, logOrgActivity, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
|
|
34
|
+
import { workflowsPermissionRefusal } from "../model/workflows-permissions.js";
|
|
34
35
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
35
36
|
import { ORG_AUTOMATION_ACTIVITY_TARGET, ORG_AUTOMATIONS_COLLECTION, ORG_AUTOMATIONS_MAX, readOrgAutomation } from "../model/org-automations.js";
|
|
36
37
|
import { createOrgAutomationRecord, deleteOrgAutomationRecord, updateOrgAutomationRecord } from "../server/org-automations-routes.js";
|
|
@@ -47,6 +48,7 @@ function defaultDeps() {
|
|
|
47
48
|
var _this;
|
|
48
49
|
return (_ref = (_this = await resolveOrgMembership(uid, orgId)) == null ? void 0 : _this.member) != null ? _ref : null;
|
|
49
50
|
},
|
|
51
|
+
holdsOrgAutomations: (orgId, member)=>memberHasOrgPermission(orgId, member, 'workflows.orgAutomations'),
|
|
50
52
|
logOrgActivity: (orgId, uid, action, target)=>logOrgActivity(orgId, {
|
|
51
53
|
uid,
|
|
52
54
|
email: null
|
|
@@ -57,15 +59,12 @@ function defaultDeps() {
|
|
|
57
59
|
}
|
|
58
60
|
const automationsOf = (firestore, orgId)=>firestore.collection('orgs').doc(orgId).collection(ORG_AUTOMATIONS_COLLECTION);
|
|
59
61
|
/** Why `automations/manage` would refuse this importer, or `null` when it would not. */ async function importerRefusal(deps, ctx) {
|
|
60
|
-
var _member_role;
|
|
61
62
|
if (!ctx.actorUid) return 'Org automations are imported by a member of the organization.';
|
|
62
63
|
const member = await deps.memberOf(ctx.actorUid, ctx.orgId);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'
|
|
67
|
-
].includes(String((_member_role = member['role']) != null ? _member_role : ''))) {
|
|
68
|
-
return 'Org automations are written by an organization owner, admin or editor.';
|
|
64
|
+
// "Manage org automations" (AGL-4047), for members of the whole workspace,
|
|
65
|
+
// as `automations/manage` asks it.
|
|
66
|
+
if (!member || !isOrgWideMember(member) || !await deps.holdsOrgAutomations(ctx.orgId, member)) {
|
|
67
|
+
return workflowsPermissionRefusal('workflows.orgAutomations');
|
|
69
68
|
}
|
|
70
69
|
const org = await deps.readOrg(ctx.orgId);
|
|
71
70
|
if (!org || !isPluginEnabled(org, BUNDLE_ID)) return 'Workflows is not on for this workspace.';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/transfer/org-automations-package.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * ORG AUTOMATIONS IN A WORKSPACE PACKAGE — the server half (AGL-3535).\n *\n * Written through `automations/manage`'s own writes\n * (`createOrgAutomationRecord`, `updateOrgAutomationRecord`,\n * `deleteOrgAutomationRecord`): its reader, its placement check, its cap,\n * its soft delete and its activity lines. The importer is held to that\n * door's rules too — an org-wide owner, admin or editor, in a workspace\n * running Workflows on a plan with org automations — so an import can do\n * nothing that door would refuse. A new automation lands switched off; a\n * replaced one keeps whether it was on.\n *=========================================*/\n\nimport { DECLARED_SUBSCRIPTION_TOPICS } from '@aglyn/aglyn/app-utils/subscription-topics'\nimport { existingPackageItemsOf, type ExistingPackageItem, type TransferReferenceKind, type TransferRowResult } from '@aglyn/aglyn/data-transfer'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport type {\n TransferApplyResult,\n TransferApplyWriter,\n TransferPackageItemContent,\n TransferPackageItemWrite,\n TransferPackageRevertResult,\n TransferPackageRevertStep,\n TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { checkEntitlement, isOrgWideMember, planLabelGrantingFeature } from '@aglyn/aglyn/server'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { getOrgDoc, logOrgActivity, resolveOrgMembership } from '@aglyn/tenant-data-admin/server/organizations'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n ORG_AUTOMATION_ACTIVITY_TARGET,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n readOrgAutomation,\n} from '../model/org-automations'\nimport {\n createOrgAutomationRecord,\n deleteOrgAutomationRecord,\n updateOrgAutomationRecord,\n} from '../server/org-automations-routes'\nimport {\n ORG_AUTOMATION_REFERENCE_KINDS,\n ORG_AUTOMATIONS_TRANSFER_KEY,\n orgAutomationPackageContent,\n rawOrgAutomationContent,\n type OrgAutomationPackageContent,\n} from './org-automations-package'\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OrgAutomationsPackageDeps {\n firestore(): Firestore\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n memberOf(uid: string, orgId: string): Promise<Record<string, unknown> | null>\n logOrgActivity(orgId: string, uid: string, action: string, target: { id: string; name: string }): Promise<void>\n}\n\nfunction defaultDeps(): OrgAutomationsPackageDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore() as unknown as Firestore,\n readOrg: async (orgId) => ((await getOrgDoc(orgId)) as Record<string, unknown> | null) ?? null,\n memberOf: async (uid, orgId) => ((await resolveOrgMembership(uid, orgId))?.member as Record<string, unknown> | undefined) ?? null,\n logOrgActivity: (orgId, uid, action, target) =>\n logOrgActivity(orgId, { uid, email: null }, action, { type: ORG_AUTOMATION_ACTIVITY_TARGET, ...target }),\n }\n}\n\nconst automationsOf = (firestore: Firestore, orgId: string) =>\n firestore.collection('orgs').doc(orgId).collection(ORG_AUTOMATIONS_COLLECTION)\n\n/** Why `automations/manage` would refuse this importer, or `null` when it would not. */\nasync function importerRefusal(deps: OrgAutomationsPackageDeps, ctx: TransferResourceContext): Promise<string | null> {\n if (!ctx.actorUid) return 'Org automations are imported by a member of the organization.'\n const member = await deps.memberOf(ctx.actorUid, ctx.orgId)\n if (!member || !isOrgWideMember(member as never) || !['owner', 'admin', 'editor'].includes(String(member['role'] ?? ''))) {\n return 'Org automations are written by an organization owner, admin or editor.'\n }\n const org = await deps.readOrg(ctx.orgId)\n if (!org || !isPluginEnabled(org as never, BUNDLE_ID)) return 'Workflows is not on for this workspace.'\n if (!checkEntitlement(org as never, 'actions')) {\n return `Org automations need the ${planLabelGrantingFeature('actions') ?? 'Pro'} plan.`\n }\n return null\n}\n\nasync function readAll(\n deps: OrgAutomationsPackageDeps,\n orgId: string,\n ids?: readonly string[],\n): Promise<Array<TransferPackageItemContent<OrgAutomationPackageContent>>> {\n const collection = automationsOf(deps.firestore(), orgId)\n const docs = ids\n ? (await Promise.all(ids.map((id) => collection.doc(id).get()))).filter((doc) => doc.exists)\n : (await collection.where('deletedAt', '==', null).get()).docs\n return docs.flatMap((doc) => {\n if (doc.get('deletedAt')) return []\n const content = orgAutomationPackageContent(doc.data())\n return content ? [{ kind: ORG_AUTOMATIONS_TRANSFER_KEY, id: doc.id, name: content.name, content }] : []\n })\n}\n\n/** The fields one write stores: the content, read by the route's reader, switched on only if it already was. */\nfunction fieldsOf(content: OrgAutomationPackageContent, name: string, enabled: boolean) {\n return readOrgAutomation({ ...content, name, enabled })\n}\n\nexport function createOrgAutomationsPackage(deps: OrgAutomationsPackageDeps = defaultDeps()) {\n /** Whether the automation is on now; a new one is off. */\n const enabledNow = async (orgId: string, id: string) =>\n (await automationsOf(deps.firestore(), orgId).doc(id).get()).get('enabled') === true\n\n return {\n async items(ctx: TransferResourceContext): Promise<ExistingPackageItem[]> {\n return existingPackageItemsOf(ORG_AUTOMATIONS_TRANSFER_KEY, await readAll(deps, ctx.orgId))\n },\n\n async readItems(ctx: TransferResourceContext, ids: readonly string[]) {\n return readAll(deps, ctx.orgId, ids)\n },\n\n async problems(ctx: TransferResourceContext, write: TransferPackageItemWrite<OrgAutomationPackageContent>) {\n const refusal = await importerRefusal(deps, ctx)\n if (refusal) return [refusal]\n const content = rawOrgAutomationContent(write.content)\n const read = fieldsOf(content, write.rename?.name ?? content.name, false)\n if (read.ok === false) return [read.problem]\n const problems: string[] = []\n if (write.decision !== 'replace') {\n const collection = automationsOf(deps.firestore(), ctx.orgId)\n if ((await collection.doc(write.targetId).get()).exists) {\n problems.push('An automation with this id was deleted from this workspace, so it cannot come back under it.')\n }\n const live = (await collection.where('deletedAt', '==', null).count().get()).data().count\n if (live >= ORG_AUTOMATIONS_MAX) {\n problems.push(`An organization holds at most ${ORG_AUTOMATIONS_MAX} org automations — delete one you no longer use first.`)\n }\n }\n return problems\n },\n\n async referenceTargets(ctx: TransferResourceContext, kinds: readonly string[]): Promise<TransferReferenceKind[]> {\n const org = deps.firestore().collection('orgs').doc(ctx.orgId)\n const named = async (collection: string, field: string) =>\n (await org.collection(collection).get()).docs\n .filter((doc) => !doc.get('deletedAt'))\n .map((doc) => {\n const name = String(doc.get(field) ?? doc.get('label') ?? '').trim()\n return { id: doc.id, ...(name ? { name } : {}) }\n })\n const answers: TransferReferenceKind[] = []\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.list)) {\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.list, label: 'Email list', targets: await named('lists', 'name') })\n }\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.topic)) {\n // Asked only when the email plugin, which owns the topic catalog and answers `email.topics`\n // (AGL-3550), is not running here — and without it the catalog is the built-ins the plugins declare.\n const targets = DECLARED_SUBSCRIPTION_TOPICS.map((topic) => ({ id: topic.id, name: topic.name }))\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.topic, label: 'Subscription topic', targets })\n }\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.member)) {\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.member, label: 'Member', targets: await named('members', 'email') })\n }\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.dataset)) {\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.dataset, label: 'Dataset', targets: await named('datasets', 'name') })\n }\n return answers\n },\n\n async writeItems(\n ctx: TransferResourceContext,\n items: ReadonlyArray<TransferPackageItemWrite<OrgAutomationPackageContent>>,\n writer: TransferApplyWriter,\n ): Promise<TransferApplyResult> {\n const refusal = await importerRefusal(deps, ctx)\n const firestore = deps.firestore()\n const results: TransferRowResult[] = []\n for (const write of items) {\n const before = await writer.alreadyApplied(write.row)\n if (before) {\n results.push(before)\n continue\n }\n const content = rawOrgAutomationContent(write.content)\n const name = write.rename?.name ?? content.name\n const replacing = write.decision === 'replace'\n const read = fieldsOf(content, name, replacing ? await enabledNow(ctx.orgId, write.targetId) : false)\n let result: TransferRowResult\n if (refusal) result = { row: write.row, outcome: 'failed', message: refusal }\n else if (read.ok === false) result = { row: write.row, outcome: 'failed', message: read.problem }\n else {\n const input = { orgId: ctx.orgId, id: write.targetId, fields: read.value, actorUid: ctx.actorUid ?? '' }\n const written = replacing ? await updateOrgAutomationRecord(firestore, input) : await createOrgAutomationRecord(firestore, input)\n if (written.ok === false) result = { row: write.row, outcome: 'failed', recordId: write.targetId, message: written.error }\n else {\n await deps.logOrgActivity(ctx.orgId, ctx.actorUid ?? '', replacing ? 'Edited an org automation' : 'Created an org automation', {\n id: written.id,\n name: read.value.name,\n })\n result = { row: write.row, outcome: replacing ? 'updated' : 'created', recordId: written.id }\n }\n }\n await writer.markApplied(result)\n results.push(result)\n }\n return { results, undo: [] }\n },\n\n async revertItems(\n ctx: TransferResourceContext,\n steps: ReadonlyArray<TransferPackageRevertStep<OrgAutomationPackageContent>>,\n ): Promise<TransferPackageRevertResult> {\n const firestore = deps.firestore()\n const outcome: TransferPackageRevertResult = { done: [], refused: [] }\n for (const step of steps) {\n if (step.action === 'delete') {\n const deleted = await deleteOrgAutomationRecord(firestore, { orgId: ctx.orgId, id: step.id, actorUid: ctx.actorUid ?? '' })\n if (deleted && !deleted.again) {\n await deps.logOrgActivity(ctx.orgId, ctx.actorUid ?? '', 'Deleted an org automation', { id: step.id, name: deleted.name })\n }\n outcome.done.push(step.id)\n continue\n }\n const content = rawOrgAutomationContent(step.content)\n const read = fieldsOf(content, content.name, await enabledNow(ctx.orgId, step.id))\n if (read.ok === false) {\n outcome.refused.push({ id: step.id, reason: read.problem })\n continue\n }\n const written = await updateOrgAutomationRecord(firestore, { orgId: ctx.orgId, id: step.id, fields: read.value, actorUid: ctx.actorUid ?? '' })\n if (written.ok === false) outcome.refused.push({ id: step.id, reason: written.error })\n else outcome.done.push(step.id)\n }\n return outcome\n },\n }\n}\n"],"names":["DECLARED_SUBSCRIPTION_TOPICS","existingPackageItemsOf","isPluginEnabled","checkEntitlement","isOrgWideMember","planLabelGrantingFeature","firebaseAdmin","getOrgDoc","logOrgActivity","resolveOrgMembership","BUNDLE_ID","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","readOrgAutomation","createOrgAutomationRecord","deleteOrgAutomationRecord","updateOrgAutomationRecord","ORG_AUTOMATION_REFERENCE_KINDS","ORG_AUTOMATIONS_TRANSFER_KEY","orgAutomationPackageContent","rawOrgAutomationContent","defaultDeps","firestore","app","readOrg","orgId","memberOf","uid","member","action","target","email","type","automationsOf","collection","doc","importerRefusal","deps","ctx","actorUid","includes","String","org","readAll","ids","docs","Promise","all","map","id","get","filter","exists","where","flatMap","content","data","kind","name","fieldsOf","enabled","createOrgAutomationsPackage","enabledNow","items","readItems","problems","write","refusal","read","rename","ok","problem","decision","targetId","push","live","count","referenceTargets","kinds","named","field","trim","answers","list","label","targets","topic","dataset","writeItems","writer","results","before","alreadyApplied","row","replacing","result","outcome","message","input","fields","value","written","recordId","error","markApplied","undo","revertItems","steps","done","refused","step","deleted","again","reason"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/transfer/org-automations-package.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * ORG AUTOMATIONS IN A WORKSPACE PACKAGE — the server half (AGL-3535).\n *\n * Written through `automations/manage`'s own writes\n * (`createOrgAutomationRecord`, `updateOrgAutomationRecord`,\n * `deleteOrgAutomationRecord`): its reader, its placement check, its cap,\n * its soft delete and its activity lines. The importer is held to that\n * door's rules too — an org-wide owner, admin or editor, in a workspace\n * running Workflows on a plan with org automations — so an import can do\n * nothing that door would refuse. A new automation lands switched off; a\n * replaced one keeps whether it was on.\n *=========================================*/\n\nimport { DECLARED_SUBSCRIPTION_TOPICS } from '@aglyn/aglyn/app-utils/subscription-topics'\nimport { existingPackageItemsOf, type ExistingPackageItem, type TransferReferenceKind, type TransferRowResult } from '@aglyn/aglyn/data-transfer'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport type {\n TransferApplyResult,\n TransferApplyWriter,\n TransferPackageItemContent,\n TransferPackageItemWrite,\n TransferPackageRevertResult,\n TransferPackageRevertStep,\n TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { checkEntitlement, isOrgWideMember, planLabelGrantingFeature } from '@aglyn/aglyn/server'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n getOrgDoc,\n logOrgActivity,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { workflowsPermissionRefusal } from '../model/workflows-permissions'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n ORG_AUTOMATION_ACTIVITY_TARGET,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n readOrgAutomation,\n} from '../model/org-automations'\nimport {\n createOrgAutomationRecord,\n deleteOrgAutomationRecord,\n updateOrgAutomationRecord,\n} from '../server/org-automations-routes'\nimport {\n ORG_AUTOMATION_REFERENCE_KINDS,\n ORG_AUTOMATIONS_TRANSFER_KEY,\n orgAutomationPackageContent,\n rawOrgAutomationContent,\n type OrgAutomationPackageContent,\n} from './org-automations-package'\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OrgAutomationsPackageDeps {\n firestore(): Firestore\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n memberOf(uid: string, orgId: string): Promise<Record<string, unknown> | null>\n /** Whether a member holds \"Manage org automations\" (AGL-4047): `memberHasOrgPermission`. */\n holdsOrgAutomations(orgId: string, member: Record<string, unknown>): Promise<boolean>\n logOrgActivity(orgId: string, uid: string, action: string, target: { id: string; name: string }): Promise<void>\n}\n\nfunction defaultDeps(): OrgAutomationsPackageDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore() as unknown as Firestore,\n readOrg: async (orgId) => ((await getOrgDoc(orgId)) as Record<string, unknown> | null) ?? null,\n memberOf: async (uid, orgId) => ((await resolveOrgMembership(uid, orgId))?.member as Record<string, unknown> | undefined) ?? null,\n holdsOrgAutomations: (orgId, member) => memberHasOrgPermission(orgId, member as never, 'workflows.orgAutomations' as never),\n logOrgActivity: (orgId, uid, action, target) =>\n logOrgActivity(orgId, { uid, email: null }, action, { type: ORG_AUTOMATION_ACTIVITY_TARGET, ...target }),\n }\n}\n\nconst automationsOf = (firestore: Firestore, orgId: string) =>\n firestore.collection('orgs').doc(orgId).collection(ORG_AUTOMATIONS_COLLECTION)\n\n/** Why `automations/manage` would refuse this importer, or `null` when it would not. */\nasync function importerRefusal(deps: OrgAutomationsPackageDeps, ctx: TransferResourceContext): Promise<string | null> {\n if (!ctx.actorUid) return 'Org automations are imported by a member of the organization.'\n const member = await deps.memberOf(ctx.actorUid, ctx.orgId)\n // \"Manage org automations\" (AGL-4047), for members of the whole workspace,\n // as `automations/manage` asks it.\n if (!member || !isOrgWideMember(member as never) || !(await deps.holdsOrgAutomations(ctx.orgId, member))) {\n return workflowsPermissionRefusal('workflows.orgAutomations')\n }\n const org = await deps.readOrg(ctx.orgId)\n if (!org || !isPluginEnabled(org as never, BUNDLE_ID)) return 'Workflows is not on for this workspace.'\n if (!checkEntitlement(org as never, 'actions')) {\n return `Org automations need the ${planLabelGrantingFeature('actions') ?? 'Pro'} plan.`\n }\n return null\n}\n\nasync function readAll(\n deps: OrgAutomationsPackageDeps,\n orgId: string,\n ids?: readonly string[],\n): Promise<Array<TransferPackageItemContent<OrgAutomationPackageContent>>> {\n const collection = automationsOf(deps.firestore(), orgId)\n const docs = ids\n ? (await Promise.all(ids.map((id) => collection.doc(id).get()))).filter((doc) => doc.exists)\n : (await collection.where('deletedAt', '==', null).get()).docs\n return docs.flatMap((doc) => {\n if (doc.get('deletedAt')) return []\n const content = orgAutomationPackageContent(doc.data())\n return content ? [{ kind: ORG_AUTOMATIONS_TRANSFER_KEY, id: doc.id, name: content.name, content }] : []\n })\n}\n\n/** The fields one write stores: the content, read by the route's reader, switched on only if it already was. */\nfunction fieldsOf(content: OrgAutomationPackageContent, name: string, enabled: boolean) {\n return readOrgAutomation({ ...content, name, enabled })\n}\n\nexport function createOrgAutomationsPackage(deps: OrgAutomationsPackageDeps = defaultDeps()) {\n /** Whether the automation is on now; a new one is off. */\n const enabledNow = async (orgId: string, id: string) =>\n (await automationsOf(deps.firestore(), orgId).doc(id).get()).get('enabled') === true\n\n return {\n async items(ctx: TransferResourceContext): Promise<ExistingPackageItem[]> {\n return existingPackageItemsOf(ORG_AUTOMATIONS_TRANSFER_KEY, await readAll(deps, ctx.orgId))\n },\n\n async readItems(ctx: TransferResourceContext, ids: readonly string[]) {\n return readAll(deps, ctx.orgId, ids)\n },\n\n async problems(ctx: TransferResourceContext, write: TransferPackageItemWrite<OrgAutomationPackageContent>) {\n const refusal = await importerRefusal(deps, ctx)\n if (refusal) return [refusal]\n const content = rawOrgAutomationContent(write.content)\n const read = fieldsOf(content, write.rename?.name ?? content.name, false)\n if (read.ok === false) return [read.problem]\n const problems: string[] = []\n if (write.decision !== 'replace') {\n const collection = automationsOf(deps.firestore(), ctx.orgId)\n if ((await collection.doc(write.targetId).get()).exists) {\n problems.push('An automation with this id was deleted from this workspace, so it cannot come back under it.')\n }\n const live = (await collection.where('deletedAt', '==', null).count().get()).data().count\n if (live >= ORG_AUTOMATIONS_MAX) {\n problems.push(`An organization holds at most ${ORG_AUTOMATIONS_MAX} org automations — delete one you no longer use first.`)\n }\n }\n return problems\n },\n\n async referenceTargets(ctx: TransferResourceContext, kinds: readonly string[]): Promise<TransferReferenceKind[]> {\n const org = deps.firestore().collection('orgs').doc(ctx.orgId)\n const named = async (collection: string, field: string) =>\n (await org.collection(collection).get()).docs\n .filter((doc) => !doc.get('deletedAt'))\n .map((doc) => {\n const name = String(doc.get(field) ?? doc.get('label') ?? '').trim()\n return { id: doc.id, ...(name ? { name } : {}) }\n })\n const answers: TransferReferenceKind[] = []\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.list)) {\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.list, label: 'Email list', targets: await named('lists', 'name') })\n }\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.topic)) {\n // Asked only when the email plugin, which owns the topic catalog and answers `email.topics`\n // (AGL-3550), is not running here — and without it the catalog is the built-ins the plugins declare.\n const targets = DECLARED_SUBSCRIPTION_TOPICS.map((topic) => ({ id: topic.id, name: topic.name }))\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.topic, label: 'Subscription topic', targets })\n }\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.member)) {\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.member, label: 'Member', targets: await named('members', 'email') })\n }\n if (kinds.includes(ORG_AUTOMATION_REFERENCE_KINDS.dataset)) {\n answers.push({ kind: ORG_AUTOMATION_REFERENCE_KINDS.dataset, label: 'Dataset', targets: await named('datasets', 'name') })\n }\n return answers\n },\n\n async writeItems(\n ctx: TransferResourceContext,\n items: ReadonlyArray<TransferPackageItemWrite<OrgAutomationPackageContent>>,\n writer: TransferApplyWriter,\n ): Promise<TransferApplyResult> {\n const refusal = await importerRefusal(deps, ctx)\n const firestore = deps.firestore()\n const results: TransferRowResult[] = []\n for (const write of items) {\n const before = await writer.alreadyApplied(write.row)\n if (before) {\n results.push(before)\n continue\n }\n const content = rawOrgAutomationContent(write.content)\n const name = write.rename?.name ?? content.name\n const replacing = write.decision === 'replace'\n const read = fieldsOf(content, name, replacing ? await enabledNow(ctx.orgId, write.targetId) : false)\n let result: TransferRowResult\n if (refusal) result = { row: write.row, outcome: 'failed', message: refusal }\n else if (read.ok === false) result = { row: write.row, outcome: 'failed', message: read.problem }\n else {\n const input = { orgId: ctx.orgId, id: write.targetId, fields: read.value, actorUid: ctx.actorUid ?? '' }\n const written = replacing ? await updateOrgAutomationRecord(firestore, input) : await createOrgAutomationRecord(firestore, input)\n if (written.ok === false) result = { row: write.row, outcome: 'failed', recordId: write.targetId, message: written.error }\n else {\n await deps.logOrgActivity(ctx.orgId, ctx.actorUid ?? '', replacing ? 'Edited an org automation' : 'Created an org automation', {\n id: written.id,\n name: read.value.name,\n })\n result = { row: write.row, outcome: replacing ? 'updated' : 'created', recordId: written.id }\n }\n }\n await writer.markApplied(result)\n results.push(result)\n }\n return { results, undo: [] }\n },\n\n async revertItems(\n ctx: TransferResourceContext,\n steps: ReadonlyArray<TransferPackageRevertStep<OrgAutomationPackageContent>>,\n ): Promise<TransferPackageRevertResult> {\n const firestore = deps.firestore()\n const outcome: TransferPackageRevertResult = { done: [], refused: [] }\n for (const step of steps) {\n if (step.action === 'delete') {\n const deleted = await deleteOrgAutomationRecord(firestore, { orgId: ctx.orgId, id: step.id, actorUid: ctx.actorUid ?? '' })\n if (deleted && !deleted.again) {\n await deps.logOrgActivity(ctx.orgId, ctx.actorUid ?? '', 'Deleted an org automation', { id: step.id, name: deleted.name })\n }\n outcome.done.push(step.id)\n continue\n }\n const content = rawOrgAutomationContent(step.content)\n const read = fieldsOf(content, content.name, await enabledNow(ctx.orgId, step.id))\n if (read.ok === false) {\n outcome.refused.push({ id: step.id, reason: read.problem })\n continue\n }\n const written = await updateOrgAutomationRecord(firestore, { orgId: ctx.orgId, id: step.id, fields: read.value, actorUid: ctx.actorUid ?? '' })\n if (written.ok === false) outcome.refused.push({ id: step.id, reason: written.error })\n else outcome.done.push(step.id)\n }\n return outcome\n },\n }\n}\n"],"names":["DECLARED_SUBSCRIPTION_TOPICS","existingPackageItemsOf","isPluginEnabled","checkEntitlement","isOrgWideMember","planLabelGrantingFeature","firebaseAdmin","getOrgDoc","logOrgActivity","memberHasOrgPermission","resolveOrgMembership","workflowsPermissionRefusal","BUNDLE_ID","ORG_AUTOMATION_ACTIVITY_TARGET","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","readOrgAutomation","createOrgAutomationRecord","deleteOrgAutomationRecord","updateOrgAutomationRecord","ORG_AUTOMATION_REFERENCE_KINDS","ORG_AUTOMATIONS_TRANSFER_KEY","orgAutomationPackageContent","rawOrgAutomationContent","defaultDeps","firestore","app","readOrg","orgId","memberOf","uid","member","holdsOrgAutomations","action","target","email","type","automationsOf","collection","doc","importerRefusal","deps","ctx","actorUid","org","readAll","ids","docs","Promise","all","map","id","get","filter","exists","where","flatMap","content","data","kind","name","fieldsOf","enabled","createOrgAutomationsPackage","enabledNow","items","readItems","problems","write","refusal","read","rename","ok","problem","decision","targetId","push","live","count","referenceTargets","kinds","named","field","String","trim","answers","includes","list","label","targets","topic","dataset","writeItems","writer","results","before","alreadyApplied","row","replacing","result","outcome","message","input","fields","value","written","recordId","error","markApplied","undo","revertItems","steps","done","refused","step","deleted","again","reason"],"mappings":"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"}
|