@aglyn/plugins-marketing 1.0.0-beta.221 → 1.0.0-beta.223

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 (127) hide show
  1. package/README.md +4 -1
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +53 -64
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +18 -3
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-edit-drawer.d.ts +6 -0
  8. package/src/lib/components/campaign-edit-drawer.js +13 -2
  9. package/src/lib/components/campaign-edit-drawer.js.map +1 -1
  10. package/src/lib/components/campaign-queries.d.ts +5 -0
  11. package/src/lib/components/campaign-queries.js +8 -1
  12. package/src/lib/components/campaign-queries.js.map +1 -1
  13. package/src/lib/components/campaign-reach-sections.d.ts +28 -0
  14. package/src/lib/components/campaign-reach-sections.js +96 -5
  15. package/src/lib/components/campaign-reach-sections.js.map +1 -1
  16. package/src/lib/components/campaign-report-card.js +5 -4
  17. package/src/lib/components/campaign-report-card.js.map +1 -1
  18. package/src/lib/components/campaigns-card.js +1 -1
  19. package/src/lib/components/campaigns-card.js.map +1 -1
  20. package/src/lib/components/conversion-attribution.component.d.ts +18 -0
  21. package/src/lib/components/conversion-attribution.component.js +177 -39
  22. package/src/lib/components/conversion-attribution.component.js.map +1 -1
  23. package/src/lib/components/email-compose-card.js +1 -1
  24. package/src/lib/components/email-compose-card.js.map +1 -1
  25. package/src/lib/components/email-detail.js +5 -4
  26. package/src/lib/components/email-detail.js.map +1 -1
  27. package/src/lib/components/email-template-report-card.d.ts +53 -0
  28. package/src/lib/components/email-template-report-card.js +469 -0
  29. package/src/lib/components/email-template-report-card.js.map +1 -0
  30. package/src/lib/components/emails-list-card.js +2 -2
  31. package/src/lib/components/emails-list-card.js.map +1 -1
  32. package/src/lib/components/host-experiments-card.component.js +3 -2
  33. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  34. package/src/lib/components/record-attribution-widget.d.ts +5 -0
  35. package/src/lib/components/record-attribution-widget.js +6 -3
  36. package/src/lib/components/record-attribution-widget.js.map +1 -1
  37. package/src/lib/components/site-runtime.d.ts +5 -0
  38. package/src/lib/components/site-runtime.js +104 -3
  39. package/src/lib/components/site-runtime.js.map +1 -1
  40. package/src/lib/declarations.server.d.ts +9 -0
  41. package/src/lib/declarations.server.js +78 -0
  42. package/src/lib/declarations.server.js.map +1 -1
  43. package/src/lib/model/campaign-container.d.ts +531 -0
  44. package/src/lib/model/campaign-container.js +532 -0
  45. package/src/lib/model/campaign-container.js.map +1 -0
  46. package/src/lib/model/campaign-conversions.d.ts +55 -4
  47. package/src/lib/model/campaign-conversions.js +60 -4
  48. package/src/lib/model/campaign-conversions.js.map +1 -1
  49. package/src/lib/model/campaign-report.d.ts +158 -0
  50. package/src/lib/model/campaign-report.js +268 -0
  51. package/src/lib/model/campaign-report.js.map +1 -0
  52. package/src/lib/model/campaign-revenue.d.ts +3 -3
  53. package/src/lib/model/campaign-revenue.js +3 -3
  54. package/src/lib/model/campaign-revenue.js.map +1 -1
  55. package/src/lib/model/campaign-send-time.js +2 -2
  56. package/src/lib/model/campaign-send-time.js.map +1 -1
  57. package/src/lib/model/campaign-visits.d.ts +74 -0
  58. package/src/lib/model/campaign-visits.js +65 -0
  59. package/src/lib/model/campaign-visits.js.map +1 -0
  60. package/src/lib/model/compile-client-automations.d.ts +2 -2
  61. package/src/lib/model/compile-client-automations.js +3 -3
  62. package/src/lib/model/compile-client-automations.js.map +1 -1
  63. package/src/lib/model/email-record.d.ts +175 -0
  64. package/src/lib/model/email-record.js +206 -0
  65. package/src/lib/model/email-record.js.map +1 -0
  66. package/src/lib/model/site-contract.d.ts +2 -2
  67. package/src/lib/model/site-contract.js.map +1 -1
  68. package/src/lib/model/template-report.d.ts +158 -0
  69. package/src/lib/model/template-report.js +249 -0
  70. package/src/lib/model/template-report.js.map +1 -0
  71. package/src/lib/plugin.js +16 -0
  72. package/src/lib/plugin.js.map +1 -1
  73. package/src/lib/preview-props.js.map +1 -1
  74. package/src/lib/server/campaign-attribution-store.d.ts +55 -0
  75. package/src/lib/server/campaign-attribution-store.js +98 -0
  76. package/src/lib/server/campaign-attribution-store.js.map +1 -0
  77. package/src/lib/server/campaign-conversion-attribution.d.ts +314 -0
  78. package/src/lib/server/campaign-conversion-attribution.js +549 -0
  79. package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
  80. package/src/lib/server/campaign-manage.js +1 -1
  81. package/src/lib/server/campaign-manage.js.map +1 -1
  82. package/src/lib/server/campaign-org-refs.js +1 -1
  83. package/src/lib/server/campaign-org-refs.js.map +1 -1
  84. package/src/lib/server/campaign-recipients.js +7 -7
  85. package/src/lib/server/campaign-recipients.js.map +1 -1
  86. package/src/lib/server/campaign-send.js +25 -19
  87. package/src/lib/server/campaign-send.js.map +1 -1
  88. package/src/lib/server/campaign-touch-targets.d.ts +61 -0
  89. package/src/lib/server/campaign-touch-targets.js +162 -0
  90. package/src/lib/server/campaign-touch-targets.js.map +1 -0
  91. package/src/lib/server/campaign-visit-beacon.d.ts +44 -0
  92. package/src/lib/server/campaign-visit-beacon.js +125 -0
  93. package/src/lib/server/campaign-visit-beacon.js.map +1 -0
  94. package/src/lib/server/conversion-creditor.d.ts +18 -0
  95. package/src/lib/server/conversion-creditor.js +238 -0
  96. package/src/lib/server/conversion-creditor.js.map +1 -0
  97. package/src/lib/server/email-campaign-touch.d.ts +81 -0
  98. package/src/lib/server/email-campaign-touch.js +227 -0
  99. package/src/lib/server/email-campaign-touch.js.map +1 -0
  100. package/src/lib/server/email-events.js +32 -31
  101. package/src/lib/server/email-events.js.map +1 -1
  102. package/src/lib/server/email-revenue-attribution.d.ts +186 -0
  103. package/src/lib/server/email-revenue-attribution.js +322 -0
  104. package/src/lib/server/email-revenue-attribution.js.map +1 -0
  105. package/src/lib/server/email-send-record-index.d.ts +34 -0
  106. package/src/lib/server/email-send-record-index.js +90 -0
  107. package/src/lib/server/email-send-record-index.js.map +1 -0
  108. package/src/lib/server/get-client-automations.js.map +1 -1
  109. package/src/lib/server/marketing-figures.js +3 -3
  110. package/src/lib/server/marketing-figures.js.map +1 -1
  111. package/src/lib/server/send-tally.d.ts +32 -0
  112. package/src/lib/server/send-tally.js +54 -0
  113. package/src/lib/server/send-tally.js.map +1 -0
  114. package/src/lib/server/site-page-enricher.js +18 -2
  115. package/src/lib/server/site-page-enricher.js.map +1 -1
  116. package/src/lib/testing/stand-in-person-records.d.ts +35 -0
  117. package/src/lib/testing/stand-in-person-records.js +67 -0
  118. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  119. package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
  120. package/src/lib/testing/stand-in-record-counts.js +70 -0
  121. package/src/lib/testing/stand-in-record-counts.js.map +1 -0
  122. package/src/lib/transfer/campaigns-package.d.ts +72 -0
  123. package/src/lib/transfer/campaigns-package.js +145 -0
  124. package/src/lib/transfer/campaigns-package.js.map +1 -0
  125. package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
  126. package/src/lib/transfer/campaigns-package.server.js +305 -0
  127. package/src/lib/transfer/campaigns-package.server.js.map +1 -0
@@ -0,0 +1,74 @@
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 A CAMPAIGN REACHED ON THE SITE (AGL-3461) — first visits and views of
19
+ * the pages filed under it.
20
+ *
21
+ * ## One document per campaign, keyed by the campaign's id
22
+ *
23
+ * `orgs/{orgId}/campaignVisitReports/{campaignId}`, beside the sequence
24
+ * rollup and on the same footing: counts, never a person, written only on the
25
+ * Admin SDK by the site collector's `campaignVisit` beacon, readable by
26
+ * whoever may open the campaign. Keyed by the campaign DOCUMENT, so the
27
+ * collection is bounded by the campaigns that exist — never by a label or a
28
+ * path a visitor's URL could mint.
29
+ *
30
+ * ## What each figure counts
31
+ *
32
+ * - **Page views** — views of the pages filed under the campaign, one per
33
+ * pageview, whoever the visitor is. It needs nothing from the visitor's
34
+ * device, so every view counts.
35
+ * - **First visits** — visitors whose first campaign touch in the
36
+ * attribution window was this campaign: a page filed under it, or a link
37
+ * carrying a `utm_campaign` label it declares. The device answers from the
38
+ * touch it already remembers (`claimFirstCampaignTouch` in
39
+ * `utm-touch.ts`): one held from the last seven days means this visit is
40
+ * not a first. A visitor whose consent posture keeps nothing on their
41
+ * device cannot be told from the next one and is not counted at all, so
42
+ * the figure is a floor, and the page says so.
43
+ *
44
+ * Both are also kept per month, under `byMonth`, so a campaign with dates can
45
+ * be read for its own window. Twelve keys a year is the bound.
46
+ */
47
+ /** The org collection the rollups live in. */
48
+ export declare const CAMPAIGN_VISIT_REPORTS_COLLECTION = "campaignVisitReports";
49
+ /** One month's figures, under `byMonth['YYYY-MM']`. */
50
+ export interface CampaignVisitMonth {
51
+ views?: number;
52
+ firstVisits?: number;
53
+ }
54
+ /** The stored shape of `orgs/{orgId}/campaignVisitReports/{campaignId}`. */
55
+ export interface CampaignVisitRollup {
56
+ views?: number;
57
+ firstVisits?: number;
58
+ byMonth?: Record<string, CampaignVisitMonth>;
59
+ }
60
+ /** What the campaign's page draws. */
61
+ export interface CampaignVisitsReport {
62
+ /** Whether anything has been counted at all. */
63
+ recorded: boolean;
64
+ views: number;
65
+ firstVisits: number;
66
+ /** The first month anything was counted, `YYYY-MM`, or `''`. */
67
+ since: string;
68
+ }
69
+ /**
70
+ * The rollup as the page reads it. `since` is the earliest month on record,
71
+ * so the figures say how far back they reach: they start when counting
72
+ * started, not when the campaign did.
73
+ */
74
+ export declare function campaignVisitsReport(rollup: CampaignVisitRollup | null | undefined): CampaignVisitsReport;
@@ -0,0 +1,65 @@
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 A CAMPAIGN REACHED ON THE SITE (AGL-3461) — first visits and views of
18
+ * the pages filed under it.
19
+ *
20
+ * ## One document per campaign, keyed by the campaign's id
21
+ *
22
+ * `orgs/{orgId}/campaignVisitReports/{campaignId}`, beside the sequence
23
+ * rollup and on the same footing: counts, never a person, written only on the
24
+ * Admin SDK by the site collector's `campaignVisit` beacon, readable by
25
+ * whoever may open the campaign. Keyed by the campaign DOCUMENT, so the
26
+ * collection is bounded by the campaigns that exist — never by a label or a
27
+ * path a visitor's URL could mint.
28
+ *
29
+ * ## What each figure counts
30
+ *
31
+ * - **Page views** — views of the pages filed under the campaign, one per
32
+ * pageview, whoever the visitor is. It needs nothing from the visitor's
33
+ * device, so every view counts.
34
+ * - **First visits** — visitors whose first campaign touch in the
35
+ * attribution window was this campaign: a page filed under it, or a link
36
+ * carrying a `utm_campaign` label it declares. The device answers from the
37
+ * touch it already remembers (`claimFirstCampaignTouch` in
38
+ * `utm-touch.ts`): one held from the last seven days means this visit is
39
+ * not a first. A visitor whose consent posture keeps nothing on their
40
+ * device cannot be told from the next one and is not counted at all, so
41
+ * the figure is a floor, and the page says so.
42
+ *
43
+ * Both are also kept per month, under `byMonth`, so a campaign with dates can
44
+ * be read for its own window. Twelve keys a year is the bound.
45
+ */ /** The org collection the rollups live in. */ export const CAMPAIGN_VISIT_REPORTS_COLLECTION = 'campaignVisitReports';
46
+ function count(raw) {
47
+ const value = Math.floor(Number(raw != null ? raw : 0));
48
+ return Number.isFinite(value) && value > 0 ? value : 0;
49
+ }
50
+ /**
51
+ * The rollup as the page reads it. `since` is the earliest month on record,
52
+ * so the figures say how far back they reach: they start when counting
53
+ * started, not when the campaign did.
54
+ */ export function campaignVisitsReport(rollup) {
55
+ var _ref, _months_;
56
+ const months = Object.keys((_ref = rollup == null ? void 0 : rollup.byMonth) != null ? _ref : {}).filter((month)=>/^\d{4}-\d{2}$/.test(month)).sort();
57
+ return {
58
+ recorded: Boolean(rollup),
59
+ views: count(rollup == null ? void 0 : rollup.views),
60
+ firstVisits: count(rollup == null ? void 0 : rollup.firstVisits),
61
+ since: (_months_ = months[0]) != null ? _months_ : ''
62
+ };
63
+ }
64
+
65
+ //# sourceMappingURL=campaign-visits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/campaign-visits.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 A CAMPAIGN REACHED ON THE SITE (AGL-3461) — first visits and views of\n * the pages filed under it.\n *\n * ## One document per campaign, keyed by the campaign's id\n *\n * `orgs/{orgId}/campaignVisitReports/{campaignId}`, beside the sequence\n * rollup and on the same footing: counts, never a person, written only on the\n * Admin SDK by the site collector's `campaignVisit` beacon, readable by\n * whoever may open the campaign. Keyed by the campaign DOCUMENT, so the\n * collection is bounded by the campaigns that exist — never by a label or a\n * path a visitor's URL could mint.\n *\n * ## What each figure counts\n *\n * - **Page views** — views of the pages filed under the campaign, one per\n * pageview, whoever the visitor is. It needs nothing from the visitor's\n * device, so every view counts.\n * - **First visits** — visitors whose first campaign touch in the\n * attribution window was this campaign: a page filed under it, or a link\n * carrying a `utm_campaign` label it declares. The device answers from the\n * touch it already remembers (`claimFirstCampaignTouch` in\n * `utm-touch.ts`): one held from the last seven days means this visit is\n * not a first. A visitor whose consent posture keeps nothing on their\n * device cannot be told from the next one and is not counted at all, so\n * the figure is a floor, and the page says so.\n *\n * Both are also kept per month, under `byMonth`, so a campaign with dates can\n * be read for its own window. Twelve keys a year is the bound.\n */\n\n/** The org collection the rollups live in. */\nexport const CAMPAIGN_VISIT_REPORTS_COLLECTION = 'campaignVisitReports'\n\n/** One month's figures, under `byMonth['YYYY-MM']`. */\nexport interface CampaignVisitMonth {\n views?: number\n firstVisits?: number\n}\n\n/** The stored shape of `orgs/{orgId}/campaignVisitReports/{campaignId}`. */\nexport interface CampaignVisitRollup {\n views?: number\n firstVisits?: number\n byMonth?: Record<string, CampaignVisitMonth>\n}\n\n/** What the campaign's page draws. */\nexport interface CampaignVisitsReport {\n /** Whether anything has been counted at all. */\n recorded: boolean\n views: number\n firstVisits: number\n /** The first month anything was counted, `YYYY-MM`, or `''`. */\n since: string\n}\n\nfunction count(raw: unknown): number {\n const value = Math.floor(Number(raw ?? 0))\n return Number.isFinite(value) && value > 0 ? value : 0\n}\n\n/**\n * The rollup as the page reads it. `since` is the earliest month on record,\n * so the figures say how far back they reach: they start when counting\n * started, not when the campaign did.\n */\nexport function campaignVisitsReport(\n rollup: CampaignVisitRollup | null | undefined,\n): CampaignVisitsReport {\n const months = Object.keys(rollup?.byMonth ?? {})\n .filter((month) => /^\\d{4}-\\d{2}$/.test(month))\n .sort()\n return {\n recorded: Boolean(rollup),\n views: count(rollup?.views),\n firstVisits: count(rollup?.firstVisits),\n since: months[0] ?? '',\n }\n}\n"],"names":["CAMPAIGN_VISIT_REPORTS_COLLECTION","count","raw","value","Math","floor","Number","isFinite","campaignVisitsReport","rollup","months","Object","keys","byMonth","filter","month","test","sort","recorded","Boolean","views","firstVisits","since"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GAED,4CAA4C,GAC5C,OAAO,MAAMA,oCAAoC,uBAAsB;AAyBvE,SAASC,MAAMC,GAAY;IACzB,MAAMC,QAAQC,KAAKC,KAAK,CAACC,OAAOJ,cAAAA,MAAO;IACvC,OAAOI,OAAOC,QAAQ,CAACJ,UAAUA,QAAQ,IAAIA,QAAQ;AACvD;AAEA;;;;CAIC,GACD,OAAO,SAASK,qBACdC,MAA8C;cASrCC;IAPT,MAAMA,SAASC,OAAOC,IAAI,SAACH,0BAAAA,OAAQI,OAAO,mBAAI,CAAC,GAC5CC,MAAM,CAAC,CAACC,QAAU,gBAAgBC,IAAI,CAACD,QACvCE,IAAI;IACP,OAAO;QACLC,UAAUC,QAAQV;QAClBW,OAAOnB,MAAMQ,0BAAAA,OAAQW,KAAK;QAC1BC,aAAapB,MAAMQ,0BAAAA,OAAQY,WAAW;QACtCC,KAAK,GAAEZ,WAAAA,MAAM,CAAC,EAAE,YAATA,WAAa;IACtB;AACF"}
@@ -14,12 +14,12 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type HostAction } from '@aglyn/aglyn/app-utils/actions';
17
+ import { type InteractionStepBase, type SiteInteraction } from '@aglyn/aglyn/app-utils/site-interactions';
18
18
  import type { ClientAutomation } from './site-contract';
19
19
  /** A raw `hosts/{hostId}/actions` doc paired with its id. */
20
20
  export interface RawHostAction {
21
21
  id: string;
22
- action: HostAction;
22
+ action: SiteInteraction<InteractionStepBase>;
23
23
  }
24
24
  export interface CompileClientAutomationsOptions {
25
25
  /** Leading-slash page path for pathPattern targeting. */
@@ -14,11 +14,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ // Import the entitlement/event predicates from the context-free actions
17
+ */ // Import the entitlement/event predicates from the context-free interactions
18
18
  // subpath, NOT the `@aglyn/aglyn` barrel: this file is in the marketing model,
19
19
  // which the plugin's SERVER graph re-exports, and the full barrel pulls client
20
20
  // React contexts (createContext) that break the RSC server build (AGL-830).
21
- import { hostActionStepsForClient, isClientActionStep, isClientStepEntitled, isSiteEventType } from "@aglyn/aglyn/app-utils/actions";
21
+ import { interactionStepsForClient, isClientActionStep, isClientStepEntitled, isSiteEventType } from "@aglyn/aglyn/app-utils/site-interactions";
22
22
  import { overlayMatchesPath } from "./overlays.js";
23
23
  /**
24
24
  * Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):
@@ -55,7 +55,7 @@ import { overlayMatchesPath } from "./overlays.js";
55
55
  * delay would look correct on the server, and the two halves of one
56
56
  * authored flow would disagree about when they happened. Everything past
57
57
  * the first wait belongs to a run the job beat has not started yet.
58
- */ const reachableSteps = hostActionStepsForClient(action.steps);
58
+ */ const reachableSteps = interactionStepsForClient(action.steps);
59
59
  // Step-level entitlement tiering (AGL-577): basic presentational steps
60
60
  // load on every plan; advanced client steps need `actions` and runJs
61
61
  // needs `webhooks` — see isClientStepEntitled.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/compile-client-automations.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// Import the entitlement/event predicates from the context-free actions\n// subpath, NOT the `@aglyn/aglyn` barrel: this file is in the marketing model,\n// which the plugin's SERVER graph re-exports, and the full barrel pulls client\n// React contexts (createContext) that break the RSC server build (AGL-830).\nimport {\n hostActionStepsForClient,\n isClientActionStep,\n isClientStepEntitled,\n isSiteEventType,\n type HostAction,\n} from '@aglyn/aglyn/app-utils/actions'\nimport { overlayMatchesPath } from './overlays'\nimport type { ClientAutomation } from './site-contract'\n\n/** A raw `hosts/{hostId}/actions` doc paired with its id. */\nexport interface RawHostAction {\n id: string\n action: HostAction\n}\n\nexport interface CompileClientAutomationsOptions {\n /** Leading-slash page path for pathPattern targeting. */\n path: string\n /**\n * `actions` entitlement (AGL-577). Basic presentational steps\n * (menu/drawer/show-hide/class/nav/alert) always load; advanced client\n * steps (overlay/showHtml/analytics) and server steps drop when false.\n */\n actionsEntitled: boolean\n /** Business-tier gate for `runJs` steps (dropped when false). */\n allowJs: boolean\n /**\n * Ignore each action's `pathPattern` (AGL-830). The besigner Preview shows\n * a single composed screen and scopes relevance by selector match, so it\n * skips path filtering rather than resolve the composed path.\n */\n matchAllPaths?: boolean\n /**\n * Drop every automation that names a `pathPattern` (AGL-2511), for a\n * surface whose own path is unknown — the designed 404 body, served from a\n * per-host cache that no request path belongs to.\n *\n * The opposite of `matchAllPaths` and deliberately so: Preview knows the\n * page and only lacks its composed address, so it keeps path-scoped\n * automations; this caller cannot know the page at all, and running an\n * automation scoped to `/pricing` on whatever URL 404'd would be a guess\n * with a side effect. Interactions authored on nodes carry no pattern, so\n * a nav's hover choreography — the reason this surface is enriched — is\n * unaffected.\n */\n dropPathScoped?: boolean\n}\n\n/**\n * Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):\n * the single place the trigger config + entitlement-trimmed client steps are\n * shaped. Both the server page-enricher (admin fetch) and the editor Preview\n * (client fetch) feed their loaded action docs through this, so live and\n * preview stay byte-for-byte consistent. No I/O, no firebase — safe on the\n * client. Malformed entries are skipped, never thrown.\n */\nexport function compileClientAutomations(\n actions: RawHostAction[],\n options: CompileClientAutomationsOptions,\n): ClientAutomation[] {\n const automations: ClientAutomation[] = []\n for (const { id, action } of actions) {\n if (\n (action as { deletedAt?: unknown }).deletedAt ||\n action.enabled === false\n ) {\n continue\n }\n const event = String(action.trigger?.event ?? '')\n if (!isSiteEventType(event)) continue\n const pathPattern = action.trigger?.pathPattern?.trim()\n if (pathPattern && options.dropPathScoped) continue\n if (\n !options.matchAllPaths &&\n pathPattern &&\n !overlayMatchesPath({ pathPatterns: [pathPattern] }, options.path)\n ) {\n continue\n }\n /*\n * TRUNCATED AT THE FIRST WAIT, before anything else is decided.\n *\n * The browser runs its slice of the list the moment the trigger fires, so\n * a client step sitting after a `wait` would execute at once — the popup\n * an author scheduled for three days later would appear immediately, the\n * delay would look correct on the server, and the two halves of one\n * authored flow would disagree about when they happened. Everything past\n * the first wait belongs to a run the job beat has not started yet.\n */\n const reachableSteps = hostActionStepsForClient(action.steps)\n // Step-level entitlement tiering (AGL-577): basic presentational steps\n // load on every plan; advanced client steps need `actions` and runJs\n // needs `webhooks` — see isClientStepEntitled.\n const clientSteps = reachableSteps.filter((step) =>\n isClientStepEntitled(step, {\n actionsEntitled: options.actionsEntitled,\n allowJs: options.allowJs,\n }),\n )\n // Server steps only dispatch (and are only re-authorized) when the org is\n // actions-entitled — never advertise them otherwise, so basic-tier pages\n // don't POST to /api/events/dispatch for a no-op.\n const hasServerSteps =\n options.actionsEntitled &&\n (action.steps ?? []).some((step) => !isClientActionStep(step))\n if (!clientSteps.length && !hasServerSteps) continue\n automations.push({\n id,\n event,\n ...(action.trigger?.selector\n ? { selector: action.trigger.selector }\n : {}),\n ...(Number(action.trigger?.threshold) > 0\n ? { threshold: Number(action.trigger.threshold) }\n : {}),\n ...(action.trigger?.oncePerVisitor === true\n ? { oncePerVisitor: true }\n : {}),\n ...(action.trigger?.oncePerSession === true\n ? { oncePerSession: true }\n : {}),\n ...(Number(action.trigger?.cooldownMinutes) >= 1\n ? { cooldownMinutes: Number(action.trigger?.cooldownMinutes) }\n : {}),\n // Repeatable UI choreography (AGL-562): menu/drawer interactions fire on\n // every occurrence instead of once per pageview.\n ...(action.trigger?.everyTime === true ? { everyTime: true } : {}),\n steps: clientSteps,\n hasServerSteps,\n })\n }\n return automations\n}\n\nexport default compileClientAutomations\n"],"names":["hostActionStepsForClient","isClientActionStep","isClientStepEntitled","isSiteEventType","overlayMatchesPath","compileClientAutomations","actions","options","automations","id","action","deletedAt","enabled","event","String","trigger","pathPattern","trim","dropPathScoped","matchAllPaths","pathPatterns","path","reachableSteps","steps","clientSteps","filter","step","actionsEntitled","allowJs","hasServerSteps","some","length","push","selector","Number","threshold","oncePerVisitor","oncePerSession","cooldownMinutes","everyTime"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,wEAAwE;AACxE,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,SACEA,wBAAwB,EACxBC,kBAAkB,EAClBC,oBAAoB,EACpBC,eAAe,QAEV,iCAAgC;AACvC,SAASC,kBAAkB,QAAQ,gBAAY;AA0C/C;;;;;;;CAOC,GACD,OAAO,SAASC,yBACdC,OAAwB,EACxBC,OAAwC;IAExC,MAAMC,cAAkC,EAAE;IAC1C,KAAK,MAAM,EAAEC,EAAE,EAAEC,MAAM,EAAE,IAAIJ,QAAS;kBA2CjCI;YApCkBA,iBAEDA,6BAAAA,kBAuCdA,kBAGOA,kBAGPA,kBAGAA,kBAGOA,kBACmBA,kBAI1BA;QAhEN,IACE,AAACA,OAAmCC,SAAS,IAC7CD,OAAOE,OAAO,KAAK,OACnB;YACA;QACF;QACA,MAAMC,QAAQC,gBAAOJ,kBAAAA,OAAOK,OAAO,qBAAdL,gBAAgBG,KAAK,mBAAI;QAC9C,IAAI,CAACV,gBAAgBU,QAAQ;QAC7B,MAAMG,eAAcN,mBAAAA,OAAOK,OAAO,sBAAdL,8BAAAA,iBAAgBM,WAAW,qBAA3BN,4BAA6BO,IAAI;QACrD,IAAID,eAAeT,QAAQW,cAAc,EAAE;QAC3C,IACE,CAACX,QAAQY,aAAa,IACtBH,eACA,CAACZ,mBAAmB;YAAEgB,cAAc;gBAACJ;aAAY;QAAC,GAAGT,QAAQc,IAAI,GACjE;YACA;QACF;QACA;;;;;;;;;KASC,GACD,MAAMC,iBAAiBtB,yBAAyBU,OAAOa,KAAK;QAC5D,uEAAuE;QACvE,qEAAqE;QACrE,+CAA+C;QAC/C,MAAMC,cAAcF,eAAeG,MAAM,CAAC,CAACC,OACzCxB,qBAAqBwB,MAAM;gBACzBC,iBAAiBpB,QAAQoB,eAAe;gBACxCC,SAASrB,QAAQqB,OAAO;YAC1B;QAEF,0EAA0E;QAC1E,yEAAyE;QACzE,kDAAkD;QAClD,MAAMC,iBACJtB,QAAQoB,eAAe,IACvB,EAACjB,gBAAAA,OAAOa,KAAK,YAAZb,gBAAgB,EAAE,EAAEoB,IAAI,CAAC,CAACJ,OAAS,CAACzB,mBAAmByB;QAC1D,IAAI,CAACF,YAAYO,MAAM,IAAI,CAACF,gBAAgB;QAC5CrB,YAAYwB,IAAI,CAAC;YACfvB;YACAI;WACIH,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgBuB,QAAQ,IACxB;YAAEA,UAAUvB,OAAOK,OAAO,CAACkB,QAAQ;QAAC,IACpC,CAAC,GACDC,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgByB,SAAS,IAAI,IACpC;YAAEA,WAAWD,OAAOxB,OAAOK,OAAO,CAACoB,SAAS;QAAE,IAC9C,CAAC,GACDzB,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB0B,cAAc,MAAK,OACnC;YAAEA,gBAAgB;QAAK,IACvB,CAAC,GACD1B,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB2B,cAAc,MAAK,OACnC;YAAEA,gBAAgB;QAAK,IACvB,CAAC,GACDH,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB4B,eAAe,KAAK,IAC3C;YAAEA,iBAAiBJ,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB4B,eAAe;QAAE,IAC3D,CAAC,GAGD5B,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB6B,SAAS,MAAK,OAAO;YAAEA,WAAW;QAAK,IAAI,CAAC;YAChEhB,OAAOC;YACPK;;IAEJ;IACA,OAAOrB;AACT;AAEA,eAAeH,yBAAwB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/compile-client-automations.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// Import the entitlement/event predicates from the context-free interactions\n// subpath, NOT the `@aglyn/aglyn` barrel: this file is in the marketing model,\n// which the plugin's SERVER graph re-exports, and the full barrel pulls client\n// React contexts (createContext) that break the RSC server build (AGL-830).\nimport {\n type ClientInteractionStep,\n type InteractionStepBase,\n interactionStepsForClient,\n isClientActionStep,\n isClientStepEntitled,\n isSiteEventType,\n type SiteInteraction,\n} from '@aglyn/aglyn/app-utils/site-interactions'\nimport { overlayMatchesPath } from './overlays'\nimport type { ClientAutomation } from './site-contract'\n\n/** A raw `hosts/{hostId}/actions` doc paired with its id. */\nexport interface RawHostAction {\n id: string\n action: SiteInteraction<InteractionStepBase>\n}\n\nexport interface CompileClientAutomationsOptions {\n /** Leading-slash page path for pathPattern targeting. */\n path: string\n /**\n * `actions` entitlement (AGL-577). Basic presentational steps\n * (menu/drawer/show-hide/class/nav/alert) always load; advanced client\n * steps (overlay/showHtml/analytics) and server steps drop when false.\n */\n actionsEntitled: boolean\n /** Business-tier gate for `runJs` steps (dropped when false). */\n allowJs: boolean\n /**\n * Ignore each action's `pathPattern` (AGL-830). The besigner Preview shows\n * a single composed screen and scopes relevance by selector match, so it\n * skips path filtering rather than resolve the composed path.\n */\n matchAllPaths?: boolean\n /**\n * Drop every automation that names a `pathPattern` (AGL-2511), for a\n * surface whose own path is unknown — the designed 404 body, served from a\n * per-host cache that no request path belongs to.\n *\n * The opposite of `matchAllPaths` and deliberately so: Preview knows the\n * page and only lacks its composed address, so it keeps path-scoped\n * automations; this caller cannot know the page at all, and running an\n * automation scoped to `/pricing` on whatever URL 404'd would be a guess\n * with a side effect. Interactions authored on nodes carry no pattern, so\n * a nav's hover choreography — the reason this surface is enriched — is\n * unaffected.\n */\n dropPathScoped?: boolean\n}\n\n/**\n * Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):\n * the single place the trigger config + entitlement-trimmed client steps are\n * shaped. Both the server page-enricher (admin fetch) and the editor Preview\n * (client fetch) feed their loaded action docs through this, so live and\n * preview stay byte-for-byte consistent. No I/O, no firebase — safe on the\n * client. Malformed entries are skipped, never thrown.\n */\nexport function compileClientAutomations(\n actions: RawHostAction[],\n options: CompileClientAutomationsOptions,\n): ClientAutomation[] {\n const automations: ClientAutomation[] = []\n for (const { id, action } of actions) {\n if (\n (action as { deletedAt?: unknown }).deletedAt ||\n action.enabled === false\n ) {\n continue\n }\n const event = String(action.trigger?.event ?? '')\n if (!isSiteEventType(event)) continue\n const pathPattern = action.trigger?.pathPattern?.trim()\n if (pathPattern && options.dropPathScoped) continue\n if (\n !options.matchAllPaths &&\n pathPattern &&\n !overlayMatchesPath({ pathPatterns: [pathPattern] }, options.path)\n ) {\n continue\n }\n /*\n * TRUNCATED AT THE FIRST WAIT, before anything else is decided.\n *\n * The browser runs its slice of the list the moment the trigger fires, so\n * a client step sitting after a `wait` would execute at once — the popup\n * an author scheduled for three days later would appear immediately, the\n * delay would look correct on the server, and the two halves of one\n * authored flow would disagree about when they happened. Everything past\n * the first wait belongs to a run the job beat has not started yet.\n */\n const reachableSteps = interactionStepsForClient(action.steps)\n // Step-level entitlement tiering (AGL-577): basic presentational steps\n // load on every plan; advanced client steps need `actions` and runJs\n // needs `webhooks` — see isClientStepEntitled.\n const clientSteps = reachableSteps.filter((step): step is ClientInteractionStep =>\n isClientStepEntitled(step, {\n actionsEntitled: options.actionsEntitled,\n allowJs: options.allowJs,\n }),\n )\n // Server steps only dispatch (and are only re-authorized) when the org is\n // actions-entitled — never advertise them otherwise, so basic-tier pages\n // don't POST to /api/events/dispatch for a no-op.\n const hasServerSteps =\n options.actionsEntitled &&\n (action.steps ?? []).some((step) => !isClientActionStep(step))\n if (!clientSteps.length && !hasServerSteps) continue\n automations.push({\n id,\n event,\n ...(action.trigger?.selector\n ? { selector: action.trigger.selector }\n : {}),\n ...(Number(action.trigger?.threshold) > 0\n ? { threshold: Number(action.trigger.threshold) }\n : {}),\n ...(action.trigger?.oncePerVisitor === true\n ? { oncePerVisitor: true }\n : {}),\n ...(action.trigger?.oncePerSession === true\n ? { oncePerSession: true }\n : {}),\n ...(Number(action.trigger?.cooldownMinutes) >= 1\n ? { cooldownMinutes: Number(action.trigger?.cooldownMinutes) }\n : {}),\n // Repeatable UI choreography (AGL-562): menu/drawer interactions fire on\n // every occurrence instead of once per pageview.\n ...(action.trigger?.everyTime === true ? { everyTime: true } : {}),\n steps: clientSteps,\n hasServerSteps,\n })\n }\n return automations\n}\n\nexport default compileClientAutomations\n"],"names":["interactionStepsForClient","isClientActionStep","isClientStepEntitled","isSiteEventType","overlayMatchesPath","compileClientAutomations","actions","options","automations","id","action","deletedAt","enabled","event","String","trigger","pathPattern","trim","dropPathScoped","matchAllPaths","pathPatterns","path","reachableSteps","steps","clientSteps","filter","step","actionsEntitled","allowJs","hasServerSteps","some","length","push","selector","Number","threshold","oncePerVisitor","oncePerSession","cooldownMinutes","everyTime"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,SAGEA,yBAAyB,EACzBC,kBAAkB,EAClBC,oBAAoB,EACpBC,eAAe,QAEV,2CAA0C;AACjD,SAASC,kBAAkB,QAAQ,gBAAY;AA0C/C;;;;;;;CAOC,GACD,OAAO,SAASC,yBACdC,OAAwB,EACxBC,OAAwC;IAExC,MAAMC,cAAkC,EAAE;IAC1C,KAAK,MAAM,EAAEC,EAAE,EAAEC,MAAM,EAAE,IAAIJ,QAAS;kBA2CjCI;YApCkBA,iBAEDA,6BAAAA,kBAuCdA,kBAGOA,kBAGPA,kBAGAA,kBAGOA,kBACmBA,kBAI1BA;QAhEN,IACE,AAACA,OAAmCC,SAAS,IAC7CD,OAAOE,OAAO,KAAK,OACnB;YACA;QACF;QACA,MAAMC,QAAQC,gBAAOJ,kBAAAA,OAAOK,OAAO,qBAAdL,gBAAgBG,KAAK,mBAAI;QAC9C,IAAI,CAACV,gBAAgBU,QAAQ;QAC7B,MAAMG,eAAcN,mBAAAA,OAAOK,OAAO,sBAAdL,8BAAAA,iBAAgBM,WAAW,qBAA3BN,4BAA6BO,IAAI;QACrD,IAAID,eAAeT,QAAQW,cAAc,EAAE;QAC3C,IACE,CAACX,QAAQY,aAAa,IACtBH,eACA,CAACZ,mBAAmB;YAAEgB,cAAc;gBAACJ;aAAY;QAAC,GAAGT,QAAQc,IAAI,GACjE;YACA;QACF;QACA;;;;;;;;;KASC,GACD,MAAMC,iBAAiBtB,0BAA0BU,OAAOa,KAAK;QAC7D,uEAAuE;QACvE,qEAAqE;QACrE,+CAA+C;QAC/C,MAAMC,cAAcF,eAAeG,MAAM,CAAC,CAACC,OACzCxB,qBAAqBwB,MAAM;gBACzBC,iBAAiBpB,QAAQoB,eAAe;gBACxCC,SAASrB,QAAQqB,OAAO;YAC1B;QAEF,0EAA0E;QAC1E,yEAAyE;QACzE,kDAAkD;QAClD,MAAMC,iBACJtB,QAAQoB,eAAe,IACvB,EAACjB,gBAAAA,OAAOa,KAAK,YAAZb,gBAAgB,EAAE,EAAEoB,IAAI,CAAC,CAACJ,OAAS,CAACzB,mBAAmByB;QAC1D,IAAI,CAACF,YAAYO,MAAM,IAAI,CAACF,gBAAgB;QAC5CrB,YAAYwB,IAAI,CAAC;YACfvB;YACAI;WACIH,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgBuB,QAAQ,IACxB;YAAEA,UAAUvB,OAAOK,OAAO,CAACkB,QAAQ;QAAC,IACpC,CAAC,GACDC,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgByB,SAAS,IAAI,IACpC;YAAEA,WAAWD,OAAOxB,OAAOK,OAAO,CAACoB,SAAS;QAAE,IAC9C,CAAC,GACDzB,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB0B,cAAc,MAAK,OACnC;YAAEA,gBAAgB;QAAK,IACvB,CAAC,GACD1B,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB2B,cAAc,MAAK,OACnC;YAAEA,gBAAgB;QAAK,IACvB,CAAC,GACDH,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB4B,eAAe,KAAK,IAC3C;YAAEA,iBAAiBJ,QAAOxB,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB4B,eAAe;QAAE,IAC3D,CAAC,GAGD5B,EAAAA,mBAAAA,OAAOK,OAAO,qBAAdL,iBAAgB6B,SAAS,MAAK,OAAO;YAAEA,WAAW;QAAK,IAAI,CAAC;YAChEhB,OAAOC;YACPK;;IAEJ;IACA,OAAOrB;AACT;AAEA,eAAeH,yBAAwB"}
@@ -0,0 +1,175 @@
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
+ * READING ONE MESSAGE RECORD.
19
+ *
20
+ * A message — an email that was or will be sent — is a document under
21
+ * `hosts/{hostId}/campaigns`. Two facts about that document are awkward
22
+ * enough that every surface reading it gets them wrong the same way, so they
23
+ * are decided here once.
24
+ */
25
+ /**
26
+ * When a message went out, or is due to.
27
+ *
28
+ * **There is no one SEND date field.** The send path writes `{status:'sent',
29
+ * sentAt}` from one branch and `{status:'scheduled', sendAtMs}` from another.
30
+ * So a list ordered on either field in Firestore does not mis-sort —
31
+ * `orderBy` DROPS every document missing the ordered field — and half the
32
+ * messages simply vanish. Sorting on this instead keeps both kinds in one
33
+ * ordered list.
34
+ *
35
+ * `sentAt` is a Firestore `Timestamp`, which the client SDK gives as an
36
+ * object with `seconds` and `toMillis()`; a document read from cache while a
37
+ * write is pending can carry a sentinel with neither, which reads as 0 and
38
+ * sorts last rather than throwing.
39
+ *
40
+ * A DRAFT correctly answers 0 here: it has no send time, and inventing one
41
+ * from its creation would put an unsent message on the timeline of mail that
42
+ * went out. Ordering a list is {@link emailListTimeMs}'s job.
43
+ *
44
+ * @returns epoch ms, or 0 when the message has no time at all.
45
+ */
46
+ export declare function emailSendTimeMs(record: Record<string, any> | null | undefined): number;
47
+ /**
48
+ * The field every writer of a message stamps when it MINTS the document.
49
+ *
50
+ * The one date on every message, which is what neither `sentAt` nor
51
+ * `sendAtMs` is — each is written by one branch of the send path and neither
52
+ * is on both kinds. A field carried by every record is the precondition for
53
+ * ordering this collection in Firestore rather than in the browser, because
54
+ * `orderBy` drops documents that lack the ordered field instead of
55
+ * mis-sorting them.
56
+ *
57
+ * Epoch milliseconds rather than a `Timestamp`, matching `sendAtMs` beside
58
+ * it: the two are compared against each other on every list that draws
59
+ * drafts and sends together, and a comparison that has to unwrap one side
60
+ * first is a comparison somebody writes wrong.
61
+ */
62
+ export declare const EMAIL_CREATED_AT_FIELD = "createdAtMs";
63
+ /**
64
+ * When a message was created, for the records that carry it.
65
+ *
66
+ * `null` — not 0 — for a record carrying no {@link EMAIL_CREATED_AT_FIELD},
67
+ * because 0 is a real instant at the far end of the sort and "we do not know"
68
+ * is not "1970". A null orders by the record's send time instead, so the list
69
+ * is correct for a record from any era.
70
+ */
71
+ export declare function emailCreatedAtMs(record: Record<string, any> | null | undefined): number | null;
72
+ /**
73
+ * WHERE A MESSAGE SITS IN A LIST OF MESSAGES.
74
+ *
75
+ * Its send time where it has one, and its creation time where it does not.
76
+ *
77
+ * A draft has neither `sentAt` nor `sendAtMs`, so ordering a list on the send
78
+ * time alone gives every draft the key 0 and files it below mail sent years
79
+ * ago — the newest thing a merchant did, at the bottom of the page, behind
80
+ * whatever paging the list has. Falling back to creation puts it where the
81
+ * merchant left it.
82
+ *
83
+ * The fallback is deliberately not the other way round. A SENT message orders
84
+ * by when it went out, never by when it was drafted: the list is a record of
85
+ * what happened, and a message drafted in March and sent in June belongs in
86
+ * June.
87
+ *
88
+ * @returns epoch ms, or 0 for a record with no time of any kind — a draft
89
+ * written before the created stamp existed, which sorts last exactly as it
90
+ * did before.
91
+ */
92
+ export declare function emailListTimeMs(record: Record<string, any> | null | undefined): number;
93
+ /**
94
+ * Reader-facing text for a message's state.
95
+ *
96
+ * The KEYS are persisted values written by the send path and read by the
97
+ * scheduled-campaign processor — nothing here may change them, including
98
+ * `canceled`, which is already the American spelling and is a stored enum in
99
+ * either case. Only the right-hand side is display text, and an unrecognized
100
+ * state falls through as itself rather than being flattened into one of
101
+ * these: a state this list cannot name is worth seeing.
102
+ *
103
+ * `draft` is an email that has been created and not sent. `sending` is the
104
+ * claim the scheduled processor and the send-now route both take before they
105
+ * mail, and it is named here because a merchant who reloads mid-send would
106
+ * otherwise be shown the raw token.
107
+ *
108
+ * ⚠️ **Not what a console row should draw.** This names the STORED value, and
109
+ * an email delivering an audience larger than one batch is stored as
110
+ * `scheduled` between runs — so a row rendering this says "Scheduled" about a
111
+ * send that has already put five hundred messages in five hundred inboxes.
112
+ * `campaignSendDisplay` in `campaign-container.ts` reads the counters beside
113
+ * the status and is what every surface here draws. This remains the honest
114
+ * answer to the narrower question of what one stored token means.
115
+ */
116
+ export declare function emailStateLabel(status: unknown): string;
117
+ /**
118
+ * Whether an email has yet been mailed to anybody.
119
+ *
120
+ * The surfaces that report on a send need this because an unsent email has no
121
+ * `stats` at all, and a report that divides into an absent denominator
122
+ * presents "not sent yet" as a delivery rate of 0% — the same class of fault
123
+ * as a rate rendered over a denominator it does not name.
124
+ */
125
+ export declare function emailIsUnsent(record: Record<string, any> | null | undefined): boolean;
126
+ /**
127
+ * Reader-facing names for the built-in audience kinds.
128
+ *
129
+ * The KEYS are the persisted `audience` values written on every message since
130
+ * the composer shipped; only the values on the right are display text. A
131
+ * `list` audience is deliberately absent — it is not a kind of audience, it
132
+ * is a NAMED one, and naming it is {@link emailAudienceLabel}'s job.
133
+ */
134
+ export declare const EMAIL_AUDIENCE_LABELS: Record<string, string>;
135
+ /**
136
+ * Which audience a message went to, named for a reader.
137
+ *
138
+ * A list send is named by the name the SEND recorded, never by the name the
139
+ * list carries today: resolving it now would let a rename rewrite the history
140
+ * of a message that went out months ago, and a deletion erase it. A send that
141
+ * recorded no name says so instead of printing a document id, which is not
142
+ * something a merchant recognises as a list.
143
+ */
144
+ export declare function emailAudienceLabel(record: Record<string, any> | null | undefined): string;
145
+ /**
146
+ * The sender a message actually left with, as this record has it.
147
+ *
148
+ * `recorded` is the whole point of the shape. The address a send goes out on
149
+ * is stamped by the send — see `sentAsStamp` — and every message sent before
150
+ * that stamp existed carries nothing, so a surface has to be able to tell an
151
+ * unrecorded sender apart from one it could compose. Composing it is the
152
+ * thing that must not happen: a site's sending identity is a setting, and
153
+ * resolving it now would answer "what would this send as today" on a report
154
+ * whose subject is a message that went out months ago, under whatever address
155
+ * the site held then.
156
+ *
157
+ * The same rule {@link emailAudienceLabel} follows for a list name, and for
158
+ * the same reason: the report says what happened, not what would happen now.
159
+ *
160
+ * `replyTo` is null on a message that set none, which is not missing
161
+ * information — a message with no `Reply-To:` takes replies at its `from`,
162
+ * and that is a fact the record states by omission rather than one it lost.
163
+ * The difference only reads correctly when `recorded` is true, which is why
164
+ * a caller must consult that first.
165
+ */
166
+ export interface EmailSentAs {
167
+ /** False on a message sent before the send began stamping its sender. */
168
+ recorded: boolean;
169
+ from: string | null;
170
+ /** The display name recipients saw, or null when the address stood alone. */
171
+ fromName: string | null;
172
+ /** Where replies were pointed, or null when they go to `from`. */
173
+ replyTo: string | null;
174
+ }
175
+ export declare function emailSentAs(record: Record<string, any> | null | undefined): EmailSentAs;
@@ -0,0 +1,206 @@
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
+ * READING ONE MESSAGE RECORD.
18
+ *
19
+ * A message — an email that was or will be sent — is a document under
20
+ * `hosts/{hostId}/campaigns`. Two facts about that document are awkward
21
+ * enough that every surface reading it gets them wrong the same way, so they
22
+ * are decided here once.
23
+ */ /**
24
+ * When a message went out, or is due to.
25
+ *
26
+ * **There is no one SEND date field.** The send path writes `{status:'sent',
27
+ * sentAt}` from one branch and `{status:'scheduled', sendAtMs}` from another.
28
+ * So a list ordered on either field in Firestore does not mis-sort —
29
+ * `orderBy` DROPS every document missing the ordered field — and half the
30
+ * messages simply vanish. Sorting on this instead keeps both kinds in one
31
+ * ordered list.
32
+ *
33
+ * `sentAt` is a Firestore `Timestamp`, which the client SDK gives as an
34
+ * object with `seconds` and `toMillis()`; a document read from cache while a
35
+ * write is pending can carry a sentinel with neither, which reads as 0 and
36
+ * sorts last rather than throwing.
37
+ *
38
+ * A DRAFT correctly answers 0 here: it has no send time, and inventing one
39
+ * from its creation would put an unsent message on the timeline of mail that
40
+ * went out. Ordering a list is {@link emailListTimeMs}'s job.
41
+ *
42
+ * @returns epoch ms, or 0 when the message has no time at all.
43
+ */ export function emailSendTimeMs(record) {
44
+ var _ref, _ref1;
45
+ var _record_staffReview;
46
+ // A send held for staff review is parked at a time no run reaches
47
+ // (AGL-3356). That is a mechanism, not a send time, and printed it reads as
48
+ // the year 9999 — so a held send answers "no time", like a draft, and the
49
+ // lists order it by when it was created.
50
+ if ((record == null ? void 0 : (_record_staffReview = record['staffReview']) == null ? void 0 : _record_staffReview['state']) === 'held' && String((_ref = record == null ? void 0 : record['status']) != null ? _ref : '') === 'scheduled') {
51
+ return 0;
52
+ }
53
+ const sentAt = record == null ? void 0 : record['sentAt'];
54
+ if (typeof (sentAt == null ? void 0 : sentAt.toMillis) === 'function') return sentAt.toMillis();
55
+ if (typeof (sentAt == null ? void 0 : sentAt.seconds) === 'number') return sentAt.seconds * 1000;
56
+ const scheduled = Number((_ref1 = record == null ? void 0 : record['sendAtMs']) != null ? _ref1 : 0);
57
+ return Number.isFinite(scheduled) ? scheduled : 0;
58
+ }
59
+ /**
60
+ * The field every writer of a message stamps when it MINTS the document.
61
+ *
62
+ * The one date on every message, which is what neither `sentAt` nor
63
+ * `sendAtMs` is — each is written by one branch of the send path and neither
64
+ * is on both kinds. A field carried by every record is the precondition for
65
+ * ordering this collection in Firestore rather than in the browser, because
66
+ * `orderBy` drops documents that lack the ordered field instead of
67
+ * mis-sorting them.
68
+ *
69
+ * Epoch milliseconds rather than a `Timestamp`, matching `sendAtMs` beside
70
+ * it: the two are compared against each other on every list that draws
71
+ * drafts and sends together, and a comparison that has to unwrap one side
72
+ * first is a comparison somebody writes wrong.
73
+ */ export const EMAIL_CREATED_AT_FIELD = 'createdAtMs';
74
+ /**
75
+ * When a message was created, for the records that carry it.
76
+ *
77
+ * `null` — not 0 — for a record carrying no {@link EMAIL_CREATED_AT_FIELD},
78
+ * because 0 is a real instant at the far end of the sort and "we do not know"
79
+ * is not "1970". A null orders by the record's send time instead, so the list
80
+ * is correct for a record from any era.
81
+ */ export function emailCreatedAtMs(record) {
82
+ var _ref;
83
+ const created = Number((_ref = record == null ? void 0 : record[EMAIL_CREATED_AT_FIELD]) != null ? _ref : Number.NaN);
84
+ return Number.isFinite(created) && created > 0 ? created : null;
85
+ }
86
+ /**
87
+ * WHERE A MESSAGE SITS IN A LIST OF MESSAGES.
88
+ *
89
+ * Its send time where it has one, and its creation time where it does not.
90
+ *
91
+ * A draft has neither `sentAt` nor `sendAtMs`, so ordering a list on the send
92
+ * time alone gives every draft the key 0 and files it below mail sent years
93
+ * ago — the newest thing a merchant did, at the bottom of the page, behind
94
+ * whatever paging the list has. Falling back to creation puts it where the
95
+ * merchant left it.
96
+ *
97
+ * The fallback is deliberately not the other way round. A SENT message orders
98
+ * by when it went out, never by when it was drafted: the list is a record of
99
+ * what happened, and a message drafted in March and sent in June belongs in
100
+ * June.
101
+ *
102
+ * @returns epoch ms, or 0 for a record with no time of any kind — a draft
103
+ * written before the created stamp existed, which sorts last exactly as it
104
+ * did before.
105
+ */ export function emailListTimeMs(record) {
106
+ return emailSendTimeMs(record) || emailCreatedAtMs(record) || 0;
107
+ }
108
+ /**
109
+ * Reader-facing text for a message's state.
110
+ *
111
+ * The KEYS are persisted values written by the send path and read by the
112
+ * scheduled-campaign processor — nothing here may change them, including
113
+ * `canceled`, which is already the American spelling and is a stored enum in
114
+ * either case. Only the right-hand side is display text, and an unrecognized
115
+ * state falls through as itself rather than being flattened into one of
116
+ * these: a state this list cannot name is worth seeing.
117
+ *
118
+ * `draft` is an email that has been created and not sent. `sending` is the
119
+ * claim the scheduled processor and the send-now route both take before they
120
+ * mail, and it is named here because a merchant who reloads mid-send would
121
+ * otherwise be shown the raw token.
122
+ *
123
+ * ⚠️ **Not what a console row should draw.** This names the STORED value, and
124
+ * an email delivering an audience larger than one batch is stored as
125
+ * `scheduled` between runs — so a row rendering this says "Scheduled" about a
126
+ * send that has already put five hundred messages in five hundred inboxes.
127
+ * `campaignSendDisplay` in `campaign-container.ts` reads the counters beside
128
+ * the status and is what every surface here draws. This remains the honest
129
+ * answer to the narrower question of what one stored token means.
130
+ */ export function emailStateLabel(status) {
131
+ const value = String(status != null ? status : '');
132
+ if (value === 'sent') return 'Sent';
133
+ if (value === 'scheduled') return 'Scheduled';
134
+ if (value === 'canceled') return 'Canceled';
135
+ if (value === 'draft') return 'Draft';
136
+ if (value === 'sending') return 'Sending';
137
+ return value || 'Unknown';
138
+ }
139
+ /**
140
+ * Whether an email has yet been mailed to anybody.
141
+ *
142
+ * The surfaces that report on a send need this because an unsent email has no
143
+ * `stats` at all, and a report that divides into an absent denominator
144
+ * presents "not sent yet" as a delivery rate of 0% — the same class of fault
145
+ * as a rate rendered over a denominator it does not name.
146
+ */ export function emailIsUnsent(record) {
147
+ var _ref;
148
+ const value = String((_ref = record == null ? void 0 : record['status']) != null ? _ref : '');
149
+ return value === 'draft' || value === 'scheduled' || value === 'sending';
150
+ }
151
+ /**
152
+ * Reader-facing names for the built-in audience kinds.
153
+ *
154
+ * The KEYS are the persisted `audience` values written on every message since
155
+ * the composer shipped; only the values on the right are display text. A
156
+ * `list` audience is deliberately absent — it is not a kind of audience, it
157
+ * is a NAMED one, and naming it is {@link emailAudienceLabel}'s job.
158
+ */ export const EMAIL_AUDIENCE_LABELS = {
159
+ leads: 'All leads',
160
+ members: 'All site members',
161
+ segment: 'A contact segment',
162
+ manual: 'Addresses typed into the composer'
163
+ };
164
+ /**
165
+ * Which audience a message went to, named for a reader.
166
+ *
167
+ * A list send is named by the name the SEND recorded, never by the name the
168
+ * list carries today: resolving it now would let a rename rewrite the history
169
+ * of a message that went out months ago, and a deletion erase it. A send that
170
+ * recorded no name says so instead of printing a document id, which is not
171
+ * something a merchant recognises as a list.
172
+ */ export function emailAudienceLabel(record) {
173
+ var _ref, _ref1;
174
+ const audience = String((_ref = record == null ? void 0 : record['audience']) != null ? _ref : '');
175
+ if (audience !== 'list') {
176
+ var _ref2, _EMAIL_AUDIENCE_LABELS_audience;
177
+ return (_ref2 = (_EMAIL_AUDIENCE_LABELS_audience = EMAIL_AUDIENCE_LABELS[audience]) != null ? _EMAIL_AUDIENCE_LABELS_audience : audience) != null ? _ref2 : 'Not recorded';
178
+ }
179
+ const name = String((_ref1 = record == null ? void 0 : record['listName']) != null ? _ref1 : '');
180
+ if (name) return name;
181
+ return (record == null ? void 0 : record['listId']) ? 'A list this send did not name' : 'A list this send did not record';
182
+ }
183
+ export function emailSentAs(record) {
184
+ var _ref, _ref1, _ref2;
185
+ const stamp = record == null ? void 0 : record['sentAs'];
186
+ const from = String((_ref = stamp == null ? void 0 : stamp['from']) != null ? _ref : '').trim();
187
+ // The ADDRESS is what makes the stamp a record of a send. A `sentAs` map
188
+ // carrying only a display name is a partial write, and reporting it as
189
+ // recorded would put a name on screen beside an address nothing supplied.
190
+ if (!from) {
191
+ return {
192
+ recorded: false,
193
+ from: null,
194
+ fromName: null,
195
+ replyTo: null
196
+ };
197
+ }
198
+ return {
199
+ recorded: true,
200
+ from,
201
+ fromName: String((_ref1 = stamp == null ? void 0 : stamp['fromName']) != null ? _ref1 : '').trim() || null,
202
+ replyTo: String((_ref2 = stamp == null ? void 0 : stamp['replyTo']) != null ? _ref2 : '').trim() || null
203
+ };
204
+ }
205
+
206
+ //# sourceMappingURL=email-record.js.map