@aglyn/plugins-workflows 1.0.0-beta.203 → 1.0.0-beta.205
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-run-history-card.component.js +9 -1
- package/src/lib/components/host-run-history-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +17 -6
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/engine/run-event-actions.js +18 -4
- package/src/lib/engine/run-event-actions.js.map +1 -1
- package/src/lib/engine/run-event-workflows.js +2 -1
- package/src/lib/engine/run-event-workflows.js.map +1 -1
- package/src/lib/engine/run-trigger-actor.d.ts +26 -0
- package/src/lib/engine/run-trigger-actor.js +52 -0
- package/src/lib/engine/run-trigger-actor.js.map +1 -0
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
|
@@ -23,6 +23,7 @@ import { eventRunSuspension } from "./site-suspension.js";
|
|
|
23
23
|
import { deferFlowEnrollment, endFlowEnrollment } from "./flow-enrollments.js";
|
|
24
24
|
import { automationRunEnv, executeWorkflow, FLOW_CLAIM_RETRY_MS, stopFlowEnrollment } from "./run-event-actions.js";
|
|
25
25
|
import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
26
|
+
import { runTriggeredByFields } from "./run-trigger-actor.js";
|
|
26
27
|
/** Bounded fan-out per event: at most this many triggered workflows run. */ const MAX_TRIGGERED_WORKFLOWS = 10;
|
|
27
28
|
/**
|
|
28
29
|
* One workflow run's row in the site's activity feed, which is the run
|
|
@@ -33,7 +34,7 @@ import { workflowHasActionSteps } from "./workflow-steps.js";
|
|
|
33
34
|
await hostRef.collection('activity').add(_extends({
|
|
34
35
|
actorId: null,
|
|
35
36
|
actorEmail: null
|
|
36
|
-
}, row, {
|
|
37
|
+
}, runTriggeredByFields(), row, {
|
|
37
38
|
// The site log's search finds a run by its workflow's name (AGL-3321).
|
|
38
39
|
searchTokens: activitySearchTokens({
|
|
39
40
|
target: row['target']
|
|
@@ -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 { 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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"}
|
|
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'\nimport { runTriggeredByFields } from './run-trigger-actor'\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 // Who set this run off (AGL-3376).\n ...runTriggeredByFields(),\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","runTriggeredByFields","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,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 { HostEventActor } from '@aglyn/tenant-runtime/host-event-listeners';
|
|
18
|
+
/** Runs `work` with `actor` as the cause of every run it writes. */
|
|
19
|
+
export declare function withRunTriggerActor<T>(actor: HostEventActor | undefined, work: () => Promise<T>): Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* The `triggeredBy` field for a run row, or nothing outside a scope. Only
|
|
22
|
+
* the keys the door knew — Firestore refuses an `undefined` value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runTriggeredByFields(): {
|
|
25
|
+
triggeredBy?: Record<string, string>;
|
|
26
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
/**
|
|
18
|
+
* Who set a run off (AGL-3376), carried beside the engine rather than
|
|
19
|
+
* through it.
|
|
20
|
+
*
|
|
21
|
+
* An event reaches a run-row writer through the listener, the fan-out to
|
|
22
|
+
* workflows and actions, their steps, chained events and nested workflows —
|
|
23
|
+
* a parameter on every one of those frames would be threaded through code
|
|
24
|
+
* that never reads it. The listener runs the engine inside this scope once,
|
|
25
|
+
* and each writer reads the scope, so an event a step raises in turn is
|
|
26
|
+
* credited to whoever caused the first one.
|
|
27
|
+
*
|
|
28
|
+
* A resumed enrollment (a wait that ended on the cron) runs outside any
|
|
29
|
+
* scope and writes no `triggeredBy`: the person who started it is not the
|
|
30
|
+
* one acting now, and the history says it was not recorded.
|
|
31
|
+
*/ const scope = new AsyncLocalStorage();
|
|
32
|
+
/** Runs `work` with `actor` as the cause of every run it writes. */ export function withRunTriggerActor(actor, work) {
|
|
33
|
+
return scope.run(actor, work);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The `triggeredBy` field for a run row, or nothing outside a scope. Only
|
|
37
|
+
* the keys the door knew — Firestore refuses an `undefined` value.
|
|
38
|
+
*/ export function runTriggeredByFields() {
|
|
39
|
+
const actor = scope.getStore();
|
|
40
|
+
if (!(actor == null ? void 0 : actor.kind)) return {};
|
|
41
|
+
const triggeredBy = {
|
|
42
|
+
kind: actor.kind
|
|
43
|
+
};
|
|
44
|
+
if (actor.uid) triggeredBy['uid'] = actor.uid;
|
|
45
|
+
if (actor.email) triggeredBy['email'] = actor.email;
|
|
46
|
+
if (actor.apiKeyName) triggeredBy['apiKeyName'] = actor.apiKeyName;
|
|
47
|
+
return {
|
|
48
|
+
triggeredBy
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=run-trigger-actor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/engine/run-trigger-actor.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 { AsyncLocalStorage } from 'node:async_hooks'\nimport type { HostEventActor } from '@aglyn/tenant-runtime/host-event-listeners'\n\n/**\n * Who set a run off (AGL-3376), carried beside the engine rather than\n * through it.\n *\n * An event reaches a run-row writer through the listener, the fan-out to\n * workflows and actions, their steps, chained events and nested workflows —\n * a parameter on every one of those frames would be threaded through code\n * that never reads it. The listener runs the engine inside this scope once,\n * and each writer reads the scope, so an event a step raises in turn is\n * credited to whoever caused the first one.\n *\n * A resumed enrollment (a wait that ended on the cron) runs outside any\n * scope and writes no `triggeredBy`: the person who started it is not the\n * one acting now, and the history says it was not recorded.\n */\nconst scope = new AsyncLocalStorage<HostEventActor | undefined>()\n\n/** Runs `work` with `actor` as the cause of every run it writes. */\nexport function withRunTriggerActor<T>(\n actor: HostEventActor | undefined,\n work: () => Promise<T>,\n): Promise<T> {\n return scope.run(actor, work)\n}\n\n/**\n * The `triggeredBy` field for a run row, or nothing outside a scope. Only\n * the keys the door knew — Firestore refuses an `undefined` value.\n */\nexport function runTriggeredByFields(): {\n triggeredBy?: Record<string, string>\n} {\n const actor = scope.getStore()\n if (!actor?.kind) return {}\n const triggeredBy: Record<string, string> = { kind: actor.kind }\n if (actor.uid) triggeredBy['uid'] = actor.uid\n if (actor.email) triggeredBy['email'] = actor.email\n if (actor.apiKeyName) triggeredBy['apiKeyName'] = actor.apiKeyName\n return { triggeredBy }\n}\n"],"names":["AsyncLocalStorage","scope","withRunTriggerActor","actor","work","run","runTriggeredByFields","getStore","kind","triggeredBy","uid","email","apiKeyName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,mBAAkB;AAGpD;;;;;;;;;;;;;;CAcC,GACD,MAAMC,QAAQ,IAAID;AAElB,kEAAkE,GAClE,OAAO,SAASE,oBACdC,KAAiC,EACjCC,IAAsB;IAEtB,OAAOH,MAAMI,GAAG,CAACF,OAAOC;AAC1B;AAEA;;;CAGC,GACD,OAAO,SAASE;IAGd,MAAMH,QAAQF,MAAMM,QAAQ;IAC5B,IAAI,EAACJ,yBAAAA,MAAOK,IAAI,GAAE,OAAO,CAAC;IAC1B,MAAMC,cAAsC;QAAED,MAAML,MAAMK,IAAI;IAAC;IAC/D,IAAIL,MAAMO,GAAG,EAAED,WAAW,CAAC,MAAM,GAAGN,MAAMO,GAAG;IAC7C,IAAIP,MAAMQ,KAAK,EAAEF,WAAW,CAAC,QAAQ,GAAGN,MAAMQ,KAAK;IACnD,IAAIR,MAAMS,UAAU,EAAEH,WAAW,CAAC,aAAa,GAAGN,MAAMS,UAAU;IAClE,OAAO;QAAEH;IAAY;AACvB"}
|
package/src/lib/server.js
CHANGED
|
@@ -260,6 +260,11 @@ function secretsMatch(expected, provided) {
|
|
|
260
260
|
await hostRef.collection('activity').add(_extends({
|
|
261
261
|
actorId: null,
|
|
262
262
|
actorEmail: null,
|
|
263
|
+
// An outside system called the hook's URL; the hook is who it was.
|
|
264
|
+
triggeredBy: {
|
|
265
|
+
kind: 'webhook',
|
|
266
|
+
name: hook.workflowName
|
|
267
|
+
},
|
|
263
268
|
action: failed ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300) : `Inbound webhook ran "${hook.workflowName}"`,
|
|
264
269
|
// The run-history shape (AGL-2222) — without `result` this execution
|
|
265
270
|
// is invisible to the Runs table, which reads a verdict and not prose.
|
package/src/lib/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/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\nimport {\n checkEntitlement,\n type HostFunction,\n type HostVariable,\n type HostWebhook,\n type HostWorkflow,\n resolveOrgEntitlements,\n runWorkflow,\n} from '@aglyn/aglyn/server'\nimport {\n registerPluginApiRoute,\n registerPluginJob,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { timingSafeEqual } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from './model/run-history'\nimport { eventRunSuspension } from './engine/site-suspension'\nimport { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsServerDeclarations } from './declarations.server'\nimport {\n automationRunEnv,\n executeWorkflow,\n runDueFlowEnrollments,\n workflowContextFromDocs,\n} from './engine/run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './engine/workflow-steps'\n\n/*==========================================\n * THE BEAT THAT MAKES A WAIT STEP REAL.\n *\n * A `wait` step suspends a run into a `flowEnrollments` row and returns; this\n * is the only thing that ever comes back for it. Without a registration here\n * the wait step would be a delay that never ends — the exact failure\n * `process-abandoned.ts` documents next door, where a Pro entitlement sat\n * behind an HTTP door nothing ever POSTed to.\n *\n * Module scope, like the commerce and bookings registrations: the runner\n * route reaches plugin jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside `registerWorkflowsApi()` would depend on which entry\n * point happened to be loaded.\n *\n * ## Every minute, and why that is cheap\n *\n * `FLOW_WAIT_MIN_MINUTES` is one, so the beat has to be one — a fifteen-minute\n * sweep would make the shortest wait anybody can author wrong by up to\n * fourteen minutes. It costs one collection-group query per minute, which\n * returns nothing at all on a platform with no flow due, because the query\n * asks for `resumeAtMs <= now` rather than for the enrolled population.\n *\n * Bounded and idempotent, the two properties the runner requires: the scan\n * budget bounds one pass, and a transactional claim means an overlapping or\n * repeated beat cannot run the same person's step twice.\n *=========================================*/\nregisterPluginJob({\n pluginId: WORKFLOWS_BUNDLE_ID,\n name: 'resume-flow-waits',\n intervalMinutes: 1,\n description:\n 'Continue automations whose wait step has ended, and time out those ' +\n 'waiting for an event that never arrived.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n await runDueFlowEnrollments(gate)\n },\n})\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentByHook = new Map<string, number[]>()\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 30\n\nfunction rateLimited(key: string): boolean {\n const now = Date.now()\n const hits = (recentByHook.get(key) ?? []).filter(\n (t) => now - t < RATE_WINDOW_MS,\n )\n hits.push(now)\n recentByHook.set(key, hits)\n return hits.length > RATE_MAX\n}\n\nfunction secretsMatch(expected: string, provided: string): boolean {\n if (!expected || expected.length !== provided.length) return false\n return timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(provided)),\n )\n}\n\n/**\n * Inbound webhook endpoint (AGL-149): external systems POST JSON to\n * `/api/hooks/{hostId}/{hookId}` with the hook's secret in\n * `x-aglyn-secret`; a valid call enrolls the configured workflow with the\n * payload's top-level primitives in scope. Business tier (`webhooks`\n * flag); runs bill against the workflow-runs meter like any other run —\n * `workflowRunsPerMonth` is checked before the run and\n * `hosts/{id}/counters/workflowRuns` is incremented after it (AGL-2228).\n * That sentence was here before either half of it was true.\n */\nconst inboundHookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const hookId = String(req.query['hookId'] ?? '')\n if (!hostId || !hookId) {\n return res.status(400).json({ error: 'Invalid webhook address' })\n }\n if (rateLimited(`${hostId}/${hookId}`)) {\n return res.status(429).json({ error: 'Too many requests' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hookSnapshot = await hostRef.collection('webhooks').doc(hookId).get()\n const hook = hookSnapshot.data() as HostWebhook | undefined\n if (\n !hook ||\n hookSnapshot.get('deletedAt') ||\n hook.direction !== 'inbound' ||\n hook.enabled === false\n ) {\n return res.status(404).json({ error: 'Unknown webhook' })\n }\n const provided = String(req.headers['x-aglyn-secret'] ?? '')\n if (!secretsMatch(hook.secret ?? '', provided)) {\n return res.status(401).json({ error: 'Bad secret' })\n }\n\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owner = await getOrgForHost(hostId)\n const org = owner?.org\n /*\n * A suspended site takes no inbound run (AGL-3356). The tenant\n * dispatcher's lockdown gate reads `hostId` from the query or body, and\n * this route carries it in the PATH, so the engine's own question is\n * asked here. A 423 rather than a 200: the caller is an integration, and\n * one told a run happened keeps sending.\n */\n const suspended = await eventRunSuspension(hostRef, org)\n if (suspended) {\n return res.status(423).json({ error: 'locked', scope: suspended.scope })\n }\n if (!checkEntitlement(org as any, 'webhooks')) {\n return res\n .status(403)\n .json({ error: 'Webhooks are not enabled on this site' })\n }\n /*\n * The MONTHLY RUN CAP, which this handler's own docblock claimed for\n * years — \"runs bill against the workflow-runs meter like any other run\"\n * — and did not have. It read neither `workflowRunsPerMonth` nor\n * `hosts/{id}/counters/workflowRuns`, and it wrote neither, so an inbound\n * hook was a workflow execution that no cap could refuse and no meter\n * could see: the customer's usage card, the usage-alerts cron and the\n * COGS rollup all read a counter this path never moved.\n *\n * The refusal is a 402 rather than the silent `return` of\n * `runEventWorkflows`: that path is fired from a visitor request whose\n * success must not depend on the workflow, where this one is an\n * integration calling us on purpose and a caller that gets a 200 for a\n * run that did not happen will keep sending. `Retry-After` is deliberately\n * absent — the month, not a delay, is what clears this.\n *\n * The plan-less/unreadable org resolves as FREE\n * (`workflowRunsPerMonth: 0`), so an org doc we cannot read refuses rather\n * than runs. `checkEntitlement(webhooks)` above already refuses free on\n * today's price list; this is not a second gate on the same fact, because\n * a staff `entitlementOverrides` can grant `webhooks` without granting\n * runs, and because the cap is what bounds a BUSINESS org's spend.\n *\n * Read-then-write, like the event path, and bounded the same way: the\n * per-hook limiter above is 30/60s, so the overshoot a race can buy is\n * bounded by concurrency rather than unbounded by design. Making it\n * atomic means a transaction around the whole run and is the same trade\n * `runEventWorkflows` declined.\n */\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n const runLimit = resolveOrgEntitlements(org as any).workflowRunsPerMonth\n const runsUsed = Number((await runCounterRef.get()).get(monthKey) ?? 0)\n if (!(runsUsed + 1 <= runLimit)) {\n return res.status(402).json({\n error: `This site has used its ${runLimit} workflow runs for the month`,\n })\n }\n\n // Top-level primitives from the JSON body join the workflow scope.\n const body =\n typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n const scope: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(body)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n scope[key.slice(0, 64)] =\n typeof value === 'string' ? value.slice(0, 2000) : value\n }\n }\n\n const [functionDocs, variableDocs, workflowDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n hostRef.collection('workflows').limit(100).get(),\n ])\n const byName = <T extends { name?: string; deletedAt?: unknown }>(\n docs: FirebaseFirestore.QueryDocumentSnapshot[],\n ) => {\n const map: Record<string, T> = {}\n for (const doc of docs) {\n const data = doc.data() as T\n if (data?.name && !data.deletedAt) map[data.name] = data\n }\n return map\n }\n const workflows = byName<HostWorkflow>(workflowDocs.docs)\n const workflow = workflows[hook.workflowName?.trim() ?? '']\n if (!workflow) {\n return res.status(422).json({ error: 'No workflow bound to this hook' })\n }\n\n const hookEvent = `hook:${hook.name ?? hookId}`\n /*\n * A workflow of function calls is evaluated, as it always has been. One\n * with Actions steps is PERFORMED by the engine's executor — the posted\n * JSON is its payload — under the Actions tier gates, step by step, and\n * it is still one run on the meter below. Its working set is built from\n * the documents read above rather than read again.\n */\n const outcome: { error: string | null; value: unknown; summary: string } =\n workflowHasActionSteps(workflow)\n ? await (async () => {\n const workflowDoc = workflowDocs.docs.find(\n (doc) =>\n !doc.get('deletedAt') &&\n doc.get('name') === hook.workflowName?.trim(),\n )\n const execution = await executeWorkflow(\n automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth: 0,\n loadWorkflowContext: async () =>\n workflowContextFromDocs(\n functionDocs.docs,\n variableDocs.docs,\n workflowDocs.docs,\n ),\n }),\n {\n kind: 'workflow',\n id: workflowDoc?.id ?? '',\n name: workflow.name ?? '',\n },\n workflow as AutomationWorkflow,\n hookEvent,\n scope,\n )\n return {\n error: execution.errors.length\n ? execution.errors.join('; ')\n : null,\n value: execution.value,\n summary: execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : `Ran ${hook.workflowName}`,\n }\n })()\n : (() => {\n const run = runWorkflow(\n workflow,\n byName<HostFunction>(functionDocs.docs),\n byName<HostVariable>(variableDocs.docs),\n { event: hookEvent, ...scope },\n { workflows },\n )\n return run.ok === false\n ? { error: run.error, value: undefined, summary: '' }\n : {\n error: null,\n value: run.value,\n summary: `Ran ${hook.workflowName}`,\n }\n })()\n const failed = outcome.error !== null\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n action: failed\n ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300)\n : `Inbound webhook ran \"${hook.workflowName}\"`,\n // The run-history shape (AGL-2222) — without `result` this execution\n // is invisible to the Runs table, which reads a verdict and not prose.\n result: failed ? 'failed' : 'succeeded',\n trigger: hookEvent,\n ...runSummaryFields(\n failed\n ? String(outcome.error).slice(0, 300)\n : outcome.summary.slice(0, 300),\n ),\n target: { type: 'workflow', id: hookId, name: hook.name ?? '' },\n // The site log's search finds a run by its hook's name (AGL-3321).\n searchTokens: activitySearchTokens({ target: { name: hook.name } }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n // The run happened, so it counts — including a FAILED one, exactly as\n // `runEventWorkflows` counts its failures. A run that executed and threw\n // spent the same compute as one that returned a value; only a run that\n // never started (the cap above, a missing workflow) must not count.\n await runCounterRef\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n\n if (failed) return res.status(422).json({ error: outcome.error })\n return res.status(200).json({ ok: true, value: outcome.value })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Webhook failed' })\n }\n}\n\n/**\n * Registers the workflows plugin's public API routes (AGL-396), and the\n * engine's host-event listener beside them for a process whose boot did not\n * (see `declarations.server.ts`).\n */\nexport function registerWorkflowsApi(): void {\n registerWorkflowsServerDeclarations()\n registerPluginApiRoute('hooks/[hostId]/[hookId]', inboundHookHandler)\n}\n\n/**\n * The console half (AGL-2919), re-exported so one entry point serves both\n * surfaces: the manifest generator resolves every server surface through\n * `@aglyn/plugins-workflows/server`, and a second entry point would be a second\n * thing to keep in step with plugins.config.json.\n */\nexport { registerWorkflowsConsoleApi } from './server-console'\n"],"names":["checkEntitlement","resolveOrgEntitlements","runWorkflow","registerPluginApiRoute","registerPluginJob","activitySearchTokens","firebaseAdmin","getOrgForHost","timingSafeEqual","FieldValue","runSummaryFields","eventRunSuspension","BUNDLE_ID","WORKFLOWS_BUNDLE_ID","registerWorkflowsServerDeclarations","automationRunEnv","executeWorkflow","runDueFlowEnrollments","workflowContextFromDocs","workflowHasActionSteps","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","recentByHook","Map","RATE_WINDOW_MS","RATE_MAX","rateLimited","key","now","Date","hits","get","filter","t","push","set","length","secretsMatch","expected","provided","Uint8Array","Buffer","from","inboundHookHandler","req","res","method","status","json","error","hostId","String","query","hookId","hook","firestore","app","hostRef","collection","doc","hookSnapshot","data","direction","enabled","headers","secret","owner","org","suspended","monthKey","toISOString","slice","runCounterRef","runLimit","workflowRunsPerMonth","runsUsed","Number","body","JSON","parse","value","Object","entries","functionDocs","variableDocs","workflowDocs","Promise","all","limit","byName","docs","map","deletedAt","workflows","workflow","workflowName","trim","hookEvent","outcome","workflowDoc","find","execution","depth","loadWorkflowContext","kind","id","errors","join","summary","outcomes","run","event","ok","undefined","failed","add","actorId","actorEmail","action","result","trigger","target","type","searchTokens","createdAt","serverTimestamp","catch","increment","merge","console","registerWorkflowsApi","registerWorkflowsConsoleApi"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAKhBC,sBAAsB,EACtBC,WAAW,QACN,sBAAqB;AAC5B,SACEC,sBAAsB,EACtBC,iBAAiB,QAEZ,sBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,yCAAwC;AAC7E,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,eAAe,QAAQ,SAAQ;AACxC,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,gBAAgB,QAAQ,yBAAqB;AACtD,SAASC,kBAAkB,QAAQ,8BAA0B;AAC7D,SAASC,aAAaC,mBAAmB,QAAQ,+BAA2B;AAC5E,SAASC,mCAAmC,QAAQ,2BAAuB;AAC3E,SACEC,gBAAgB,EAChBC,eAAe,EACfC,qBAAqB,EACrBC,uBAAuB,QAClB,gCAA4B;AACnC,SAEEC,sBAAsB,QACjB,6BAAyB;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;2CAyB2C,GAC3Cf,kBAAkB;IAChBgB,UAAUP;IACVQ,MAAM;IACNC,iBAAiB;IACjBC,aACE,wEACA;IACFC,UAAU;QAAEC,OAAO;IAAW;IAC9BC,SAAS,OAAOC;QACd,MAAMV,sBAAsBU;IAC9B;AACF;AAEA,8DAA8D;AAC9D,MAAMC,eAAe,IAAIC;AACzB,MAAMC,iBAAiB;AACvB,MAAMC,WAAW;AAEjB,SAASC,YAAYC,GAAW;QAEhBL;IADd,MAAMM,MAAMC,KAAKD,GAAG;IACpB,MAAME,OAAO,EAACR,oBAAAA,aAAaS,GAAG,CAACJ,gBAAjBL,oBAAyB,EAAE,EAAEU,MAAM,CAC/C,CAACC,IAAML,MAAMK,IAAIT;IAEnBM,KAAKI,IAAI,CAACN;IACVN,aAAaa,GAAG,CAACR,KAAKG;IACtB,OAAOA,KAAKM,MAAM,GAAGX;AACvB;AAEA,SAASY,aAAaC,QAAgB,EAAEC,QAAgB;IACtD,IAAI,CAACD,YAAYA,SAASF,MAAM,KAAKG,SAASH,MAAM,EAAE,OAAO;IAC7D,OAAOlC,gBACL,IAAIsC,WAAWC,OAAOC,IAAI,CAACJ,YAC3B,IAAIE,WAAWC,OAAOC,IAAI,CAACH;AAE/B;AAEA;;;;;;;;;CASC,GACD,MAAMI,qBAAuC,OAAOC,KAAKC;QAIjCD,mBACAA;IAJtB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,SAASC,QAAOP,oBAAAA,IAAIQ,KAAK,CAAC,SAAS,YAAnBR,oBAAuB;IAC7C,MAAMS,SAASF,QAAOP,oBAAAA,IAAIQ,KAAK,CAAC,SAAS,YAAnBR,oBAAuB;IAC7C,IAAI,CAACM,UAAU,CAACG,QAAQ;QACtB,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;IACjE;IACA,IAAIvB,YAAY,GAAGwB,OAAO,CAAC,EAAEG,QAAQ,GAAG;QACtC,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAoB;IAC3D;IAEA,IAAI;YAasBL,2BACNU,cAuDM,MAWjBV,iBAkCmBU,YAkFwBA;YAvFvBA;QA5G3B,MAAMC,YAAYvD,cAAcwD,GAAG,GAAGD,SAAS;QAC/C,MAAME,UAAUF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACT;QAClD,MAAMU,eAAe,MAAMH,QAAQC,UAAU,CAAC,YAAYC,GAAG,CAACN,QAAQtB,GAAG;QACzE,MAAMuB,OAAOM,aAAaC,IAAI;QAC9B,IACE,CAACP,QACDM,aAAa7B,GAAG,CAAC,gBACjBuB,KAAKQ,SAAS,KAAK,aACnBR,KAAKS,OAAO,KAAK,OACjB;YACA,OAAOlB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkB;QACzD;QACA,MAAMV,WAAWY,QAAOP,4BAAAA,IAAIoB,OAAO,CAAC,iBAAiB,YAA7BpB,4BAAiC;QACzD,IAAI,CAACP,cAAaiB,eAAAA,KAAKW,MAAM,YAAXX,eAAe,IAAIf,WAAW;YAC9C,OAAOM,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAa;QACpD;QAEA,wDAAwD;QACxD,MAAMiB,QAAQ,MAAMjE,cAAciD;QAClC,MAAMiB,MAAMD,yBAAAA,MAAOC,GAAG;QACtB;;;;;;KAMC,GACD,MAAMC,YAAY,MAAM/D,mBAAmBoD,SAASU;QACpD,IAAIC,WAAW;YACb,OAAOvB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;gBAAU9B,OAAOiD,UAAUjD,KAAK;YAAC;QACxE;QACA,IAAI,CAACzB,iBAAiByE,KAAY,aAAa;YAC7C,OAAOtB,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAwC;QAC3D;QACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4BC,GACD,MAAMoB,WAAW,IAAIxC,OAAOyC,WAAW,GAAGC,KAAK,CAAC,GAAG;QACnD,MAAMC,gBAAgBf,QAAQC,UAAU,CAAC,YAAYC,GAAG,CAAC;QACzD,MAAMc,WAAW9E,uBAAuBwE,KAAYO,oBAAoB;QACxE,MAAMC,WAAWC,QAAO,OAAA,AAAC,CAAA,MAAMJ,cAAczC,GAAG,EAAC,EAAGA,GAAG,CAACsC,qBAAhC,OAA6C;QACrE,IAAI,CAAEM,CAAAA,WAAW,KAAKF,QAAO,GAAI;YAC/B,OAAO5B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO,CAAC,uBAAuB,EAAEwB,SAAS,4BAA4B,CAAC;YACzE;QACF;QAEA,mEAAmE;QACnE,MAAMI,OACJ,OAAOjC,IAAIiC,IAAI,KAAK,WAChBC,KAAKC,KAAK,CAACnC,IAAIiC,IAAI,IAAI,SACtBjC,YAAAA,IAAIiC,IAAI,YAARjC,YAAY,CAAC;QACpB,MAAMzB,QAAmD,CAAC;QAC1D,KAAK,MAAM,CAACQ,KAAKqD,MAAM,IAAIC,OAAOC,OAAO,CAACL,MAAO;YAC/C,IACE,OAAOG,UAAU,YACjB,OAAOA,UAAU,YACjB,OAAOA,UAAU,WACjB;gBACA7D,KAAK,CAACQ,IAAI4C,KAAK,CAAC,GAAG,IAAI,GACrB,OAAOS,UAAU,WAAWA,MAAMT,KAAK,CAAC,GAAG,QAAQS;YACvD;QACF;QAEA,MAAM,CAACG,cAAcC,cAAcC,aAAa,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACnE9B,QAAQC,UAAU,CAAC,aAAa8B,KAAK,CAAC,KAAKzD,GAAG;YAC9C0B,QAAQC,UAAU,CAAC,aAAa8B,KAAK,CAAC,KAAKzD,GAAG;YAC9C0B,QAAQC,UAAU,CAAC,aAAa8B,KAAK,CAAC,KAAKzD,GAAG;SAC/C;QACD,MAAM0D,SAAS,CACbC;YAEA,MAAMC,MAAyB,CAAC;YAChC,KAAK,MAAMhC,OAAO+B,KAAM;gBACtB,MAAM7B,OAAOF,IAAIE,IAAI;gBACrB,IAAIA,CAAAA,wBAAAA,KAAM9C,IAAI,KAAI,CAAC8C,KAAK+B,SAAS,EAAED,GAAG,CAAC9B,KAAK9C,IAAI,CAAC,GAAG8C;YACtD;YACA,OAAO8B;QACT;QACA,MAAME,YAAYJ,OAAqBJ,aAAaK,IAAI;QACxD,MAAMI,WAAWD,SAAS,UAACvC,qBAAAA,KAAKyC,YAAY,qBAAjBzC,mBAAmB0C,IAAI,qBAAM,GAAG;QAC3D,IAAI,CAACF,UAAU;YACb,OAAOjD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAiC;QACxE;QAEA,MAAMgD,YAAY,CAAC,KAAK,GAAE3C,aAAAA,KAAKvC,IAAI,YAATuC,aAAaD,QAAQ;QAC/C;;;;;;KAMC,GACD,MAAM6C,UACJrF,uBAAuBiF,YACnB,MAAM,AAAC,CAAA;sBAsBKA;YArBV,MAAMK,cAAcd,aAAaK,IAAI,CAACU,IAAI,CACxC,CAACzC;oBAEqBL;uBADpB,CAACK,IAAI5B,GAAG,CAAC,gBACT4B,IAAI5B,GAAG,CAAC,cAAYuB,qBAAAA,KAAKyC,YAAY,qBAAjBzC,mBAAmB0C,IAAI;;YAE/C,MAAMK,YAAY,MAAM3F,gBACtBD,iBAAiB;gBACfyC;gBACAO;gBACAS;gBACAoC,OAAO;gBACPC,qBAAqB,UACnB3F,wBACEuE,aAAaO,IAAI,EACjBN,aAAaM,IAAI,EACjBL,aAAaK,IAAI;YAEvB,IACA;gBACEc,MAAM;gBACNC,EAAE,UAAEN,+BAAAA,YAAaM,EAAE,mBAAI;gBACvB1F,IAAI,GAAE+E,iBAAAA,SAAS/E,IAAI,YAAb+E,iBAAiB;YACzB,GACAA,UACAG,WACA9E;YAEF,OAAO;gBACL8B,OAAOoD,UAAUK,MAAM,CAACtE,MAAM,GAC1BiE,UAAUK,MAAM,CAACC,IAAI,CAAC,QACtB;gBACJ3B,OAAOqB,UAAUrB,KAAK;gBACtB4B,SAASP,UAAUQ,QAAQ,CAACzE,MAAM,GAC9BiE,UAAUQ,QAAQ,CAACF,IAAI,CAAC,SACxB,CAAC,IAAI,EAAErD,KAAKyC,YAAY,EAAE;YAChC;QACF,CAAA,MACA,AAAC,CAAA;YACC,MAAMe,MAAMlH,YACVkG,UACAL,OAAqBN,aAAaO,IAAI,GACtCD,OAAqBL,aAAaM,IAAI,GACtC;gBAAEqB,OAAOd;eAAc9E,QACvB;gBAAE0E;YAAU;YAEd,OAAOiB,IAAIE,EAAE,KAAK,QACd;gBAAE/D,OAAO6D,IAAI7D,KAAK;gBAAE+B,OAAOiC;gBAAWL,SAAS;YAAG,IAClD;gBACE3D,OAAO;gBACP+B,OAAO8B,IAAI9B,KAAK;gBAChB4B,SAAS,CAAC,IAAI,EAAEtD,KAAKyC,YAAY,EAAE;YACrC;QACN,CAAA;QACN,MAAMmB,SAAShB,QAAQjD,KAAK,KAAK;QACjC,MAAMQ,QACHC,UAAU,CAAC,YACXyD,GAAG,CAAC;YACHC,SAAS;YACTC,YAAY;YACZC,QAAQJ,SACJ,CAAC,4BAA4B,EAAEhB,QAAQjD,KAAK,EAAE,CAACsB,KAAK,CAAC,GAAG,OACxD,CAAC,qBAAqB,EAAEjB,KAAKyC,YAAY,CAAC,CAAC,CAAC;YAChD,qEAAqE;YACrE,uEAAuE;YACvEwB,QAAQL,SAAS,WAAW;YAC5BM,SAASvB;WACN7F,iBACD8G,SACI/D,OAAO+C,QAAQjD,KAAK,EAAEsB,KAAK,CAAC,GAAG,OAC/B2B,QAAQU,OAAO,CAACrC,KAAK,CAAC,GAAG;YAE/BkD,QAAQ;gBAAEC,MAAM;gBAAYjB,IAAIpD;gBAAQtC,IAAI,GAAEuC,cAAAA,KAAKvC,IAAI,YAATuC,cAAa;YAAG;YAC9D,mEAAmE;YACnEqE,cAAc5H,qBAAqB;gBAAE0H,QAAQ;oBAAE1G,MAAMuC,KAAKvC,IAAI;gBAAC;YAAE;YACjE6G,WAAWzH,WAAW0H,eAAe;YAEtCC,KAAK,CAAC,IAAMb;QAEf,sEAAsE;QACtE,yEAAyE;QACzE,uEAAuE;QACvE,oEAAoE;QACpE,MAAMzC,cACHrC,GAAG,CAAC;YAAE,CAACkC,SAAS,EAAElE,WAAW4H,SAAS,CAAC;QAAG,GAAG;YAAEC,OAAO;QAAK,GAC3DF,KAAK,CAAC,IAAMb;QAEf,IAAIC,QAAQ,OAAOrE,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAOiD,QAAQjD,KAAK;QAAC;QAC/D,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEgE,IAAI;YAAMhC,OAAOkB,QAAQlB,KAAK;QAAC;IAC/D,EAAE,OAAO/B,OAAO;QACdgF,QAAQhF,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;IACxD;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASiF;IACd1H;IACAX,uBAAuB,2BAA2B8C;AACpD;AAEA;;;;;CAKC,GACD,SAASwF,2BAA2B,QAAQ,sBAAkB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/workflows/src/lib/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\nimport {\n checkEntitlement,\n type HostFunction,\n type HostVariable,\n type HostWebhook,\n type HostWorkflow,\n resolveOrgEntitlements,\n runWorkflow,\n} from '@aglyn/aglyn/server'\nimport {\n registerPluginApiRoute,\n registerPluginJob,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { activitySearchTokens } from '@aglyn/aglyn/app-utils/activity-search'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { timingSafeEqual } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { runSummaryFields } from './model/run-history'\nimport { eventRunSuspension } from './engine/site-suspension'\nimport { BUNDLE_ID as WORKFLOWS_BUNDLE_ID } from './constants/bundle-common'\nimport { registerWorkflowsServerDeclarations } from './declarations.server'\nimport {\n automationRunEnv,\n executeWorkflow,\n runDueFlowEnrollments,\n workflowContextFromDocs,\n} from './engine/run-event-actions'\nimport {\n type AutomationWorkflow,\n workflowHasActionSteps,\n} from './engine/workflow-steps'\n\n/*==========================================\n * THE BEAT THAT MAKES A WAIT STEP REAL.\n *\n * A `wait` step suspends a run into a `flowEnrollments` row and returns; this\n * is the only thing that ever comes back for it. Without a registration here\n * the wait step would be a delay that never ends — the exact failure\n * `process-abandoned.ts` documents next door, where a Pro entitlement sat\n * behind an HTTP door nothing ever POSTed to.\n *\n * Module scope, like the commerce and bookings registrations: the runner\n * route reaches plugin jobs through `ensureAll(['tenantApi'])`, so a\n * registration inside `registerWorkflowsApi()` would depend on which entry\n * point happened to be loaded.\n *\n * ## Every minute, and why that is cheap\n *\n * `FLOW_WAIT_MIN_MINUTES` is one, so the beat has to be one — a fifteen-minute\n * sweep would make the shortest wait anybody can author wrong by up to\n * fourteen minutes. It costs one collection-group query per minute, which\n * returns nothing at all on a platform with no flow due, because the query\n * asks for `resumeAtMs <= now` rather than for the enrolled population.\n *\n * Bounded and idempotent, the two properties the runner requires: the scan\n * budget bounds one pass, and a transactional claim means an overlapping or\n * repeated beat cannot run the same person's step twice.\n *=========================================*/\nregisterPluginJob({\n pluginId: WORKFLOWS_BUNDLE_ID,\n name: 'resume-flow-waits',\n intervalMinutes: 1,\n description:\n 'Continue automations whose wait step has ended, and time out those ' +\n 'waiting for an event that never arrived.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n await runDueFlowEnrollments(gate)\n },\n})\n\n// Best-effort per-instance rate limit (mirrors forms/submit).\nconst recentByHook = new Map<string, number[]>()\nconst RATE_WINDOW_MS = 60_000\nconst RATE_MAX = 30\n\nfunction rateLimited(key: string): boolean {\n const now = Date.now()\n const hits = (recentByHook.get(key) ?? []).filter(\n (t) => now - t < RATE_WINDOW_MS,\n )\n hits.push(now)\n recentByHook.set(key, hits)\n return hits.length > RATE_MAX\n}\n\nfunction secretsMatch(expected: string, provided: string): boolean {\n if (!expected || expected.length !== provided.length) return false\n return timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(provided)),\n )\n}\n\n/**\n * Inbound webhook endpoint (AGL-149): external systems POST JSON to\n * `/api/hooks/{hostId}/{hookId}` with the hook's secret in\n * `x-aglyn-secret`; a valid call enrolls the configured workflow with the\n * payload's top-level primitives in scope. Business tier (`webhooks`\n * flag); runs bill against the workflow-runs meter like any other run —\n * `workflowRunsPerMonth` is checked before the run and\n * `hosts/{id}/counters/workflowRuns` is incremented after it (AGL-2228).\n * That sentence was here before either half of it was true.\n */\nconst inboundHookHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query['hostId'] ?? '')\n const hookId = String(req.query['hookId'] ?? '')\n if (!hostId || !hookId) {\n return res.status(400).json({ error: 'Invalid webhook address' })\n }\n if (rateLimited(`${hostId}/${hookId}`)) {\n return res.status(429).json({ error: 'Too many requests' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hookSnapshot = await hostRef.collection('webhooks').doc(hookId).get()\n const hook = hookSnapshot.data() as HostWebhook | undefined\n if (\n !hook ||\n hookSnapshot.get('deletedAt') ||\n hook.direction !== 'inbound' ||\n hook.enabled === false\n ) {\n return res.status(404).json({ error: 'Unknown webhook' })\n }\n const provided = String(req.headers['x-aglyn-secret'] ?? '')\n if (!secretsMatch(hook.secret ?? '', provided)) {\n return res.status(401).json({ error: 'Bad secret' })\n }\n\n // Plan/quota gates ride the owning org's doc (AGL-238).\n const owner = await getOrgForHost(hostId)\n const org = owner?.org\n /*\n * A suspended site takes no inbound run (AGL-3356). The tenant\n * dispatcher's lockdown gate reads `hostId` from the query or body, and\n * this route carries it in the PATH, so the engine's own question is\n * asked here. A 423 rather than a 200: the caller is an integration, and\n * one told a run happened keeps sending.\n */\n const suspended = await eventRunSuspension(hostRef, org)\n if (suspended) {\n return res.status(423).json({ error: 'locked', scope: suspended.scope })\n }\n if (!checkEntitlement(org as any, 'webhooks')) {\n return res\n .status(403)\n .json({ error: 'Webhooks are not enabled on this site' })\n }\n /*\n * The MONTHLY RUN CAP, which this handler's own docblock claimed for\n * years — \"runs bill against the workflow-runs meter like any other run\"\n * — and did not have. It read neither `workflowRunsPerMonth` nor\n * `hosts/{id}/counters/workflowRuns`, and it wrote neither, so an inbound\n * hook was a workflow execution that no cap could refuse and no meter\n * could see: the customer's usage card, the usage-alerts cron and the\n * COGS rollup all read a counter this path never moved.\n *\n * The refusal is a 402 rather than the silent `return` of\n * `runEventWorkflows`: that path is fired from a visitor request whose\n * success must not depend on the workflow, where this one is an\n * integration calling us on purpose and a caller that gets a 200 for a\n * run that did not happen will keep sending. `Retry-After` is deliberately\n * absent — the month, not a delay, is what clears this.\n *\n * The plan-less/unreadable org resolves as FREE\n * (`workflowRunsPerMonth: 0`), so an org doc we cannot read refuses rather\n * than runs. `checkEntitlement(webhooks)` above already refuses free on\n * today's price list; this is not a second gate on the same fact, because\n * a staff `entitlementOverrides` can grant `webhooks` without granting\n * runs, and because the cap is what bounds a BUSINESS org's spend.\n *\n * Read-then-write, like the event path, and bounded the same way: the\n * per-hook limiter above is 30/60s, so the overshoot a race can buy is\n * bounded by concurrency rather than unbounded by design. Making it\n * atomic means a transaction around the whole run and is the same trade\n * `runEventWorkflows` declined.\n */\n const monthKey = new Date().toISOString().slice(0, 7)\n const runCounterRef = hostRef.collection('counters').doc('workflowRuns')\n const runLimit = resolveOrgEntitlements(org as any).workflowRunsPerMonth\n const runsUsed = Number((await runCounterRef.get()).get(monthKey) ?? 0)\n if (!(runsUsed + 1 <= runLimit)) {\n return res.status(402).json({\n error: `This site has used its ${runLimit} workflow runs for the month`,\n })\n }\n\n // Top-level primitives from the JSON body join the workflow scope.\n const body =\n typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n const scope: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(body)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n scope[key.slice(0, 64)] =\n typeof value === 'string' ? value.slice(0, 2000) : value\n }\n }\n\n const [functionDocs, variableDocs, workflowDocs] = await Promise.all([\n hostRef.collection('functions').limit(100).get(),\n hostRef.collection('variables').limit(100).get(),\n hostRef.collection('workflows').limit(100).get(),\n ])\n const byName = <T extends { name?: string; deletedAt?: unknown }>(\n docs: FirebaseFirestore.QueryDocumentSnapshot[],\n ) => {\n const map: Record<string, T> = {}\n for (const doc of docs) {\n const data = doc.data() as T\n if (data?.name && !data.deletedAt) map[data.name] = data\n }\n return map\n }\n const workflows = byName<HostWorkflow>(workflowDocs.docs)\n const workflow = workflows[hook.workflowName?.trim() ?? '']\n if (!workflow) {\n return res.status(422).json({ error: 'No workflow bound to this hook' })\n }\n\n const hookEvent = `hook:${hook.name ?? hookId}`\n /*\n * A workflow of function calls is evaluated, as it always has been. One\n * with Actions steps is PERFORMED by the engine's executor — the posted\n * JSON is its payload — under the Actions tier gates, step by step, and\n * it is still one run on the meter below. Its working set is built from\n * the documents read above rather than read again.\n */\n const outcome: { error: string | null; value: unknown; summary: string } =\n workflowHasActionSteps(workflow)\n ? await (async () => {\n const workflowDoc = workflowDocs.docs.find(\n (doc) =>\n !doc.get('deletedAt') &&\n doc.get('name') === hook.workflowName?.trim(),\n )\n const execution = await executeWorkflow(\n automationRunEnv({\n hostId,\n hostRef,\n owner,\n depth: 0,\n loadWorkflowContext: async () =>\n workflowContextFromDocs(\n functionDocs.docs,\n variableDocs.docs,\n workflowDocs.docs,\n ),\n }),\n {\n kind: 'workflow',\n id: workflowDoc?.id ?? '',\n name: workflow.name ?? '',\n },\n workflow as AutomationWorkflow,\n hookEvent,\n scope,\n )\n return {\n error: execution.errors.length\n ? execution.errors.join('; ')\n : null,\n value: execution.value,\n summary: execution.outcomes.length\n ? execution.outcomes.join(' · ')\n : `Ran ${hook.workflowName}`,\n }\n })()\n : (() => {\n const run = runWorkflow(\n workflow,\n byName<HostFunction>(functionDocs.docs),\n byName<HostVariable>(variableDocs.docs),\n { event: hookEvent, ...scope },\n { workflows },\n )\n return run.ok === false\n ? { error: run.error, value: undefined, summary: '' }\n : {\n error: null,\n value: run.value,\n summary: `Ran ${hook.workflowName}`,\n }\n })()\n const failed = outcome.error !== null\n await hostRef\n .collection('activity')\n .add({\n actorId: null,\n actorEmail: null,\n // An outside system called the hook's URL; the hook is who it was.\n triggeredBy: { kind: 'webhook', name: hook.workflowName },\n action: failed\n ? `Inbound webhook run failed: ${outcome.error}`.slice(0, 300)\n : `Inbound webhook ran \"${hook.workflowName}\"`,\n // The run-history shape (AGL-2222) — without `result` this execution\n // is invisible to the Runs table, which reads a verdict and not prose.\n result: failed ? 'failed' : 'succeeded',\n trigger: hookEvent,\n ...runSummaryFields(\n failed\n ? String(outcome.error).slice(0, 300)\n : outcome.summary.slice(0, 300),\n ),\n target: { type: 'workflow', id: hookId, name: hook.name ?? '' },\n // The site log's search finds a run by its hook's name (AGL-3321).\n searchTokens: activitySearchTokens({ target: { name: hook.name } }),\n createdAt: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n // The run happened, so it counts — including a FAILED one, exactly as\n // `runEventWorkflows` counts its failures. A run that executed and threw\n // spent the same compute as one that returned a value; only a run that\n // never started (the cap above, a missing workflow) must not count.\n await runCounterRef\n .set({ [monthKey]: FieldValue.increment(1) }, { merge: true })\n .catch(() => undefined)\n\n if (failed) return res.status(422).json({ error: outcome.error })\n return res.status(200).json({ ok: true, value: outcome.value })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Webhook failed' })\n }\n}\n\n/**\n * Registers the workflows plugin's public API routes (AGL-396), and the\n * engine's host-event listener beside them for a process whose boot did not\n * (see `declarations.server.ts`).\n */\nexport function registerWorkflowsApi(): void {\n registerWorkflowsServerDeclarations()\n registerPluginApiRoute('hooks/[hostId]/[hookId]', inboundHookHandler)\n}\n\n/**\n * The console half (AGL-2919), re-exported so one entry point serves both\n * surfaces: the manifest generator resolves every server surface through\n * `@aglyn/plugins-workflows/server`, and a second entry point would be a second\n * thing to keep in step with plugins.config.json.\n */\nexport { registerWorkflowsConsoleApi } from './server-console'\n"],"names":["checkEntitlement","resolveOrgEntitlements","runWorkflow","registerPluginApiRoute","registerPluginJob","activitySearchTokens","firebaseAdmin","getOrgForHost","timingSafeEqual","FieldValue","runSummaryFields","eventRunSuspension","BUNDLE_ID","WORKFLOWS_BUNDLE_ID","registerWorkflowsServerDeclarations","automationRunEnv","executeWorkflow","runDueFlowEnrollments","workflowContextFromDocs","workflowHasActionSteps","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","recentByHook","Map","RATE_WINDOW_MS","RATE_MAX","rateLimited","key","now","Date","hits","get","filter","t","push","set","length","secretsMatch","expected","provided","Uint8Array","Buffer","from","inboundHookHandler","req","res","method","status","json","error","hostId","String","query","hookId","hook","firestore","app","hostRef","collection","doc","hookSnapshot","data","direction","enabled","headers","secret","owner","org","suspended","monthKey","toISOString","slice","runCounterRef","runLimit","workflowRunsPerMonth","runsUsed","Number","body","JSON","parse","value","Object","entries","functionDocs","variableDocs","workflowDocs","Promise","all","limit","byName","docs","map","deletedAt","workflows","workflow","workflowName","trim","hookEvent","outcome","workflowDoc","find","execution","depth","loadWorkflowContext","kind","id","errors","join","summary","outcomes","run","event","ok","undefined","failed","add","actorId","actorEmail","triggeredBy","action","result","trigger","target","type","searchTokens","createdAt","serverTimestamp","catch","increment","merge","console","registerWorkflowsApi","registerWorkflowsConsoleApi"],"mappings":"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"}
|