@aglyn/aglyn 1.0.0-beta.152 → 1.0.0-beta.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +10 -10
  2. package/src/lib/app-utils/crm-csv.d.ts +10 -3
  3. package/src/lib/app-utils/crm-csv.js +37 -2
  4. package/src/lib/app-utils/crm-csv.js.map +1 -1
  5. package/src/lib/app-utils/crm-email-templates.js +15 -0
  6. package/src/lib/app-utils/crm-email-templates.js.map +1 -1
  7. package/src/lib/app-utils/crm.d.ts +69 -2
  8. package/src/lib/app-utils/crm.js +126 -1
  9. package/src/lib/app-utils/crm.js.map +1 -1
  10. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  11. package/src/lib/app-utils/docs-help.generated.js +2 -0
  12. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  13. package/src/lib/app-utils/docs-index.generated.js +25 -11
  14. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  15. package/src/lib/app-utils/email-starter-policy.js +5 -3
  16. package/src/lib/app-utils/email-starter-policy.js.map +1 -1
  17. package/src/lib/app-utils/plan-entitlements.d.ts +34 -0
  18. package/src/lib/app-utils/plan-entitlements.js +40 -0
  19. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  20. package/src/lib/app-utils/server.d.ts +0 -1
  21. package/src/lib/app-utils/server.js +0 -1
  22. package/src/lib/app-utils/server.js.map +1 -1
  23. package/src/lib/plugin-manager/first-party-plugins.generated.js +1 -0
  24. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  25. package/src/lib/plugin-manager/plugin-artifact-inventory.d.ts +145 -0
  26. package/src/lib/plugin-manager/plugin-artifact-inventory.js +126 -0
  27. package/src/lib/plugin-manager/plugin-artifact-inventory.js.map +1 -0
  28. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +32 -1
  29. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  30. package/src/lib/plugin-manager/plugin-lead-conversion.d.ts +67 -0
  31. package/src/lib/plugin-manager/plugin-lead-conversion.js +93 -0
  32. package/src/lib/plugin-manager/plugin-lead-conversion.js.map +1 -0
  33. package/src/lib/plugin-manager/plugin-org-capacity.d.ts +12 -0
  34. package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -1
  35. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +8 -1
  36. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  37. package/src/lib/app-utils/marketplace-verification.d.ts +0 -95
  38. package/src/lib/app-utils/marketplace-verification.js +0 -81
  39. package/src/lib/app-utils/marketplace-verification.js.map +0 -1
  40. package/src/lib/app-utils/publisher-attestation.d.ts +0 -99
  41. package/src/lib/app-utils/publisher-attestation.js +0 -158
  42. package/src/lib/app-utils/publisher-attestation.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-contact-capture.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A person met on a site, handed to whichever plugin keeps people (AGL-3124).\n *\n * Four silos capture the same thing and none of them is the record system: a\n * form submission, a site member signing up, an order, a booking. Each holds\n * an address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each one imports the writer, which\n * makes the highest-fan-in edge in the repo — thirty files in one plugin alone\n * — and makes the record system's plugin something a storefront cannot take a\n * payment without.\n *\n * So the silo stops importing and starts DECLARING: it registers the capture\n * door it owns, and hands a capture to this seam. The plugin that keeps people\n * registers the writer and does every part of the work that is a record\n * system's to do:\n *\n * - normalizing and keying the address, and deciding whether this is a new\n * person or a visit from one the workspace already holds;\n * - the audience band, the erasure rows, and every other refusal — answered,\n * never thrown, so a capture that is refused never costs the silo the order\n * or the submission it was recording;\n * - what a stage, an owner, a company and a tag mean, and what the arrival\n * of a new person sets off.\n *\n * The silo decides none of that. It reports what it saw.\n *\n * ## Why the profile is opaque\n *\n * `profile` is the owner's own field names with scalar values, not a core\n * type. A person's record shape belongs to the plugin that models it, and a\n * typed core shape would be that model spelled in the platform again — which\n * is the defect this seam exists to retire, not a smaller version of it. A key\n * the owner does not document is the owner's to ignore.\n *\n * `source` is the silo's own word for its door, and it is declared rather than\n * enumerated here for the same reason: core listing `form`, `member`, `order`\n * and `booking` would be core holding the plugin set again. A silo registers\n * its source with {@link registerPluginContactSource}, which is what lets a\n * timeline label an entry and a link point back at the thing that captured the\n * person, without the platform knowing what any of them are.\n *\n * ## One writer; many silos\n *\n * A workspace keeps one set of people, so the writer is a slot: a second\n * plugin's writer is refused naming both, and the incumbent keeps serving. A\n * SOURCE is per silo and there are many, but one source word has one owner —\n * two plugins claiming `order` would make the label and the link ambiguous at\n * the point they are read.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-record-timeline`, the registry authenticates nobody. A silo has\n * already decided, in its own terms and before it asks, that the capture is\n * the workspace's to record — that the submission was accepted, that the order\n * was paid.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** The person, as the capturing silo saw them. */\nexport interface PluginContactIdentity {\n /**\n * The address they gave. Raw: the owner normalizes and keys on it, and a\n * silo that normalized first would be keeping a second copy of that rule.\n */\n email: unknown\n name?: string | null\n}\n\n/** What happened, in the capturing silo's own words. */\nexport interface PluginContactInteraction {\n /** The silo's declared source word — see {@link registerPluginContactSource}. */\n source: string\n /** When it happened, epoch ms; the owner uses now when absent. */\n atMs?: number\n /**\n * The document it happened on, in the silo's terms\n * (`formSubmissions/{id}`, `orders/{id}`), so a timeline entry can point\n * back at it. Absent where the door leaves nothing to open.\n */\n refId?: string | null\n /** One line a person reads on the timeline. */\n summary?: string | null\n}\n\nexport interface PluginContactCaptureRequest {\n orgId: string\n /** The site the person was met on: the brand whose form or checkout it was. */\n hostId: string\n identity: PluginContactIdentity\n interaction: PluginContactInteraction\n /**\n * An explicit marketing opt-in the capture surface carried, recorded against\n * `hostId`. Absent is not consent, and filing a capture under a campaign is\n * not consent either.\n */\n marketingConsent?: boolean\n /** Tags the silo puts on this capture; the owner adds, never replaces. */\n tags?: readonly string[]\n /** The campaigns the capture SURFACE is filed under — the merchant's own act. */\n campaignIds?: readonly string[]\n /** Money the capture was worth, in cents, where the silo took some. */\n purchaseCents?: number\n /** The currency {@link purchaseCents} is in, lowercase, when the silo knows. */\n purchaseCurrency?: string\n /**\n * The earliest stage this capture describes, in the OWNER's words. A floor\n * and not a value: the owner fills an empty stage and advances an earlier\n * one, and never moves anybody back, so a repeat customer filling in a\n * contact form is still a customer.\n */\n lifecycleFloor?: string\n /**\n * Profile fields the silo knows, keyed by the owner's own field names.\n * Only the keys given are written, so a silo that knows the phone number\n * does not blank a title somebody typed.\n */\n profile?: Readonly<Record<string, unknown>>\n /**\n * Facts about THIS CAPTURE that the owner models and the platform does not\n * (AGL-3080) — which form was filled in, which page it was on, where the\n * visitor arrived from.\n *\n * Opaque for the reason {@link profile} is: every one of these is a concept\n * belonging to the plugin that keeps people or to the silo that saw it, and\n * a typed core field for each would be those models spelled out in the\n * platform again. `profile` describes the PERSON and outlives the visit;\n * this describes the visit and does not.\n *\n * A key the owner does not recognize is the owner's to ignore, and a silo\n * that sends nothing here loses nothing: every field is an enrichment of a\n * capture that is already complete without it.\n */\n detail?: Readonly<Record<string, unknown>>\n}\n\n/**\n * The owner's answer. `created: false` is a visit by somebody the workspace\n * already held, which is an interaction and not a new person.\n *\n * A refusal is RETURNED. Every one of these silos has already done the thing\n * it was recording, so a throw here would cost an accepted submission or a\n * paid order for a record it could not keep.\n */\nexport type PluginContactCaptured =\n | { ok: true; contactId: string; created: boolean }\n | {\n ok: false\n /**\n * `invalid-email`: nothing usable to key a person on. `band`: the\n * workspace is at the audience it may hold. `erased`: the workspace\n * erased this person and a capture must not quietly rebuild them.\n * `error`: the owner failed, and said so rather than silently.\n */\n reason: 'invalid-email' | 'band' | 'erased' | 'error'\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\nexport interface PluginContactCaptureWriter {\n /** Records one capture; a refusal is returned, never thrown. */\n capture(\n request: PluginContactCaptureRequest,\n ): Promise<PluginContactCaptured>\n}\n\n/** A capture door a silo owns. */\nexport interface PluginContactSourceDeclaration {\n /** The silo's word for the door, as its captures carry it. */\n source: string\n /** What a timeline entry from this door says: `Form submission`. */\n label: string\n /**\n * What a link from that entry says when there is something to open —\n * `Open submission`. Absent where the door leaves nothing to open, which\n * is the honest answer for an import or a by-hand add.\n */\n openLabel?: string\n /**\n * The record kind the silo's own document is, for\n * `plugin-record-routes` to address it with. Absent where the door has no\n * record of its own to open.\n */\n recordKind?: string\n}\n\n/** A source declaration with the silo that made it. */\nexport type ResolvedPluginContactSource = PluginContactSourceDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_CONTACT_CAPTURE =\n definePluginServiceContract<PluginContactCaptureWriter>(\n 'core.contact-capture',\n { multiple: false },\n )\n\nexport const PLUGIN_CONTACT_SOURCES =\n definePluginServiceContract<PluginContactSourceDeclaration>(\n 'core.contact-capture-sources',\n { multiple: true },\n )\n\n/**\n * Registers the plugin that keeps people. The owner is the loader's marker\n * when a register fn is running, else `options.pluginId`; with neither the\n * registration throws, and a second plugin's writer is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginContactCaptureWriter(\n writer: PluginContactCaptureWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CONTACT_CAPTURE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Declares a capture door. One source word has one owner. */\nexport function registerPluginContactSource(\n declaration: PluginContactSourceDeclaration,\n options?: { pluginId?: string },\n): void {\n const key = declaration.source?.trim() ?? ''\n if (!key) throw new Error('a contact capture source needs a source word')\n if (!declaration.label?.trim()) {\n throw new Error(`contact capture source \"${key}\" needs a label`)\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `contact capture source \"${key}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(\n PLUGIN_CONTACT_SOURCES,\n { ...declaration, source: key },\n {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n },\n )\n}\n\nexport interface ResolvedPluginContactCaptureWriter {\n /** The plugin that keeps people. */\n pluginId: string\n writer: PluginContactCaptureWriter\n}\n\n/** The writer with its owner, or `null` when no plugin keeps people here. */\nexport function pluginContactCaptureWriter(): ResolvedPluginContactCaptureWriter | null {\n const entry = resolvePluginServices(PLUGIN_CONTACT_CAPTURE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n\n/**\n * Hands one capture to the plugin that keeps people, or answers `null` when\n * no plugin does.\n *\n * `null` and a refusal are different answers and a silo treats them\n * differently: `null` is a workspace with no record system, where there is\n * nothing to record and nothing has gone wrong; a refusal is a record system\n * that considered this person and declined.\n */\nexport async function capturePluginContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured | null> {\n const found = pluginContactCaptureWriter()\n if (!found) return null\n return found.writer.capture(request)\n}\n\n/** Every declared capture door, with its silo, in registration order. */\nexport function listPluginContactSources(): ResolvedPluginContactSource[] {\n return resolvePluginServices(PLUGIN_CONTACT_SOURCES).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n\n/** One declared capture door by its source word, or `null`. */\nexport function pluginContactSource(\n source: string,\n): ResolvedPluginContactSource | null {\n const key = source.trim()\n const entry = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_CONTACT_CAPTURE","multiple","PLUGIN_CONTACT_SOURCES","registerPluginContactCaptureWriter","writer","options","pluginId","registerPluginContactSource","declaration","key","source","trim","Error","label","incumbent","find","entry","pluginContactCaptureWriter","impl","capturePluginContact","request","found","capture","listPluginContactSources","map","pluginContactSource","one"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAkM1B,OAAO,MAAMC,yBACXH,4BACE,wBACA;IAAEI,UAAU;AAAM,GACnB;AAEH,OAAO,MAAMC,yBACXL,4BACE,gCACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;;CAKC,GACD,OAAO,SAASE,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BP,sBAAsBE,wBAAwBI,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,4DAA4D,GAC5D,OAAO,SAASC,4BACdC,WAA2C,EAC3CH,OAA+B;cAObT,OAAAA;QALNY,qBAEPA;IAFL,MAAMC,eAAMD,sBAAAA,YAAYE,MAAM,qBAAlBF,oBAAoBG,IAAI,qBAAM;IAC1C,IAAI,CAACF,KAAK,MAAM,IAAIG,MAAM;IAC1B,IAAI,GAACJ,qBAAAA,YAAYK,KAAK,qBAAjBL,mBAAmBG,IAAI,KAAI;QAC9B,MAAM,IAAIC,MAAM,CAAC,wBAAwB,EAAEH,IAAI,eAAe,CAAC;IACjE;IACA,MAAMH,WAAW,EAACV,SAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASC,QAAQ,YAA7CV,QAAiD,IAAIe,IAAI;IAC3E,MAAMG,YAAYf,sBAAsBG,wBAAwBa,IAAI,CAClE,CAACC,QAAUA,MAAMP,GAAG,KAAKA;IAE3B,IAAIK,aAAaR,YAAYQ,UAAUR,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAIM,MACR,CAAC,wBAAwB,EAAEH,IAAI,yBAAyB,CAAC,GACvD,CAAC,CAAC,EAAEK,UAAUR,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAEtD;IACAR,sBACEI,wBACA,aAAKM;QAAaE,QAAQD;QAC1B,aACMJ,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;QAC1DG;;AAGN;AAQA,2EAA2E,GAC3E,OAAO,SAASQ;IACd,MAAMD,QAAQjB,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOgB,QAAQ;QAAEV,UAAUU,MAAMV,QAAQ;QAAEF,QAAQY,MAAME,IAAI;IAAC,IAAI;AACpE;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,qBACpBC,OAAoC;IAEpC,MAAMC,QAAQJ;IACd,IAAI,CAACI,OAAO,OAAO;IACnB,OAAOA,MAAMjB,MAAM,CAACkB,OAAO,CAACF;AAC9B;AAEA,uEAAuE,GACvE,OAAO,SAASG;IACd,OAAOxB,sBAAsBG,wBAAwBsB,GAAG,CAAC,CAACR,QAAW,aAChEA,MAAME,IAAI;YACbZ,UAAUU,MAAMV,QAAQ;;AAE5B;AAEA,6DAA6D,GAC7D,OAAO,SAASmB,oBACdf,MAAc;IAEd,MAAMD,MAAMC,OAAOC,IAAI;IACvB,MAAMK,QAAQjB,sBAAsBG,wBAAwBa,IAAI,CAC9D,CAACW,MAAQA,IAAIjB,GAAG,KAAKA;IAEvB,OAAOO,QAAQ,aAAKA,MAAME,IAAI;QAAEZ,UAAUU,MAAMV,QAAQ;SAAK;AAC/D"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-contact-capture.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A person met on a site, handed to whichever plugin keeps people (AGL-3124).\n *\n * Four silos capture the same thing and none of them is the record system: a\n * form submission, a site member signing up, an order, a booking. Each holds\n * an address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each one imports the writer, which\n * makes the highest-fan-in edge in the repo — thirty files in one plugin alone\n * — and makes the record system's plugin something a storefront cannot take a\n * payment without.\n *\n * So the silo stops importing and starts DECLARING: it registers the capture\n * door it owns, and hands a capture to this seam. The plugin that keeps people\n * registers the writer and does every part of the work that is a record\n * system's to do:\n *\n * - normalizing and keying the address, and deciding whether this is a new\n * person or a visit from one the workspace already holds;\n * - the audience band, the erasure rows, and every other refusal — answered,\n * never thrown, so a capture that is refused never costs the silo the order\n * or the submission it was recording;\n * - what a stage, an owner, a company and a tag mean, and what the arrival\n * of a new person sets off.\n *\n * The silo decides none of that. It reports what it saw.\n *\n * ## Why the profile is opaque\n *\n * `profile` is the owner's own field names with scalar values, not a core\n * type. A person's record shape belongs to the plugin that models it, and a\n * typed core shape would be that model spelled in the platform again — which\n * is the defect this seam exists to retire, not a smaller version of it. A key\n * the owner does not document is the owner's to ignore.\n *\n * `source` is the silo's own word for its door, and it is declared rather than\n * enumerated here for the same reason: core listing `form`, `member`, `order`\n * and `booking` would be core holding the plugin set again. A silo registers\n * its source with {@link registerPluginContactSource}, which is what lets a\n * timeline label an entry and a link point back at the thing that captured the\n * person, without the platform knowing what any of them are.\n *\n * ## One writer; many silos\n *\n * A workspace keeps one set of people, so the writer is a slot: a second\n * plugin's writer is refused naming both, and the incumbent keeps serving. A\n * SOURCE is per silo and there are many, but one source word has one owner —\n * two plugins claiming `order` would make the label and the link ambiguous at\n * the point they are read.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-record-timeline`, the registry authenticates nobody. A silo has\n * already decided, in its own terms and before it asks, that the capture is\n * the workspace's to record — that the submission was accepted, that the order\n * was paid.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** The person, as the capturing silo saw them. */\nexport interface PluginContactIdentity {\n /**\n * The address they gave. Raw: the owner normalizes and keys on it, and a\n * silo that normalized first would be keeping a second copy of that rule.\n */\n email: unknown\n name?: string | null\n}\n\n/** What happened, in the capturing silo's own words. */\nexport interface PluginContactInteraction {\n /** The silo's declared source word — see {@link registerPluginContactSource}. */\n source: string\n /** When it happened, epoch ms; the owner uses now when absent. */\n atMs?: number\n /**\n * The document it happened on, in the silo's terms\n * (`formSubmissions/{id}`, `orders/{id}`), so a timeline entry can point\n * back at it. Absent where the door leaves nothing to open.\n */\n refId?: string | null\n /** One line a person reads on the timeline. */\n summary?: string | null\n}\n\nexport interface PluginContactCaptureRequest {\n orgId: string\n /** The site the person was met on: the brand whose form or checkout it was. */\n hostId: string\n identity: PluginContactIdentity\n interaction: PluginContactInteraction\n /**\n * An explicit marketing opt-in the capture surface carried, recorded against\n * `hostId`. Absent is not consent, and filing a capture under a campaign is\n * not consent either.\n */\n marketingConsent?: boolean\n /** Tags the silo puts on this capture; the owner adds, never replaces. */\n tags?: readonly string[]\n /** The campaigns the capture SURFACE is filed under — the merchant's own act. */\n campaignIds?: readonly string[]\n /** Money the capture was worth, in cents, where the silo took some. */\n purchaseCents?: number\n /** The currency {@link purchaseCents} is in, lowercase, when the silo knows. */\n purchaseCurrency?: string\n /**\n * The earliest stage this capture describes, in the OWNER's words. A floor\n * and not a value: the owner fills an empty stage and advances an earlier\n * one, and never moves anybody back, so a repeat customer filling in a\n * contact form is still a customer.\n */\n lifecycleFloor?: string\n /**\n * What kind of door this is, which decides WHICH RECORD the person lands\n * on (AGL-3232) — the owner's rule is Salesforce's: one person is one\n * record, a lead until they are qualified and a contact after.\n *\n * - `lead`: a lead surface — a form whose author routes it to leads, a\n * booking request. The person is filed as a LEAD and nothing else,\n * unless the workspace already holds them as a contact, in which case\n * the capture lands on the contact and no lead is filed.\n * - `relationship`: an act that makes the person a known relationship —\n * a member account, a purchase. The person is filed as a CONTACT, and\n * an open lead the site held for them is stamped converted onto it.\n * - `touch`, the default: everything else — a form without lead routing,\n * a newsletter opt-in. The capture lands on the open lead when the site\n * holds one for the address, and on the contact otherwise.\n */\n surface?: 'lead' | 'relationship' | 'touch'\n /**\n * Profile fields the silo knows, keyed by the owner's own field names.\n * Only the keys given are written, so a silo that knows the phone number\n * does not blank a title somebody typed.\n */\n profile?: Readonly<Record<string, unknown>>\n /**\n * Facts about THIS CAPTURE that the owner models and the platform does not\n * (AGL-3080) — which form was filled in, which page it was on, where the\n * visitor arrived from.\n *\n * Opaque for the reason {@link profile} is: every one of these is a concept\n * belonging to the plugin that keeps people or to the silo that saw it, and\n * a typed core field for each would be those models spelled out in the\n * platform again. `profile` describes the PERSON and outlives the visit;\n * this describes the visit and does not.\n *\n * A key the owner does not recognize is the owner's to ignore, and a silo\n * that sends nothing here loses nothing: every field is an enrichment of a\n * capture that is already complete without it.\n */\n detail?: Readonly<Record<string, unknown>>\n}\n\n/**\n * The owner's answer. `created: false` is a visit by somebody the workspace\n * already held, which is an interaction and not a new person.\n *\n * A refusal is RETURNED. Every one of these silos has already done the thing\n * it was recording, so a throw here would cost an accepted submission or a\n * paid order for a record it could not keep.\n */\nexport type PluginContactCaptured =\n /** The capture landed on a contact — the record every capture used to make. */\n | { ok: true; record: 'contact'; contactId: string; created: boolean }\n /**\n * The capture landed on a LEAD (AGL-3232): a lead surface met somebody the\n * workspace does not hold as a contact, or a touch reached an open lead.\n * `leadId` is the person key the lead is filed under on `hostId`.\n */\n | { ok: true; record: 'lead'; leadId: string; created: boolean }\n | {\n ok: false\n /**\n * `invalid-email`: nothing usable to key a person on. `band`: the\n * workspace is at the audience it may hold. `erased`: the workspace\n * erased this person and a capture must not quietly rebuild them.\n * `error`: the owner failed, and said so rather than silently.\n */\n reason: 'invalid-email' | 'band' | 'erased' | 'error'\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\nexport interface PluginContactCaptureWriter {\n /** Records one capture; a refusal is returned, never thrown. */\n capture(\n request: PluginContactCaptureRequest,\n ): Promise<PluginContactCaptured>\n}\n\n/** A capture door a silo owns. */\nexport interface PluginContactSourceDeclaration {\n /** The silo's word for the door, as its captures carry it. */\n source: string\n /** What a timeline entry from this door says: `Form submission`. */\n label: string\n /**\n * What a link from that entry says when there is something to open —\n * `Open submission`. Absent where the door leaves nothing to open, which\n * is the honest answer for an import or a by-hand add.\n */\n openLabel?: string\n /**\n * The record kind the silo's own document is, for\n * `plugin-record-routes` to address it with. Absent where the door has no\n * record of its own to open.\n */\n recordKind?: string\n}\n\n/** A source declaration with the silo that made it. */\nexport type ResolvedPluginContactSource = PluginContactSourceDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_CONTACT_CAPTURE =\n definePluginServiceContract<PluginContactCaptureWriter>(\n 'core.contact-capture',\n { multiple: false },\n )\n\nexport const PLUGIN_CONTACT_SOURCES =\n definePluginServiceContract<PluginContactSourceDeclaration>(\n 'core.contact-capture-sources',\n { multiple: true },\n )\n\n/**\n * Registers the plugin that keeps people. The owner is the loader's marker\n * when a register fn is running, else `options.pluginId`; with neither the\n * registration throws, and a second plugin's writer is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginContactCaptureWriter(\n writer: PluginContactCaptureWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CONTACT_CAPTURE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Declares a capture door. One source word has one owner. */\nexport function registerPluginContactSource(\n declaration: PluginContactSourceDeclaration,\n options?: { pluginId?: string },\n): void {\n const key = declaration.source?.trim() ?? ''\n if (!key) throw new Error('a contact capture source needs a source word')\n if (!declaration.label?.trim()) {\n throw new Error(`contact capture source \"${key}\" needs a label`)\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `contact capture source \"${key}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(\n PLUGIN_CONTACT_SOURCES,\n { ...declaration, source: key },\n {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n },\n )\n}\n\nexport interface ResolvedPluginContactCaptureWriter {\n /** The plugin that keeps people. */\n pluginId: string\n writer: PluginContactCaptureWriter\n}\n\n/** The writer with its owner, or `null` when no plugin keeps people here. */\nexport function pluginContactCaptureWriter(): ResolvedPluginContactCaptureWriter | null {\n const entry = resolvePluginServices(PLUGIN_CONTACT_CAPTURE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n\n/**\n * Hands one capture to the plugin that keeps people, or answers `null` when\n * no plugin does.\n *\n * `null` and a refusal are different answers and a silo treats them\n * differently: `null` is a workspace with no record system, where there is\n * nothing to record and nothing has gone wrong; a refusal is a record system\n * that considered this person and declined.\n */\nexport async function capturePluginContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured | null> {\n const found = pluginContactCaptureWriter()\n if (!found) return null\n return found.writer.capture(request)\n}\n\n/** Every declared capture door, with its silo, in registration order. */\nexport function listPluginContactSources(): ResolvedPluginContactSource[] {\n return resolvePluginServices(PLUGIN_CONTACT_SOURCES).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n\n/** One declared capture door by its source word, or `null`. */\nexport function pluginContactSource(\n source: string,\n): ResolvedPluginContactSource | null {\n const key = source.trim()\n const entry = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_CONTACT_CAPTURE","multiple","PLUGIN_CONTACT_SOURCES","registerPluginContactCaptureWriter","writer","options","pluginId","registerPluginContactSource","declaration","key","source","trim","Error","label","incumbent","find","entry","pluginContactCaptureWriter","impl","capturePluginContact","request","found","capture","listPluginContactSources","map","pluginContactSource","one"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA0N1B,OAAO,MAAMC,yBACXH,4BACE,wBACA;IAAEI,UAAU;AAAM,GACnB;AAEH,OAAO,MAAMC,yBACXL,4BACE,gCACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;;CAKC,GACD,OAAO,SAASE,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BP,sBAAsBE,wBAAwBI,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,4DAA4D,GAC5D,OAAO,SAASC,4BACdC,WAA2C,EAC3CH,OAA+B;cAObT,OAAAA;QALNY,qBAEPA;IAFL,MAAMC,eAAMD,sBAAAA,YAAYE,MAAM,qBAAlBF,oBAAoBG,IAAI,qBAAM;IAC1C,IAAI,CAACF,KAAK,MAAM,IAAIG,MAAM;IAC1B,IAAI,GAACJ,qBAAAA,YAAYK,KAAK,qBAAjBL,mBAAmBG,IAAI,KAAI;QAC9B,MAAM,IAAIC,MAAM,CAAC,wBAAwB,EAAEH,IAAI,eAAe,CAAC;IACjE;IACA,MAAMH,WAAW,EAACV,SAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASC,QAAQ,YAA7CV,QAAiD,IAAIe,IAAI;IAC3E,MAAMG,YAAYf,sBAAsBG,wBAAwBa,IAAI,CAClE,CAACC,QAAUA,MAAMP,GAAG,KAAKA;IAE3B,IAAIK,aAAaR,YAAYQ,UAAUR,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAIM,MACR,CAAC,wBAAwB,EAAEH,IAAI,yBAAyB,CAAC,GACvD,CAAC,CAAC,EAAEK,UAAUR,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAEtD;IACAR,sBACEI,wBACA,aAAKM;QAAaE,QAAQD;QAC1B,aACMJ,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;QAC1DG;;AAGN;AAQA,2EAA2E,GAC3E,OAAO,SAASQ;IACd,MAAMD,QAAQjB,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOgB,QAAQ;QAAEV,UAAUU,MAAMV,QAAQ;QAAEF,QAAQY,MAAME,IAAI;IAAC,IAAI;AACpE;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,qBACpBC,OAAoC;IAEpC,MAAMC,QAAQJ;IACd,IAAI,CAACI,OAAO,OAAO;IACnB,OAAOA,MAAMjB,MAAM,CAACkB,OAAO,CAACF;AAC9B;AAEA,uEAAuE,GACvE,OAAO,SAASG;IACd,OAAOxB,sBAAsBG,wBAAwBsB,GAAG,CAAC,CAACR,QAAW,aAChEA,MAAME,IAAI;YACbZ,UAAUU,MAAMV,QAAQ;;AAE5B;AAEA,6DAA6D,GAC7D,OAAO,SAASmB,oBACdf,MAAc;IAEd,MAAMD,MAAMC,OAAOC,IAAI;IACvB,MAAMK,QAAQjB,sBAAsBG,wBAAwBa,IAAI,CAC9D,CAACW,MAAQA,IAAIjB,GAAG,KAAKA;IAEvB,OAAOO,QAAQ,aAAKA,MAAME,IAAI;QAAEZ,UAAUU,MAAMV,QAAQ;SAAK;AAC/D"}
@@ -0,0 +1,67 @@
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
+ /** Which door converted the lead. */
18
+ export type PluginLeadConversionBy =
19
+ /** A member, from the console's convert dialog. */
20
+ 'member'
21
+ /** An API key, over `POST /v1/leads/{id}/convert`. */
22
+ | 'api'
23
+ /** The person opened a member account. */
24
+ | 'signup'
25
+ /** The person bought something. */
26
+ | 'purchase'
27
+ /** The one-time migration to the one-record model. */
28
+ | 'backfill';
29
+ /** The lead that converted, and the contact it became. */
30
+ export interface PluginLeadConversionRequest {
31
+ orgId: string;
32
+ /** The site the lead lived under. */
33
+ hostId: string;
34
+ /** `hosts/{hostId}/leads/{leadId}` — the person key. */
35
+ leadId: string;
36
+ /** `orgs/{orgId}/contacts/{contactId}`. */
37
+ contactId: string;
38
+ /** The person's address, normalized. */
39
+ email: string;
40
+ by: PluginLeadConversionBy;
41
+ }
42
+ /**
43
+ * What a listener reports: counts and flags, never content. A `null`
44
+ * field is a figure the listener could not measure, which is not zero.
45
+ */
46
+ export type PluginLeadConversionReport = Readonly<Record<string, number | boolean | null>>;
47
+ export type PluginLeadConversionListener = (request: PluginLeadConversionRequest) => Promise<PluginLeadConversionReport>;
48
+ /**
49
+ * Registers a plugin's conversion listener. Owner = the loader's marker
50
+ * inside a register fn, else `options.pluginId`; a listener with neither
51
+ * throws. One listener per plugin: registering again replaces the plugin's
52
+ * earlier listener in place, so a module evaluated twice does not move
53
+ * records twice.
54
+ */
55
+ export declare function registerPluginLeadConversionListener(listener: PluginLeadConversionListener, options?: {
56
+ pluginId?: string;
57
+ }): void;
58
+ /** The plugins with a listener in place, in registration order. Only for specs and the guards. */
59
+ export declare function listPluginLeadConversionListeners(): readonly string[];
60
+ /** Only for specs: forgets every listener. */
61
+ export declare function resetPluginLeadConversionListenersForTests(): void;
62
+ /**
63
+ * Runs every listener, in registration order, and answers each plugin's
64
+ * report by plugin id: the listener's own report, or `null` for one that
65
+ * threw. Never throws.
66
+ */
67
+ export declare function runPluginLeadConversionListeners(request: PluginLeadConversionRequest): Promise<Record<string, PluginLeadConversionReport | null>>;
@@ -0,0 +1,93 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * A plugin's share of a LEAD CONVERSION (AGL-3233).
19
+ *
20
+ * When a lead becomes a contact — by the convert dialog, over the REST API,
21
+ * or on its own when the person signs up or buys — the record system moves
22
+ * what it filed on the lead onto the contact: the activities, the tasks.
23
+ * A plugin that keeps records ABOUT the lead where those sweeps do not
24
+ * reach — a sequence enrollment naming the lead, say — registers a
25
+ * listener here, and the conversion runs every listener with the lead and
26
+ * the contact it became, so the plugin can re-point its own records.
27
+ *
28
+ * Listeners run AFTER the lead is stamped converted, so a listener that
29
+ * reads the lead finds `convertedContactId` on it, and after the record
30
+ * system's own moves, so a listener that reads the contact's timeline
31
+ * finds the lead's history already there.
32
+ *
33
+ * Listeners run in registration order and are ISOLATED: a throw is logged
34
+ * against its plugin, recorded as `null`, and does not stop the next one or
35
+ * the conversion — which has already happened. The `null` is how the
36
+ * conversion's report says that plugin's records may still name the lead.
37
+ *
38
+ * Registered from a plugin's declarations — `serverDeclarations`, or
39
+ * `consoleServerDeclarations` for a plugin whose server code runs only in
40
+ * the console — so the listener is in place in a process that never loaded
41
+ * the plugin's API surface. The core never imports a plugin; this is how a
42
+ * conversion the tenancy runtime performs reaches a plugin's records.
43
+ */ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
44
+ const registrations = [];
45
+ /**
46
+ * Registers a plugin's conversion listener. Owner = the loader's marker
47
+ * inside a register fn, else `options.pluginId`; a listener with neither
48
+ * throws. One listener per plugin: registering again replaces the plugin's
49
+ * earlier listener in place, so a module evaluated twice does not move
50
+ * records twice.
51
+ */ export function registerPluginLeadConversionListener(listener, options) {
52
+ var _ref, _getRegisteringPluginId;
53
+ const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref : '').trim();
54
+ if (!pluginId) {
55
+ throw new Error('a plugin lead-conversion listener was registered with no owner: pass ' + '{ pluginId } when registering outside a plugin register fn');
56
+ }
57
+ const index = registrations.findIndex((entry)=>entry.pluginId === pluginId);
58
+ if (index >= 0) registrations[index] = {
59
+ pluginId,
60
+ listener
61
+ };
62
+ else registrations.push({
63
+ pluginId,
64
+ listener
65
+ });
66
+ }
67
+ /** The plugins with a listener in place, in registration order. Only for specs and the guards. */ export function listPluginLeadConversionListeners() {
68
+ return registrations.map((entry)=>entry.pluginId);
69
+ }
70
+ /** Only for specs: forgets every listener. */ export function resetPluginLeadConversionListenersForTests() {
71
+ registrations.length = 0;
72
+ }
73
+ /**
74
+ * Runs every listener, in registration order, and answers each plugin's
75
+ * report by plugin id: the listener's own report, or `null` for one that
76
+ * threw. Never throws.
77
+ */ export async function runPluginLeadConversionListeners(request) {
78
+ const reports = {};
79
+ for (const { pluginId, listener } of [
80
+ ...registrations
81
+ ]){
82
+ try {
83
+ reports[pluginId] = await listener(_extends({}, request));
84
+ } catch (error) {
85
+ reports[pluginId] = null;
86
+ // The ids, never the address: this line outlives the person's records.
87
+ console.error(`[plugins] ${pluginId} failed to follow lead ${request.leadId} to contact ${request.contactId} in org ${request.orgId}`, error);
88
+ }
89
+ }
90
+ return reports;
91
+ }
92
+
93
+ //# sourceMappingURL=plugin-lead-conversion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-lead-conversion.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A plugin's share of a LEAD CONVERSION (AGL-3233).\n *\n * When a lead becomes a contact — by the convert dialog, over the REST API,\n * or on its own when the person signs up or buys — the record system moves\n * what it filed on the lead onto the contact: the activities, the tasks.\n * A plugin that keeps records ABOUT the lead where those sweeps do not\n * reach — a sequence enrollment naming the lead, say — registers a\n * listener here, and the conversion runs every listener with the lead and\n * the contact it became, so the plugin can re-point its own records.\n *\n * Listeners run AFTER the lead is stamped converted, so a listener that\n * reads the lead finds `convertedContactId` on it, and after the record\n * system's own moves, so a listener that reads the contact's timeline\n * finds the lead's history already there.\n *\n * Listeners run in registration order and are ISOLATED: a throw is logged\n * against its plugin, recorded as `null`, and does not stop the next one or\n * the conversion — which has already happened. The `null` is how the\n * conversion's report says that plugin's records may still name the lead.\n *\n * Registered from a plugin's declarations — `serverDeclarations`, or\n * `consoleServerDeclarations` for a plugin whose server code runs only in\n * the console — so the listener is in place in a process that never loaded\n * the plugin's API surface. The core never imports a plugin; this is how a\n * conversion the tenancy runtime performs reaches a plugin's records.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** Which door converted the lead. */\nexport type PluginLeadConversionBy =\n /** A member, from the console's convert dialog. */\n | 'member'\n /** An API key, over `POST /v1/leads/{id}/convert`. */\n | 'api'\n /** The person opened a member account. */\n | 'signup'\n /** The person bought something. */\n | 'purchase'\n /** The one-time migration to the one-record model. */\n | 'backfill'\n\n/** The lead that converted, and the contact it became. */\nexport interface PluginLeadConversionRequest {\n orgId: string\n /** The site the lead lived under. */\n hostId: string\n /** `hosts/{hostId}/leads/{leadId}` — the person key. */\n leadId: string\n /** `orgs/{orgId}/contacts/{contactId}`. */\n contactId: string\n /** The person's address, normalized. */\n email: string\n by: PluginLeadConversionBy\n}\n\n/**\n * What a listener reports: counts and flags, never content. A `null`\n * field is a figure the listener could not measure, which is not zero.\n */\nexport type PluginLeadConversionReport = Readonly<Record<string, number | boolean | null>>\n\nexport type PluginLeadConversionListener = (\n request: PluginLeadConversionRequest,\n) => Promise<PluginLeadConversionReport>\n\ninterface Registration {\n pluginId: string\n listener: PluginLeadConversionListener\n}\n\nconst registrations: Registration[] = []\n\n/**\n * Registers a plugin's conversion listener. Owner = the loader's marker\n * inside a register fn, else `options.pluginId`; a listener with neither\n * throws. One listener per plugin: registering again replaces the plugin's\n * earlier listener in place, so a module evaluated twice does not move\n * records twice.\n */\nexport function registerPluginLeadConversionListener(\n listener: PluginLeadConversionListener,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'a plugin lead-conversion listener was registered with no owner: pass ' +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n const index = registrations.findIndex((entry) => entry.pluginId === pluginId)\n if (index >= 0) registrations[index] = { pluginId, listener }\n else registrations.push({ pluginId, listener })\n}\n\n/** The plugins with a listener in place, in registration order. Only for specs and the guards. */\nexport function listPluginLeadConversionListeners(): readonly string[] {\n return registrations.map((entry) => entry.pluginId)\n}\n\n/** Only for specs: forgets every listener. */\nexport function resetPluginLeadConversionListenersForTests(): void {\n registrations.length = 0\n}\n\n/**\n * Runs every listener, in registration order, and answers each plugin's\n * report by plugin id: the listener's own report, or `null` for one that\n * threw. Never throws.\n */\nexport async function runPluginLeadConversionListeners(\n request: PluginLeadConversionRequest,\n): Promise<Record<string, PluginLeadConversionReport | null>> {\n const reports: Record<string, PluginLeadConversionReport | null> = {}\n for (const { pluginId, listener } of [...registrations]) {\n try {\n reports[pluginId] = await listener({ ...request })\n } catch (error) {\n reports[pluginId] = null\n // The ids, never the address: this line outlives the person's records.\n console.error(\n `[plugins] ${pluginId} failed to follow lead ${request.leadId} to contact ${request.contactId} in org ${request.orgId}`,\n error,\n )\n }\n }\n return reports\n}\n"],"names":["getRegisteringPluginId","registrations","registerPluginLeadConversionListener","listener","options","pluginId","trim","Error","index","findIndex","entry","push","listPluginLeadConversionListeners","map","resetPluginLeadConversionListenersForTests","length","runPluginLeadConversionListeners","request","reports","error","console","leadId","contactId","orgId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AA4CxE,MAAMC,gBAAgC,EAAE;AAExC;;;;;;CAMC,GACD,OAAO,SAASC,qCACdC,QAAsC,EACtCC,OAA+B;QAEbJ,MAAAA;IAAlB,MAAMK,WAAW,EAACL,QAAAA,0BAAAA,oCAAAA,0BAA4BI,2BAAAA,QAASC,QAAQ,YAA7CL,OAAiD,IAAIM,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,0EACE;IAEN;IACA,MAAMC,QAAQP,cAAcQ,SAAS,CAAC,CAACC,QAAUA,MAAML,QAAQ,KAAKA;IACpE,IAAIG,SAAS,GAAGP,aAAa,CAACO,MAAM,GAAG;QAAEH;QAAUF;IAAS;SACvDF,cAAcU,IAAI,CAAC;QAAEN;QAAUF;IAAS;AAC/C;AAEA,gGAAgG,GAChG,OAAO,SAASS;IACd,OAAOX,cAAcY,GAAG,CAAC,CAACH,QAAUA,MAAML,QAAQ;AACpD;AAEA,4CAA4C,GAC5C,OAAO,SAASS;IACdb,cAAcc,MAAM,GAAG;AACzB;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iCACpBC,OAAoC;IAEpC,MAAMC,UAA6D,CAAC;IACpE,KAAK,MAAM,EAAEb,QAAQ,EAAEF,QAAQ,EAAE,IAAI;WAAIF;KAAc,CAAE;QACvD,IAAI;YACFiB,OAAO,CAACb,SAAS,GAAG,MAAMF,SAAS,aAAKc;QAC1C,EAAE,OAAOE,OAAO;YACdD,OAAO,CAACb,SAAS,GAAG;YACpB,uEAAuE;YACvEe,QAAQD,KAAK,CACX,CAAC,UAAU,EAAEd,SAAS,uBAAuB,EAAEY,QAAQI,MAAM,CAAC,YAAY,EAAEJ,QAAQK,SAAS,CAAC,QAAQ,EAAEL,QAAQM,KAAK,EAAE,EACvHJ;QAEJ;IACF;IACA,OAAOD;AACT"}
@@ -43,6 +43,18 @@ export interface PluginOrgCapacityDeclaration {
43
43
  * word "includes" makes false while clearing an org a plan change strands.
44
44
  */
45
45
  includedEntitlement: string;
46
+ /**
47
+ * The `PLAN_ENTITLEMENTS` field holding how far BUYING can raise it — the
48
+ * ceiling the two gates above deliberately do not measure against, and the
49
+ * one a console meter has to, because what it draws is the limit a create
50
+ * is refused at: included, plus what the org bought, clamped to this.
51
+ *
52
+ * A FIELD NAME, never a number. Every declaration here names an entitlement
53
+ * and lets core resolve it, so what a plan sells stays one table on core's
54
+ * side of the line — a declaration carrying `10` would be a second copy of
55
+ * a price list, in a plugin, going stale the first time a plan changed.
56
+ */
57
+ purchaseCeilingEntitlement: string;
46
58
  nouns: PluginOrgCapacityNouns;
47
59
  }
48
60
  /** A declaration with the plugin that made it. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-org-capacity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * AN ORG CAPACITY A PLUGIN BACKS (AGL-3080).\n *\n * An org holds a fixed number of some things, its plan includes a number of\n * them, and it can buy more on top. Three of those existed and core named all\n * three: sites, team seats and DATASETS. The first two are the platform's own\n * — every workspace has sites and people, plugin or no plugin. A dataset is\n * the data plugin's, and core knew its collection path, its noun, its plural,\n * the words a refusal says about it and which entitlement field to measure it\n * against.\n *\n * That mattered because the same three names were spelled out in three places\n * with two different vocabularies — `OverLimitKind` calls it `datasets` and\n * `CapacityAddonKind` calls it `datasets` but calls a site `hosts` — and each\n * had its own noun table. A fourth capacity would have meant editing all of\n * them, and a plugin that shipped one could not add it at all.\n *\n * So the plugin declares the capacity, once, and core's two gates read it.\n *\n * ## What core keeps\n *\n * The MONEY. Which add-on kinds exist, what they cost, what a plan includes,\n * whether a reduction is refused and what the refusal says structurally are\n * all core's, and none of them move. A declaration names the collection to\n * count, the entitlement field that holds the included figure, the add-on kind\n * it stacks on, and the words for one and many. It decides nothing about\n * whether the customer may do the thing.\n *\n * ## Compiled, and deliberately without a runtime registrar\n *\n * The readers are a downgrade REFUSAL (`/api/billing/subscription`) and the\n * warning the customer reads before choosing, and both must list the same\n * capacities or the refusal contradicts the warning that preceded it. A\n * registry that one of those processes had not filled would drop a capacity\n * from the comparison, and the plan change that strands it would be allowed\n * with nothing red — the AGL-3025 shape, on money. So these are compiled from\n * `plugins.config.json` like the switchboard catalog, and there is no\n * `registerPluginOrgCapacity`: a registrar nothing can safely call is worse\n * than no registrar, because the next reader assumes it works.\n */\n\nimport { PLUGIN_ORG_CAPACITIES_DECLARED } from './first-party-plugins.generated'\n\n/** What one and many of the thing are called, and what the money buys. */\nexport interface PluginOrgCapacityNouns {\n /** \"dataset\" — used when the quantity is exactly one. */\n one: string\n /** \"datasets\" — used for every other quantity, and as the held noun. */\n many: string\n /**\n * \"extra datasets\" — what the PURCHASE is called. An org holds \"team\n * members\" and buys \"extra team seats\", and a refusal that used one word\n * for both would be telling somebody to delete a seat.\n */\n addon: string\n}\n\nexport interface PluginOrgCapacityDeclaration {\n /** The capacity's id, as both gates and any stored row spell it. */\n kind: string\n /** Where it sorts among the capacities; core's own are 10 and 20. */\n order: number\n /** The subcollection under `orgs/{orgId}` whose documents are counted. */\n collection: string\n /** The `AddonKind` this capacity is sold as. */\n addonKind: string\n /**\n * The `PLAN_ENTITLEMENTS` field holding what a plan INCLUDES — never the\n * purchase ceiling. The two are far apart (Starter includes 3 datasets and\n * sells up to 10) and measuring against the ceiling prints a number the\n * word \"includes\" makes false while clearing an org a plan change strands.\n */\n includedEntitlement: string\n nouns: PluginOrgCapacityNouns\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginOrgCapacity = PluginOrgCapacityDeclaration & {\n pluginId: string\n}\n\n/** Every declared org capacity, in declared order. */\nexport function pluginOrgCapacities(): readonly ResolvedPluginOrgCapacity[] {\n return PLUGIN_ORG_CAPACITIES_DECLARED\n}\n\n/** One declared capacity by kind, or `null`. */\nexport function pluginOrgCapacity(\n kind: string,\n): ResolvedPluginOrgCapacity | null {\n return PLUGIN_ORG_CAPACITIES_DECLARED.find((one) => one.kind === kind) ?? null\n}\n\n/** One declared capacity by the add-on kind it is sold as, or `null`. */\nexport function pluginOrgCapacityForAddon(\n addonKind: string,\n): ResolvedPluginOrgCapacity | null {\n return (\n PLUGIN_ORG_CAPACITIES_DECLARED.find((one) => one.addonKind === addonKind) ??\n null\n )\n}\n"],"names":["PLUGIN_ORG_CAPACITIES_DECLARED","pluginOrgCapacities","pluginOrgCapacity","kind","find","one","pluginOrgCapacityForAddon","addonKind"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,SAASA,8BAA8B,QAAQ,qCAAiC;AAwChF,oDAAoD,GACpD,OAAO,SAASC;IACd,OAAOD;AACT;AAEA,8CAA8C,GAC9C,OAAO,SAASE,kBACdC,IAAY;QAELH;IAAP,QAAOA,uCAAAA,+BAA+BI,IAAI,CAAC,CAACC,MAAQA,IAAIF,IAAI,KAAKA,iBAA1DH,uCAAmE;AAC5E;AAEA,uEAAuE,GACvE,OAAO,SAASM,0BACdC,SAAiB;QAGfP;IADF,QACEA,uCAAAA,+BAA+BI,IAAI,CAAC,CAACC,MAAQA,IAAIE,SAAS,KAAKA,sBAA/DP,uCACA;AAEJ"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-org-capacity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * AN ORG CAPACITY A PLUGIN BACKS (AGL-3080).\n *\n * An org holds a fixed number of some things, its plan includes a number of\n * them, and it can buy more on top. Three of those existed and core named all\n * three: sites, team seats and DATASETS. The first two are the platform's own\n * — every workspace has sites and people, plugin or no plugin. A dataset is\n * the data plugin's, and core knew its collection path, its noun, its plural,\n * the words a refusal says about it and which entitlement field to measure it\n * against.\n *\n * That mattered because the same three names were spelled out in three places\n * with two different vocabularies — `OverLimitKind` calls it `datasets` and\n * `CapacityAddonKind` calls it `datasets` but calls a site `hosts` — and each\n * had its own noun table. A fourth capacity would have meant editing all of\n * them, and a plugin that shipped one could not add it at all.\n *\n * So the plugin declares the capacity, once, and core's two gates read it.\n *\n * ## What core keeps\n *\n * The MONEY. Which add-on kinds exist, what they cost, what a plan includes,\n * whether a reduction is refused and what the refusal says structurally are\n * all core's, and none of them move. A declaration names the collection to\n * count, the entitlement field that holds the included figure, the add-on kind\n * it stacks on, and the words for one and many. It decides nothing about\n * whether the customer may do the thing.\n *\n * ## Compiled, and deliberately without a runtime registrar\n *\n * The readers are a downgrade REFUSAL (`/api/billing/subscription`) and the\n * warning the customer reads before choosing, and both must list the same\n * capacities or the refusal contradicts the warning that preceded it. A\n * registry that one of those processes had not filled would drop a capacity\n * from the comparison, and the plan change that strands it would be allowed\n * with nothing red — the AGL-3025 shape, on money. So these are compiled from\n * `plugins.config.json` like the switchboard catalog, and there is no\n * `registerPluginOrgCapacity`: a registrar nothing can safely call is worse\n * than no registrar, because the next reader assumes it works.\n */\n\nimport { PLUGIN_ORG_CAPACITIES_DECLARED } from './first-party-plugins.generated'\n\n/** What one and many of the thing are called, and what the money buys. */\nexport interface PluginOrgCapacityNouns {\n /** \"dataset\" — used when the quantity is exactly one. */\n one: string\n /** \"datasets\" — used for every other quantity, and as the held noun. */\n many: string\n /**\n * \"extra datasets\" — what the PURCHASE is called. An org holds \"team\n * members\" and buys \"extra team seats\", and a refusal that used one word\n * for both would be telling somebody to delete a seat.\n */\n addon: string\n}\n\nexport interface PluginOrgCapacityDeclaration {\n /** The capacity's id, as both gates and any stored row spell it. */\n kind: string\n /** Where it sorts among the capacities; core's own are 10 and 20. */\n order: number\n /** The subcollection under `orgs/{orgId}` whose documents are counted. */\n collection: string\n /** The `AddonKind` this capacity is sold as. */\n addonKind: string\n /**\n * The `PLAN_ENTITLEMENTS` field holding what a plan INCLUDES — never the\n * purchase ceiling. The two are far apart (Starter includes 3 datasets and\n * sells up to 10) and measuring against the ceiling prints a number the\n * word \"includes\" makes false while clearing an org a plan change strands.\n */\n includedEntitlement: string\n /**\n * The `PLAN_ENTITLEMENTS` field holding how far BUYING can raise it — the\n * ceiling the two gates above deliberately do not measure against, and the\n * one a console meter has to, because what it draws is the limit a create\n * is refused at: included, plus what the org bought, clamped to this.\n *\n * A FIELD NAME, never a number. Every declaration here names an entitlement\n * and lets core resolve it, so what a plan sells stays one table on core's\n * side of the line — a declaration carrying `10` would be a second copy of\n * a price list, in a plugin, going stale the first time a plan changed.\n */\n purchaseCeilingEntitlement: string\n nouns: PluginOrgCapacityNouns\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginOrgCapacity = PluginOrgCapacityDeclaration & {\n pluginId: string\n}\n\n/** Every declared org capacity, in declared order. */\nexport function pluginOrgCapacities(): readonly ResolvedPluginOrgCapacity[] {\n return PLUGIN_ORG_CAPACITIES_DECLARED\n}\n\n/** One declared capacity by kind, or `null`. */\nexport function pluginOrgCapacity(\n kind: string,\n): ResolvedPluginOrgCapacity | null {\n return PLUGIN_ORG_CAPACITIES_DECLARED.find((one) => one.kind === kind) ?? null\n}\n\n/** One declared capacity by the add-on kind it is sold as, or `null`. */\nexport function pluginOrgCapacityForAddon(\n addonKind: string,\n): ResolvedPluginOrgCapacity | null {\n return (\n PLUGIN_ORG_CAPACITIES_DECLARED.find((one) => one.addonKind === addonKind) ??\n null\n )\n}\n"],"names":["PLUGIN_ORG_CAPACITIES_DECLARED","pluginOrgCapacities","pluginOrgCapacity","kind","find","one","pluginOrgCapacityForAddon","addonKind"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,SAASA,8BAA8B,QAAQ,qCAAiC;AAoDhF,oDAAoD,GACpD,OAAO,SAASC;IACd,OAAOD;AACT;AAEA,8CAA8C,GAC9C,OAAO,SAASE,kBACdC,IAAY;QAELH;IAAP,QAAOA,uCAAAA,+BAA+BI,IAAI,CAAC,CAACC,MAAQA,IAAIF,IAAI,KAAKA,iBAA1DH,uCAAmE;AAC5E;AAEA,uEAAuE,GACvE,OAAO,SAASM,0BACdC,SAAiB;QAGfP;IADF,QACEA,uCAAAA,+BAA+BI,IAAI,CAAC,CAACC,MAAQA,IAAIE,SAAS,KAAKA,sBAA/DP,uCACA;AAEJ"}
@@ -50,11 +50,18 @@
50
50
  * the workspace's to write — that the record is one it acted on, for the
51
51
  * member it acted for.
52
52
  */
53
- /** The record an entry is filed on: any of the three, as the owner links them. */
53
+ /** The record an entry is filed on: any of the four, as the owner links them. */
54
54
  export interface PluginRecordLink {
55
55
  contactId?: string;
56
56
  companyId?: string;
57
57
  dealId?: string;
58
+ /**
59
+ * A lead not yet converted (AGL-3234) — the record system's own id for
60
+ * it, addressed under `hostId`. A sequence's emails and tasks land on the
61
+ * lead's page while the person is one, and follow the lead to the contact
62
+ * it becomes.
63
+ */
64
+ leadId?: string;
58
65
  }
59
66
  /** Where an entry is filed, and by which plugin. */
60
67
  export interface PluginRecordEntryContext {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-timeline.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 definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * An entry one plugin files on a record another plugin keeps (AGL-2981).\n *\n * A person, a company and a deal are records of the workspace's record\n * system, and what happened with them — an email sent, a reply received, a\n * call to make — belongs on their timeline, whichever plugin it happened in.\n * A plugin that sends mail or books a meeting must not write the record\n * system's documents itself: it would restate the owner's rules — where an\n * entry lives, who may see it, how a copy of the same email is told apart,\n * how many entries a record may carry — and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the record system registers a WRITER here, from its server surfaces,\n * and a caller asks for it. Every rule stays the owner's:\n *\n * - the entry is stamped with the scope a record made on the site would\n * carry, so it is visible to exactly who sees the record;\n * - an EMAIL is filed once per `Message-ID`, under the id the owner files a\n * copy of the same message by through any other door — a copy the rep\n * forwarded to a capture address is the same entry, not a second one;\n * - any other entry, and every task, is filed once per the caller's own\n * `dedupeKey`, so a caller that runs again finds what it wrote rather\n * than writing it twice;\n * - a record whose log is full, or a workspace whose plan carries no record\n * system, is refused — answered, never thrown.\n *\n * A single-implementation contract: a workspace keeps one record system.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-resource-drafts`, the registry authenticates nobody. The\n * caller decides, in its own terms and before it asks, that the entry is\n * the workspace's to write — that the record is one it acted on, for the\n * member it acted for.\n */\n\n/** The record an entry is filed on: any of the three, as the owner links them. */\nexport interface PluginRecordLink {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/** Where an entry is filed, and by which plugin. */\nexport interface PluginRecordEntryContext {\n orgId: string\n /** The site whose scope the entry carries: as if a member made it there. */\n hostId: string\n link: PluginRecordLink\n /** The plugin filing it, recorded on the entry. */\n sourcePluginId: string\n}\n\n/** What an activity is, in the record system's own words. */\nexport type PluginRecordActivityKind = 'email' | 'call' | 'meeting' | 'note' | 'other'\n\n/** The message an `email` activity is about. */\nexport interface PluginRecordEmail {\n /** `outbound` for one the workspace wrote; `inbound` for one written to it. */\n direction: 'outbound' | 'inbound'\n subject: string\n /** The address on the message's From. */\n from: string\n /** The address it was written to, when one is worth showing. */\n to: string | null\n /** The `Message-ID` header, angle brackets included: what files it once. */\n messageId: string\n /** The `Message-ID` it answered, when it answered one. */\n inReplyTo?: string | null\n}\n\nexport interface PluginRecordActivityRequest extends PluginRecordEntryContext {\n kind: PluginRecordActivityKind\n /** When it happened, epoch ms. */\n atMs: number\n /** Plain text: an email's excerpt, a note's words. The owner bounds it. */\n body: string\n /** The member it is by, or `''` for none. */\n byUid: string\n byName?: string | null\n /** Required for an `email`, and read only for one. */\n email?: PluginRecordEmail | null\n /** Required for every kind but `email`: the caller's id for this entry. */\n dedupeKey?: string | null\n}\n\n/** What a task is, in the record system's own words. */\nexport type PluginRecordTaskKind = 'call' | 'email' | 'meeting' | 'todo'\n\nexport interface PluginRecordTaskRequest extends PluginRecordEntryContext {\n /** The caller's id for this task: the same key never makes a second task. */\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n /** When it is due, epoch ms. */\n dueAtMs: number\n /** The member it is for, or `''` for nobody. */\n assigneeUid: string\n /** The member who made it happen, or `''` when nobody did. */\n createdByUid: string\n}\n\n/**\n * The owner's answer. `created: false` is the entry a previous write — or a\n * copy through another door — already filed, under the same `id`.\n */\nexport type PluginRecordWrite =\n | { ok: true; id: string; created: boolean }\n | {\n ok: false\n status: 400 | 403 | 404 | 409\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\nexport interface PluginRecordTimelineWriter {\n logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite>\n createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite>\n}\n\nexport const PLUGIN_RECORD_TIMELINE = definePluginServiceContract<PluginRecordTimelineWriter>(\n 'core.record-timeline',\n { multiple: false },\n)\n\n/**\n * Registers the workspace's record timeline writer. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`; with neither\n * the registration throws, and a second plugin's writer is refused naming\n * both — the incumbent keeps serving.\n */\nexport function registerPluginRecordTimelineWriter(\n writer: PluginRecordTimelineWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECORD_TIMELINE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginRecordTimelineWriter {\n /** The plugin that keeps the records. */\n pluginId: string\n writer: PluginRecordTimelineWriter\n}\n\n/** The writer, with its owner, or `null` when no plugin keeps records here. */\nexport function pluginRecordTimelineWriter(): ResolvedPluginRecordTimelineWriter | null {\n const entry = resolvePluginServices(PLUGIN_RECORD_TIMELINE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_TIMELINE","multiple","registerPluginRecordTimelineWriter","writer","options","pluginId","pluginRecordTimelineWriter","entry","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA4H1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,6EAA6E,GAC7E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,QAAQI,MAAMC,IAAI;IAAC,IAAI;AACpE"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-timeline.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 definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * An entry one plugin files on a record another plugin keeps (AGL-2981).\n *\n * A person, a company and a deal are records of the workspace's record\n * system, and what happened with them — an email sent, a reply received, a\n * call to make — belongs on their timeline, whichever plugin it happened in.\n * A plugin that sends mail or books a meeting must not write the record\n * system's documents itself: it would restate the owner's rules — where an\n * entry lives, who may see it, how a copy of the same email is told apart,\n * how many entries a record may carry — and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the record system registers a WRITER here, from its server surfaces,\n * and a caller asks for it. Every rule stays the owner's:\n *\n * - the entry is stamped with the scope a record made on the site would\n * carry, so it is visible to exactly who sees the record;\n * - an EMAIL is filed once per `Message-ID`, under the id the owner files a\n * copy of the same message by through any other door — a copy the rep\n * forwarded to a capture address is the same entry, not a second one;\n * - any other entry, and every task, is filed once per the caller's own\n * `dedupeKey`, so a caller that runs again finds what it wrote rather\n * than writing it twice;\n * - a record whose log is full, or a workspace whose plan carries no record\n * system, is refused — answered, never thrown.\n *\n * A single-implementation contract: a workspace keeps one record system.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-resource-drafts`, the registry authenticates nobody. The\n * caller decides, in its own terms and before it asks, that the entry is\n * the workspace's to write — that the record is one it acted on, for the\n * member it acted for.\n */\n\n/** The record an entry is filed on: any of the four, as the owner links them. */\nexport interface PluginRecordLink {\n contactId?: string\n companyId?: string\n dealId?: string\n /**\n * A lead not yet converted (AGL-3234) — the record system's own id for\n * it, addressed under `hostId`. A sequence's emails and tasks land on the\n * lead's page while the person is one, and follow the lead to the contact\n * it becomes.\n */\n leadId?: string\n}\n\n/** Where an entry is filed, and by which plugin. */\nexport interface PluginRecordEntryContext {\n orgId: string\n /** The site whose scope the entry carries: as if a member made it there. */\n hostId: string\n link: PluginRecordLink\n /** The plugin filing it, recorded on the entry. */\n sourcePluginId: string\n}\n\n/** What an activity is, in the record system's own words. */\nexport type PluginRecordActivityKind = 'email' | 'call' | 'meeting' | 'note' | 'other'\n\n/** The message an `email` activity is about. */\nexport interface PluginRecordEmail {\n /** `outbound` for one the workspace wrote; `inbound` for one written to it. */\n direction: 'outbound' | 'inbound'\n subject: string\n /** The address on the message's From. */\n from: string\n /** The address it was written to, when one is worth showing. */\n to: string | null\n /** The `Message-ID` header, angle brackets included: what files it once. */\n messageId: string\n /** The `Message-ID` it answered, when it answered one. */\n inReplyTo?: string | null\n}\n\nexport interface PluginRecordActivityRequest extends PluginRecordEntryContext {\n kind: PluginRecordActivityKind\n /** When it happened, epoch ms. */\n atMs: number\n /** Plain text: an email's excerpt, a note's words. The owner bounds it. */\n body: string\n /** The member it is by, or `''` for none. */\n byUid: string\n byName?: string | null\n /** Required for an `email`, and read only for one. */\n email?: PluginRecordEmail | null\n /** Required for every kind but `email`: the caller's id for this entry. */\n dedupeKey?: string | null\n}\n\n/** What a task is, in the record system's own words. */\nexport type PluginRecordTaskKind = 'call' | 'email' | 'meeting' | 'todo'\n\nexport interface PluginRecordTaskRequest extends PluginRecordEntryContext {\n /** The caller's id for this task: the same key never makes a second task. */\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n /** When it is due, epoch ms. */\n dueAtMs: number\n /** The member it is for, or `''` for nobody. */\n assigneeUid: string\n /** The member who made it happen, or `''` when nobody did. */\n createdByUid: string\n}\n\n/**\n * The owner's answer. `created: false` is the entry a previous write — or a\n * copy through another door — already filed, under the same `id`.\n */\nexport type PluginRecordWrite =\n | { ok: true; id: string; created: boolean }\n | {\n ok: false\n status: 400 | 403 | 404 | 409\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\nexport interface PluginRecordTimelineWriter {\n logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite>\n createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite>\n}\n\nexport const PLUGIN_RECORD_TIMELINE = definePluginServiceContract<PluginRecordTimelineWriter>(\n 'core.record-timeline',\n { multiple: false },\n)\n\n/**\n * Registers the workspace's record timeline writer. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`; with neither\n * the registration throws, and a second plugin's writer is refused naming\n * both — the incumbent keeps serving.\n */\nexport function registerPluginRecordTimelineWriter(\n writer: PluginRecordTimelineWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECORD_TIMELINE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginRecordTimelineWriter {\n /** The plugin that keeps the records. */\n pluginId: string\n writer: PluginRecordTimelineWriter\n}\n\n/** The writer, with its owner, or `null` when no plugin keeps records here. */\nexport function pluginRecordTimelineWriter(): ResolvedPluginRecordTimelineWriter | null {\n const entry = resolvePluginServices(PLUGIN_RECORD_TIMELINE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_TIMELINE","multiple","registerPluginRecordTimelineWriter","writer","options","pluginId","pluginRecordTimelineWriter","entry","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAmI1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,6EAA6E,GAC7E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,QAAQI,MAAMC,IAAI;IAAC,IAAI;AACpE"}
@@ -1,95 +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
- * Who may ask for the Verified badge, and when (AGL-1217).
19
- *
20
- * In core rather than beside the rest of the marketplace model for the same
21
- * reason `marketplace-provenance` is (AGL-1016): the console's staff review
22
- * route needs this policy, and `scope:app` may not depend on `aglyn:addons`.
23
- * The marketplace model re-exports everything here, so publishing code keeps
24
- * importing it from one place.
25
- *
26
- * A leaf module with no imports — it is reached from the browser, from React
27
- * Server Components and from API routes, and every barrel that could carry it
28
- * is illegal in at least one of those.
29
- */
30
- export type VerificationRequestState = 'pending' | 'granted' | 'declined' | 'withdrawn';
31
- /**
32
- * Firestore timestamps arrive in two shapes: an SDK `Timestamp` carrying
33
- * `toMillis()`, and the plain `{seconds}` object a serialized doc decays into.
34
- * Policy has to read both or it silently scores a real date as absent.
35
- */
36
- export type TimestampLike = {
37
- toMillis?: () => number;
38
- seconds?: number;
39
- } | null | undefined;
40
- /** Milliseconds for either timestamp shape; `null` when there is no date. */
41
- export declare function timestampMs(value: TimestampLike): number | null;
42
- /**
43
- * A publisher's ask for the Verified badge.
44
- *
45
- * Deliberately NOT a member of `ListingReviewStatus`. That union's
46
- * consequential axis is live vs. not live — `listed` is what makes a plugin
47
- * installable by every workspace — and a listing can be live *and* awaiting a
48
- * verification decision at the same time. A `verification_requested` member
49
- * would have to answer "is this live?" and both answers are wrong.
50
- *
51
- * A record rather than a `verificationRequestedAt` timestamp because
52
- * verification may become a paid offering. A timestamp has to be rewritten the
53
- * day that lands; this absorbs an order reference without a migration.
54
- */
55
- export interface ListingVerificationRequest {
56
- requestedAt?: TimestampLike;
57
- /** Publisher uid — a person asked, not "the org asked". */
58
- requestedBy?: string;
59
- state: VerificationRequestState;
60
- decidedAt?: TimestampLike;
61
- /** Staff uid, or the publisher's own on a withdrawal. */
62
- decidedBy?: string;
63
- declineReason?: string;
64
- /** Room for a payment reference when verification becomes purchasable. */
65
- orderRef?: string;
66
- }
67
- /**
68
- * How long a publisher waits after a decline before asking again.
69
- *
70
- * Queue hygiene, not punishment: without it a publisher can re-request in a
71
- * loop and staff lose the signal that a request means something changed.
72
- */
73
- export declare const VERIFICATION_DECLINE_COOLDOWN_DAYS = 30;
74
- /** Why a publisher may not ask right now — `null` when they may. */
75
- export type VerificationRequestBlock = 'not-publisher' | 'already-verified' | 'not-listed' | 'already-pending' | 'cooling-down';
76
- /** The listing fields this policy reads, structurally — core cannot see the
77
- * marketplace model's `MarketplaceListing`. */
78
- export interface VerifiableListing {
79
- profileId?: string;
80
- reviewStatus?: string;
81
- verificationRequest?: ListingVerificationRequest;
82
- }
83
- /**
84
- * A reason rather than a boolean, so the UI can say which and so the server
85
- * and the button cannot drift into disagreeing about the rule. Both call this.
86
- */
87
- export declare function verificationRequestBlock(options: {
88
- listing: VerifiableListing | null | undefined;
89
- /** The org the viewer is acting as. */
90
- viewerOrgId: string | null | undefined;
91
- /** `Date.now()` on the client; server time on the server. */
92
- nowMs: number;
93
- }): VerificationRequestBlock | null;
94
- /** What to tell the publisher for each block reason. */
95
- export declare const VERIFICATION_BLOCK_MESSAGES: Record<VerificationRequestBlock, string>;
@@ -1,81 +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
- * Who may ask for the Verified badge, and when (AGL-1217).
18
- *
19
- * In core rather than beside the rest of the marketplace model for the same
20
- * reason `marketplace-provenance` is (AGL-1016): the console's staff review
21
- * route needs this policy, and `scope:app` may not depend on `aglyn:addons`.
22
- * The marketplace model re-exports everything here, so publishing code keeps
23
- * importing it from one place.
24
- *
25
- * A leaf module with no imports — it is reached from the browser, from React
26
- * Server Components and from API routes, and every barrel that could carry it
27
- * is illegal in at least one of those.
28
- */ /** Milliseconds for either timestamp shape; `null` when there is no date. */ export function timestampMs(value) {
29
- if (!value) return null;
30
- if (typeof value.toMillis === 'function') {
31
- const ms = value.toMillis();
32
- return Number.isFinite(ms) ? ms : null;
33
- }
34
- if (typeof value.seconds === 'number') return value.seconds * 1000;
35
- return null;
36
- }
37
- /**
38
- * How long a publisher waits after a decline before asking again.
39
- *
40
- * Queue hygiene, not punishment: without it a publisher can re-request in a
41
- * loop and staff lose the signal that a request means something changed.
42
- */ export const VERIFICATION_DECLINE_COOLDOWN_DAYS = 30;
43
- /**
44
- * A reason rather than a boolean, so the UI can say which and so the server
45
- * and the button cannot drift into disagreeing about the rule. Both call this.
46
- */ export function verificationRequestBlock(options) {
47
- const { listing, viewerOrgId, nowMs } = options;
48
- if (!listing || !viewerOrgId || listing.profileId !== viewerOrgId) {
49
- return 'not-publisher';
50
- }
51
- if (listing.reviewStatus === 'verified') return 'already-verified';
52
- // Only a LIVE listing can be verified. Asking while `submitted`, `in_review`
53
- // or `rejected` is asking for a badge on something customers cannot install,
54
- // and it would put verification requests into a queue that already has a
55
- // different reason to look at the listing.
56
- //
57
- // A legacy listing with NO status is treated as `listed` elsewhere, but not
58
- // here: verification is a deliberate act, and an absent field is not enough
59
- // to hang a badge request on.
60
- if (listing.reviewStatus !== 'listed') return 'not-listed';
61
- const request = listing.verificationRequest;
62
- if ((request == null ? void 0 : request.state) === 'pending') return 'already-pending';
63
- if ((request == null ? void 0 : request.state) === 'declined') {
64
- const decidedAt = timestampMs(request.decidedAt);
65
- // Fail closed on an undated decline. Assuming it is old enough turns a
66
- // missing field into a free retry, which is what the cooldown prevents.
67
- if (decidedAt === null) return 'cooling-down';
68
- const elapsedDays = (nowMs - decidedAt) / 86400000;
69
- if (elapsedDays < VERIFICATION_DECLINE_COOLDOWN_DAYS) return 'cooling-down';
70
- }
71
- return null;
72
- }
73
- /** What to tell the publisher for each block reason. */ export const VERIFICATION_BLOCK_MESSAGES = {
74
- 'not-publisher': 'Only the publishing organization can request verification.',
75
- 'already-verified': 'This listing is already verified.',
76
- 'not-listed': 'Only a listed plugin can be verified. This one is still going through review.',
77
- 'already-pending': 'A verification request is already waiting for a decision.',
78
- 'cooling-down': `Verification was declined recently. You can ask again ${VERIFICATION_DECLINE_COOLDOWN_DAYS} days after the decision.`
79
- };
80
-
81
- //# sourceMappingURL=marketplace-verification.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/marketplace-verification.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Who may ask for the Verified badge, and when (AGL-1217).\n *\n * In core rather than beside the rest of the marketplace model for the same\n * reason `marketplace-provenance` is (AGL-1016): the console's staff review\n * route needs this policy, and `scope:app` may not depend on `aglyn:addons`.\n * The marketplace model re-exports everything here, so publishing code keeps\n * importing it from one place.\n *\n * A leaf module with no imports — it is reached from the browser, from React\n * Server Components and from API routes, and every barrel that could carry it\n * is illegal in at least one of those.\n */\n\nexport type VerificationRequestState =\n | 'pending'\n | 'granted'\n | 'declined'\n | 'withdrawn'\n\n/**\n * Firestore timestamps arrive in two shapes: an SDK `Timestamp` carrying\n * `toMillis()`, and the plain `{seconds}` object a serialized doc decays into.\n * Policy has to read both or it silently scores a real date as absent.\n */\nexport type TimestampLike =\n | { toMillis?: () => number; seconds?: number }\n | null\n | undefined\n\n/** Milliseconds for either timestamp shape; `null` when there is no date. */\nexport function timestampMs(value: TimestampLike): number | null {\n if (!value) return null\n if (typeof value.toMillis === 'function') {\n const ms = value.toMillis()\n return Number.isFinite(ms) ? ms : null\n }\n if (typeof value.seconds === 'number') return value.seconds * 1000\n return null\n}\n\n/**\n * A publisher's ask for the Verified badge.\n *\n * Deliberately NOT a member of `ListingReviewStatus`. That union's\n * consequential axis is live vs. not live — `listed` is what makes a plugin\n * installable by every workspace — and a listing can be live *and* awaiting a\n * verification decision at the same time. A `verification_requested` member\n * would have to answer \"is this live?\" and both answers are wrong.\n *\n * A record rather than a `verificationRequestedAt` timestamp because\n * verification may become a paid offering. A timestamp has to be rewritten the\n * day that lands; this absorbs an order reference without a migration.\n */\nexport interface ListingVerificationRequest {\n requestedAt?: TimestampLike\n /** Publisher uid — a person asked, not \"the org asked\". */\n requestedBy?: string\n state: VerificationRequestState\n decidedAt?: TimestampLike\n /** Staff uid, or the publisher's own on a withdrawal. */\n decidedBy?: string\n declineReason?: string\n /** Room for a payment reference when verification becomes purchasable. */\n orderRef?: string\n}\n\n/**\n * How long a publisher waits after a decline before asking again.\n *\n * Queue hygiene, not punishment: without it a publisher can re-request in a\n * loop and staff lose the signal that a request means something changed.\n */\nexport const VERIFICATION_DECLINE_COOLDOWN_DAYS = 30\n\n/** Why a publisher may not ask right now — `null` when they may. */\nexport type VerificationRequestBlock =\n | 'not-publisher'\n | 'already-verified'\n | 'not-listed'\n | 'already-pending'\n | 'cooling-down'\n\n/** The listing fields this policy reads, structurally — core cannot see the\n * marketplace model's `MarketplaceListing`. */\nexport interface VerifiableListing {\n profileId?: string\n reviewStatus?: string\n verificationRequest?: ListingVerificationRequest\n}\n\n/**\n * A reason rather than a boolean, so the UI can say which and so the server\n * and the button cannot drift into disagreeing about the rule. Both call this.\n */\nexport function verificationRequestBlock(options: {\n listing: VerifiableListing | null | undefined\n /** The org the viewer is acting as. */\n viewerOrgId: string | null | undefined\n /** `Date.now()` on the client; server time on the server. */\n nowMs: number\n}): VerificationRequestBlock | null {\n const { listing, viewerOrgId, nowMs } = options\n if (!listing || !viewerOrgId || listing.profileId !== viewerOrgId) {\n return 'not-publisher'\n }\n if (listing.reviewStatus === 'verified') return 'already-verified'\n // Only a LIVE listing can be verified. Asking while `submitted`, `in_review`\n // or `rejected` is asking for a badge on something customers cannot install,\n // and it would put verification requests into a queue that already has a\n // different reason to look at the listing.\n //\n // A legacy listing with NO status is treated as `listed` elsewhere, but not\n // here: verification is a deliberate act, and an absent field is not enough\n // to hang a badge request on.\n if (listing.reviewStatus !== 'listed') return 'not-listed'\n\n const request = listing.verificationRequest\n if (request?.state === 'pending') return 'already-pending'\n if (request?.state === 'declined') {\n const decidedAt = timestampMs(request.decidedAt)\n // Fail closed on an undated decline. Assuming it is old enough turns a\n // missing field into a free retry, which is what the cooldown prevents.\n if (decidedAt === null) return 'cooling-down'\n const elapsedDays = (nowMs - decidedAt) / 86_400_000\n if (elapsedDays < VERIFICATION_DECLINE_COOLDOWN_DAYS) return 'cooling-down'\n }\n return null\n}\n\n/** What to tell the publisher for each block reason. */\nexport const VERIFICATION_BLOCK_MESSAGES: Record<\n VerificationRequestBlock,\n string\n> = {\n 'not-publisher': 'Only the publishing organization can request verification.',\n 'already-verified': 'This listing is already verified.',\n 'not-listed':\n 'Only a listed plugin can be verified. This one is still going through review.',\n 'already-pending': 'A verification request is already waiting for a decision.',\n 'cooling-down': `Verification was declined recently. You can ask again ${VERIFICATION_DECLINE_COOLDOWN_DAYS} days after the decision.`,\n}\n"],"names":["timestampMs","value","toMillis","ms","Number","isFinite","seconds","VERIFICATION_DECLINE_COOLDOWN_DAYS","verificationRequestBlock","options","listing","viewerOrgId","nowMs","profileId","reviewStatus","request","verificationRequest","state","decidedAt","elapsedDays","VERIFICATION_BLOCK_MESSAGES"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;CAYC,GAkBD,2EAA2E,GAC3E,OAAO,SAASA,YAAYC,KAAoB;IAC9C,IAAI,CAACA,OAAO,OAAO;IACnB,IAAI,OAAOA,MAAMC,QAAQ,KAAK,YAAY;QACxC,MAAMC,KAAKF,MAAMC,QAAQ;QACzB,OAAOE,OAAOC,QAAQ,CAACF,MAAMA,KAAK;IACpC;IACA,IAAI,OAAOF,MAAMK,OAAO,KAAK,UAAU,OAAOL,MAAMK,OAAO,GAAG;IAC9D,OAAO;AACT;AA4BA;;;;;CAKC,GACD,OAAO,MAAMC,qCAAqC,GAAE;AAkBpD;;;CAGC,GACD,OAAO,SAASC,yBAAyBC,OAMxC;IACC,MAAM,EAAEC,OAAO,EAAEC,WAAW,EAAEC,KAAK,EAAE,GAAGH;IACxC,IAAI,CAACC,WAAW,CAACC,eAAeD,QAAQG,SAAS,KAAKF,aAAa;QACjE,OAAO;IACT;IACA,IAAID,QAAQI,YAAY,KAAK,YAAY,OAAO;IAChD,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,2CAA2C;IAC3C,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,8BAA8B;IAC9B,IAAIJ,QAAQI,YAAY,KAAK,UAAU,OAAO;IAE9C,MAAMC,UAAUL,QAAQM,mBAAmB;IAC3C,IAAID,CAAAA,2BAAAA,QAASE,KAAK,MAAK,WAAW,OAAO;IACzC,IAAIF,CAAAA,2BAAAA,QAASE,KAAK,MAAK,YAAY;QACjC,MAAMC,YAAYlB,YAAYe,QAAQG,SAAS;QAC/C,uEAAuE;QACvE,wEAAwE;QACxE,IAAIA,cAAc,MAAM,OAAO;QAC/B,MAAMC,cAAc,AAACP,CAAAA,QAAQM,SAAQ,IAAK;QAC1C,IAAIC,cAAcZ,oCAAoC,OAAO;IAC/D;IACA,OAAO;AACT;AAEA,sDAAsD,GACtD,OAAO,MAAMa,8BAGT;IACF,iBAAiB;IACjB,oBAAoB;IACpB,cACE;IACF,mBAAmB;IACnB,gBAAgB,CAAC,sDAAsD,EAAEb,mCAAmC,yBAAyB,CAAC;AACxI,EAAC"}