@aglyn/plugins-outreach 1.0.0-beta.220 → 1.0.0-beta.222

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 (116) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/do-not-contact-domains.d.ts +6 -2
  3. package/src/lib/components/do-not-contact-domains.js +73 -11
  4. package/src/lib/components/do-not-contact-domains.js.map +1 -1
  5. package/src/lib/components/enrollments-table.d.ts +6 -0
  6. package/src/lib/components/enrollments-table.js +77 -4
  7. package/src/lib/components/enrollments-table.js.map +1 -1
  8. package/src/lib/components/sequence-detail.js +3 -1
  9. package/src/lib/components/sequence-detail.js.map +1 -1
  10. package/src/lib/components/sequence-report-card.js +7 -3
  11. package/src/lib/components/sequence-report-card.js.map +1 -1
  12. package/src/lib/components/use-outreach-crm.d.ts +18 -15
  13. package/src/lib/components/use-outreach-crm.js +118 -87
  14. package/src/lib/components/use-outreach-crm.js.map +1 -1
  15. package/src/lib/components/use-outreach-data.d.ts +2 -2
  16. package/src/lib/components/use-outreach-data.js.map +1 -1
  17. package/src/lib/constants/transfer-resources.d.ts +33 -0
  18. package/src/lib/constants/transfer-resources.js +38 -0
  19. package/src/lib/constants/transfer-resources.js.map +1 -0
  20. package/src/lib/declarations.console-server.d.ts +7 -1
  21. package/src/lib/declarations.console-server.js +66 -1
  22. package/src/lib/declarations.console-server.js.map +1 -1
  23. package/src/lib/engine/click-tracking.js +1 -1
  24. package/src/lib/engine/click-tracking.js.map +1 -1
  25. package/src/lib/engine/ip-range.d.ts +26 -0
  26. package/src/lib/engine/ip-range.js +78 -0
  27. package/src/lib/engine/ip-range.js.map +1 -0
  28. package/src/lib/engine/open-source.d.ts +24 -0
  29. package/src/lib/engine/open-source.js +130 -0
  30. package/src/lib/engine/open-source.js.map +1 -0
  31. package/src/lib/engine/open-tracking.d.ts +15 -2
  32. package/src/lib/engine/open-tracking.js +59 -16
  33. package/src/lib/engine/open-tracking.js.map +1 -1
  34. package/src/lib/engine/outreach-access.d.ts +47 -0
  35. package/src/lib/engine/outreach-access.js +56 -0
  36. package/src/lib/engine/outreach-access.js.map +1 -0
  37. package/src/lib/enrollment/enroll-people.d.ts +10 -10
  38. package/src/lib/enrollment/enroll-people.js +30 -25
  39. package/src/lib/enrollment/enroll-people.js.map +1 -1
  40. package/src/lib/enrollment/gate-lookups.js +30 -15
  41. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  42. package/src/lib/enrollment/message-templates.d.ts +24 -0
  43. package/src/lib/enrollment/message-templates.js +35 -0
  44. package/src/lib/enrollment/message-templates.js.map +1 -0
  45. package/src/lib/model/enrollment-engagement.d.ts +16 -0
  46. package/src/lib/model/enrollment-engagement.js +16 -0
  47. package/src/lib/model/enrollment-engagement.js.map +1 -1
  48. package/src/lib/model/enrollment-history.d.ts +8 -1
  49. package/src/lib/model/enrollment-history.js +17 -5
  50. package/src/lib/model/enrollment-history.js.map +1 -1
  51. package/src/lib/model/enrollment-timeline.d.ts +3 -0
  52. package/src/lib/model/enrollment-timeline.js +15 -4
  53. package/src/lib/model/enrollment-timeline.js.map +1 -1
  54. package/src/lib/model/outreach.types.d.ts +12 -3
  55. package/src/lib/model/outreach.types.js +12 -1
  56. package/src/lib/model/outreach.types.js.map +1 -1
  57. package/src/lib/model/sequence-report.d.ts +14 -7
  58. package/src/lib/model/sequence-report.js +12 -8
  59. package/src/lib/model/sequence-report.js.map +1 -1
  60. package/src/lib/plugin.js +18 -0
  61. package/src/lib/plugin.js.map +1 -1
  62. package/src/lib/routes/curate-routes.js +4 -6
  63. package/src/lib/routes/curate-routes.js.map +1 -1
  64. package/src/lib/routes/enroll-routes.d.ts +0 -12
  65. package/src/lib/routes/enroll-routes.js +52 -92
  66. package/src/lib/routes/enroll-routes.js.map +1 -1
  67. package/src/lib/routes/platform-deps.d.ts +4 -16
  68. package/src/lib/routes/platform-deps.js +5 -25
  69. package/src/lib/routes/platform-deps.js.map +1 -1
  70. package/src/lib/routes/preview-routes.js +25 -13
  71. package/src/lib/routes/preview-routes.js.map +1 -1
  72. package/src/lib/routes/register-routes.js +7 -5
  73. package/src/lib/routes/register-routes.js.map +1 -1
  74. package/src/lib/routes/route-gate.js +6 -17
  75. package/src/lib/routes/route-gate.js.map +1 -1
  76. package/src/lib/routes/sequence-routes.d.ts +46 -0
  77. package/src/lib/routes/sequence-routes.js +155 -63
  78. package/src/lib/routes/sequence-routes.js.map +1 -1
  79. package/src/lib/runtime/click-events.js +8 -8
  80. package/src/lib/runtime/click-events.js.map +1 -1
  81. package/src/lib/runtime/click-route.js +5 -2
  82. package/src/lib/runtime/click-route.js.map +1 -1
  83. package/src/lib/runtime/lead-records.d.ts +18 -10
  84. package/src/lib/runtime/lead-records.js +14 -60
  85. package/src/lib/runtime/lead-records.js.map +1 -1
  86. package/src/lib/runtime/open-events.d.ts +8 -0
  87. package/src/lib/runtime/open-events.js +10 -3
  88. package/src/lib/runtime/open-events.js.map +1 -1
  89. package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
  90. package/src/lib/runtime/platform-runtime-deps.js +31 -24
  91. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  92. package/src/lib/runtime/send-job.js +20 -17
  93. package/src/lib/runtime/send-job.js.map +1 -1
  94. package/src/lib/runtime/sync-job.js +10 -5
  95. package/src/lib/runtime/sync-job.js.map +1 -1
  96. package/src/lib/storage/do-not-contact-store.d.ts +8 -0
  97. package/src/lib/storage/do-not-contact-store.js +13 -0
  98. package/src/lib/storage/do-not-contact-store.js.map +1 -1
  99. package/src/lib/testing/stand-in-record-system.d.ts +54 -0
  100. package/src/lib/testing/stand-in-record-system.js +243 -0
  101. package/src/lib/testing/stand-in-record-system.js.map +1 -0
  102. package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
  103. package/src/lib/transfer/do-not-contact-transfer.js +801 -0
  104. package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
  105. package/src/lib/transfer/plan-gate.d.ts +25 -0
  106. package/src/lib/transfer/plan-gate.js +35 -0
  107. package/src/lib/transfer/plan-gate.js.map +1 -0
  108. package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
  109. package/src/lib/transfer/platform-transfer-deps.js +39 -0
  110. package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
  111. package/src/lib/transfer/sequences-package.d.ts +48 -0
  112. package/src/lib/transfer/sequences-package.js +93 -0
  113. package/src/lib/transfer/sequences-package.js.map +1 -0
  114. package/src/lib/transfer/sequences-package.server.d.ts +31 -0
  115. package/src/lib/transfer/sequences-package.server.js +207 -0
  116. package/src/lib/transfer/sequences-package.server.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/lead-records.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 * WHAT A SEQUENCE DOES TO A LEAD'S OWN RECORD (AGL-3234).\n *\n * Three touches, all on `orgs/{orgId}/leads/{leadId}` — the record system's\n * document, written in the record system's vocabulary (`@aglyn/aglyn`'s lead\n * statuses), never through an import of the CRM plugin:\n *\n * - a SENT STEP moves a lead nobody has touched from New to Nurturing.\n * Automated email is reaching it, so it is not untouched — the digest's\n * unworked list would otherwise name every lead a sequence is working —\n * but no person has engaged yet. Only a send moves it, never the\n * enrollment: a step that never goes out has reached nobody.\n * - a REPLY moves a lead that is New or Nurturing to Working. Somebody\n * wrote back; the lead is being worked, and a queue that still showed it\n * as New would send a rep to open a conversation already open. A lead\n * already Working, closed, or converted is left as it is — a reply does\n * not reopen a verdict or move a contact's record.\n * - a CONVERSION re-points every enrollment made on the lead at the contact\n * it became: the record system tells every plugin through its\n * lead-conversion seam, and this is Sequences' share of it. The\n * enrollment keeps its id, its thread and its place in the steps, and\n * keeps naming the lead beside the contact, so both pages list it.\n *\n * Neither throws: a status that could not be moved is one a rep moves by\n * hand, and an enrollment that could not follow its lead is one the next\n * send stops as `contact_missing` and a rep re-enrolls.\n */\n\nimport { type CrmLeadStatus, crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Firestore refuses a batch of more than 500 writes. */\nconst BATCH_LIMIT = 450\n\n/**\n * Move a lead forward to `to` when it stands in one of `from`; `true` when\n * it moved. A converted lead is a contact's history and never moves.\n */\nasync function advanceOutreachLead(\n firestore: Firestore,\n input: { orgId: string; leadId: string },\n from: readonly CrmLeadStatus[],\n to: CrmLeadStatus,\n): Promise<boolean> {\n try {\n // A lead is one org row (AGL-3275), so the site no longer names it.\n const ref = firestore.collection('orgs').doc(input.orgId).collection('leads').doc(input.leadId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (lead['convertedContactId'] || !from.includes(crmLeadStatus(lead as never))) return false\n await ref.set({ status: to, updatedAt: FieldValue.serverTimestamp() }, { merge: true })\n return true\n } catch (error) {\n console.error(`[outreach] the lead could not be marked ${to}`, input.orgId, input.leadId, error)\n return false\n }\n}\n\n/** A lead nobody has touched, once a step was sent to it: Nurturing. `true` when it moved. */\nexport function markOutreachLeadNurturing(\n firestore: Firestore,\n input: { orgId: string; leadId: string },\n): Promise<boolean> {\n return advanceOutreachLead(firestore, input, ['new'], 'nurturing')\n}\n\n/** A lead no person has engaged, once somebody replied: Working. `true` when it moved. */\nexport function markOutreachLeadWorking(\n firestore: Firestore,\n input: { orgId: string; leadId: string },\n): Promise<boolean> {\n return advanceOutreachLead(firestore, input, ['new', 'nurturing'], 'working')\n}\n\n/** Every enrollment made on the lead now names the contact it became. */\nexport async function followOutreachLeadToContact(\n firestore: Firestore,\n input: Pick<PluginLeadConversionRequest, 'orgId' | 'leadId' | 'contactId'>,\n): Promise<PluginLeadConversionReport> {\n const enrollments = outreachOrgCollection(firestore, input.orgId, 'enrollments')\n const rows = await enrollments.where('leadId', '==', input.leadId).get()\n const refs = rows.docs\n .filter((row) => row.get('target') !== 'contact' || String(row.get('contactId') ?? '') !== input.contactId)\n .map((row) => row.ref)\n for (let start = 0; start < refs.length; start += BATCH_LIMIT) {\n const batch = firestore.batch()\n for (const ref of refs.slice(start, start + BATCH_LIMIT)) {\n batch.update(ref, { target: 'contact', contactId: input.contactId, updatedAtMs: Date.now() })\n }\n await batch.commit()\n }\n return { enrollments: refs.length }\n}\n"],"names":["crmLeadStatus","FieldValue","outreachOrgCollection","BATCH_LIMIT","advanceOutreachLead","firestore","input","from","to","snapshot","ref","collection","doc","orgId","leadId","get","exists","lead","data","includes","set","status","updatedAt","serverTimestamp","merge","error","console","markOutreachLeadNurturing","markOutreachLeadWorking","followOutreachLeadToContact","enrollments","rows","where","refs","docs","filter","row","String","contactId","map","start","length","batch","slice","update","target","updatedAtMs","Date","now","commit"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,SAA6BA,aAAa,QAAQ,6BAA4B;AAK9E,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,qBAAqB,QAAQ,iCAA6B;AAInE,uDAAuD,GACvD,MAAMC,cAAc;AAEpB;;;CAGC,GACD,eAAeC,oBACbC,SAAoB,EACpBC,KAAwC,EACxCC,IAA8B,EAC9BC,EAAiB;IAEjB,IAAI;YAKWC;QAJb,oEAAoE;QACpE,MAAMC,MAAML,UAAUM,UAAU,CAAC,QAAQC,GAAG,CAACN,MAAMO,KAAK,EAAEF,UAAU,CAAC,SAASC,GAAG,CAACN,MAAMQ,MAAM;QAC9F,MAAML,WAAW,MAAMC,IAAIK,GAAG;QAC9B,IAAI,CAACN,SAASO,MAAM,EAAE,OAAO;QAC7B,MAAMC,QAAOR,iBAAAA,SAASS,IAAI,cAAbT,iBAAmB,CAAC;QACjC,IAAIQ,IAAI,CAAC,qBAAqB,IAAI,CAACV,KAAKY,QAAQ,CAACnB,cAAciB,QAAiB,OAAO;QACvF,MAAMP,IAAIU,GAAG,CAAC;YAAEC,QAAQb;YAAIc,WAAWrB,WAAWsB,eAAe;QAAG,GAAG;YAAEC,OAAO;QAAK;QACrF,OAAO;IACT,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,CAAC,wCAAwC,EAAEjB,IAAI,EAAEF,MAAMO,KAAK,EAAEP,MAAMQ,MAAM,EAAEW;QAC1F,OAAO;IACT;AACF;AAEA,4FAA4F,GAC5F,OAAO,SAASE,0BACdtB,SAAoB,EACpBC,KAAwC;IAExC,OAAOF,oBAAoBC,WAAWC,OAAO;QAAC;KAAM,EAAE;AACxD;AAEA,wFAAwF,GACxF,OAAO,SAASsB,wBACdvB,SAAoB,EACpBC,KAAwC;IAExC,OAAOF,oBAAoBC,WAAWC,OAAO;QAAC;QAAO;KAAY,EAAE;AACrE;AAEA,uEAAuE,GACvE,OAAO,eAAeuB,4BACpBxB,SAAoB,EACpBC,KAA0E;IAE1E,MAAMwB,cAAc5B,sBAAsBG,WAAWC,MAAMO,KAAK,EAAE;IAClE,MAAMkB,OAAO,MAAMD,YAAYE,KAAK,CAAC,UAAU,MAAM1B,MAAMQ,MAAM,EAAEC,GAAG;IACtE,MAAMkB,OAAOF,KAAKG,IAAI,CACnBC,MAAM,CAAC,CAACC;YAAkDA;eAA1CA,IAAIrB,GAAG,CAAC,cAAc,aAAasB,QAAOD,WAAAA,IAAIrB,GAAG,CAAC,wBAARqB,WAAwB,QAAQ9B,MAAMgC,SAAS;OACzGC,GAAG,CAAC,CAACH,MAAQA,IAAI1B,GAAG;IACvB,IAAK,IAAI8B,QAAQ,GAAGA,QAAQP,KAAKQ,MAAM,EAAED,SAASrC,YAAa;QAC7D,MAAMuC,QAAQrC,UAAUqC,KAAK;QAC7B,KAAK,MAAMhC,OAAOuB,KAAKU,KAAK,CAACH,OAAOA,QAAQrC,aAAc;YACxDuC,MAAME,MAAM,CAAClC,KAAK;gBAAEmC,QAAQ;gBAAWP,WAAWhC,MAAMgC,SAAS;gBAAEQ,aAAaC,KAAKC,GAAG;YAAG;QAC7F;QACA,MAAMN,MAAMO,MAAM;IACpB;IACA,OAAO;QAAEnB,aAAaG,KAAKQ,MAAM;IAAC;AACpC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/lead-records.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 * WHAT A SEQUENCE DOES WHEN A LEAD BECOMES A CONTACT (AGL-3234).\n *\n * A CONVERSION re-points every enrollment made on the lead at the contact it\n * became: the record system tells every plugin through its lead-conversion\n * seam, and this is Sequences' share of it. The enrollment keeps its id, its\n * thread and its place in the steps, and keeps naming the lead beside the\n * contact, so both pages list it.\n *\n * What a sequence does to the lead's OWN record — a sent step moving a New\n * lead to Nurturing, a reply moving it to Working — is the record system's\n * to write, and Sequences tells it through the record email-state seam\n * (`stampRecordEmailReach`, `stampRecordEmailReply`, AGL-3080) rather than\n * writing the lead here.\n *\n * Never throws for a missing enrollment: one that could not follow its lead\n * is one the next send stops as `contact_missing` and a rep re-enrolls.\n */\n\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Firestore refuses a batch of more than 500 writes. */\nconst BATCH_LIMIT = 450\n\n/** Every enrollment made on the lead now names the contact it became. */\nexport async function followOutreachLeadToContact(\n firestore: Firestore,\n input: Pick<PluginLeadConversionRequest, 'orgId' | 'leadId' | 'contactId'>,\n): Promise<PluginLeadConversionReport> {\n const enrollments = outreachOrgCollection(firestore, input.orgId, 'enrollments')\n const rows = await enrollments.where('leadId', '==', input.leadId).get()\n const refs = rows.docs\n .filter((row) => row.get('target') !== 'contact' || String(row.get('contactId') ?? '') !== input.contactId)\n .map((row) => row.ref)\n for (let start = 0; start < refs.length; start += BATCH_LIMIT) {\n const batch = firestore.batch()\n for (const ref of refs.slice(start, start + BATCH_LIMIT)) {\n batch.update(ref, { target: 'contact', contactId: input.contactId, updatedAtMs: Date.now() })\n }\n await batch.commit()\n }\n return { enrollments: refs.length }\n}\n"],"names":["outreachOrgCollection","BATCH_LIMIT","followOutreachLeadToContact","firestore","input","enrollments","orgId","rows","where","leadId","get","refs","docs","filter","row","String","contactId","map","ref","start","length","batch","slice","update","target","updatedAtMs","Date","now","commit"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAMD,SAASA,qBAAqB,QAAQ,iCAA6B;AAInE,uDAAuD,GACvD,MAAMC,cAAc;AAEpB,uEAAuE,GACvE,OAAO,eAAeC,4BACpBC,SAAoB,EACpBC,KAA0E;IAE1E,MAAMC,cAAcL,sBAAsBG,WAAWC,MAAME,KAAK,EAAE;IAClE,MAAMC,OAAO,MAAMF,YAAYG,KAAK,CAAC,UAAU,MAAMJ,MAAMK,MAAM,EAAEC,GAAG;IACtE,MAAMC,OAAOJ,KAAKK,IAAI,CACnBC,MAAM,CAAC,CAACC;YAAkDA;eAA1CA,IAAIJ,GAAG,CAAC,cAAc,aAAaK,QAAOD,WAAAA,IAAIJ,GAAG,CAAC,wBAARI,WAAwB,QAAQV,MAAMY,SAAS;OACzGC,GAAG,CAAC,CAACH,MAAQA,IAAII,GAAG;IACvB,IAAK,IAAIC,QAAQ,GAAGA,QAAQR,KAAKS,MAAM,EAAED,SAASlB,YAAa;QAC7D,MAAMoB,QAAQlB,UAAUkB,KAAK;QAC7B,KAAK,MAAMH,OAAOP,KAAKW,KAAK,CAACH,OAAOA,QAAQlB,aAAc;YACxDoB,MAAME,MAAM,CAACL,KAAK;gBAAEM,QAAQ;gBAAWR,WAAWZ,MAAMY,SAAS;gBAAES,aAAaC,KAAKC,GAAG;YAAG;QAC7F;QACA,MAAMN,MAAMO,MAAM;IACpB;IACA,OAAO;QAAEvB,aAAaM,KAAKS,MAAM;IAAC;AACpC"}
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import { type OutreachOpenSource } from '../engine/open-source';
17
18
  import { type OutreachOpenJudgement } from '../engine/open-tracking';
18
19
  import { type OutreachEnrollment } from '../model/outreach.types';
19
20
  import type { OutreachOpenTarget } from './click-link';
@@ -34,6 +35,8 @@ import type { OutreachRuntimeDeps } from './runtime-deps';
34
35
  */
35
36
  /** What one recorded open turned out to be. */
36
37
  export interface OutreachOpenOutcome extends OutreachOpenJudgement {
38
+ /** The network the fetch came from (AGL-3488); `null` when no address was read. */
39
+ source: OutreachOpenSource | null;
37
40
  /** Whether it was this person's first human open — the sequence's `uniqueOpens`. */
38
41
  first: boolean;
39
42
  /** The enrollment it was recorded against, or `null` when there is none. */
@@ -52,4 +55,9 @@ export declare function recordOutreachOpen(deps: Pick<OutreachRuntimeDeps, 'fire
52
55
  target: OutreachOpenTarget;
53
56
  method: string;
54
57
  userAgent: string | null | undefined;
58
+ /**
59
+ * The fetch's address (AGL-3488), read for the network it belongs to and
60
+ * dropped: the row keeps the network, never the address.
61
+ */
62
+ address?: string | null;
55
63
  }): Promise<OutreachOpenOutcome>;
@@ -15,7 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { FieldValue } from "firebase-admin/firestore";
18
- import { judgeOutreachOpen, OUTREACH_PROXY_OPEN_REASONS } from "../engine/open-tracking.js";
18
+ import { outreachOpenSourceOf } from "../engine/open-source.js";
19
+ import { judgeOutreachOpen, outreachOpenAgentEvidence, OUTREACH_PROXY_OPEN_REASONS } from "../engine/open-tracking.js";
19
20
  import { outreachLoggedEngagementRows, readOutreachEngagement } from "../model/enrollment-engagement.js";
20
21
  import { outreachHistoryEntryId, outreachOpenHistoryRow } from "../model/enrollment-history.js";
21
22
  import { OUTREACH_ENROLLMENT_HISTORY, OUTREACH_ENROLLMENT_HISTORY_MAX } from "../model/outreach.types.js";
@@ -24,6 +25,7 @@ import { outreachStepSentAtMs } from "./click-events.js";
24
25
  const nothing = ()=>({
25
26
  human: false,
26
27
  machineReason: null,
28
+ source: null,
27
29
  first: false,
28
30
  enrollment: null
29
31
  });
@@ -42,6 +44,7 @@ const nothing = ()=>({
42
44
  const firestore = deps.firestore();
43
45
  const { orgId, enrollmentId, stepIndex } = input.target;
44
46
  const nowMs = deps.now();
47
+ const source = outreachOpenSourceOf(input.address);
45
48
  const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId);
46
49
  const outcome = await firestore.runTransaction(async (transaction)=>{
47
50
  var _held_firstOpenAtMs;
@@ -52,7 +55,8 @@ const nothing = ()=>({
52
55
  const judgement = judgeOutreachOpen({
53
56
  method: input.method,
54
57
  userAgent: input.userAgent,
55
- sinceSentMs: sent === null ? null : nowMs - sent
58
+ sinceSentMs: sent === null ? null : nowMs - sent,
59
+ source
56
60
  });
57
61
  const held = readOutreachEngagement(enrollment.engagement);
58
62
  const first = judgement.human && held.firstOpenAtMs === null;
@@ -76,10 +80,13 @@ const nothing = ()=>({
76
80
  atMs: nowMs,
77
81
  stepIndex,
78
82
  human: judgement.human,
79
- machineReason: judgement.machineReason
83
+ machineReason: judgement.machineReason,
84
+ userAgent: outreachOpenAgentEvidence(input.userAgent),
85
+ source
80
86
  }));
81
87
  }
82
88
  return _extends({}, judgement, {
89
+ source,
83
90
  first,
84
91
  enrollment: _extends({}, enrollment, {
85
92
  engagement
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/open-events.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 { FieldValue } from 'firebase-admin/firestore'\nimport {\n judgeOutreachOpen,\n OUTREACH_PROXY_OPEN_REASONS,\n type OutreachOpenJudgement,\n} from '../engine/open-tracking'\nimport { outreachLoggedEngagementRows, readOutreachEngagement } from '../model/enrollment-engagement'\nimport { outreachHistoryEntryId, outreachOpenHistoryRow } from '../model/enrollment-history'\nimport {\n OUTREACH_ENROLLMENT_HISTORY,\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n type OutreachEnrollment,\n type OutreachEnrollmentEngagement,\n} from '../model/outreach.types'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { outreachStepSentAtMs } from './click-events'\nimport type { OutreachOpenTarget } from './click-link'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * RECORDING AN OPEN (AGL-3395).\n *\n * A fetch of a tracking image, written in the places a click is\n * (`./click-events.ts`) and for the same questions: the enrollment (\"did\n * this person open it?\"), its history (\"when, and was it them?\") and the\n * sequence (\"what is its open rate?\"). There is no per-destination rollup —\n * an image has no destination — and nothing is filed on the person's CRM\n * record: an open is too weak a signal to put in front of a rep as an act,\n * and a proxy's fetch would put one there for nobody.\n *\n * Every write happens after the image has been answered, and a failure costs\n * a number rather than a broken image.\n */\n\n/** What one recorded open turned out to be. */\nexport interface OutreachOpenOutcome extends OutreachOpenJudgement {\n /** Whether it was this person's first human open — the sequence's `uniqueOpens`. */\n first: boolean\n /** The enrollment it was recorded against, or `null` when there is none. */\n enrollment: OutreachEnrollment | null\n}\n\nconst nothing = (): OutreachOpenOutcome => ({ human: false, machineReason: null, first: false, enrollment: null })\n\n/**\n * Records one fetch of a tracking image.\n *\n * In a transaction over the enrollment, as a click is: two fetches at once\n * must not both read `opens` of nought, and whether this is the person's\n * FIRST open is decided from the read that writes it. An enrollment that no\n * longer exists belongs to a person or a workspace already erased, and\n * nothing is recorded for them anywhere.\n */\nexport async function recordOutreachOpen(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now'>,\n input: {\n target: OutreachOpenTarget\n method: string\n userAgent: string | null | undefined\n },\n): Promise<OutreachOpenOutcome> {\n // A test's image (AGL-3325) names no enrollment and counts on nothing.\n if (input.target.test) return nothing()\n const firestore = deps.firestore()\n const { orgId, enrollmentId, stepIndex } = input.target\n const nowMs = deps.now()\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId)\n\n const outcome = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return nothing()\n const sent = outreachStepSentAtMs(enrollment, stepIndex)\n const judgement = judgeOutreachOpen({\n method: input.method,\n userAgent: input.userAgent,\n sinceSentMs: sent === null ? null : nowMs - sent,\n })\n const held = readOutreachEngagement(enrollment.engagement)\n const first = judgement.human && held.firstOpenAtMs === null\n const logged = outreachLoggedEngagementRows(held) < OUTREACH_ENROLLMENT_HISTORY_MAX\n const engagement: OutreachEnrollmentEngagement = judgement.human\n ? {\n ...held,\n opens: held.opens + 1,\n firstOpenAtMs: held.firstOpenAtMs ?? nowMs,\n lastOpenAtMs: nowMs,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n : {\n ...held,\n machineOpens: held.machineOpens + 1,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n // `updatedAtMs` is left alone, as a click leaves it: a proxy fetching an\n // image is not something that happened to the enrollment.\n transaction.update(ref, { engagement })\n if (logged) {\n transaction.set(\n ref.collection(OUTREACH_ENROLLMENT_HISTORY).doc(outreachHistoryEntryId(nowMs)),\n outreachOpenHistoryRow({\n atMs: nowMs,\n stepIndex,\n human: judgement.human,\n machineReason: judgement.machineReason,\n }),\n )\n }\n return { ...judgement, first, enrollment: { ...enrollment, engagement } } as OutreachOpenOutcome\n })\n\n if (outcome.enrollment?.sequenceId) {\n await bumpSequenceOpenStats(firestore, outcome, { orgId, sequenceId: outcome.enrollment.sequenceId, nowMs })\n }\n return outcome\n}\n\n/**\n * The sequence's open counters: blind increments outside the enrollment's\n * transaction, for the reason the click counters are — one sequence holds\n * every recipient, and a transaction over it would serialise them all.\n */\nasync function bumpSequenceOpenStats(\n firestore: FirebaseFirestore.Firestore,\n outcome: OutreachOpenOutcome,\n input: { orgId: string; sequenceId: string; nowMs: number },\n): Promise<void> {\n const proxy = outcome.machineReason !== null && OUTREACH_PROXY_OPEN_REASONS.includes(outcome.machineReason)\n const patch: Record<string, unknown> = outcome.human\n ? {\n 'stats.opens': FieldValue.increment(1),\n 'stats.lastOpenAtMs': input.nowMs,\n ...(outcome.first ? { 'stats.uniqueOpens': FieldValue.increment(1) } : {}),\n }\n : {\n 'stats.machineOpens': FieldValue.increment(1),\n ...(proxy ? { 'stats.proxyOpens': FieldValue.increment(1) } : {}),\n }\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update(patch)\n .catch((error: unknown) => {\n // A sequence deleted while its mail is still out there: the open is\n // on the enrollment, and the aggregate has nowhere to go.\n console.warn('[outreach] an open could not be counted on its sequence', error)\n })\n}\n"],"names":["FieldValue","judgeOutreachOpen","OUTREACH_PROXY_OPEN_REASONS","outreachLoggedEngagementRows","readOutreachEngagement","outreachHistoryEntryId","outreachOpenHistoryRow","OUTREACH_ENROLLMENT_HISTORY","OUTREACH_ENROLLMENT_HISTORY_MAX","outreachOrgCollection","readStoredOutreachEnrollment","outreachStepSentAtMs","nothing","human","machineReason","first","enrollment","recordOutreachOpen","deps","input","outcome","target","test","firestore","orgId","enrollmentId","stepIndex","nowMs","now","ref","doc","runTransaction","transaction","held","snapshot","get","exists","data","undefined","sent","judgement","method","userAgent","sinceSentMs","engagement","firstOpenAtMs","logged","opens","lastOpenAtMs","loggedOpens","machineOpens","update","set","collection","atMs","sequenceId","bumpSequenceOpenStats","proxy","includes","patch","increment","catch","error","console","warn"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/open-events.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 { FieldValue } from 'firebase-admin/firestore'\nimport { outreachOpenSourceOf, type OutreachOpenSource } from '../engine/open-source'\nimport {\n judgeOutreachOpen,\n outreachOpenAgentEvidence,\n OUTREACH_PROXY_OPEN_REASONS,\n type OutreachOpenJudgement,\n} from '../engine/open-tracking'\nimport { outreachLoggedEngagementRows, readOutreachEngagement } from '../model/enrollment-engagement'\nimport { outreachHistoryEntryId, outreachOpenHistoryRow } from '../model/enrollment-history'\nimport {\n OUTREACH_ENROLLMENT_HISTORY,\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n type OutreachEnrollment,\n type OutreachEnrollmentEngagement,\n} from '../model/outreach.types'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { outreachStepSentAtMs } from './click-events'\nimport type { OutreachOpenTarget } from './click-link'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * RECORDING AN OPEN (AGL-3395).\n *\n * A fetch of a tracking image, written in the places a click is\n * (`./click-events.ts`) and for the same questions: the enrollment (\"did\n * this person open it?\"), its history (\"when, and was it them?\") and the\n * sequence (\"what is its open rate?\"). There is no per-destination rollup —\n * an image has no destination — and nothing is filed on the person's CRM\n * record: an open is too weak a signal to put in front of a rep as an act,\n * and a proxy's fetch would put one there for nobody.\n *\n * Every write happens after the image has been answered, and a failure costs\n * a number rather than a broken image.\n */\n\n/** What one recorded open turned out to be. */\nexport interface OutreachOpenOutcome extends OutreachOpenJudgement {\n /** The network the fetch came from (AGL-3488); `null` when no address was read. */\n source: OutreachOpenSource | null\n /** Whether it was this person's first human open — the sequence's `uniqueOpens`. */\n first: boolean\n /** The enrollment it was recorded against, or `null` when there is none. */\n enrollment: OutreachEnrollment | null\n}\n\nconst nothing = (): OutreachOpenOutcome => ({\n human: false,\n machineReason: null,\n source: null,\n first: false,\n enrollment: null,\n})\n\n/**\n * Records one fetch of a tracking image.\n *\n * In a transaction over the enrollment, as a click is: two fetches at once\n * must not both read `opens` of nought, and whether this is the person's\n * FIRST open is decided from the read that writes it. An enrollment that no\n * longer exists belongs to a person or a workspace already erased, and\n * nothing is recorded for them anywhere.\n */\nexport async function recordOutreachOpen(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now'>,\n input: {\n target: OutreachOpenTarget\n method: string\n userAgent: string | null | undefined\n /**\n * The fetch's address (AGL-3488), read for the network it belongs to and\n * dropped: the row keeps the network, never the address.\n */\n address?: string | null\n },\n): Promise<OutreachOpenOutcome> {\n // A test's image (AGL-3325) names no enrollment and counts on nothing.\n if (input.target.test) return nothing()\n const firestore = deps.firestore()\n const { orgId, enrollmentId, stepIndex } = input.target\n const nowMs = deps.now()\n const source = outreachOpenSourceOf(input.address)\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId)\n\n const outcome = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return nothing()\n const sent = outreachStepSentAtMs(enrollment, stepIndex)\n const judgement = judgeOutreachOpen({\n method: input.method,\n userAgent: input.userAgent,\n sinceSentMs: sent === null ? null : nowMs - sent,\n source,\n })\n const held = readOutreachEngagement(enrollment.engagement)\n const first = judgement.human && held.firstOpenAtMs === null\n const logged = outreachLoggedEngagementRows(held) < OUTREACH_ENROLLMENT_HISTORY_MAX\n const engagement: OutreachEnrollmentEngagement = judgement.human\n ? {\n ...held,\n opens: held.opens + 1,\n firstOpenAtMs: held.firstOpenAtMs ?? nowMs,\n lastOpenAtMs: nowMs,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n : {\n ...held,\n machineOpens: held.machineOpens + 1,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n // `updatedAtMs` is left alone, as a click leaves it: a proxy fetching an\n // image is not something that happened to the enrollment.\n transaction.update(ref, { engagement })\n if (logged) {\n transaction.set(\n ref.collection(OUTREACH_ENROLLMENT_HISTORY).doc(outreachHistoryEntryId(nowMs)),\n outreachOpenHistoryRow({\n atMs: nowMs,\n stepIndex,\n human: judgement.human,\n machineReason: judgement.machineReason,\n userAgent: outreachOpenAgentEvidence(input.userAgent),\n source,\n }),\n )\n }\n return { ...judgement, source, first, enrollment: { ...enrollment, engagement } } as OutreachOpenOutcome\n })\n\n if (outcome.enrollment?.sequenceId) {\n await bumpSequenceOpenStats(firestore, outcome, { orgId, sequenceId: outcome.enrollment.sequenceId, nowMs })\n }\n return outcome\n}\n\n/**\n * The sequence's open counters: blind increments outside the enrollment's\n * transaction, for the reason the click counters are — one sequence holds\n * every recipient, and a transaction over it would serialise them all.\n */\nasync function bumpSequenceOpenStats(\n firestore: FirebaseFirestore.Firestore,\n outcome: OutreachOpenOutcome,\n input: { orgId: string; sequenceId: string; nowMs: number },\n): Promise<void> {\n const proxy = outcome.machineReason !== null && OUTREACH_PROXY_OPEN_REASONS.includes(outcome.machineReason)\n const patch: Record<string, unknown> = outcome.human\n ? {\n 'stats.opens': FieldValue.increment(1),\n 'stats.lastOpenAtMs': input.nowMs,\n ...(outcome.first ? { 'stats.uniqueOpens': FieldValue.increment(1) } : {}),\n }\n : {\n 'stats.machineOpens': FieldValue.increment(1),\n ...(proxy ? { 'stats.proxyOpens': FieldValue.increment(1) } : {}),\n }\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update(patch)\n .catch((error: unknown) => {\n // A sequence deleted while its mail is still out there: the open is\n // on the enrollment, and the aggregate has nowhere to go.\n console.warn('[outreach] an open could not be counted on its sequence', error)\n })\n}\n"],"names":["FieldValue","outreachOpenSourceOf","judgeOutreachOpen","outreachOpenAgentEvidence","OUTREACH_PROXY_OPEN_REASONS","outreachLoggedEngagementRows","readOutreachEngagement","outreachHistoryEntryId","outreachOpenHistoryRow","OUTREACH_ENROLLMENT_HISTORY","OUTREACH_ENROLLMENT_HISTORY_MAX","outreachOrgCollection","readStoredOutreachEnrollment","outreachStepSentAtMs","nothing","human","machineReason","source","first","enrollment","recordOutreachOpen","deps","input","outcome","target","test","firestore","orgId","enrollmentId","stepIndex","nowMs","now","address","ref","doc","runTransaction","transaction","held","snapshot","get","exists","data","undefined","sent","judgement","method","userAgent","sinceSentMs","engagement","firstOpenAtMs","logged","opens","lastOpenAtMs","loggedOpens","machineOpens","update","set","collection","atMs","sequenceId","bumpSequenceOpenStats","proxy","includes","patch","increment","catch","error","console","warn"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,2BAA0B;AACrD,SAASC,oBAAoB,QAAiC,2BAAuB;AACrF,SACEC,iBAAiB,EACjBC,yBAAyB,EACzBC,2BAA2B,QAEtB,6BAAyB;AAChC,SAASC,4BAA4B,EAAEC,sBAAsB,QAAQ,oCAAgC;AACrG,SAASC,sBAAsB,EAAEC,sBAAsB,QAAQ,iCAA6B;AAC5F,SACEC,2BAA2B,EAC3BC,+BAA+B,QAG1B,6BAAyB;AAChC,SAASC,qBAAqB,EAAEC,4BAA4B,QAAQ,iCAA6B;AACjG,SAASC,oBAAoB,QAAQ,oBAAgB;AA6BrD,MAAMC,UAAU,IAA4B,CAAA;QAC1CC,OAAO;QACPC,eAAe;QACfC,QAAQ;QACRC,OAAO;QACPC,YAAY;IACd,CAAA;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,mBACpBC,IAAoD,EACpDC,KASC;QAwDGC;IAtDJ,uEAAuE;IACvE,IAAID,MAAME,MAAM,CAACC,IAAI,EAAE,OAAOX;IAC9B,MAAMY,YAAYL,KAAKK,SAAS;IAChC,MAAM,EAAEC,KAAK,EAAEC,YAAY,EAAEC,SAAS,EAAE,GAAGP,MAAME,MAAM;IACvD,MAAMM,QAAQT,KAAKU,GAAG;IACtB,MAAMd,SAAShB,qBAAqBqB,MAAMU,OAAO;IACjD,MAAMC,MAAMtB,sBAAsBe,WAAWC,OAAO,eAAeO,GAAG,CAACN;IAEvE,MAAML,UAAU,MAAMG,UAAUS,cAAc,CAAC,OAAOC;YAkB/BC;QAjBrB,MAAMC,WAAW,MAAMF,YAAYG,GAAG,CAACN;QACvC,MAAMd,aAAaP,6BAA6BgB,cAAcU,SAASE,MAAM,GAAGF,SAASG,IAAI,KAAKC;QAClG,IAAI,CAACvB,YAAY,OAAOL;QACxB,MAAM6B,OAAO9B,qBAAqBM,YAAYU;QAC9C,MAAMe,YAAY1C,kBAAkB;YAClC2C,QAAQvB,MAAMuB,MAAM;YACpBC,WAAWxB,MAAMwB,SAAS;YAC1BC,aAAaJ,SAAS,OAAO,OAAOb,QAAQa;YAC5C1B;QACF;QACA,MAAMoB,OAAO/B,uBAAuBa,WAAW6B,UAAU;QACzD,MAAM9B,QAAQ0B,UAAU7B,KAAK,IAAIsB,KAAKY,aAAa,KAAK;QACxD,MAAMC,SAAS7C,6BAA6BgC,QAAQ3B;QACpD,MAAMsC,aAA2CJ,UAAU7B,KAAK,GAC5D,aACKsB;YACHc,OAAOd,KAAKc,KAAK,GAAG;YACpBF,aAAa,GAAEZ,sBAAAA,KAAKY,aAAa,YAAlBZ,sBAAsBP;YACrCsB,cAActB;YACduB,aAAahB,KAAKgB,WAAW,GAAIH,CAAAA,SAAS,IAAI,CAAA;aAEhD,aACKb;YACHiB,cAAcjB,KAAKiB,YAAY,GAAG;YAClCD,aAAahB,KAAKgB,WAAW,GAAIH,CAAAA,SAAS,IAAI,CAAA;;QAEpD,yEAAyE;QACzE,0DAA0D;QAC1Dd,YAAYmB,MAAM,CAACtB,KAAK;YAAEe;QAAW;QACrC,IAAIE,QAAQ;YACVd,YAAYoB,GAAG,CACbvB,IAAIwB,UAAU,CAAChD,6BAA6ByB,GAAG,CAAC3B,uBAAuBuB,SACvEtB,uBAAuB;gBACrBkD,MAAM5B;gBACND;gBACAd,OAAO6B,UAAU7B,KAAK;gBACtBC,eAAe4B,UAAU5B,aAAa;gBACtC8B,WAAW3C,0BAA0BmB,MAAMwB,SAAS;gBACpD7B;YACF;QAEJ;QACA,OAAO,aAAK2B;YAAW3B;YAAQC;YAAOC,YAAY,aAAKA;gBAAY6B;;;IACrE;IAEA,KAAIzB,sBAAAA,QAAQJ,UAAU,qBAAlBI,oBAAoBoC,UAAU,EAAE;QAClC,MAAMC,sBAAsBlC,WAAWH,SAAS;YAAEI;YAAOgC,YAAYpC,QAAQJ,UAAU,CAACwC,UAAU;YAAE7B;QAAM;IAC5G;IACA,OAAOP;AACT;AAEA;;;;CAIC,GACD,eAAeqC,sBACblC,SAAsC,EACtCH,OAA4B,EAC5BD,KAA2D;IAE3D,MAAMuC,QAAQtC,QAAQP,aAAa,KAAK,QAAQZ,4BAA4B0D,QAAQ,CAACvC,QAAQP,aAAa;IAC1G,MAAM+C,QAAiCxC,QAAQR,KAAK,GAChD;QACE,eAAef,WAAWgE,SAAS,CAAC;QACpC,sBAAsB1C,MAAMQ,KAAK;OAC7BP,QAAQL,KAAK,GAAG;QAAE,qBAAqBlB,WAAWgE,SAAS,CAAC;IAAG,IAAI,CAAC,KAE1E;QACE,sBAAsBhE,WAAWgE,SAAS,CAAC;OACvCH,QAAQ;QAAE,oBAAoB7D,WAAWgE,SAAS,CAAC;IAAG,IAAI,CAAC;IAErE,MAAMrD,sBAAsBe,WAAWJ,MAAMK,KAAK,EAAE,aACjDO,GAAG,CAACZ,MAAMqC,UAAU,EACpBJ,MAAM,CAACQ,OACPE,KAAK,CAAC,CAACC;QACN,oEAAoE;QACpE,0DAA0D;QAC1DC,QAAQC,IAAI,CAAC,2DAA2DF;IAC1E;AACJ"}
@@ -18,12 +18,16 @@ import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
18
18
  import type { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
19
19
  import type { OutreachCampaignCredit, OutreachRuntimeDeps } from './runtime-deps';
20
20
  /**
21
- * The platform's campaign attribution, for what a sequence produced
22
- * (AGL-3254): the same join every form, booking and campaign email is
23
- * credited through, so a sequence's numbers and a campaign send's are two
24
- * readings of one rule. Each writer already never throws.
21
+ * What a sequence produced (AGL-3254), handed to whichever plugin credits
22
+ * outcomes (`plugin-conversion-credit`): the same contract every form,
23
+ * booking and campaign email is credited through, so a sequence's numbers
24
+ * and a campaign send's are two readings of one rule. A workspace whose
25
+ * plugins credit nothing credits nothing here either; the seam never throws.
26
+ *
27
+ * The campaign a sequence is filed under is what its mail is credited to,
28
+ * and the sequence and the enrollment ride with it as the mail's own facts.
25
29
  */
26
- export declare function platformOutreachCampaignCredit(firestore: () => FirebaseFirestore.Firestore): OutreachCampaignCredit;
30
+ export declare function platformOutreachCampaignCredit(): OutreachCampaignCredit;
27
31
  /**
28
32
  * The platform's own reach for the sending runtime (AGL-2981). Loaded only
29
33
  * when a job runs or the unsubscribe route answers — never at boot — so the
@@ -18,8 +18,7 @@ import { platformMailDnsResolver } from "@aglyn/tenant-data-admin/server/email-d
18
18
  import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
19
19
  import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
20
20
  import { pluginRecordTimelineWriter } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
21
- import { attributeCampaignConversion, creditCampaignSequenceOutcome } from "@aglyn/tenant-data-admin/server/campaign-conversion-attribution";
22
- import { recordEmailCampaignTouch } from "@aglyn/tenant-data-admin/server/email-delivery-log";
21
+ import { creditConversion, creditConversionOutcome, recordConversionClick } from "@aglyn/aglyn/plugin-manager/plugin-conversion-credit";
23
22
  import { suppressEmail } from "@aglyn/tenant-data-admin/server/email-suppression";
24
23
  import { recordTopicOptOut } from "@aglyn/tenant-data-admin/server/topic-subscriptions";
25
24
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
@@ -41,45 +40,53 @@ import { createOutreachPersonEraser } from "./person-erasure.js";
41
40
  import { outreachUnsubscribeUrl } from "./unsubscribe-link.js";
42
41
  import { createOutreachUnsubscribeRoute } from "./unsubscribe-route.js";
43
42
  /**
44
- * The platform's campaign attribution, for what a sequence produced
45
- * (AGL-3254): the same join every form, booking and campaign email is
46
- * credited through, so a sequence's numbers and a campaign send's are two
47
- * readings of one rule. Each writer already never throws.
48
- */ export function platformOutreachCampaignCredit(firestore) {
43
+ * What a sequence produced (AGL-3254), handed to whichever plugin credits
44
+ * outcomes (`plugin-conversion-credit`): the same contract every form,
45
+ * booking and campaign email is credited through, so a sequence's numbers
46
+ * and a campaign send's are two readings of one rule. A workspace whose
47
+ * plugins credit nothing credits nothing here either; the seam never throws.
48
+ *
49
+ * The campaign a sequence is filed under is what its mail is credited to,
50
+ * and the sequence and the enrollment ride with it as the mail's own facts.
51
+ */ export function platformOutreachCampaignCredit() {
49
52
  return {
50
53
  async credit ({ hostId, orgId, campaignIds, outcome, atMs }) {
51
- await creditCampaignSequenceOutcome({
54
+ await creditConversionOutcome({
52
55
  hostId,
53
56
  orgId,
54
- campaignIds,
57
+ containerIds: campaignIds,
55
58
  outcome,
56
59
  atMs
57
- }, firestore());
60
+ });
58
61
  },
59
62
  async attributeRecord ({ hostId, kind, refId, campaignId, sequenceId, enrollmentId, atMs }) {
60
- await attributeCampaignConversion({
63
+ await creditConversion({
61
64
  hostId,
62
65
  kind,
63
66
  refId,
64
- touch: {
65
- channel: 'sequence',
66
- campaignId,
67
- sequenceId,
68
- enrollmentId,
69
- touchedAtMs: atMs
67
+ click: {
68
+ hostId,
69
+ creditTo: campaignId,
70
+ atMs,
71
+ via: {
72
+ sequenceId,
73
+ enrollmentId
74
+ }
70
75
  },
71
76
  convertedAtMs: atMs
72
- }, firestore());
77
+ });
73
78
  },
74
79
  async recordTouch ({ hostId, email, campaignId, sequenceId, enrollmentId, atMs }) {
75
- await recordEmailCampaignTouch({
80
+ await recordConversionClick({
76
81
  email,
77
82
  hostId,
78
- campaignId,
83
+ creditTo: campaignId,
79
84
  atMs,
80
- sequenceId,
81
- enrollmentId
82
- }, firestore());
85
+ via: {
86
+ sequenceId,
87
+ enrollmentId
88
+ }
89
+ });
83
90
  }
84
91
  };
85
92
  }
@@ -93,7 +100,7 @@ import { createOutreachUnsubscribeRoute } from "./unsubscribe-route.js";
93
100
  firestore,
94
101
  now: Date.now,
95
102
  random: Math.random,
96
- campaignCredit: platformOutreachCampaignCredit(firestore),
103
+ campaignCredit: platformOutreachCampaignCredit(),
97
104
  openMailbox: (mailboxId)=>openOutreachMailboxClient(firestore(), {
98
105
  mailboxId
99
106
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/platform-runtime-deps.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 { SALES_TOPIC_ID } from '../model/sales-topic'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n attributeCampaignConversion,\n creditCampaignSequenceOutcome,\n} from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution'\nimport { recordEmailCampaignTouch } from '@aglyn/tenant-data-admin/server/email-delivery-log'\nimport { suppressEmail } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { recordTopicOptOut } from '@aglyn/tenant-data-admin/server/topic-subscriptions'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { sendOrgMemberNotice } from '@aglyn/tenant-data-admin/server/org-member-notice'\nimport { logOrgActivity } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveTrackingLinkOrigin } from '@aglyn/tenant-data-admin/server/tracking-hosts'\nimport { sendingWorkspaceFor } from '@aglyn/tenant-data-admin/server/outbound-send-review'\nimport { screenTenantMessage } from '@aglyn/shared-util-email/outbound-screen-gate'\nimport { stampRecordEmailState } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport { composeOutreachMailboxNotice } from '../engine/mailbox-notice'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { outreachShortLinkUrl } from './click-link'\nimport { createOutreachClickRoute, createOutreachShortLinkRoute } from './click-route'\nimport { createOutreachPersonEraser } from './person-erasure'\nimport type { OutreachCampaignCredit, OutreachRuntimeDeps } from './runtime-deps'\nimport { outreachUnsubscribeUrl } from './unsubscribe-link'\nimport { createOutreachUnsubscribeRoute } from './unsubscribe-route'\n\n/**\n * The platform's campaign attribution, for what a sequence produced\n * (AGL-3254): the same join every form, booking and campaign email is\n * credited through, so a sequence's numbers and a campaign send's are two\n * readings of one rule. Each writer already never throws.\n */\nexport function platformOutreachCampaignCredit(\n firestore: () => FirebaseFirestore.Firestore,\n): OutreachCampaignCredit {\n return {\n async credit({ hostId, orgId, campaignIds, outcome, atMs }) {\n await creditCampaignSequenceOutcome({ hostId, orgId, campaignIds, outcome, atMs }, firestore())\n },\n async attributeRecord({ hostId, kind, refId, campaignId, sequenceId, enrollmentId, atMs }) {\n await attributeCampaignConversion(\n {\n hostId,\n kind,\n refId,\n touch: { channel: 'sequence', campaignId, sequenceId, enrollmentId, touchedAtMs: atMs },\n convertedAtMs: atMs,\n },\n firestore(),\n )\n },\n async recordTouch({ hostId, email, campaignId, sequenceId, enrollmentId, atMs }) {\n await recordEmailCampaignTouch({ email, hostId, campaignId, atMs, sequenceId, enrollmentId }, firestore())\n },\n }\n}\n\n/**\n * The platform's own reach for the sending runtime (AGL-2981). Loaded only\n * when a job runs or the unsubscribe route answers — never at boot — so the\n * console pays for these modules when Outreach does work.\n */\nexport function platformOutreachRuntimeDeps(): OutreachRuntimeDeps {\n const firestore = () => firebaseAdmin.app().firestore()\n return {\n firestore,\n now: Date.now,\n random: Math.random,\n campaignCredit: platformOutreachCampaignCredit(firestore),\n openMailbox: (mailboxId) => openOutreachMailboxClient(firestore(), { mailboxId }),\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n async orgRefusal(orgId, org) {\n if (!isPluginEnabled(org as { enabledPlugins?: string[] }, OUTREACH_PLUGIN_ID)) return 'plugin-disabled'\n if (!checkEntitlement(org, 'outreach')) return 'not-entitled'\n const released = await filterEnabledPluginsByReleaseFlags([OUTREACH_PLUGIN_ID], { orgId })\n return released.length ? null : 'not-released'\n },\n async sendRefusal({ org, uid }) {\n const locked = await getLockdownVerdict({ org, uid, intent: 'write' })\n return locked ? `locked:${locked.scope}` : null\n },\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n logOrgActivity: (orgId, actor, action, target) => logOrgActivity(orgId, actor, action, target),\n async optOutOfSalesTopic({ hostId, email }) {\n await recordTopicOptOut(hostId, email, SALES_TOPIC_ID, { firestore: firestore() })\n },\n async suppressBouncedEmail({ email, hostId }) {\n // The runtime stamps the record itself, with the richer verdict — the\n // domain block, the enrollment — so the list's own stamp stands down.\n await suppressEmail({ email, reason: 'bounce', context: 'outreach', hostId, stampRecord: false, firestore: firestore() })\n },\n async stampRecordEmailState(stamp) {\n await stampRecordEmailState(stamp)\n },\n async notifyMailboxOwner(notice) {\n // The Mailboxes page, by the organization's slug — the page Resume and\n // Reconnect live on (AGL-3244). Without a slug or a console origin the\n // notice still goes, and says where the page is instead.\n const org = await firestore().collection('orgs').doc(notice.orgId).get()\n const slug = typeof org.get('slug') === 'string' ? String(org.get('slug')) : ''\n const origin = canonicalConsoleOrigin()\n const mailboxesUrl = slug && origin ? `${origin}/${encodeURIComponent(slug)}/outreach/mailboxes` : null\n const email = composeOutreachMailboxNotice(notice, { mailboxesUrl })\n const result = await sendOrgMemberNotice({\n orgId: notice.orgId,\n uids: [notice.connectedByUid],\n includeAdmins: true,\n subject: email.subject,\n text: email.text,\n context: 'outreach-mailbox-notice',\n })\n if (!result.sent) console.warn(`[outreach] the mailbox owner was not emailed: ${result.reason}`)\n },\n unsubscribeUrl: (target) => outreachUnsubscribeUrl({ origin: canonicalConsoleOrigin(), target }),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n // The sending domain's own `links.` host once the organization has\n // verified it (AGL-3306). A static import like every other\n // `tenant-data-admin` module here: a dynamic one marks the whole library\n // lazy-loaded, and `@nx/enforce-module-boundaries` then refuses every\n // static import of it across the plugin and the console.\n clickLinkOrigin({ orgId, senderAddress }) {\n return resolveTrackingLinkOrigin(firestore(), orgId, senderAddress)\n },\n // The one phishing screen every tenant message passes (AGL-3356). The\n // review module's import installs its store, so a hold files the same\n // abuse-queue row a held newsletter or campaign does.\n async screenMessage({ orgId, org, hostId, host, subject, text, fromName, fromAddress }) {\n const refusal = await screenTenantMessage(\n {\n workspace: sendingWorkspaceFor({ hostId, orgId, org, host }),\n subject,\n fromName,\n fromAddress: fromAddress ?? null,\n bodies: [text],\n context: 'outreach sequence',\n },\n 'outreach email',\n )\n return refusal?.detail ?? null\n },\n }\n}\n\n/** The unsubscribe route on the platform's own reach. */\nexport function platformOutreachUnsubscribeRoute(): PluginWebApiHandler {\n return createOutreachUnsubscribeRoute(platformOutreachRuntimeDeps())\n}\n\n/** The click-tracking redirect on the platform's own reach (AGL-3239). */\nexport function platformOutreachClickRoute(): PluginWebApiHandler {\n return createOutreachClickRoute(platformOutreachRuntimeDeps())\n}\n\n/** The short tracking link's redirect on the platform's own reach (AGL-3297). */\nexport function platformOutreachShortLinkRoute(): PluginWebApiHandler {\n return createOutreachShortLinkRoute(platformOutreachRuntimeDeps())\n}\n\n/** The person eraser on the platform's own Firestore. */\nexport function platformOutreachPersonEraser(): PluginPersonEraser {\n return createOutreachPersonEraser({ firestore: () => firebaseAdmin.app().firestore() })\n}\n"],"names":["SALES_TOPIC_ID","platformMailDnsResolver","checkEntitlement","isPluginEnabled","pluginRecordTimelineWriter","attributeCampaignConversion","creditCampaignSequenceOutcome","recordEmailCampaignTouch","suppressEmail","recordTopicOptOut","firebaseAdmin","getLockdownVerdict","sendOrgMemberNotice","logOrgActivity","filterEnabledPluginsByReleaseFlags","resolveTrackingLinkOrigin","sendingWorkspaceFor","screenTenantMessage","stampRecordEmailState","OUTREACH_PLUGIN_ID","composeOutreachMailboxNotice","openOutreachMailboxClient","canonicalConsoleOrigin","outreachShortLinkUrl","createOutreachClickRoute","createOutreachShortLinkRoute","createOutreachPersonEraser","outreachUnsubscribeUrl","createOutreachUnsubscribeRoute","platformOutreachCampaignCredit","firestore","credit","hostId","orgId","campaignIds","outcome","atMs","attributeRecord","kind","refId","campaignId","sequenceId","enrollmentId","touch","channel","touchedAtMs","convertedAtMs","recordTouch","email","platformOutreachRuntimeDeps","app","now","Date","random","Math","campaignCredit","openMailbox","mailboxId","resolveMx","domain","resolveAddress","Promise","resolve","orgRefusal","org","released","length","sendRefusal","uid","locked","intent","scope","timeline","writer","actor","action","target","optOutOfSalesTopic","suppressBouncedEmail","reason","context","stampRecord","stamp","notifyMailboxOwner","notice","collection","doc","get","slug","String","origin","mailboxesUrl","encodeURIComponent","result","uids","connectedByUid","includeAdmins","subject","text","sent","console","warn","unsubscribeUrl","clickLinkUrl","linkId","trackingOrigin","clickLinkOrigin","senderAddress","screenMessage","host","fromName","fromAddress","refusal","workspace","bodies","detail","platformOutreachUnsubscribeRoute","platformOutreachClickRoute","platformOutreachShortLinkRoute","platformOutreachPersonEraser"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/platform-runtime-deps.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 { SALES_TOPIC_ID } from '../model/sales-topic'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n creditConversion,\n creditConversionOutcome,\n recordConversionClick,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { suppressEmail } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { recordTopicOptOut } from '@aglyn/tenant-data-admin/server/topic-subscriptions'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { sendOrgMemberNotice } from '@aglyn/tenant-data-admin/server/org-member-notice'\nimport { logOrgActivity } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveTrackingLinkOrigin } from '@aglyn/tenant-data-admin/server/tracking-hosts'\nimport { sendingWorkspaceFor } from '@aglyn/tenant-data-admin/server/outbound-send-review'\nimport { screenTenantMessage } from '@aglyn/shared-util-email/outbound-screen-gate'\nimport { stampRecordEmailState } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport { composeOutreachMailboxNotice } from '../engine/mailbox-notice'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { outreachShortLinkUrl } from './click-link'\nimport { createOutreachClickRoute, createOutreachShortLinkRoute } from './click-route'\nimport { createOutreachPersonEraser } from './person-erasure'\nimport type { OutreachCampaignCredit, OutreachRuntimeDeps } from './runtime-deps'\nimport { outreachUnsubscribeUrl } from './unsubscribe-link'\nimport { createOutreachUnsubscribeRoute } from './unsubscribe-route'\n\n/**\n * What a sequence produced (AGL-3254), handed to whichever plugin credits\n * outcomes (`plugin-conversion-credit`): the same contract every form,\n * booking and campaign email is credited through, so a sequence's numbers\n * and a campaign send's are two readings of one rule. A workspace whose\n * plugins credit nothing credits nothing here either; the seam never throws.\n *\n * The campaign a sequence is filed under is what its mail is credited to,\n * and the sequence and the enrollment ride with it as the mail's own facts.\n */\nexport function platformOutreachCampaignCredit(): OutreachCampaignCredit {\n return {\n async credit({ hostId, orgId, campaignIds, outcome, atMs }) {\n await creditConversionOutcome({ hostId, orgId, containerIds: campaignIds, outcome, atMs })\n },\n async attributeRecord({ hostId, kind, refId, campaignId, sequenceId, enrollmentId, atMs }) {\n await creditConversion({\n hostId,\n kind,\n refId,\n click: { hostId, creditTo: campaignId, atMs, via: { sequenceId, enrollmentId } },\n convertedAtMs: atMs,\n })\n },\n async recordTouch({ hostId, email, campaignId, sequenceId, enrollmentId, atMs }) {\n await recordConversionClick({\n email,\n hostId,\n creditTo: campaignId,\n atMs,\n via: { sequenceId, enrollmentId },\n })\n },\n }\n}\n\n/**\n * The platform's own reach for the sending runtime (AGL-2981). Loaded only\n * when a job runs or the unsubscribe route answers — never at boot — so the\n * console pays for these modules when Outreach does work.\n */\nexport function platformOutreachRuntimeDeps(): OutreachRuntimeDeps {\n const firestore = () => firebaseAdmin.app().firestore()\n return {\n firestore,\n now: Date.now,\n random: Math.random,\n campaignCredit: platformOutreachCampaignCredit(),\n openMailbox: (mailboxId) => openOutreachMailboxClient(firestore(), { mailboxId }),\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n async orgRefusal(orgId, org) {\n if (!isPluginEnabled(org as { enabledPlugins?: string[] }, OUTREACH_PLUGIN_ID)) return 'plugin-disabled'\n if (!checkEntitlement(org, 'outreach')) return 'not-entitled'\n const released = await filterEnabledPluginsByReleaseFlags([OUTREACH_PLUGIN_ID], { orgId })\n return released.length ? null : 'not-released'\n },\n async sendRefusal({ org, uid }) {\n const locked = await getLockdownVerdict({ org, uid, intent: 'write' })\n return locked ? `locked:${locked.scope}` : null\n },\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n logOrgActivity: (orgId, actor, action, target) => logOrgActivity(orgId, actor, action, target),\n async optOutOfSalesTopic({ hostId, email }) {\n await recordTopicOptOut(hostId, email, SALES_TOPIC_ID, { firestore: firestore() })\n },\n async suppressBouncedEmail({ email, hostId }) {\n // The runtime stamps the record itself, with the richer verdict — the\n // domain block, the enrollment — so the list's own stamp stands down.\n await suppressEmail({ email, reason: 'bounce', context: 'outreach', hostId, stampRecord: false, firestore: firestore() })\n },\n async stampRecordEmailState(stamp) {\n await stampRecordEmailState(stamp)\n },\n async notifyMailboxOwner(notice) {\n // The Mailboxes page, by the organization's slug — the page Resume and\n // Reconnect live on (AGL-3244). Without a slug or a console origin the\n // notice still goes, and says where the page is instead.\n const org = await firestore().collection('orgs').doc(notice.orgId).get()\n const slug = typeof org.get('slug') === 'string' ? String(org.get('slug')) : ''\n const origin = canonicalConsoleOrigin()\n const mailboxesUrl = slug && origin ? `${origin}/${encodeURIComponent(slug)}/outreach/mailboxes` : null\n const email = composeOutreachMailboxNotice(notice, { mailboxesUrl })\n const result = await sendOrgMemberNotice({\n orgId: notice.orgId,\n uids: [notice.connectedByUid],\n includeAdmins: true,\n subject: email.subject,\n text: email.text,\n context: 'outreach-mailbox-notice',\n })\n if (!result.sent) console.warn(`[outreach] the mailbox owner was not emailed: ${result.reason}`)\n },\n unsubscribeUrl: (target) => outreachUnsubscribeUrl({ origin: canonicalConsoleOrigin(), target }),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n // The sending domain's own `links.` host once the organization has\n // verified it (AGL-3306). A static import like every other\n // `tenant-data-admin` module here: a dynamic one marks the whole library\n // lazy-loaded, and `@nx/enforce-module-boundaries` then refuses every\n // static import of it across the plugin and the console.\n clickLinkOrigin({ orgId, senderAddress }) {\n return resolveTrackingLinkOrigin(firestore(), orgId, senderAddress)\n },\n // The one phishing screen every tenant message passes (AGL-3356). The\n // review module's import installs its store, so a hold files the same\n // abuse-queue row a held newsletter or campaign does.\n async screenMessage({ orgId, org, hostId, host, subject, text, fromName, fromAddress }) {\n const refusal = await screenTenantMessage(\n {\n workspace: sendingWorkspaceFor({ hostId, orgId, org, host }),\n subject,\n fromName,\n fromAddress: fromAddress ?? null,\n bodies: [text],\n context: 'outreach sequence',\n },\n 'outreach email',\n )\n return refusal?.detail ?? null\n },\n }\n}\n\n/** The unsubscribe route on the platform's own reach. */\nexport function platformOutreachUnsubscribeRoute(): PluginWebApiHandler {\n return createOutreachUnsubscribeRoute(platformOutreachRuntimeDeps())\n}\n\n/** The click-tracking redirect on the platform's own reach (AGL-3239). */\nexport function platformOutreachClickRoute(): PluginWebApiHandler {\n return createOutreachClickRoute(platformOutreachRuntimeDeps())\n}\n\n/** The short tracking link's redirect on the platform's own reach (AGL-3297). */\nexport function platformOutreachShortLinkRoute(): PluginWebApiHandler {\n return createOutreachShortLinkRoute(platformOutreachRuntimeDeps())\n}\n\n/** The person eraser on the platform's own Firestore. */\nexport function platformOutreachPersonEraser(): PluginPersonEraser {\n return createOutreachPersonEraser({ firestore: () => firebaseAdmin.app().firestore() })\n}\n"],"names":["SALES_TOPIC_ID","platformMailDnsResolver","checkEntitlement","isPluginEnabled","pluginRecordTimelineWriter","creditConversion","creditConversionOutcome","recordConversionClick","suppressEmail","recordTopicOptOut","firebaseAdmin","getLockdownVerdict","sendOrgMemberNotice","logOrgActivity","filterEnabledPluginsByReleaseFlags","resolveTrackingLinkOrigin","sendingWorkspaceFor","screenTenantMessage","stampRecordEmailState","OUTREACH_PLUGIN_ID","composeOutreachMailboxNotice","openOutreachMailboxClient","canonicalConsoleOrigin","outreachShortLinkUrl","createOutreachClickRoute","createOutreachShortLinkRoute","createOutreachPersonEraser","outreachUnsubscribeUrl","createOutreachUnsubscribeRoute","platformOutreachCampaignCredit","credit","hostId","orgId","campaignIds","outcome","atMs","containerIds","attributeRecord","kind","refId","campaignId","sequenceId","enrollmentId","click","creditTo","via","convertedAtMs","recordTouch","email","platformOutreachRuntimeDeps","firestore","app","now","Date","random","Math","campaignCredit","openMailbox","mailboxId","resolveMx","domain","resolveAddress","Promise","resolve","orgRefusal","org","released","length","sendRefusal","uid","locked","intent","scope","timeline","writer","actor","action","target","optOutOfSalesTopic","suppressBouncedEmail","reason","context","stampRecord","stamp","notifyMailboxOwner","notice","collection","doc","get","slug","String","origin","mailboxesUrl","encodeURIComponent","result","uids","connectedByUid","includeAdmins","subject","text","sent","console","warn","unsubscribeUrl","clickLinkUrl","linkId","trackingOrigin","clickLinkOrigin","senderAddress","screenMessage","host","fromName","fromAddress","refusal","workspace","bodies","detail","platformOutreachUnsubscribeRoute","platformOutreachClickRoute","platformOutreachShortLinkRoute","platformOutreachPersonEraser"],"mappings":"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"}
@@ -15,8 +15,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
18
- import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
19
- import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
20
18
  import { FieldValue } from "firebase-admin/firestore";
21
19
  import { randomUUID } from "node:crypto";
22
20
  import { composeOutreachEmail } from "../engine/compose.js";
@@ -25,6 +23,7 @@ import { planOutreachStepCompletion } from "../engine/enrollment-state.js";
25
23
  import { evaluateOutreachGates } from "../engine/gates.js";
26
24
  import { outreachSentToday, outreachTickAllowance, selectDueOutreachEnrollments } from "../engine/sending-capacity.js";
27
25
  import { readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
26
+ import { readOutreachTemplateBody } from "../enrollment/message-templates.js";
28
27
  import { readOutreachGateLookups } from "../enrollment/gate-lookups.js";
29
28
  import { mailboxRef } from "../mailboxes/mailbox-credentials.js";
30
29
  import { outreachEffectiveDailyCap, outreachLocalDay } from "../mailboxes/mailbox-settings.js";
@@ -40,7 +39,7 @@ import { creditOutreachFirstSend } from "./campaign-credit.js";
40
39
  import { newOutreachLinkId, outreachStoredLink, outreachStoredOpen } from "./click-link.js";
41
40
  import { applyOutreachEvent } from "./enrollment-events.js";
42
41
  import { outreachSiteHeld } from "./host-suspension.js";
43
- import { markOutreachLeadNurturing } from "./lead-records.js";
42
+ import { stampRecordEmailReach } from "@aglyn/aglyn/plugin-manager/plugin-record-email-state";
44
43
  import { outreachSendDigest, withRecentSend } from "./mailbox-health-store.js";
45
44
  import { noteOutreachMailboxReconnectRequired } from "./mailbox-notices.js";
46
45
  import { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from "./timeline.js";
@@ -261,7 +260,7 @@ async function runMailbox(deps, firestore, input) {
261
260
  // Each by the record it names — the contact, or the lead while the
262
261
  // person is one (AGL-3234) — keyed by that record's id.
263
262
  const [candidatesRead, lookupsRead, host] = await Promise.all([
264
- readOutreachEnrollCandidates(firestore, {
263
+ readOutreachEnrollCandidates({
265
264
  orgId,
266
265
  hostId,
267
266
  contactGroupId,
@@ -608,11 +607,10 @@ async function openClient(deps, run) {
608
607
  claimMessageId: null
609
608
  });
610
609
  }
611
- /** A CRM template's body, read once per run. */ async function templateBody(firestore, run, templateId) {
610
+ /** A template's body, read through the record system once per run. */ async function templateBody(run, templateId) {
612
611
  var _run_templates_get;
613
612
  if (!run.templates.has(templateId)) {
614
- const snapshot = await firestore.collection('orgs').doc(run.orgId).collection(CRM_COLLECTIONS.emailTemplates).doc(templateId).get();
615
- run.templates.set(templateId, snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data()).body : null);
613
+ run.templates.set(templateId, await readOutreachTemplateBody(run.orgId, templateId));
616
614
  }
617
615
  return (_run_templates_get = run.templates.get(templateId)) != null ? _run_templates_get : null;
618
616
  }
@@ -796,7 +794,7 @@ async function runEmailStep(deps, firestore, run, input) {
796
794
  name: input.siteName
797
795
  }
798
796
  },
799
- templateBody: (step == null ? void 0 : step.kind) === 'email' && step.templateId ? await templateBody(firestore, run, step.templateId) : null,
797
+ templateBody: (step == null ? void 0 : step.kind) === 'email' && step.templateId ? await templateBody(run, step.templateId) : null,
800
798
  listUnsubscribeUrl: unsubscribe.status === 'ready' ? unsubscribe.url : null,
801
799
  listUnsubscribeMailto: unsubscribe.status === 'ready' ? unsubscribe.mailto : null
802
800
  });
@@ -993,17 +991,22 @@ async function runEmailStep(deps, firestore, run, input) {
993
991
  enrollment,
994
992
  atMs: record.atMs
995
993
  });
996
- await nurtureEnrolledLead(firestore, run, enrollment);
994
+ await nurtureEnrolledLead(run, enrollment);
997
995
  }
998
996
  /**
999
- * A lead an email step reached is no longer untouched (AGL-3446): New moves
1000
- * to Nurturing. Called only once a send is recorded, so an enrollment whose
1001
- * steps never go out leaves its lead where it stood.
1002
- */ async function nurtureEnrolledLead(firestore, run, enrollment) {
1003
- if (enrollment.target !== 'lead' || !enrollment.leadId) return;
1004
- await markOutreachLeadNurturing(firestore, {
997
+ * A lead an email step reached is no longer untouched (AGL-3446): the record
998
+ * system hears the send was delivered, and moves a New lead to Nurturing
999
+ * through the record email-state seam's `reached` (AGL-3080). Called only
1000
+ * once a send is recorded, so an enrollment whose steps never go out leaves
1001
+ * its lead where it stood. Never throws.
1002
+ */ async function nurtureEnrolledLead(run, enrollment) {
1003
+ if (enrollment.target !== 'lead' || !enrollment.leadId || !enrollment.email) return;
1004
+ await stampRecordEmailReach({
1005
1005
  orgId: run.orgId,
1006
- leadId: enrollment.leadId
1006
+ hostId: enrollment.hostId,
1007
+ emails: [
1008
+ enrollment.email
1009
+ ]
1007
1010
  });
1008
1011
  }
1009
1012
  /**
@@ -1068,7 +1071,7 @@ async function runEmailStep(deps, firestore, run, input) {
1068
1071
  enrollment,
1069
1072
  atMs
1070
1073
  });
1071
- await nurtureEnrolledLead(firestore, run, enrollment);
1074
+ await nurtureEnrolledLead(run, enrollment);
1072
1075
  }
1073
1076
  return completed;
1074
1077
  }