@aglyn/tenant-runtime 1.0.0-beta.217 → 1.0.0-beta.219
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/README.md +2 -1
- package/package.json +3 -3
- package/src/lib/apply-publish-schedule.js +28 -9
- package/src/lib/apply-publish-schedule.js.map +1 -1
- package/src/lib/collection-fallback-nodes.js +5 -4
- package/src/lib/collection-fallback-nodes.js.map +1 -1
- package/src/lib/compose-screen-nodes.js +47 -35
- package/src/lib/compose-screen-nodes.js.map +1 -1
- package/src/lib/emit-host-event.d.ts +2 -2
- package/src/lib/emit-host-event.js.map +1 -1
- package/src/lib/get-collection-content.d.ts +2 -2
- package/src/lib/get-collection-content.js.map +1 -1
- package/src/lib/get-plugin-installs.js +8 -3
- package/src/lib/get-plugin-installs.js.map +1 -1
- package/src/lib/get-variables.d.ts +0 -7
- package/src/lib/get-variables.js +3 -27
- package/src/lib/get-variables.js.map +1 -1
- package/src/lib/host-event-listeners.d.ts +12 -11
- package/src/lib/host-event-listeners.js +4 -4
- package/src/lib/host-event-listeners.js.map +1 -1
- package/src/lib/convert-host-lead.d.ts +0 -189
- package/src/lib/convert-host-lead.js +0 -518
- package/src/lib/convert-host-lead.js.map +0 -1
- package/src/lib/get-datasets.d.ts +0 -45
- package/src/lib/get-datasets.js +0 -169
- package/src/lib/get-datasets.js.map +0 -1
- package/src/lib/stamp-form-dataset-bindings.d.ts +0 -32
- package/src/lib/stamp-form-dataset-bindings.js +0 -59
- package/src/lib/stamp-form-dataset-bindings.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-variables.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * Part of the per-host compose bundle every page render pays (AGL-1302):\n * variables
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-variables.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * Part of the per-host compose bundle every page render pays (AGL-1302):\n * variables and functions are two ~100-doc queries per render. Both fail\n * open, so a cache-layer failure degrades to the uncached read rather than\n * an empty map. The publish path busts `tenant-data:{hostId}` on top.\n */\nconst HOST_BINDINGS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\nconst cachedBindingsRead = async <T>(\n name: string,\n hostId: string,\n read: () => Promise<Record<string, T>>,\n): Promise<Record<string, T>> => {\n try {\n return await withRenderCache({\n key: [name, hostId],\n revalidate: HOST_BINDINGS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error(error)\n return read()\n }\n}\n\n/**\n * Fetches the host's variables keyed by doc id (AGL-185/194) for\n * `{{var:id}}` tokens; expression scopes read names off the values.\n * Fail-open: on error an empty map is returned and tokens render\n * literally/empty.\n */\nexport async function getVariables(options: {\n hostId: string\n}): Promise<Record<string, Aglyn.HostVariable>> {\n return cachedBindingsRead('tenant-variables', options.hostId, () =>\n readVariables(options),\n )\n}\n\nasync function readVariables(options: {\n hostId: string\n}): Promise<Record<string, Aglyn.HostVariable>> {\n const variables: Record<string, Aglyn.HostVariable> = {}\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(options.hostId)\n .collection('variables')\n .limit(100)\n .get()\n for (const docSnapshot of snapshot.docs) {\n const data = docSnapshot.data() as Aglyn.HostVariable\n if (data?.name && !((data as any).deletedAt)) {\n variables[docSnapshot.id] = data\n }\n }\n } catch (error) {\n console.error(error)\n }\n return variables\n}\n\n/**\n * Fetches the host's function definitions keyed by name for\n * `{{fn:name(...)}}` bindings (AGL-93). Fail-open like variables.\n */\nexport async function getFunctions(options: {\n hostId: string\n}): Promise<Record<string, Aglyn.HostFunction>> {\n return cachedBindingsRead('tenant-functions', options.hostId, () =>\n readFunctions(options),\n )\n}\n\nasync function readFunctions(options: {\n hostId: string\n}): Promise<Record<string, Aglyn.HostFunction>> {\n const functions: Record<string, Aglyn.HostFunction> = {}\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(options.hostId)\n .collection('functions')\n .limit(100)\n .get()\n for (const docSnapshot of snapshot.docs) {\n const data = docSnapshot.data() as Aglyn.HostFunction\n if (data?.name && !(data as any).deletedAt) {\n // Plain evaluator fields only: doc timestamps aren't\n // JSON-serializable and definitions embed into page props.\n const definition = {\n name: data.name,\n parameters: data.parameters ?? [],\n variables: data.variables ?? [],\n operations: data.operations ?? [],\n ...(data.returnValue && { returnValue: data.returnValue }),\n }\n // Keyed by doc id ({{fn:id(...)}}, AGL-185/194) AND name so\n // legacy workflow steps that stored functionName keep resolving\n // (AGL-261).\n functions[docSnapshot.id] = definition\n if (!functions[data.name]) functions[data.name] = definition\n }\n }\n } catch (error) {\n console.error(error)\n }\n return functions\n}\n\nexport default getVariables\n"],"names":["firebaseAdmin","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","HOST_BINDINGS_TTL_SECONDS","cachedBindingsRead","name","hostId","read","key","revalidate","tags","error","console","getVariables","options","readVariables","variables","snapshot","app","firestore","collection","doc","limit","get","docSnapshot","docs","data","deletedAt","id","getFunctions","readFunctions","functions","definition","parameters","operations","returnValue"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAE9C;;;;;CAKC,GACD,MAAMC,4BAA4BH;AAElC,MAAMI,qBAAqB,OACzBC,MACAC,QACAC;IAEA,IAAI;QACF,OAAO,MAAML,gBAAgB;YAC3BM,KAAK;gBAACH;gBAAMC;aAAO;YACnBG,YAAYN;YACZO,MAAM;gBAACT,cAAcK;aAAQ;YAC7BC;QACF;IACF,EAAE,OAAOI,OAAO;QACdC,QAAQD,KAAK,CAACA;QACd,OAAOJ;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeM,aAAaC,OAElC;IACC,OAAOV,mBAAmB,oBAAoBU,QAAQR,MAAM,EAAE,IAC5DS,cAAcD;AAElB;AAEA,eAAeC,cAAcD,OAE5B;IACC,MAAME,YAAgD,CAAC;IACvD,IAAI;QACF,MAAMC,WAAW,MAAMlB,cACpBmB,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACP,QAAQR,MAAM,EAClBc,UAAU,CAAC,aACXE,KAAK,CAAC,KACNC,GAAG;QACN,KAAK,MAAMC,eAAeP,SAASQ,IAAI,CAAE;YACvC,MAAMC,OAAOF,YAAYE,IAAI;YAC7B,IAAIA,CAAAA,wBAAAA,KAAMrB,IAAI,KAAI,CAAE,AAACqB,KAAaC,SAAS,EAAG;gBAC5CX,SAAS,CAACQ,YAAYI,EAAE,CAAC,GAAGF;YAC9B;QACF;IACF,EAAE,OAAOf,OAAO;QACdC,QAAQD,KAAK,CAACA;IAChB;IACA,OAAOK;AACT;AAEA;;;CAGC,GACD,OAAO,eAAea,aAAaf,OAElC;IACC,OAAOV,mBAAmB,oBAAoBU,QAAQR,MAAM,EAAE,IAC5DwB,cAAchB;AAElB;AAEA,eAAegB,cAAchB,OAE5B;IACC,MAAMiB,YAAgD,CAAC;IACvD,IAAI;QACF,MAAMd,WAAW,MAAMlB,cACpBmB,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACP,QAAQR,MAAM,EAClBc,UAAU,CAAC,aACXE,KAAK,CAAC,KACNC,GAAG;QACN,KAAK,MAAMC,eAAeP,SAASQ,IAAI,CAAE;YACvC,MAAMC,OAAOF,YAAYE,IAAI;YAC7B,IAAIA,CAAAA,wBAAAA,KAAMrB,IAAI,KAAI,CAAC,AAACqB,KAAaC,SAAS,EAAE;oBAK5BD,kBACDA,iBACCA;gBANd,qDAAqD;gBACrD,2DAA2D;gBAC3D,MAAMM,aAAa;oBACjB3B,MAAMqB,KAAKrB,IAAI;oBACf4B,UAAU,GAAEP,mBAAAA,KAAKO,UAAU,YAAfP,mBAAmB,EAAE;oBACjCV,SAAS,GAAEU,kBAAAA,KAAKV,SAAS,YAAdU,kBAAkB,EAAE;oBAC/BQ,UAAU,GAAER,mBAAAA,KAAKQ,UAAU,YAAfR,mBAAmB,EAAE;mBAC7BA,KAAKS,WAAW,IAAI;oBAAEA,aAAaT,KAAKS,WAAW;gBAAC;gBAE1D,4DAA4D;gBAC5D,gEAAgE;gBAChE,aAAa;gBACbJ,SAAS,CAACP,YAAYI,EAAE,CAAC,GAAGI;gBAC5B,IAAI,CAACD,SAAS,CAACL,KAAKrB,IAAI,CAAC,EAAE0B,SAAS,CAACL,KAAKrB,IAAI,CAAC,GAAG2B;YACpD;QACF;IACF,EAAE,OAAOrB,OAAO;QACdC,QAAQD,KAAK,CAACA;IAChB;IACA,OAAOoB;AACT;AAEA,eAAelB,aAAY"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
17
|
+
import type { SiteAlert } from '@aglyn/aglyn/server';
|
|
18
18
|
/**
|
|
19
19
|
* WHO HEARS A HOST EVENT.
|
|
20
20
|
*
|
|
@@ -75,15 +75,16 @@ export interface HostEventListener {
|
|
|
75
75
|
* Resolves the site alerts a request/response emitter shows its visitor —
|
|
76
76
|
* a form submission returns them in its response — or nothing.
|
|
77
77
|
*/
|
|
78
|
-
onEvent(hostId: string, event: string, payload: HostEventPayload, context?: HostEventContext): Promise<readonly
|
|
78
|
+
onEvent(hostId: string, event: string, payload: HostEventPayload, context?: HostEventContext): Promise<readonly SiteAlert[] | void>;
|
|
79
79
|
/**
|
|
80
|
-
* Runs ONE
|
|
81
|
-
* it evaluated itself — a scroll depth, a
|
|
82
|
-
* already decided the trigger matched, so
|
|
83
|
-
*
|
|
84
|
-
* does not own the id resolves
|
|
80
|
+
* Runs the server half of ONE page interaction, by id, that a published
|
|
81
|
+
* page fired for a site event it evaluated itself — a scroll depth, a
|
|
82
|
+
* click, exit intent. The page has already decided the trigger matched, so
|
|
83
|
+
* the listener runs what that one interaction names rather than everything
|
|
84
|
+
* listening for the event; a listener that does not own the id resolves
|
|
85
|
+
* nothing.
|
|
85
86
|
*/
|
|
86
|
-
onDispatch?(hostId: string,
|
|
87
|
+
onDispatch?(hostId: string, interactionId: string, event: string, payload: HostEventPayload, context?: HostEventContext): Promise<readonly SiteAlert[] | void>;
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
89
90
|
* Subscribes a plugin to every host event. Registering again under the same
|
|
@@ -99,9 +100,9 @@ export declare function resetHostEventListenersForTests(): void;
|
|
|
99
100
|
* Hands one event to every listener, concurrently, and gathers the alerts
|
|
100
101
|
* they produced. Never rejects.
|
|
101
102
|
*/
|
|
102
|
-
export declare function runHostEventListeners(hostId: string, event: string, payload?: HostEventPayload, context?: HostEventContext): Promise<
|
|
103
|
+
export declare function runHostEventListeners(hostId: string, event: string, payload?: HostEventPayload, context?: HostEventContext): Promise<SiteAlert[]>;
|
|
103
104
|
/**
|
|
104
|
-
* Hands one page-dispatched
|
|
105
|
+
* Hands one page-dispatched interaction to the listeners that dispatch, and
|
|
105
106
|
* gathers the alerts it produced. Never rejects.
|
|
106
107
|
*/
|
|
107
|
-
export declare function dispatchHostAutomation(hostId: string,
|
|
108
|
+
export declare function dispatchHostAutomation(hostId: string, interactionId: string, event: string, payload?: HostEventPayload, context?: HostEventContext): Promise<SiteAlert[]>;
|
|
@@ -73,16 +73,16 @@
|
|
|
73
73
|
return settled.flat();
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Hands one page-dispatched
|
|
76
|
+
* Hands one page-dispatched interaction to the listeners that dispatch, and
|
|
77
77
|
* gathers the alerts it produced. Never rejects.
|
|
78
|
-
*/ export async function dispatchHostAutomation(hostId,
|
|
78
|
+
*/ export async function dispatchHostAutomation(hostId, interactionId, event, payload = {}, context = {}) {
|
|
79
79
|
const alerts = [];
|
|
80
80
|
for (const { pluginId, listener } of registrations){
|
|
81
81
|
if (!listener.onDispatch) continue;
|
|
82
82
|
try {
|
|
83
|
-
alerts.push(...alertsOf(await listener.onDispatch(hostId,
|
|
83
|
+
alerts.push(...alertsOf(await listener.onDispatch(hostId, interactionId, event, payload, context)));
|
|
84
84
|
} catch (error) {
|
|
85
|
-
console.error(`[host-events] ${pluginId} failed to dispatch ${
|
|
85
|
+
console.error(`[host-events] ${pluginId} failed to dispatch ${interactionId}`, hostId, error);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
return alerts;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/host-event-listeners.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostActionAlert } from '@aglyn/aglyn/server'\n\n/**\n * WHO HEARS A HOST EVENT.\n *\n * A host event — a form submitted, a member signed up, a deal won — is raised\n * by the server door that performed the write, through `emitHostEvent`. What\n * runs because of it is not the runtime's business: an automation engine, an\n * integration, a marketplace plugin. Each registers a listener here and the\n * runtime fans every event out to all of them, knowing none by name.\n *\n * ## Registered by a call, at boot\n *\n * A listener is registered by a function the platform calls BY NAME — a\n * plugin's `serverDeclarations` entry, which both apps run once per process\n * from `instrumentation.ts`, and its API register functions, which the plugin\n * loader runs. Never by a bare `import './x'`: a bundler drops a module its\n * package does not list as a side effect, and jest does not, so a registration\n * that relies on one passes every spec and runs in no build (AGL-3025).\n *\n * Boot registration is what lets a core route that never loads a plugin — the\n * form submission, the page-view collector — still reach every listener.\n *\n * ## Isolated, and never thrown into the emitter\n *\n * The event has already happened: the submission is stored, the member exists.\n * A listener that throws is logged against its plugin and the others still\n * run, and the emitting request never sees the failure.\n */\n\n/** What an event carries into everything that listens for it. */\nexport type HostEventPayload = Record<string, string | number | boolean>\n\n/**\n * Who caused an event (AGL-3376): the console member who moved the deal, the\n * integration's key that added the contact, the visitor who submitted the\n * form, or the platform acting on its own (a Stripe webhook, a schedule).\n *\n * Kept OUT of the payload on purpose. The payload seeds a workflow's\n * expression scope and condition editor, and a site's automations have no\n * business reading a console member's address; this rides beside it, for\n * the record of who set a run off.\n */\nexport interface HostEventActor {\n kind: 'member' | 'visitor' | 'apiKey' | 'platform'\n /** The acting account, for a member. */\n uid?: string | null\n /** The address the actor had when they acted, when the door knew it. */\n email?: string | null\n /** The key's name, for an integration. */\n apiKeyName?: string | null\n}\n\n/** What rides beside an event's payload. */\nexport interface HostEventContext {\n /** Absent when the door that raised the event did not know. */\n actor?: HostEventActor\n}\n\nexport interface HostEventListener {\n /**\n * Runs whatever this plugin does when `event` happens on the site.\n *\n * Resolves the site alerts a request/response emitter shows its visitor —\n * a form submission returns them in its response — or nothing.\n */\n onEvent(\n hostId: string,\n event: string,\n payload: HostEventPayload,\n context?: HostEventContext,\n ): Promise<readonly HostActionAlert[] | void>\n /**\n * Runs ONE automation, by id, that a published page fired for a site event\n * it evaluated itself — a scroll depth, a click, exit intent. The page has\n * already decided the trigger matched, so the listener runs that one\n * automation rather than every automation on the event; a listener that\n * does not own the id resolves nothing.\n */\n onDispatch?(\n hostId: string,\n automationId: string,\n event: string,\n payload: HostEventPayload,\n context?: HostEventContext,\n ): Promise<readonly HostActionAlert[] | void>\n}\n\ninterface Registration {\n pluginId: string\n listener: HostEventListener\n}\n\nconst registrations: Registration[] = []\n\n/** Whether the no-listener warning has been printed in this process. */\nlet warnedEmpty = false\n\n/** A listener's answer as a list: nothing it said counts as no alerts. */\nfunction alertsOf(answer: readonly HostActionAlert[] | void): HostActionAlert[] {\n return Array.isArray(answer) ? [...answer] : []\n}\n\n/**\n * Subscribes a plugin to every host event. Registering again under the same\n * plugin id replaces the earlier listener in place, so a register function\n * the platform calls twice — boot, then the API surface — leaves one.\n */\nexport function registerHostEventListener(\n pluginId: string,\n listener: HostEventListener,\n): void {\n const id = String(pluginId ?? '').trim()\n if (!id) {\n throw new Error('a host event listener needs the id of the plugin it belongs to')\n }\n const at = registrations.findIndex((entry) => entry.pluginId === id)\n if (at >= 0) registrations[at] = { pluginId: id, listener }\n else registrations.push({ pluginId: id, listener })\n}\n\n/** The plugins listening, in registration order, for diagnostics. */\nexport function listHostEventListeners(): string[] {\n return registrations.map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every listener. */\nexport function resetHostEventListenersForTests(): void {\n registrations.length = 0\n warnedEmpty = false\n}\n\n/**\n * Hands one event to every listener, concurrently, and gathers the alerts\n * they produced. Never rejects.\n */\nexport async function runHostEventListeners(\n hostId: string,\n event: string,\n payload: HostEventPayload = {},\n context: HostEventContext = {},\n): Promise<HostActionAlert[]> {\n if (!registrations.length) {\n /*\n * Said once per process rather than per event. No listener means the\n * plugins' server declarations never ran here — every automation on\n * every site served by this process is silent, and a line in the log is\n * the only place that is visible.\n */\n if (!warnedEmpty) {\n warnedEmpty = true\n console.warn(\n `[host-events] nothing is listening; \"${event}\" on ${hostId} ran no ` +\n 'automation. Listeners register from the plugins’ server ' +\n 'declarations at boot.',\n )\n }\n return []\n }\n const settled = await Promise.all(\n registrations.map(async ({ pluginId, listener }) => {\n try {\n return alertsOf(await listener.onEvent(hostId, event, payload, context))\n } catch (error) {\n console.error(`[host-events] ${pluginId} failed on ${event}`, hostId, error)\n return []\n }\n }),\n )\n return settled.flat()\n}\n\n/**\n * Hands one page-dispatched automation to the listeners that dispatch, and\n * gathers the alerts it produced. Never rejects.\n */\nexport async function dispatchHostAutomation(\n hostId: string,\n automationId: string,\n event: string,\n payload: HostEventPayload = {},\n context: HostEventContext = {},\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n for (const { pluginId, listener } of registrations) {\n if (!listener.onDispatch) continue\n try {\n alerts.push(\n ...alertsOf(\n await listener.onDispatch(hostId, automationId, event, payload, context),\n ),\n )\n } catch (error) {\n console.error(\n `[host-events] ${pluginId} failed to dispatch ${automationId}`,\n hostId,\n error,\n )\n }\n }\n return alerts\n}\n"],"names":["registrations","warnedEmpty","alertsOf","answer","Array","isArray","registerHostEventListener","pluginId","listener","id","String","trim","Error","at","findIndex","entry","push","listHostEventListeners","map","resetHostEventListenersForTests","length","runHostEventListeners","hostId","event","payload","context","console","warn","settled","Promise","all","onEvent","error","flat","dispatchHostAutomation","automationId","alerts","onDispatch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GA+FD,MAAMA,gBAAgC,EAAE;AAExC,sEAAsE,GACtE,IAAIC,cAAc;AAElB,wEAAwE,GACxE,SAASC,SAASC,MAAyC;IACzD,OAAOC,MAAMC,OAAO,CAACF,UAAU;WAAIA;KAAO,GAAG,EAAE;AACjD;AAEA;;;;CAIC,GACD,OAAO,SAASG,0BACdC,QAAgB,EAChBC,QAA2B;IAE3B,MAAMC,KAAKC,OAAOH,mBAAAA,WAAY,IAAII,IAAI;IACtC,IAAI,CAACF,IAAI;QACP,MAAM,IAAIG,MAAM;IAClB;IACA,MAAMC,KAAKb,cAAcc,SAAS,CAAC,CAACC,QAAUA,MAAMR,QAAQ,KAAKE;IACjE,IAAII,MAAM,GAAGb,aAAa,CAACa,GAAG,GAAG;QAAEN,UAAUE;QAAID;IAAS;SACrDR,cAAcgB,IAAI,CAAC;QAAET,UAAUE;QAAID;IAAS;AACnD;AAEA,mEAAmE,GACnE,OAAO,SAASS;IACd,OAAOjB,cAAckB,GAAG,CAAC,CAACH,QAAUA,MAAMR,QAAQ;AACpD;AAEA,sCAAsC,GACtC,OAAO,SAASY;IACdnB,cAAcoB,MAAM,GAAG;IACvBnB,cAAc;AAChB;AAEA;;;CAGC,GACD,OAAO,eAAeoB,sBACpBC,MAAc,EACdC,KAAa,EACbC,UAA4B,CAAC,CAAC,EAC9BC,UAA4B,CAAC,CAAC;IAE9B,IAAI,CAACzB,cAAcoB,MAAM,EAAE;QACzB;;;;;KAKC,GACD,IAAI,CAACnB,aAAa;YAChBA,cAAc;YACdyB,QAAQC,IAAI,CACV,CAAC,qCAAqC,EAAEJ,MAAM,KAAK,EAAED,OAAO,QAAQ,CAAC,GACnE,6DACA;QAEN;QACA,OAAO,EAAE;IACX;IACA,MAAMM,UAAU,MAAMC,QAAQC,GAAG,CAC/B9B,cAAckB,GAAG,CAAC,OAAO,EAAEX,QAAQ,EAAEC,QAAQ,EAAE;QAC7C,IAAI;YACF,OAAON,SAAS,MAAMM,SAASuB,OAAO,CAACT,QAAQC,OAAOC,SAASC;QACjE,EAAE,OAAOO,OAAO;YACdN,QAAQM,KAAK,CAAC,CAAC,cAAc,EAAEzB,SAAS,WAAW,EAAEgB,OAAO,EAAED,QAAQU;YACtE,OAAO,EAAE;QACX;IACF;IAEF,OAAOJ,QAAQK,IAAI;AACrB;AAEA;;;CAGC,GACD,OAAO,eAAeC,uBACpBZ,MAAc,EACda,YAAoB,EACpBZ,KAAa,EACbC,UAA4B,CAAC,CAAC,EAC9BC,UAA4B,CAAC,CAAC;IAE9B,MAAMW,SAA4B,EAAE;IACpC,KAAK,MAAM,EAAE7B,QAAQ,EAAEC,QAAQ,EAAE,IAAIR,cAAe;QAClD,IAAI,CAACQ,SAAS6B,UAAU,EAAE;QAC1B,IAAI;YACFD,OAAOpB,IAAI,IACNd,SACD,MAAMM,SAAS6B,UAAU,CAACf,QAAQa,cAAcZ,OAAOC,SAASC;QAGtE,EAAE,OAAOO,OAAO;YACdN,QAAQM,KAAK,CACX,CAAC,cAAc,EAAEzB,SAAS,oBAAoB,EAAE4B,cAAc,EAC9Db,QACAU;QAEJ;IACF;IACA,OAAOI;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/host-event-listeners.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { SiteAlert } from '@aglyn/aglyn/server'\n\n/**\n * WHO HEARS A HOST EVENT.\n *\n * A host event — a form submitted, a member signed up, a deal won — is raised\n * by the server door that performed the write, through `emitHostEvent`. What\n * runs because of it is not the runtime's business: an automation engine, an\n * integration, a marketplace plugin. Each registers a listener here and the\n * runtime fans every event out to all of them, knowing none by name.\n *\n * ## Registered by a call, at boot\n *\n * A listener is registered by a function the platform calls BY NAME — a\n * plugin's `serverDeclarations` entry, which both apps run once per process\n * from `instrumentation.ts`, and its API register functions, which the plugin\n * loader runs. Never by a bare `import './x'`: a bundler drops a module its\n * package does not list as a side effect, and jest does not, so a registration\n * that relies on one passes every spec and runs in no build (AGL-3025).\n *\n * Boot registration is what lets a core route that never loads a plugin — the\n * form submission, the page-view collector — still reach every listener.\n *\n * ## Isolated, and never thrown into the emitter\n *\n * The event has already happened: the submission is stored, the member exists.\n * A listener that throws is logged against its plugin and the others still\n * run, and the emitting request never sees the failure.\n */\n\n/** What an event carries into everything that listens for it. */\nexport type HostEventPayload = Record<string, string | number | boolean>\n\n/**\n * Who caused an event (AGL-3376): the console member who moved the deal, the\n * integration's key that added the contact, the visitor who submitted the\n * form, or the platform acting on its own (a Stripe webhook, a schedule).\n *\n * Kept OUT of the payload on purpose. The payload seeds a workflow's\n * expression scope and condition editor, and a site's automations have no\n * business reading a console member's address; this rides beside it, for\n * the record of who set a run off.\n */\nexport interface HostEventActor {\n kind: 'member' | 'visitor' | 'apiKey' | 'platform'\n /** The acting account, for a member. */\n uid?: string | null\n /** The address the actor had when they acted, when the door knew it. */\n email?: string | null\n /** The key's name, for an integration. */\n apiKeyName?: string | null\n}\n\n/** What rides beside an event's payload. */\nexport interface HostEventContext {\n /** Absent when the door that raised the event did not know. */\n actor?: HostEventActor\n}\n\nexport interface HostEventListener {\n /**\n * Runs whatever this plugin does when `event` happens on the site.\n *\n * Resolves the site alerts a request/response emitter shows its visitor —\n * a form submission returns them in its response — or nothing.\n */\n onEvent(\n hostId: string,\n event: string,\n payload: HostEventPayload,\n context?: HostEventContext,\n ): Promise<readonly SiteAlert[] | void>\n /**\n * Runs the server half of ONE page interaction, by id, that a published\n * page fired for a site event it evaluated itself — a scroll depth, a\n * click, exit intent. The page has already decided the trigger matched, so\n * the listener runs what that one interaction names rather than everything\n * listening for the event; a listener that does not own the id resolves\n * nothing.\n */\n onDispatch?(\n hostId: string,\n interactionId: string,\n event: string,\n payload: HostEventPayload,\n context?: HostEventContext,\n ): Promise<readonly SiteAlert[] | void>\n}\n\ninterface Registration {\n pluginId: string\n listener: HostEventListener\n}\n\nconst registrations: Registration[] = []\n\n/** Whether the no-listener warning has been printed in this process. */\nlet warnedEmpty = false\n\n/** A listener's answer as a list: nothing it said counts as no alerts. */\nfunction alertsOf(answer: readonly SiteAlert[] | void): SiteAlert[] {\n return Array.isArray(answer) ? [...answer] : []\n}\n\n/**\n * Subscribes a plugin to every host event. Registering again under the same\n * plugin id replaces the earlier listener in place, so a register function\n * the platform calls twice — boot, then the API surface — leaves one.\n */\nexport function registerHostEventListener(\n pluginId: string,\n listener: HostEventListener,\n): void {\n const id = String(pluginId ?? '').trim()\n if (!id) {\n throw new Error('a host event listener needs the id of the plugin it belongs to')\n }\n const at = registrations.findIndex((entry) => entry.pluginId === id)\n if (at >= 0) registrations[at] = { pluginId: id, listener }\n else registrations.push({ pluginId: id, listener })\n}\n\n/** The plugins listening, in registration order, for diagnostics. */\nexport function listHostEventListeners(): string[] {\n return registrations.map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every listener. */\nexport function resetHostEventListenersForTests(): void {\n registrations.length = 0\n warnedEmpty = false\n}\n\n/**\n * Hands one event to every listener, concurrently, and gathers the alerts\n * they produced. Never rejects.\n */\nexport async function runHostEventListeners(\n hostId: string,\n event: string,\n payload: HostEventPayload = {},\n context: HostEventContext = {},\n): Promise<SiteAlert[]> {\n if (!registrations.length) {\n /*\n * Said once per process rather than per event. No listener means the\n * plugins' server declarations never ran here — every automation on\n * every site served by this process is silent, and a line in the log is\n * the only place that is visible.\n */\n if (!warnedEmpty) {\n warnedEmpty = true\n console.warn(\n `[host-events] nothing is listening; \"${event}\" on ${hostId} ran no ` +\n 'automation. Listeners register from the plugins’ server ' +\n 'declarations at boot.',\n )\n }\n return []\n }\n const settled = await Promise.all(\n registrations.map(async ({ pluginId, listener }) => {\n try {\n return alertsOf(await listener.onEvent(hostId, event, payload, context))\n } catch (error) {\n console.error(`[host-events] ${pluginId} failed on ${event}`, hostId, error)\n return []\n }\n }),\n )\n return settled.flat()\n}\n\n/**\n * Hands one page-dispatched interaction to the listeners that dispatch, and\n * gathers the alerts it produced. Never rejects.\n */\nexport async function dispatchHostAutomation(\n hostId: string,\n interactionId: string,\n event: string,\n payload: HostEventPayload = {},\n context: HostEventContext = {},\n): Promise<SiteAlert[]> {\n const alerts: SiteAlert[] = []\n for (const { pluginId, listener } of registrations) {\n if (!listener.onDispatch) continue\n try {\n alerts.push(\n ...alertsOf(\n await listener.onDispatch(hostId, interactionId, event, payload, context),\n ),\n )\n } catch (error) {\n console.error(\n `[host-events] ${pluginId} failed to dispatch ${interactionId}`,\n hostId,\n error,\n )\n }\n }\n return alerts\n}\n"],"names":["registrations","warnedEmpty","alertsOf","answer","Array","isArray","registerHostEventListener","pluginId","listener","id","String","trim","Error","at","findIndex","entry","push","listHostEventListeners","map","resetHostEventListenersForTests","length","runHostEventListeners","hostId","event","payload","context","console","warn","settled","Promise","all","onEvent","error","flat","dispatchHostAutomation","interactionId","alerts","onDispatch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAgGD,MAAMA,gBAAgC,EAAE;AAExC,sEAAsE,GACtE,IAAIC,cAAc;AAElB,wEAAwE,GACxE,SAASC,SAASC,MAAmC;IACnD,OAAOC,MAAMC,OAAO,CAACF,UAAU;WAAIA;KAAO,GAAG,EAAE;AACjD;AAEA;;;;CAIC,GACD,OAAO,SAASG,0BACdC,QAAgB,EAChBC,QAA2B;IAE3B,MAAMC,KAAKC,OAAOH,mBAAAA,WAAY,IAAII,IAAI;IACtC,IAAI,CAACF,IAAI;QACP,MAAM,IAAIG,MAAM;IAClB;IACA,MAAMC,KAAKb,cAAcc,SAAS,CAAC,CAACC,QAAUA,MAAMR,QAAQ,KAAKE;IACjE,IAAII,MAAM,GAAGb,aAAa,CAACa,GAAG,GAAG;QAAEN,UAAUE;QAAID;IAAS;SACrDR,cAAcgB,IAAI,CAAC;QAAET,UAAUE;QAAID;IAAS;AACnD;AAEA,mEAAmE,GACnE,OAAO,SAASS;IACd,OAAOjB,cAAckB,GAAG,CAAC,CAACH,QAAUA,MAAMR,QAAQ;AACpD;AAEA,sCAAsC,GACtC,OAAO,SAASY;IACdnB,cAAcoB,MAAM,GAAG;IACvBnB,cAAc;AAChB;AAEA;;;CAGC,GACD,OAAO,eAAeoB,sBACpBC,MAAc,EACdC,KAAa,EACbC,UAA4B,CAAC,CAAC,EAC9BC,UAA4B,CAAC,CAAC;IAE9B,IAAI,CAACzB,cAAcoB,MAAM,EAAE;QACzB;;;;;KAKC,GACD,IAAI,CAACnB,aAAa;YAChBA,cAAc;YACdyB,QAAQC,IAAI,CACV,CAAC,qCAAqC,EAAEJ,MAAM,KAAK,EAAED,OAAO,QAAQ,CAAC,GACnE,6DACA;QAEN;QACA,OAAO,EAAE;IACX;IACA,MAAMM,UAAU,MAAMC,QAAQC,GAAG,CAC/B9B,cAAckB,GAAG,CAAC,OAAO,EAAEX,QAAQ,EAAEC,QAAQ,EAAE;QAC7C,IAAI;YACF,OAAON,SAAS,MAAMM,SAASuB,OAAO,CAACT,QAAQC,OAAOC,SAASC;QACjE,EAAE,OAAOO,OAAO;YACdN,QAAQM,KAAK,CAAC,CAAC,cAAc,EAAEzB,SAAS,WAAW,EAAEgB,OAAO,EAAED,QAAQU;YACtE,OAAO,EAAE;QACX;IACF;IAEF,OAAOJ,QAAQK,IAAI;AACrB;AAEA;;;CAGC,GACD,OAAO,eAAeC,uBACpBZ,MAAc,EACda,aAAqB,EACrBZ,KAAa,EACbC,UAA4B,CAAC,CAAC,EAC9BC,UAA4B,CAAC,CAAC;IAE9B,MAAMW,SAAsB,EAAE;IAC9B,KAAK,MAAM,EAAE7B,QAAQ,EAAEC,QAAQ,EAAE,IAAIR,cAAe;QAClD,IAAI,CAACQ,SAAS6B,UAAU,EAAE;QAC1B,IAAI;YACFD,OAAOpB,IAAI,IACNd,SACD,MAAMM,SAAS6B,UAAU,CAACf,QAAQa,eAAeZ,OAAOC,SAASC;QAGvE,EAAE,OAAOO,OAAO;YACdN,QAAQM,KAAK,CACX,CAAC,cAAc,EAAEzB,SAAS,oBAAoB,EAAE4B,eAAe,EAC/Db,QACAU;QAEJ;IACF;IACA,OAAOI;AACT"}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Aglyn LLC
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* A lead becomes a contact, and optionally a company and a deal (AGL-2608,
|
|
19
|
-
* AGL-2627).
|
|
20
|
-
*
|
|
21
|
-
* ## Why one function under two doors
|
|
22
|
-
*
|
|
23
|
-
* The console's convert dialog posts to the CRM plugin's `crm/lead-convert`
|
|
24
|
-
* route, and an integration posts to `POST /v1/leads/{id}/convert`. The two
|
|
25
|
-
* doors authenticate differently — an ID token against the site's role, an
|
|
26
|
-
* API key against its scopes — and answer in different envelopes, but what
|
|
27
|
-
* they DO has to be one thing: a conversion that opened a second deal for
|
|
28
|
-
* one person because two callers reached the same lead through two
|
|
29
|
-
* implementations is the bug this file exists to make impossible. The doors
|
|
30
|
-
* own their transport; this owns the writes.
|
|
31
|
-
*
|
|
32
|
-
* It lives here, in the tenancy runtime, rather than beside the plugin
|
|
33
|
-
* route it came from, because the console's REST layer may not import a
|
|
34
|
-
* feature plugin (the app→addons boundary), while both the plugin and the
|
|
35
|
-
* console may import the runtime — which already holds the capture door
|
|
36
|
-
* and the owner assignment this conversion goes through.
|
|
37
|
-
*
|
|
38
|
-
* ## Why a server function and not four client writes
|
|
39
|
-
*
|
|
40
|
-
* The lead is a host document the browser may edit, and a company or a deal
|
|
41
|
-
* is a client-creatable org document. What the browser cannot do is CREATE A
|
|
42
|
-
* CONTACT: the contacts collection is written only by `captureHostContact`
|
|
43
|
-
* (the runtime's wrapper over `upsertHostContact`, so `contactCreated`
|
|
44
|
-
* fires), because that function is the dedupe — one human, one row, found by
|
|
45
|
-
* normalized address across every site in the org — and the audience-band
|
|
46
|
-
* gate. A client that wrote a contact document of its own would mint a
|
|
47
|
-
* second row for a person the org already holds, and would do it past the
|
|
48
|
-
* band.
|
|
49
|
-
*
|
|
50
|
-
* Once the contact has to come from the server, the rest follows it: the
|
|
51
|
-
* conversion is one act with four writes, and a caller that made three of
|
|
52
|
-
* them and lost the connection before the fourth leaves a lead marked
|
|
53
|
-
* qualified with no contact, or a deal pointing at a contact that was never
|
|
54
|
-
* stamped on the lead. Here the order is fixed — contact, then company, then
|
|
55
|
-
* deal, then the lead — and the lead is stamped LAST, so a lead that carries
|
|
56
|
-
* `convertedContactId` names a contact that exists.
|
|
57
|
-
*
|
|
58
|
-
* ## Idempotent on the lead
|
|
59
|
-
*
|
|
60
|
-
* A lead already carrying `convertedContactId` answers with the ids it has
|
|
61
|
-
* and creates nothing more. A double-click on the dialog's button, a retry
|
|
62
|
-
* after a slow response, or a Zapier zap replaying a step must not open a
|
|
63
|
-
* second deal for one person.
|
|
64
|
-
*
|
|
65
|
-
* ## The actor
|
|
66
|
-
*
|
|
67
|
-
* A signed-in member converting from the console owns what nobody else
|
|
68
|
-
* claimed: their uid is the owner of last resort, written directly rather
|
|
69
|
-
* than through the deliberate reassignment, because they may be staff
|
|
70
|
-
* converting on a workspace's behalf and a roster check would refuse the one
|
|
71
|
-
* person who is actually here. An API key is not a person and is on no
|
|
72
|
-
* roster, so for an `api` actor that last step is skipped and a contact
|
|
73
|
-
* nobody chose an owner for — and no rule assigned — stays unowned, which
|
|
74
|
-
* the console renders honestly as unassigned. Either way the caller has
|
|
75
|
-
* already been authorized by its door; nothing here asks again.
|
|
76
|
-
*/
|
|
77
|
-
import { type CrmDealStage, type CrmPipeline } from '@aglyn/aglyn/server';
|
|
78
|
-
/** Who is converting, as far as the writes need to know. */
|
|
79
|
-
export interface LeadConvertActor {
|
|
80
|
-
/**
|
|
81
|
-
* What `createdByUid` records on every record the conversion opens, and
|
|
82
|
-
* who the audit line names. A member's uid, or the literal `'api'` for a
|
|
83
|
-
* key — the same attribution the REST creates stamp.
|
|
84
|
-
*/
|
|
85
|
-
uid: string;
|
|
86
|
-
email?: string | null;
|
|
87
|
-
/**
|
|
88
|
-
* `member`: a signed-in person, who becomes the owner when nobody else
|
|
89
|
-
* does. `api`: a key, which cannot own a record — see the header.
|
|
90
|
-
*/
|
|
91
|
-
kind: 'member' | 'api';
|
|
92
|
-
/**
|
|
93
|
-
* For an `api` actor, the key's name — what the audit line attributes the
|
|
94
|
-
* conversion to, since a key has no address to be named by.
|
|
95
|
-
*/
|
|
96
|
-
apiKeyName?: string | null;
|
|
97
|
-
}
|
|
98
|
-
/** What a conversion is asked to do. Validated by the door before it gets here. */
|
|
99
|
-
export interface ConvertHostLeadInput {
|
|
100
|
-
firestore: FirebaseFirestore.Firestore;
|
|
101
|
-
hostId: string;
|
|
102
|
-
orgId: string;
|
|
103
|
-
/** The org document, as the door resolved it. */
|
|
104
|
-
org: Record<string, unknown>;
|
|
105
|
-
/** `hosts/{hostId}/leads/{leadId}` — the document id, which is the person key. */
|
|
106
|
-
leadId: string;
|
|
107
|
-
actor: LeadConvertActor;
|
|
108
|
-
/**
|
|
109
|
-
* Who owns the resulting contact (and deal). Defaults to the lead's own
|
|
110
|
-
* owner; failing that the org's assignment rules and the site's default
|
|
111
|
-
* owner decide (AGL-2618), and failing those a member actor — somebody
|
|
112
|
-
* converted this person, and a record with no owner is one nobody follows
|
|
113
|
-
* up.
|
|
114
|
-
*/
|
|
115
|
-
ownerUid?: string;
|
|
116
|
-
/** Link an existing `orgs/{orgId}/companies/{companyId}`. */
|
|
117
|
-
companyId?: string;
|
|
118
|
-
/** Or create one. Ignored when `companyId` is given. Already normalized. */
|
|
119
|
-
createCompany?: {
|
|
120
|
-
name: string;
|
|
121
|
-
domain: string | null;
|
|
122
|
-
} | null;
|
|
123
|
-
/** Open a deal in the org's default pipeline. Already normalized. */
|
|
124
|
-
deal?: {
|
|
125
|
-
title: string;
|
|
126
|
-
amountCents: number | null;
|
|
127
|
-
/** Lowercase ISO 4217. */
|
|
128
|
-
currency: string;
|
|
129
|
-
/** A stage of the default pipeline; its first open stage when absent. */
|
|
130
|
-
stageId?: string;
|
|
131
|
-
} | null;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Why a conversion did not happen. Each is a state of the data, not a fault
|
|
135
|
-
* of the request — the door has already refused a malformed body — and each
|
|
136
|
-
* leaves the lead unconverted so a retry after the remedy finds it where it
|
|
137
|
-
* was.
|
|
138
|
-
*/
|
|
139
|
-
export type ConvertHostLeadRefusal =
|
|
140
|
-
/** No document at `hosts/{hostId}/leads/{leadId}`. */
|
|
141
|
-
'unknown-lead'
|
|
142
|
-
/** The lead's address cannot become a contact. */
|
|
143
|
-
| 'no-email'
|
|
144
|
-
/**
|
|
145
|
-
* The capture door produced no contact — it swallows its own failures and
|
|
146
|
-
* drops a creation past a Free org's audience band. Nothing was written.
|
|
147
|
-
*/
|
|
148
|
-
| 'contact-not-created'
|
|
149
|
-
/**
|
|
150
|
-
* The site holds an erasure for the lead's address (AGL-2623): the person
|
|
151
|
-
* asked to be forgotten, and the capture door refuses to rebuild a record
|
|
152
|
-
* for them — by a conversion as by a form. Nothing was written, and the
|
|
153
|
-
* lead stays as it was; the erasure removes it when it runs.
|
|
154
|
-
*/
|
|
155
|
-
| 'erased'
|
|
156
|
-
/** The CRM records band is full (AGL-2611); the contact stands, nothing more was opened. */
|
|
157
|
-
| 'band-full'
|
|
158
|
-
/** `companyId` names no company in this organization. The contact stands. */
|
|
159
|
-
| 'unknown-company'
|
|
160
|
-
/** The default pipeline has no stages to open a deal in. The contact stands. */
|
|
161
|
-
| 'no-stages';
|
|
162
|
-
export type ConvertHostLeadResult = {
|
|
163
|
-
ok: true;
|
|
164
|
-
contactId: string;
|
|
165
|
-
companyId?: string;
|
|
166
|
-
dealId?: string;
|
|
167
|
-
/** The lead was converted before this call; nothing was created now. */
|
|
168
|
-
alreadyConverted: boolean;
|
|
169
|
-
} | {
|
|
170
|
-
ok: false;
|
|
171
|
-
reason: ConvertHostLeadRefusal;
|
|
172
|
-
};
|
|
173
|
-
/**
|
|
174
|
-
* The stage a new deal opens in.
|
|
175
|
-
*
|
|
176
|
-
* The caller's choice when the pipeline has it, otherwise the first OPEN
|
|
177
|
-
* stage by order — a deal created by converting a lead is by definition not
|
|
178
|
-
* yet won or lost, so defaulting to a closed stage would record an outcome
|
|
179
|
-
* nobody reached. A pipeline with no open stage at all falls back to its
|
|
180
|
-
* first stage rather than refusing: the merchant edited their pipeline into
|
|
181
|
-
* that shape, and a conversion that failed because of it would read as a bug
|
|
182
|
-
* in the lead.
|
|
183
|
-
*/
|
|
184
|
-
export declare function stageForNewDeal(pipeline: Pick<CrmPipeline, 'stages'>, requestedStageId: string | undefined): CrmDealStage | null;
|
|
185
|
-
/**
|
|
186
|
-
* Convert one lead. Throws only on infrastructure failure; every refusal
|
|
187
|
-
* the data can produce is a `ConvertHostLeadResult`.
|
|
188
|
-
*/
|
|
189
|
-
export declare function convertHostLead(input: ConvertHostLeadInput): Promise<ConvertHostLeadResult>;
|