@aglyn/plugins-workflows 1.0.0-beta.199 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -12
- package/src/lib/components/host-actions-card.component.js +2 -1
- package/src/lib/components/host-actions-card.component.js.map +1 -1
- package/src/lib/components/host-run-history-card.component.d.ts +7 -4
- package/src/lib/components/host-run-history-card.component.js +86 -163
- package/src/lib/components/host-run-history-card.component.js.map +1 -1
- package/src/lib/components/host-workflows-card.component.js +2 -1
- package/src/lib/components/host-workflows-card.component.js.map +1 -1
- package/src/lib/components/site-org-automations-panel.component.js +2 -1
- package/src/lib/components/site-org-automations-panel.component.js.map +1 -1
- package/src/lib/engine/crm-action-steps.js +8 -3
- package/src/lib/engine/crm-action-steps.js.map +1 -1
- package/src/lib/engine/run-event-actions.js +95 -24
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-workflows.js +18 -8
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/site-suspension.d.ts +45 -0
- package/src/lib/engine/site-suspension.js +54 -0
- package/src/lib/engine/site-suspension.js.map +1 -0
- package/src/lib/model/run-history.d.ts +79 -0
- package/src/lib/model/run-history.js +180 -0
- package/src/lib/model/run-history.js.map +1 -0
- package/src/lib/server.js +26 -4
- package/src/lib/server.js.map +1 -1
|
@@ -15,8 +15,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { ACTION_MAX_EVENT_DEPTH, checkEntitlement, evaluateExpression, FLOW_TIMED_OUT_FIELD, resolveOrgEntitlements, runWorkflow } from "@aglyn/aglyn/server";
|
|
18
|
+
import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
|
|
18
19
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
19
20
|
import { FieldValue } from "firebase-admin/firestore";
|
|
21
|
+
import { runSummaryFields } from "../model/run-history.js";
|
|
22
|
+
import { eventRunSuspension } from "./site-suspension.js";
|
|
20
23
|
import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
|
|
21
24
|
import { automationRunEnv, executeWorkflow, FLOW_CLAIM_RETRY_MS, stopFlowEnrollment } from "./run-event-actions.js";
|
|
22
25
|
import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
@@ -31,6 +34,10 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
31
34
|
actorId: null,
|
|
32
35
|
actorEmail: null
|
|
33
36
|
}, row, {
|
|
37
|
+
// The site log's search finds a run by its workflow's name (AGL-3321).
|
|
38
|
+
searchTokens: activitySearchTokens({
|
|
39
|
+
target: row['target']
|
|
40
|
+
}),
|
|
34
41
|
createdAt: FieldValue.serverTimestamp()
|
|
35
42
|
})).catch(()=>undefined);
|
|
36
43
|
}
|
|
@@ -43,11 +50,11 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
43
50
|
*/ export function workflowRunRow(execution, input) {
|
|
44
51
|
const failed = execution.errors.length > 0;
|
|
45
52
|
const action = failed ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(0, 300) : execution.ending === 'waiting' ? `Workflow is waiting, on ${input.event}` : `Workflow ran on ${input.event}`;
|
|
46
|
-
return {
|
|
53
|
+
return _extends({
|
|
47
54
|
action,
|
|
48
55
|
result: failed ? 'failed' : 'succeeded',
|
|
49
|
-
trigger: input.event
|
|
50
|
-
|
|
56
|
+
trigger: input.event
|
|
57
|
+
}, runSummaryFields((execution.outcomes.length ? execution.outcomes.join(' · ') : 'Ran').slice(0, 300)), {
|
|
51
58
|
status: failed ? 'error' : 'ok',
|
|
52
59
|
durationMs: input.durationMs,
|
|
53
60
|
target: {
|
|
@@ -55,7 +62,7 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
55
62
|
id: input.workflowId,
|
|
56
63
|
name: input.workflowName
|
|
57
64
|
}
|
|
58
|
-
};
|
|
65
|
+
});
|
|
59
66
|
}
|
|
60
67
|
/**
|
|
61
68
|
* Event-triggered workflow runner (AGL-128): loads workflows whose
|
|
@@ -113,6 +120,9 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
113
120
|
// read of it here would be billed on every host event and used for
|
|
114
121
|
// nothing.
|
|
115
122
|
const owner = await getOrgForHost(hostId);
|
|
123
|
+
// A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.
|
|
124
|
+
// Asked before the cap so a locked site's events bill no runs.
|
|
125
|
+
if (await eventRunSuspension(hostRef, owner == null ? void 0 : owner.org)) return alerts;
|
|
116
126
|
{
|
|
117
127
|
var _counterSnapshot_get;
|
|
118
128
|
const org = owner == null ? void 0 : owner.org;
|
|
@@ -214,11 +224,11 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
214
224
|
*
|
|
215
225
|
* `status` stays: the dashboard activity feed renders it, and removing
|
|
216
226
|
* it would trade one silent gap for another.
|
|
217
|
-
*/ await recordWorkflowRun(hostRef, {
|
|
227
|
+
*/ await recordWorkflowRun(hostRef, _extends({
|
|
218
228
|
action,
|
|
219
229
|
result: run.ok === false ? 'failed' : 'succeeded',
|
|
220
|
-
trigger: event
|
|
221
|
-
|
|
230
|
+
trigger: event
|
|
231
|
+
}, runSummaryFields(run.ok === false ? String(run.error).slice(0, 300) : 'Ran'), {
|
|
222
232
|
status: run.ok === false ? 'error' : 'ok',
|
|
223
233
|
durationMs,
|
|
224
234
|
target: {
|
|
@@ -226,7 +236,7 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
226
236
|
id: doc.id,
|
|
227
237
|
name: (_workflow_name = workflow.name) != null ? _workflow_name : ''
|
|
228
238
|
}
|
|
229
|
-
});
|
|
239
|
+
}));
|
|
230
240
|
}
|
|
231
241
|
// Filtered-out workflows do not bill — only executed runs count.
|
|
232
242
|
if (executed > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-workflows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ACTION_MAX_EVENT_DEPTH,\n checkEntitlement,\n evaluateExpression,\n FLOW_TIMED_OUT_FIELD,\n type HostActionAlert,\n type HostEventType,\n type HostFunction,\n type HostVariable,\n type HostWorkflow,\n resolveOrgEntitlements,\n runWorkflow,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n automationRunEnv,\n executeWorkflow,\n FLOW_CLAIM_RETRY_MS,\n stopFlowEnrollment,\n type WorkflowContext,\n type WorkflowExecution,\n} from './run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './workflow-steps'\n\n/** Bounded fan-out per event: at most this many triggered workflows run. */\nconst MAX_TRIGGERED_WORKFLOWS = 10\n\n/**\n * One workflow run's row in the site's activity feed, which is the run\n * history the Workflows tab reads.\n *\n * The one place a workflow's run is written, whichever door started it.\n */\nasync function recordWorkflowRun(\n hostRef: FirebaseFirestore.DocumentReference,\n row: Record<string, unknown>,\n): Promise<void> {\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n ...row,\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * The history row for a run of a workflow with Actions steps, in the Actions\n * run's phrasing (AGL-2171) — what each step did, joined — so the run table\n * shows every step's outcome whichever engine the step came from. `status`\n * and `durationMs` ride along as they do on every workflow run, for the\n * dashboard feed that renders them.\n */\nexport function workflowRunRow(\n execution: Pick<WorkflowExecution, 'ending' | 'errors' | 'outcomes'>,\n input: {\n event: string\n workflowId: string\n workflowName: string\n durationMs: number\n },\n): Record<string, unknown> {\n const failed = execution.errors.length > 0\n const action = failed\n ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(\n 0,\n 300,\n )\n : execution.ending === 'waiting'\n ? `Workflow is waiting, on ${input.event}`\n : `Workflow ran on ${input.event}`\n return {\n action,\n result: failed ? 'failed' : 'succeeded',\n trigger: input.event,\n summary: (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\n status: failed ? 'error' : 'ok',\n durationMs: input.durationMs,\n target: {\n type: 'workflow',\n id: input.workflowId,\n name: input.workflowName,\n },\n }\n}\n\n/**\n * Event-triggered workflow runner (AGL-128): loads workflows whose\n * `trigger.event` matches, evaluates optional trigger filters over the\n * event payload, runs each, and logs runs to the host activity feed.\n * Fire-and-forget from the tenant's event sites (form submit, pageview\n * collector, membership APIs) — it never throws, and a failure must not\n * break the request that emitted the event.\n *\n * A workflow of function calls runs through the pure `runWorkflow`\n * evaluator exactly as it always has. A workflow with Actions steps runs\n * through `executeWorkflow`, which performs them with the Actions executors;\n * the site alerts its steps raise are what this resolves.\n *\n * ONE RUN, METERED ONCE: either kind counts one run on the workflow meter,\n * `workflowRunsPerMonth`, however many steps it has and whatever they are.\n * Its Actions steps take the Actions tier gates step by step, and none of\n * them counts an action run.\n *\n * `depth` is the chain guard actions run under (`ACTION_MAX_EVENT_DEPTH`):\n * an event a workflow's own step raised — a stage it set — is heard one\n * level deeper, so a workflow that sets the stage it listens for stops.\n */\nexport async function runEventWorkflows(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n depth = 0,\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n if (depth > ACTION_MAX_EVENT_DEPTH) return alerts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const triggered = await hostRef\n .collection('workflows')\n .where('trigger.event', '==', event)\n .limit(MAX_TRIGGERED_WORKFLOWS)\n .get()\n const workflows = triggered.docs.filter((doc) => !doc.get('deletedAt'))\n if (!workflows.length) return alerts\n // Full workflow map for nested function→workflow calls (AGL-129).\n const allWorkflowDocs = await hostRef\n .collection('workflows')\n .limit(100)\n .get()\n // Double-keyed by doc id AND name (AGL-261): id references are\n // rename-safe; legacy name references keep resolving.\n // Each carries its id, so a workflow a step names by its legacy name\n // still runs as the automation it is (see `WorkflowContext`).\n const workflowMap: WorkflowContext['workflows'] = {}\n for (const doc of allWorkflowDocs.docs) {\n const data = doc.data() as any\n if (data.deletedAt) continue\n const withId = { ...data, $id: doc.id }\n workflowMap[doc.id] = withId\n if (data?.name) workflowMap[data.name] = withId\n }\n\n // Monthly run cap by the owning org's plan (AGL-165) — dark-launch\n // rule: workspaces without a plan are uncapped, like every other gate.\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n // Run caps ride the owning org's doc (AGL-238) — read through\n // `getOrgForHost` below. The host document itself is not consulted: the\n // cap, the counter and the workflow definitions all live elsewhere, so a\n // read of it here would be billed on every host event and used for\n // nothing.\n const owner = await getOrgForHost(hostId)\n {\n const org = owner?.org\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const limit = resolveOrgEntitlements(\n org as any,\n ).workflowRunsPerMonth\n const counterSnapshot = await runCounterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n if (used + workflows.length > limit) return alerts\n }\n\n const [functionDocs, variableDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n ])\n const functions: Record<string, HostFunction> = {}\n for (const doc of functionDocs.docs) {\n const definition = doc.data() as any\n if (definition.deletedAt) continue\n functions[doc.id] = definition\n if (definition.name) functions[definition.name] = definition\n }\n const variables: Record<string, HostVariable> = {}\n for (const doc of variableDocs.docs) {\n const variable = doc.data() as any\n if (!variable.deletedAt && variable.name) {\n variables[variable.name] = variable\n }\n }\n\n let executed = 0\n for (const doc of workflows) {\n const workflow = doc.data() as AutomationWorkflow\n const filter = workflow.trigger?.filter?.trim()\n if (filter) {\n try {\n const scope: HostEventPayload = { event, ...payload }\n if (!evaluateExpression(filter, scope)) continue\n } catch {\n continue // A broken filter never fires.\n }\n }\n const startedAt = Date.now()\n if (workflowHasActionSteps(workflow)) {\n /*\n * A WORKFLOW WITH ACTIONS STEPS is performed, not evaluated. The\n * maps above are this run's working set, so the executor reads them\n * rather than a second copy; the plan gates come off the org\n * document the cap already read.\n */\n const context: WorkflowContext = {\n functions,\n variables,\n workflows: workflowMap,\n }\n const env = automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth,\n alerts,\n loadWorkflowContext: async () => context,\n })\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n event,\n payload,\n )\n executed += 1\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n continue\n }\n const run = runWorkflow(\n workflow as HostWorkflow,\n functions,\n variables,\n { event, ...payload },\n { workflows: workflowMap },\n )\n const durationMs = Date.now() - startedAt\n executed += 1\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter build tsconfig otherwise (same quirk as runWorkflow).\n const action =\n run.ok === false\n ? `Workflow failed on ${event}: ${run.error}`.slice(0, 300)\n : `Workflow ran on ${event}`\n /*\n * The run-history shape, not just `status` + `durationMs` (AGL-2222).\n *\n * `actionRunResult()` recognises a run by `result`, falling back to the\n * prose prefix `Action ran on`. This writer's prose begins\n * `Workflow ran on`, so every workflow execution was filtered out of the\n * very table AGL-2171 built — the Runs dialog on the Workflows tab read\n * \"No runs yet\" however many times a workflow had run. The `durationMs`\n * caption in that table could never render either, because the only rows\n * carrying a duration were the ones being dropped.\n *\n * `status` stays: the dashboard activity feed renders it, and removing\n * it would trade one silent gap for another.\n */\n await recordWorkflowRun(hostRef, {\n action,\n result: run.ok === false ? 'failed' : 'succeeded',\n trigger: event,\n summary: run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\n status: run.ok === false ? 'error' : 'ok',\n durationMs,\n target: { type: 'workflow', id: doc.id, name: workflow.name ?? '' },\n })\n }\n // Filtered-out workflows do not bill — only executed runs count.\n if (executed > 0) {\n await runCounterRef\n .set(\n { [monthKey]: FieldValue.increment(executed) },\n { merge: true },\n )\n .catch(() => undefined)\n }\n } catch (error) {\n console.error('runEventWorkflows failed', hostId, event, error)\n }\n return alerts\n}\n\n/**\n * Continues a WORKFLOW's enrollment from where its wait ended — the\n * workflow twin of the action resume, and asked the same questions.\n *\n * - The kill switch is the workflow document: deleted, and the people inside\n * it stop, exactly as they do when an action is deleted or switched off.\n * - The plan is re-asked: a wait is an Actions step, so a workspace whose\n * plan has lost the actions builder stops the flow rather than finishing\n * it on a plan that no longer carries it.\n * - The snapshot runs, not the workflow's current steps, and the results the\n * function calls bound before the wait ride in its payload.\n * - Counted on the workflow meter, never refused by it: the person is already\n * inside, and the gate belongs at enrollment.\n */\nexport async function resumeWorkflowEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const doc = await hostRef\n .collection('workflows')\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n if (!doc?.exists || doc.get('deletedAt')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'the workflow was deleted',\n )\n }\n const workflow = doc.data() as AutomationWorkflow\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!checkEntitlement(owner?.org as any, 'actions')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'this site’s plan no longer includes automations',\n )\n }\n const env = automationRunEnv({ hostId, hostRef, owner, depth: 0 })\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on an action's resume: the step after a\n // `waitForEvent` reads it to tell the clock from the event.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const startedAt = Date.now()\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps,\n enrollmentRef: ref,\n },\n )\n if (execution.ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return execution.ending\n }\n if (execution.ending !== 'waiting') await endFlowEnrollment(ref)\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event: enrollment.event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('workflowRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return execution.ending\n}\n\nexport default runEventWorkflows\n"],"names":["ACTION_MAX_EVENT_DEPTH","checkEntitlement","evaluateExpression","FLOW_TIMED_OUT_FIELD","resolveOrgEntitlements","runWorkflow","firebaseAdmin","getOrgForHost","FieldValue","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","summary","outcomes","status","durationMs","target","type","id","workflowId","name","workflowName","runEventWorkflows","hostId","payload","depth","alerts","firestore","app","doc","triggered","where","limit","get","workflows","docs","filter","allWorkflowDocs","workflowMap","data","deletedAt","withId","$id","monthKey","Date","toISOString","runCounterRef","owner","counterSnapshot","org","workflowRunsPerMonth","used","Number","functionDocs","variableDocs","Promise","all","functions","definition","variables","variable","executed","workflow","trim","scope","startedAt","now","context","env","loadWorkflowContext","kind","run","ok","error","String","set","increment","merge","console","resumeWorkflowEnrollment","enrollment","ref","options","nowMs","actionId","exists","timedOut","startIndex","nextStepIndex","steps","enrollmentRef"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-event-workflows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ACTION_MAX_EVENT_DEPTH,\n checkEntitlement,\n evaluateExpression,\n FLOW_TIMED_OUT_FIELD,\n type HostActionAlert,\n type HostEventType,\n type HostFunction,\n type HostVariable,\n type HostWorkflow,\n resolveOrgEntitlements,\n runWorkflow,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport type { HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from '../model/run-history'\nimport { eventRunSuspension } from './site-suspension'\nimport {\n deferFlowEnrollment,\n endFlowEnrollment,\n type FlowEnrollment,\n} from './flow-enrollments'\nimport {\n automationRunEnv,\n executeWorkflow,\n FLOW_CLAIM_RETRY_MS,\n stopFlowEnrollment,\n type WorkflowContext,\n type WorkflowExecution,\n} from './run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './workflow-steps'\n\n/** Bounded fan-out per event: at most this many triggered workflows run. */\nconst MAX_TRIGGERED_WORKFLOWS = 10\n\n/**\n * One workflow run's row in the site's activity feed, which is the run\n * history the Workflows tab reads.\n *\n * The one place a workflow's run is written, whichever door started it.\n */\nasync function recordWorkflowRun(\n hostRef: FirebaseFirestore.DocumentReference,\n row: Record<string, unknown>,\n): Promise<void> {\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n ...row,\n // The site log's search finds a run by its workflow's name (AGL-3321).\n searchTokens: activitySearchTokens({\n target: row['target'] as { name?: unknown } | undefined,\n }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n}\n\n/**\n * The history row for a run of a workflow with Actions steps, in the Actions\n * run's phrasing (AGL-2171) — what each step did, joined — so the run table\n * shows every step's outcome whichever engine the step came from. `status`\n * and `durationMs` ride along as they do on every workflow run, for the\n * dashboard feed that renders them.\n */\nexport function workflowRunRow(\n execution: Pick<WorkflowExecution, 'ending' | 'errors' | 'outcomes'>,\n input: {\n event: string\n workflowId: string\n workflowName: string\n durationMs: number\n },\n): Record<string, unknown> {\n const failed = execution.errors.length > 0\n const action = failed\n ? `Workflow ran on ${input.event} with errors: ${execution.errors.join('; ')}`.slice(\n 0,\n 300,\n )\n : execution.ending === 'waiting'\n ? `Workflow is waiting, on ${input.event}`\n : `Workflow ran on ${input.event}`\n return {\n action,\n result: failed ? 'failed' : 'succeeded',\n trigger: input.event,\n // The summary and its search tokens, written together (AGL-3321).\n ...runSummaryFields(\n (execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : 'Ran'\n ).slice(0, 300),\n ),\n status: failed ? 'error' : 'ok',\n durationMs: input.durationMs,\n target: {\n type: 'workflow',\n id: input.workflowId,\n name: input.workflowName,\n },\n }\n}\n\n/**\n * Event-triggered workflow runner (AGL-128): loads workflows whose\n * `trigger.event` matches, evaluates optional trigger filters over the\n * event payload, runs each, and logs runs to the host activity feed.\n * Fire-and-forget from the tenant's event sites (form submit, pageview\n * collector, membership APIs) — it never throws, and a failure must not\n * break the request that emitted the event.\n *\n * A workflow of function calls runs through the pure `runWorkflow`\n * evaluator exactly as it always has. A workflow with Actions steps runs\n * through `executeWorkflow`, which performs them with the Actions executors;\n * the site alerts its steps raise are what this resolves.\n *\n * ONE RUN, METERED ONCE: either kind counts one run on the workflow meter,\n * `workflowRunsPerMonth`, however many steps it has and whatever they are.\n * Its Actions steps take the Actions tier gates step by step, and none of\n * them counts an action run.\n *\n * `depth` is the chain guard actions run under (`ACTION_MAX_EVENT_DEPTH`):\n * an event a workflow's own step raised — a stage it set — is heard one\n * level deeper, so a workflow that sets the stage it listens for stops.\n */\nexport async function runEventWorkflows(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n depth = 0,\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n if (depth > ACTION_MAX_EVENT_DEPTH) return alerts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const triggered = await hostRef\n .collection('workflows')\n .where('trigger.event', '==', event)\n .limit(MAX_TRIGGERED_WORKFLOWS)\n .get()\n const workflows = triggered.docs.filter((doc) => !doc.get('deletedAt'))\n if (!workflows.length) return alerts\n // Full workflow map for nested function→workflow calls (AGL-129).\n const allWorkflowDocs = await hostRef\n .collection('workflows')\n .limit(100)\n .get()\n // Double-keyed by doc id AND name (AGL-261): id references are\n // rename-safe; legacy name references keep resolving.\n // Each carries its id, so a workflow a step names by its legacy name\n // still runs as the automation it is (see `WorkflowContext`).\n const workflowMap: WorkflowContext['workflows'] = {}\n for (const doc of allWorkflowDocs.docs) {\n const data = doc.data() as any\n if (data.deletedAt) continue\n const withId = { ...data, $id: doc.id }\n workflowMap[doc.id] = withId\n if (data?.name) workflowMap[data.name] = withId\n }\n\n // Monthly run cap by the owning org's plan (AGL-165) — dark-launch\n // rule: workspaces without a plan are uncapped, like every other gate.\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n // Run caps ride the owning org's doc (AGL-238) — read through\n // `getOrgForHost` below. The host document itself is not consulted: the\n // cap, the counter and the workflow definitions all live elsewhere, so a\n // read of it here would be billed on every host event and used for\n // nothing.\n const owner = await getOrgForHost(hostId)\n // A suspended site runs nothing (AGL-3356): see `eventRunSuspension`.\n // Asked before the cap so a locked site's events bill no runs.\n if (await eventRunSuspension(hostRef, owner?.org)) return alerts\n {\n const org = owner?.org\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs.\n const limit = resolveOrgEntitlements(\n org as any,\n ).workflowRunsPerMonth\n const counterSnapshot = await runCounterRef.get()\n const used = Number(counterSnapshot.get(monthKey) ?? 0)\n if (used + workflows.length > limit) return alerts\n }\n\n const [functionDocs, variableDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n ])\n const functions: Record<string, HostFunction> = {}\n for (const doc of functionDocs.docs) {\n const definition = doc.data() as any\n if (definition.deletedAt) continue\n functions[doc.id] = definition\n if (definition.name) functions[definition.name] = definition\n }\n const variables: Record<string, HostVariable> = {}\n for (const doc of variableDocs.docs) {\n const variable = doc.data() as any\n if (!variable.deletedAt && variable.name) {\n variables[variable.name] = variable\n }\n }\n\n let executed = 0\n for (const doc of workflows) {\n const workflow = doc.data() as AutomationWorkflow\n const filter = workflow.trigger?.filter?.trim()\n if (filter) {\n try {\n const scope: HostEventPayload = { event, ...payload }\n if (!evaluateExpression(filter, scope)) continue\n } catch {\n continue // A broken filter never fires.\n }\n }\n const startedAt = Date.now()\n if (workflowHasActionSteps(workflow)) {\n /*\n * A WORKFLOW WITH ACTIONS STEPS is performed, not evaluated. The\n * maps above are this run's working set, so the executor reads them\n * rather than a second copy; the plan gates come off the org\n * document the cap already read.\n */\n const context: WorkflowContext = {\n functions,\n variables,\n workflows: workflowMap,\n }\n const env = automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth,\n alerts,\n loadWorkflowContext: async () => context,\n })\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n event,\n payload,\n )\n executed += 1\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n continue\n }\n const run = runWorkflow(\n workflow as HostWorkflow,\n functions,\n variables,\n { event, ...payload },\n { workflows: workflowMap },\n )\n const durationMs = Date.now() - startedAt\n executed += 1\n // `=== false` (not `!run.ok`): the union fails to narrow under the\n // stricter build tsconfig otherwise (same quirk as runWorkflow).\n const action =\n run.ok === false\n ? `Workflow failed on ${event}: ${run.error}`.slice(0, 300)\n : `Workflow ran on ${event}`\n /*\n * The run-history shape, not just `status` + `durationMs` (AGL-2222).\n *\n * `actionRunResult()` recognises a run by `result`, falling back to the\n * prose prefix `Action ran on`. This writer's prose begins\n * `Workflow ran on`, so every workflow execution was filtered out of the\n * very table AGL-2171 built — the Runs dialog on the Workflows tab read\n * \"No runs yet\" however many times a workflow had run. The `durationMs`\n * caption in that table could never render either, because the only rows\n * carrying a duration were the ones being dropped.\n *\n * `status` stays: the dashboard activity feed renders it, and removing\n * it would trade one silent gap for another.\n */\n await recordWorkflowRun(hostRef, {\n action,\n result: run.ok === false ? 'failed' : 'succeeded',\n trigger: event,\n ...runSummaryFields(\n run.ok === false ? String(run.error).slice(0, 300) : 'Ran',\n ),\n status: run.ok === false ? 'error' : 'ok',\n durationMs,\n target: { type: 'workflow', id: doc.id, name: workflow.name ?? '' },\n })\n }\n // Filtered-out workflows do not bill — only executed runs count.\n if (executed > 0) {\n await runCounterRef\n .set(\n { [monthKey]: FieldValue.increment(executed) },\n { merge: true },\n )\n .catch(() => undefined)\n }\n } catch (error) {\n console.error('runEventWorkflows failed', hostId, event, error)\n }\n return alerts\n}\n\n/**\n * Continues a WORKFLOW's enrollment from where its wait ended — the\n * workflow twin of the action resume, and asked the same questions.\n *\n * - The kill switch is the workflow document: deleted, and the people inside\n * it stop, exactly as they do when an action is deleted or switched off.\n * - The plan is re-asked: a wait is an Actions step, so a workspace whose\n * plan has lost the actions builder stops the flow rather than finishing\n * it on a plan that no longer carries it.\n * - The snapshot runs, not the workflow's current steps, and the results the\n * function calls bound before the wait ride in its payload.\n * - Counted on the workflow meter, never refused by it: the person is already\n * inside, and the gate belongs at enrollment.\n */\nexport async function resumeWorkflowEnrollment(\n enrollment: FlowEnrollment,\n ref: FirebaseFirestore.DocumentReference,\n options: { timedOut?: boolean; nowMs: number },\n): Promise<'ran' | 'waiting' | 'exited' | 'deferred' | 'stopped'> {\n const { nowMs } = options\n const hostId = enrollment.hostId\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const doc = await hostRef\n .collection('workflows')\n .doc(enrollment.actionId)\n .get()\n .catch(() => null)\n if (!doc?.exists || doc.get('deletedAt')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'the workflow was deleted',\n )\n }\n const workflow = doc.data() as AutomationWorkflow\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!checkEntitlement(owner?.org as any, 'actions')) {\n return await stopFlowEnrollment(\n enrollment,\n ref,\n 'this site’s plan no longer includes automations',\n )\n }\n const env = automationRunEnv({ hostId, hostRef, owner, depth: 0 })\n const payload: HostEventPayload = {\n ...((enrollment.payload ?? {}) as HostEventPayload),\n // The timeout BRANCH, as on an action's resume: the step after a\n // `waitForEvent` reads it to tell the clock from the event.\n ...(options.timedOut ? { [FLOW_TIMED_OUT_FIELD]: true } : {}),\n }\n const startedAt = Date.now()\n const execution = await executeWorkflow(\n env,\n { kind: 'workflow', id: doc.id, name: workflow.name ?? '' },\n workflow,\n enrollment.event,\n payload,\n {\n startIndex: enrollment.nextStepIndex,\n steps: enrollment.steps,\n enrollmentRef: ref,\n },\n )\n if (execution.ending === 'deferred') {\n await deferFlowEnrollment(ref, nowMs + FLOW_CLAIM_RETRY_MS, nowMs)\n return execution.ending\n }\n if (execution.ending !== 'waiting') await endFlowEnrollment(ref)\n await recordWorkflowRun(\n hostRef,\n workflowRunRow(execution, {\n event: enrollment.event,\n workflowId: doc.id,\n workflowName: workflow.name ?? '',\n durationMs: Date.now() - startedAt,\n }),\n )\n const monthKey = new Date(nowMs).toISOString().slice(0, 7)\n await hostRef\n .collection('counters')\n .doc('workflowRuns')\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n return execution.ending\n}\n\nexport default runEventWorkflows\n"],"names":["ACTION_MAX_EVENT_DEPTH","checkEntitlement","evaluateExpression","FLOW_TIMED_OUT_FIELD","resolveOrgEntitlements","runWorkflow","activitySearchTokens","firebaseAdmin","getOrgForHost","FieldValue","runSummaryFields","eventRunSuspension","deferFlowEnrollment","endFlowEnrollment","automationRunEnv","executeWorkflow","FLOW_CLAIM_RETRY_MS","stopFlowEnrollment","workflowHasActionSteps","MAX_TRIGGERED_WORKFLOWS","recordWorkflowRun","hostRef","row","collection","add","actorId","actorEmail","searchTokens","target","createdAt","serverTimestamp","catch","undefined","workflowRunRow","execution","input","failed","errors","length","action","event","join","slice","ending","result","trigger","outcomes","status","durationMs","type","id","workflowId","name","workflowName","runEventWorkflows","hostId","payload","depth","alerts","firestore","app","doc","triggered","where","limit","get","workflows","docs","filter","allWorkflowDocs","workflowMap","data","deletedAt","withId","$id","monthKey","Date","toISOString","runCounterRef","owner","org","counterSnapshot","workflowRunsPerMonth","used","Number","functionDocs","variableDocs","Promise","all","functions","definition","variables","variable","executed","workflow","trim","scope","startedAt","now","context","env","loadWorkflowContext","kind","run","ok","error","String","set","increment","merge","console","resumeWorkflowEnrollment","enrollment","ref","options","nowMs","actionId","exists","timedOut","startIndex","nextStepIndex","steps","enrollmentRef"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 LockdownState } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* The FULL lock that stops an event-triggered run on this site, or null
|
|
20
|
+
* (AGL-3356).
|
|
21
|
+
*
|
|
22
|
+
* An event run has no caller the dispatcher could refuse: a form submission,
|
|
23
|
+
* a page view, a paid order or an inbound webhook fires it, and the run
|
|
24
|
+
* mails, writes and calls out on the site's behalf. The incident behind this
|
|
25
|
+
* was a workspace locked for phishing whose `contactCreated` workflow could
|
|
26
|
+
* still send, so the engine asks before it runs anything.
|
|
27
|
+
*
|
|
28
|
+
* FULL locks only, for the reason the sending identity gives: an event run is
|
|
29
|
+
* not resumable — the event has happened — so skipping one during a
|
|
30
|
+
* read-only maintenance window would lose, say, the receipt workflow for an
|
|
31
|
+
* order paid a minute before the window began. A staff suspension for abuse,
|
|
32
|
+
* fraud or billing is a full lock.
|
|
33
|
+
*
|
|
34
|
+
* `org` is the document the runner already holds for its plan gates; the
|
|
35
|
+
* host document is read here, once per event that has something to run.
|
|
36
|
+
* A read that fails answers "not locked", matching every other lockdown
|
|
37
|
+
* reader's fail-open posture — and the sending identity, which refuses a
|
|
38
|
+
* suspended site on its own, still stands behind every `sendEmail` step.
|
|
39
|
+
*/
|
|
40
|
+
export declare function eventRunSuspension(hostRef: {
|
|
41
|
+
get?: () => Promise<{
|
|
42
|
+
exists?: boolean;
|
|
43
|
+
data?: () => unknown;
|
|
44
|
+
}>;
|
|
45
|
+
}, org: unknown, nowMs?: number): Promise<LockdownState | null>;
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { lockdownMode, siteLockdownFromDocs } from "@aglyn/aglyn/server";
|
|
17
|
+
/**
|
|
18
|
+
* The FULL lock that stops an event-triggered run on this site, or null
|
|
19
|
+
* (AGL-3356).
|
|
20
|
+
*
|
|
21
|
+
* An event run has no caller the dispatcher could refuse: a form submission,
|
|
22
|
+
* a page view, a paid order or an inbound webhook fires it, and the run
|
|
23
|
+
* mails, writes and calls out on the site's behalf. The incident behind this
|
|
24
|
+
* was a workspace locked for phishing whose `contactCreated` workflow could
|
|
25
|
+
* still send, so the engine asks before it runs anything.
|
|
26
|
+
*
|
|
27
|
+
* FULL locks only, for the reason the sending identity gives: an event run is
|
|
28
|
+
* not resumable — the event has happened — so skipping one during a
|
|
29
|
+
* read-only maintenance window would lose, say, the receipt workflow for an
|
|
30
|
+
* order paid a minute before the window began. A staff suspension for abuse,
|
|
31
|
+
* fraud or billing is a full lock.
|
|
32
|
+
*
|
|
33
|
+
* `org` is the document the runner already holds for its plan gates; the
|
|
34
|
+
* host document is read here, once per event that has something to run.
|
|
35
|
+
* A read that fails answers "not locked", matching every other lockdown
|
|
36
|
+
* reader's fail-open posture — and the sending identity, which refuses a
|
|
37
|
+
* suspended site on its own, still stands behind every `sendEmail` step.
|
|
38
|
+
*/ export async function eventRunSuspension(hostRef, org, nowMs = Date.now()) {
|
|
39
|
+
let host;
|
|
40
|
+
try {
|
|
41
|
+
var _ref;
|
|
42
|
+
const snapshot = await (hostRef.get == null ? void 0 : hostRef.get.call(hostRef));
|
|
43
|
+
host = (snapshot == null ? void 0 : snapshot.exists) ? (_ref = snapshot.data == null ? void 0 : snapshot.data.call(snapshot)) != null ? _ref : null : null;
|
|
44
|
+
} catch (unused) {
|
|
45
|
+
host = null;
|
|
46
|
+
}
|
|
47
|
+
const lock = siteLockdownFromDocs({
|
|
48
|
+
org: org != null ? org : null,
|
|
49
|
+
host: host
|
|
50
|
+
}, nowMs);
|
|
51
|
+
return lock && lockdownMode(lock) === 'full' ? lock : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=site-suspension.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/site-suspension.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type LockdownState,\n lockdownMode,\n siteLockdownFromDocs,\n} from '@aglyn/aglyn/server'\n\n/**\n * The FULL lock that stops an event-triggered run on this site, or null\n * (AGL-3356).\n *\n * An event run has no caller the dispatcher could refuse: a form submission,\n * a page view, a paid order or an inbound webhook fires it, and the run\n * mails, writes and calls out on the site's behalf. The incident behind this\n * was a workspace locked for phishing whose `contactCreated` workflow could\n * still send, so the engine asks before it runs anything.\n *\n * FULL locks only, for the reason the sending identity gives: an event run is\n * not resumable — the event has happened — so skipping one during a\n * read-only maintenance window would lose, say, the receipt workflow for an\n * order paid a minute before the window began. A staff suspension for abuse,\n * fraud or billing is a full lock.\n *\n * `org` is the document the runner already holds for its plan gates; the\n * host document is read here, once per event that has something to run.\n * A read that fails answers \"not locked\", matching every other lockdown\n * reader's fail-open posture — and the sending identity, which refuses a\n * suspended site on its own, still stands behind every `sendEmail` step.\n */\nexport async function eventRunSuspension(\n hostRef: { get?: () => Promise<{ exists?: boolean; data?: () => unknown }> },\n org: unknown,\n nowMs: number = Date.now(),\n): Promise<LockdownState | null> {\n let host: Record<string, unknown> | null\n try {\n const snapshot = await hostRef.get?.()\n host = snapshot?.exists\n ? ((snapshot.data?.() as Record<string, unknown>) ?? null)\n : null\n } catch {\n host = null\n }\n const lock = siteLockdownFromDocs(\n { org: (org as never) ?? null, host: host as never },\n nowMs,\n )\n return lock && lockdownMode(lock) === 'full' ? lock : null\n}\n"],"names":["lockdownMode","siteLockdownFromDocs","eventRunSuspension","hostRef","org","nowMs","Date","now","host","snapshot","get","exists","data","lock"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,YAAY,EACZC,oBAAoB,QACf,sBAAqB;AAE5B;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,eAAeC,mBACpBC,OAA4E,EAC5EC,GAAY,EACZC,QAAgBC,KAAKC,GAAG,EAAE;IAE1B,IAAIC;IACJ,IAAI;;QACF,MAAMC,WAAW,OAAMN,QAAQO,GAAG,oBAAXP,QAAQO,GAAG,MAAXP;QACvBK,OAAOC,CAAAA,4BAAAA,SAAUE,MAAM,YACjBF,SAASG,IAAI,oBAAbH,SAASG,IAAI,MAAbH,4BAAiD,OACnD;IACN,EAAE,eAAM;QACND,OAAO;IACT;IACA,MAAMK,OAAOZ,qBACX;QAAEG,GAAG,EAAGA,cAAAA,MAAiB;QAAMI,MAAMA;IAAc,GACnDH;IAEF,OAAOQ,QAAQb,aAAaa,UAAU,SAASA,OAAO;AACxD"}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
|
|
18
|
+
import type { ListQueryDeclaration, ListQueryFilter } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
19
|
+
/** A run's verdict, as every run writer stores it. */
|
|
20
|
+
export declare const RUN_RESULTS: readonly ["succeeded", "failed", "skipped"];
|
|
21
|
+
export type RunResultValue = (typeof RUN_RESULTS)[number];
|
|
22
|
+
/** The written word-prefix tokens of a run's summary, for the search box. */
|
|
23
|
+
export declare const RUN_SUMMARY_TOKENS = "summaryTokens";
|
|
24
|
+
/**
|
|
25
|
+
* What a run writer spreads beside `summary`: the summary itself and its
|
|
26
|
+
* search tokens, so the two cannot be written apart.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runSummaryFields(summary: string): {
|
|
29
|
+
summary: string;
|
|
30
|
+
summaryTokens: string[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What the Filters panel offers, each a predicate the query serves beneath
|
|
34
|
+
* its newest-first order: Time is that order's own field, so its ranges cost
|
|
35
|
+
* no index of their own; Trigger and Result are equalities.
|
|
36
|
+
*
|
|
37
|
+
* `What happened` has no column filter: its words are the search box's,
|
|
38
|
+
* which reads the same tokens, and a `contains` beside the search would be a
|
|
39
|
+
* second array clause the query cannot hold.
|
|
40
|
+
*/
|
|
41
|
+
export declare const RUN_HISTORY_QUERY: ListQueryDeclaration;
|
|
42
|
+
export declare const RUN_HISTORY_HEADERS: Readonly<Record<string, string>>;
|
|
43
|
+
/**
|
|
44
|
+
* The history's scope: this automation's entries, and only runs.
|
|
45
|
+
*
|
|
46
|
+
* "Only runs" is `result in [all three]` — unless the reader has asked for
|
|
47
|
+
* a Result, whose own equality already names nothing but runs. The two are
|
|
48
|
+
* never both on the query: a second predicate on the same field would only
|
|
49
|
+
* repeat the first, and the plan serves an `equals` or `isAnyOf` on Result
|
|
50
|
+
* unconditionally, so dropping the scope never lets a non-run through.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runHistoryBase(targetId: string | undefined, clauses: readonly ListFilterClause[]): ListQueryFilter[];
|
|
53
|
+
/** The base's shape, for the index enumeration. */
|
|
54
|
+
export declare const RUN_HISTORY_BASE_PATHS: readonly [{
|
|
55
|
+
readonly path: "target.id";
|
|
56
|
+
}, {
|
|
57
|
+
readonly path: "result";
|
|
58
|
+
}];
|
|
59
|
+
/** A run entry as stored, as far as completing it is concerned. */
|
|
60
|
+
export interface RunEntryLike {
|
|
61
|
+
action?: unknown;
|
|
62
|
+
result?: unknown;
|
|
63
|
+
trigger?: unknown;
|
|
64
|
+
summary?: unknown;
|
|
65
|
+
summaryTokens?: unknown;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What a stored activity entry lacks to be found by the run history's query,
|
|
69
|
+
* or `null` when it is not a run or already complete. The definition the
|
|
70
|
+
* backfill restates (`tools/scripts/backfill-activity-run-fields.mjs`), held
|
|
71
|
+
* to the same worked examples (`tools/scripts/lib/activity-run-fields.fixtures.json`).
|
|
72
|
+
*
|
|
73
|
+
* A run recorded before its structured fields carried only the prose line
|
|
74
|
+
* `Action ran on <event>` — with ` with errors: <what failed>` when it
|
|
75
|
+
* failed — so the verdict, the event and the summary are read back from it,
|
|
76
|
+
* the way the history has always shown such a row. An entry that is neither
|
|
77
|
+
* a stored verdict nor that prose is not a run and is left alone.
|
|
78
|
+
*/
|
|
79
|
+
export declare function runEntryCompletion(entry: RunEntryLike): Record<string, unknown> | null;
|
|
@@ -0,0 +1,180 @@
|
|
|
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 { nameSearchTokens } from "@aglyn/aglyn/app-utils/name-search";
|
|
17
|
+
/*
|
|
18
|
+
* THE RUN HISTORY, AS A QUERY (AGL-3321).
|
|
19
|
+
*
|
|
20
|
+
* A run is an entry in `hosts/{hostId}/activity` that carries a verdict:
|
|
21
|
+
* `result` is `succeeded`, `failed` or `skipped`, beside the `trigger` event
|
|
22
|
+
* and the `summary` of what each step did. The activity feed also holds
|
|
23
|
+
* publishes, media saves and member changes, so the history's query is
|
|
24
|
+
* scoped to the automation (`target.id`) AND to runs (`result`) — and every
|
|
25
|
+
* clause the Filters panel accepts, and the search word, is another
|
|
26
|
+
* predicate on that same query. Pages come from the query, so a page of ten
|
|
27
|
+
* is ten runs and a filtered page is ten matches, however far back they sit.
|
|
28
|
+
*
|
|
29
|
+
* Every run writer stamps `summaryTokens` beside the summary
|
|
30
|
+
* ({@link runSummaryFields}), and `tools/scripts/backfill-activity-run-fields.mjs`
|
|
31
|
+
* completes the entries written before: `result`, `trigger` and `summary`
|
|
32
|
+
* from the prose line of a run recorded before those fields existed, and the
|
|
33
|
+
* tokens on every run. A run without a `result` is invisible to the query,
|
|
34
|
+
* which is why the backfill is part of shipping this.
|
|
35
|
+
*/ /** A run's verdict, as every run writer stores it. */ export const RUN_RESULTS = [
|
|
36
|
+
'succeeded',
|
|
37
|
+
'failed',
|
|
38
|
+
'skipped'
|
|
39
|
+
];
|
|
40
|
+
/** The written word-prefix tokens of a run's summary, for the search box. */ export const RUN_SUMMARY_TOKENS = 'summaryTokens';
|
|
41
|
+
/**
|
|
42
|
+
* What a run writer spreads beside `summary`: the summary itself and its
|
|
43
|
+
* search tokens, so the two cannot be written apart.
|
|
44
|
+
*/ export function runSummaryFields(summary) {
|
|
45
|
+
return {
|
|
46
|
+
summary,
|
|
47
|
+
summaryTokens: nameSearchTokens(summary)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What the Filters panel offers, each a predicate the query serves beneath
|
|
52
|
+
* its newest-first order: Time is that order's own field, so its ranges cost
|
|
53
|
+
* no index of their own; Trigger and Result are equalities.
|
|
54
|
+
*
|
|
55
|
+
* `What happened` has no column filter: its words are the search box's,
|
|
56
|
+
* which reads the same tokens, and a `contains` beside the search would be a
|
|
57
|
+
* second array clause the query cannot hold.
|
|
58
|
+
*/ export const RUN_HISTORY_QUERY = {
|
|
59
|
+
fields: [
|
|
60
|
+
{
|
|
61
|
+
column: 'createdAtMs',
|
|
62
|
+
kind: 'date',
|
|
63
|
+
path: 'createdAt',
|
|
64
|
+
operators: [
|
|
65
|
+
'is',
|
|
66
|
+
'after',
|
|
67
|
+
'onOrAfter',
|
|
68
|
+
'before',
|
|
69
|
+
'onOrBefore'
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
column: 'trigger',
|
|
74
|
+
kind: 'exact',
|
|
75
|
+
path: 'trigger',
|
|
76
|
+
operators: [
|
|
77
|
+
'equals',
|
|
78
|
+
'isAnyOf'
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
column: 'result',
|
|
83
|
+
kind: 'exact',
|
|
84
|
+
path: 'result',
|
|
85
|
+
operators: [
|
|
86
|
+
'equals',
|
|
87
|
+
'isAnyOf'
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
sorts: [
|
|
92
|
+
{
|
|
93
|
+
path: 'createdAt',
|
|
94
|
+
direction: 'desc',
|
|
95
|
+
column: 'createdAtMs'
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
search: {
|
|
99
|
+
tokensPath: RUN_SUMMARY_TOKENS
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export const RUN_HISTORY_HEADERS = {
|
|
103
|
+
createdAtMs: 'Time',
|
|
104
|
+
trigger: 'Trigger',
|
|
105
|
+
result: 'Result'
|
|
106
|
+
};
|
|
107
|
+
/** A clause on Result the query serves — which is then what keeps it to runs. */ const namesResult = (clause)=>clause.field === 'result' && (clause.op === 'equals' || clause.op === 'isAnyOf') && clause.value.trim() !== '';
|
|
108
|
+
/**
|
|
109
|
+
* The history's scope: this automation's entries, and only runs.
|
|
110
|
+
*
|
|
111
|
+
* "Only runs" is `result in [all three]` — unless the reader has asked for
|
|
112
|
+
* a Result, whose own equality already names nothing but runs. The two are
|
|
113
|
+
* never both on the query: a second predicate on the same field would only
|
|
114
|
+
* repeat the first, and the plan serves an `equals` or `isAnyOf` on Result
|
|
115
|
+
* unconditionally, so dropping the scope never lets a non-run through.
|
|
116
|
+
*/ export function runHistoryBase(targetId, clauses) {
|
|
117
|
+
const base = [];
|
|
118
|
+
if (targetId) base.push({
|
|
119
|
+
path: 'target.id',
|
|
120
|
+
op: '==',
|
|
121
|
+
value: targetId
|
|
122
|
+
});
|
|
123
|
+
if (!clauses.some(namesResult)) base.push({
|
|
124
|
+
path: 'result',
|
|
125
|
+
op: 'in',
|
|
126
|
+
value: [
|
|
127
|
+
...RUN_RESULTS
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
/** The base's shape, for the index enumeration. */ export const RUN_HISTORY_BASE_PATHS = [
|
|
133
|
+
{
|
|
134
|
+
path: 'target.id'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
path: 'result'
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
/**
|
|
141
|
+
* What a stored activity entry lacks to be found by the run history's query,
|
|
142
|
+
* or `null` when it is not a run or already complete. The definition the
|
|
143
|
+
* backfill restates (`tools/scripts/backfill-activity-run-fields.mjs`), held
|
|
144
|
+
* to the same worked examples (`tools/scripts/lib/activity-run-fields.fixtures.json`).
|
|
145
|
+
*
|
|
146
|
+
* A run recorded before its structured fields carried only the prose line
|
|
147
|
+
* `Action ran on <event>` — with ` with errors: <what failed>` when it
|
|
148
|
+
* failed — so the verdict, the event and the summary are read back from it,
|
|
149
|
+
* the way the history has always shown such a row. An entry that is neither
|
|
150
|
+
* a stored verdict nor that prose is not a run and is left alone.
|
|
151
|
+
*/ export function runEntryCompletion(entry) {
|
|
152
|
+
var _entry_result, _entry_action;
|
|
153
|
+
const stored = String((_entry_result = entry.result) != null ? _entry_result : '').trim();
|
|
154
|
+
const action = String((_entry_action = entry.action) != null ? _entry_action : '').trim();
|
|
155
|
+
const legacy = action.startsWith('Action ran on');
|
|
156
|
+
if (!RUN_RESULTS.includes(stored) && !legacy) return null;
|
|
157
|
+
const patch = {};
|
|
158
|
+
if (!RUN_RESULTS.includes(stored)) {
|
|
159
|
+
patch['result'] = action.includes('with errors:') ? 'failed' : 'succeeded';
|
|
160
|
+
}
|
|
161
|
+
if (typeof entry.trigger !== 'string' || !entry.trigger) {
|
|
162
|
+
var _exec;
|
|
163
|
+
const event = legacy ? (_exec = /^Action ran on (\S+)/.exec(action)) == null ? void 0 : _exec[1] : undefined;
|
|
164
|
+
if (event) patch['trigger'] = event;
|
|
165
|
+
}
|
|
166
|
+
let summary = typeof entry.summary === 'string' ? entry.summary.trim() : '';
|
|
167
|
+
if (!summary) {
|
|
168
|
+
const errors = /with errors:\s*(.+)$/.exec(action);
|
|
169
|
+
summary = errors ? errors[1] : legacy ? 'Ran' : action;
|
|
170
|
+
patch['summary'] = summary;
|
|
171
|
+
}
|
|
172
|
+
const tokens = nameSearchTokens(summary);
|
|
173
|
+
const held = Array.isArray(entry.summaryTokens) ? entry.summaryTokens : null;
|
|
174
|
+
if (!held || held.length !== tokens.length || held.some((token, at)=>token !== tokens[at])) {
|
|
175
|
+
patch['summaryTokens'] = tokens;
|
|
176
|
+
}
|
|
177
|
+
return Object.keys(patch).length ? patch : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=run-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/model/run-history.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 { nameSearchTokens } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE RUN HISTORY, AS A QUERY (AGL-3321).\n *\n * A run is an entry in `hosts/{hostId}/activity` that carries a verdict:\n * `result` is `succeeded`, `failed` or `skipped`, beside the `trigger` event\n * and the `summary` of what each step did. The activity feed also holds\n * publishes, media saves and member changes, so the history's query is\n * scoped to the automation (`target.id`) AND to runs (`result`) — and every\n * clause the Filters panel accepts, and the search word, is another\n * predicate on that same query. Pages come from the query, so a page of ten\n * is ten runs and a filtered page is ten matches, however far back they sit.\n *\n * Every run writer stamps `summaryTokens` beside the summary\n * ({@link runSummaryFields}), and `tools/scripts/backfill-activity-run-fields.mjs`\n * completes the entries written before: `result`, `trigger` and `summary`\n * from the prose line of a run recorded before those fields existed, and the\n * tokens on every run. A run without a `result` is invisible to the query,\n * which is why the backfill is part of shipping this.\n */\n\n/** A run's verdict, as every run writer stores it. */\nexport const RUN_RESULTS = ['succeeded', 'failed', 'skipped'] as const\nexport type RunResultValue = (typeof RUN_RESULTS)[number]\n\n/** The written word-prefix tokens of a run's summary, for the search box. */\nexport const RUN_SUMMARY_TOKENS = 'summaryTokens'\n\n/**\n * What a run writer spreads beside `summary`: the summary itself and its\n * search tokens, so the two cannot be written apart.\n */\nexport function runSummaryFields(summary: string): {\n summary: string\n summaryTokens: string[]\n} {\n return { summary, summaryTokens: nameSearchTokens(summary) }\n}\n\n/**\n * What the Filters panel offers, each a predicate the query serves beneath\n * its newest-first order: Time is that order's own field, so its ranges cost\n * no index of their own; Trigger and Result are equalities.\n *\n * `What happened` has no column filter: its words are the search box's,\n * which reads the same tokens, and a `contains` beside the search would be a\n * second array clause the query cannot hold.\n */\nexport const RUN_HISTORY_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'createdAtMs',\n kind: 'date',\n path: 'createdAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'trigger',\n kind: 'exact',\n path: 'trigger',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'result',\n kind: 'exact',\n path: 'result',\n operators: ['equals', 'isAnyOf'],\n },\n ] satisfies ListFilterField[],\n sorts: [{ path: 'createdAt', direction: 'desc', column: 'createdAtMs' }],\n search: { tokensPath: RUN_SUMMARY_TOKENS },\n}\n\nexport const RUN_HISTORY_HEADERS: Readonly<Record<string, string>> = {\n createdAtMs: 'Time',\n trigger: 'Trigger',\n result: 'Result',\n}\n\n/** A clause on Result the query serves — which is then what keeps it to runs. */\nconst namesResult = (clause: ListFilterClause): boolean =>\n clause.field === 'result' &&\n (clause.op === 'equals' || clause.op === 'isAnyOf') &&\n clause.value.trim() !== ''\n\n/**\n * The history's scope: this automation's entries, and only runs.\n *\n * \"Only runs\" is `result in [all three]` — unless the reader has asked for\n * a Result, whose own equality already names nothing but runs. The two are\n * never both on the query: a second predicate on the same field would only\n * repeat the first, and the plan serves an `equals` or `isAnyOf` on Result\n * unconditionally, so dropping the scope never lets a non-run through.\n */\nexport function runHistoryBase(\n targetId: string | undefined,\n clauses: readonly ListFilterClause[],\n): ListQueryFilter[] {\n const base: ListQueryFilter[] = []\n if (targetId) base.push({ path: 'target.id', op: '==', value: targetId })\n if (!clauses.some(namesResult))\n base.push({ path: 'result', op: 'in', value: [...RUN_RESULTS] })\n return base\n}\n\n/** The base's shape, for the index enumeration. */\nexport const RUN_HISTORY_BASE_PATHS = [\n { path: 'target.id' },\n { path: 'result' },\n] as const\n\n/** A run entry as stored, as far as completing it is concerned. */\nexport interface RunEntryLike {\n action?: unknown\n result?: unknown\n trigger?: unknown\n summary?: unknown\n summaryTokens?: unknown\n}\n\n/**\n * What a stored activity entry lacks to be found by the run history's query,\n * or `null` when it is not a run or already complete. The definition the\n * backfill restates (`tools/scripts/backfill-activity-run-fields.mjs`), held\n * to the same worked examples (`tools/scripts/lib/activity-run-fields.fixtures.json`).\n *\n * A run recorded before its structured fields carried only the prose line\n * `Action ran on <event>` — with ` with errors: <what failed>` when it\n * failed — so the verdict, the event and the summary are read back from it,\n * the way the history has always shown such a row. An entry that is neither\n * a stored verdict nor that prose is not a run and is left alone.\n */\nexport function runEntryCompletion(\n entry: RunEntryLike,\n): Record<string, unknown> | null {\n const stored = String(entry.result ?? '').trim()\n const action = String(entry.action ?? '').trim()\n const legacy = action.startsWith('Action ran on')\n if (!(RUN_RESULTS as readonly string[]).includes(stored) && !legacy)\n return null\n const patch: Record<string, unknown> = {}\n if (!(RUN_RESULTS as readonly string[]).includes(stored)) {\n patch['result'] = action.includes('with errors:') ? 'failed' : 'succeeded'\n }\n if (typeof entry.trigger !== 'string' || !entry.trigger) {\n const event = legacy ? /^Action ran on (\\S+)/.exec(action)?.[1] : undefined\n if (event) patch['trigger'] = event\n }\n let summary = typeof entry.summary === 'string' ? entry.summary.trim() : ''\n if (!summary) {\n const errors = /with errors:\\s*(.+)$/.exec(action)\n summary = errors ? errors[1] : legacy ? 'Ran' : action\n patch['summary'] = summary\n }\n const tokens = nameSearchTokens(summary)\n const held = Array.isArray(entry.summaryTokens) ? entry.summaryTokens : null\n if (\n !held ||\n held.length !== tokens.length ||\n held.some((token, at) => token !== tokens[at])\n ) {\n patch['summaryTokens'] = tokens\n }\n return Object.keys(patch).length ? patch : null\n}\n"],"names":["nameSearchTokens","RUN_RESULTS","RUN_SUMMARY_TOKENS","runSummaryFields","summary","summaryTokens","RUN_HISTORY_QUERY","fields","column","kind","path","operators","sorts","direction","search","tokensPath","RUN_HISTORY_HEADERS","createdAtMs","trigger","result","namesResult","clause","field","op","value","trim","runHistoryBase","targetId","clauses","base","push","some","RUN_HISTORY_BASE_PATHS","runEntryCompletion","entry","stored","String","action","legacy","startsWith","includes","patch","event","exec","undefined","errors","tokens","held","Array","isArray","length","token","at","Object","keys"],"mappings":"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"}
|