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

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 (54) hide show
  1. package/README.md +3 -1
  2. package/package.json +16 -16
  3. package/src/lib/components/besigner-section-experiments.d.ts +20 -0
  4. package/src/lib/components/besigner-section-experiments.js +128 -0
  5. package/src/lib/components/besigner-section-experiments.js.map +1 -0
  6. package/src/lib/components/campaign-conversions-card.js +1 -1
  7. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  8. package/src/lib/components/campaign-members-section.js +4 -3
  9. package/src/lib/components/campaign-members-section.js.map +1 -1
  10. package/src/lib/components/campaign-reach-sections.js +4 -1
  11. package/src/lib/components/campaign-reach-sections.js.map +1 -1
  12. package/src/lib/components/campaign-report-card.js +2 -2
  13. package/src/lib/components/campaign-report-card.js.map +1 -1
  14. package/src/lib/components/conversion-attribution.component.d.ts +1 -1
  15. package/src/lib/components/conversion-attribution.component.js +1 -1
  16. package/src/lib/components/conversion-attribution.component.js.map +1 -1
  17. package/src/lib/components/record-attribution-widget.js +1 -1
  18. package/src/lib/components/record-attribution-widget.js.map +1 -1
  19. package/src/lib/components/site-runtime.js +2 -2
  20. package/src/lib/components/site-runtime.js.map +1 -1
  21. package/src/lib/constants/operator-alerts.js +4 -1
  22. package/src/lib/constants/operator-alerts.js.map +1 -1
  23. package/src/lib/declarations.server.d.ts +3 -1
  24. package/src/lib/declarations.server.js +17 -1
  25. package/src/lib/declarations.server.js.map +1 -1
  26. package/src/lib/model/campaign-conversions.d.ts +301 -0
  27. package/src/lib/model/campaign-conversions.js +252 -0
  28. package/src/lib/model/campaign-conversions.js.map +1 -0
  29. package/src/lib/model/campaign-kind.d.ts +50 -0
  30. package/src/lib/model/campaign-kind.js +52 -0
  31. package/src/lib/model/campaign-kind.js.map +1 -0
  32. package/src/lib/model/campaign-members-query.js +1 -1
  33. package/src/lib/model/campaign-members-query.js.map +1 -1
  34. package/src/lib/model/campaign-revenue.d.ts +327 -0
  35. package/src/lib/model/campaign-revenue.js +332 -0
  36. package/src/lib/model/campaign-revenue.js.map +1 -0
  37. package/src/lib/model/index.d.ts +3 -0
  38. package/src/lib/model/index.js +4 -1
  39. package/src/lib/model/index.js.map +1 -1
  40. package/src/lib/model/overlay-record-list.d.ts +33 -0
  41. package/src/lib/model/overlay-record-list.js +53 -0
  42. package/src/lib/model/overlay-record-list.js.map +1 -0
  43. package/src/lib/plugin.js +12 -0
  44. package/src/lib/plugin.js.map +1 -1
  45. package/src/lib/server/campaign-manage.js +19 -60
  46. package/src/lib/server/campaign-manage.js.map +1 -1
  47. package/src/lib/server/campaign-send.d.ts +1 -1
  48. package/src/lib/server/campaign-send.js +23 -7
  49. package/src/lib/server/campaign-send.js.map +1 -1
  50. package/src/lib/server/email-events.js +9 -10
  51. package/src/lib/server/email-events.js.map +1 -1
  52. package/src/lib/server/overlay-beacon.d.ts +46 -0
  53. package/src/lib/server/overlay-beacon.js +76 -0
  54. package/src/lib/server/overlay-beacon.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailRecipientsCard","RecordAttributionWidget","registerMarketingConsole","zone","label","surface","description","pluginId","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordLists } from './model/overlay-record-list'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/** The site's section experiments, reported to the besigner's Interactions section. */\nconst BesignerSectionExperiments = lazy(\n () => import('./components/besigner-section-experiments'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n // The site's overlays, for another plugin's picker to list (AGL-3080).\n registerMarketingRecordLists()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The section experiments the besigner's Interactions section badges an\n // element with and starts on a page's element; reported, never drawn.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,\n widgetId: 'marketing-besigner-section-experiments',\n title: 'Section experiments',\n Component: BesignerSectionExperiments,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordLists","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailRecipientsCard","RecordAttributionWidget","BesignerSectionExperiments","registerMarketingConsole","zone","label","surface","description","pluginId","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","besignerInteractions","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
@@ -88,7 +88,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
88
88
  */ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
89
89
  import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
90
90
  import { logResourceDuplicated } from "@aglyn/tenant-data-admin/server/duplicate-activity";
91
- import { restampCrmListFieldsOf } from "@aglyn/tenant-data-admin/server/crm-records";
92
91
  import { restampFormsInCampaign } from "./campaign-form-flags.js";
93
92
  import { DUPLICATE_BUSY_MESSAGE, duplicateDisplayName, uniqueDuplicateName } from "@aglyn/aglyn/app-utils/duplicate-resource";
94
93
  import { claimAttempt, createResourceUid } from "@aglyn/aglyn/server";
@@ -97,11 +96,12 @@ import { claimAttempt, createResourceUid } from "@aglyn/aglyn/server";
97
96
  * are what the contact pass and the organization door need, and taking them
98
97
  * from `@aglyn/tenant-data-admin`'s index would pull the whole Next server
99
98
  * pipeline back into this file's graph.
100
- */ import { consentGroupForSite, getOrgDoc, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
101
- import { CAMPAIGN_MEMBER_HOST_COLLECTIONS, CAMPAIGN_MEMBERSHIP_FIELD, contactCampaignFieldPath, isOrgWideMember } from "@aglyn/aglyn/server";
99
+ */ import { getOrgDoc, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
100
+ import { isOrgWideMember } from "@aglyn/aglyn/server";
102
101
  import { CAMPAIGN_SEND_CONTAINER_FIELD, CAMPAIGN_SEND_HOST_FIELD, campaignPlacedOnHost, campaignSiteIds, campaignVisibleTo } from "@aglyn/shared-ui-email-campaigns/model";
103
102
  import { campaignSendSiteStamp, orgCampaignSends, orgEmailCampaigns, sendHostId, sendIsOnHost } from "./campaign-org-refs.js";
104
103
  import { SCREEN_KIND_EMAIL } from "@aglyn/aglyn/app-utils/screen-route";
104
+ import { CAMPAIGN_MEMBER_HOST_COLLECTIONS, CAMPAIGN_MEMBERSHIP_FIELD } from "../model/campaign-kind.js";
105
105
  import { campaignContainerSearchFields, campaignSendSearchFields } from "../model/campaign-list-query.js";
106
106
  import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
107
107
  import { runPluginMembershipDetachers } from "@aglyn/aglyn/plugin-manager/plugin-membership-detach";
@@ -188,9 +188,6 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
188
188
  * campaign and a landing page is in several, so removing one must not be
189
189
  * removing all of them.
190
190
  *
191
- * `query` rather than a collection ref, because the contact pass hands in a
192
- * path this file cannot build for itself.
193
- *
194
191
  * @returns how many were detached, and whether any were left.
195
192
  */ async function detachMembership(firestore, collectionRef, fieldPath, campaignId) {
196
193
  let detached = 0;
@@ -210,11 +207,6 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
210
207
  } : {}));
211
208
  }
212
209
  await batch.commit();
213
- // A lead's campaigns are what the Leads list's Campaign filter reads
214
- // under a site (AGL-3321): restamped from each lead as it now stands.
215
- if (collectionRef.id === 'leads') {
216
- await restampCrmListFieldsOf(page.docs.map((member)=>member.ref), 'leads');
217
- }
218
210
  // A form's `inCampaign` is what the Forms list's "In a campaign" filter
219
211
  // asks (AGL-3330), and `arrayRemove` cannot say whether it emptied the
220
212
  // array: restamped from each form as it now stands.
@@ -247,9 +239,9 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
247
239
  };
248
240
  }
249
241
  /**
250
- * Clears the campaign off every form, screen, lead and contact holding it,
251
- * on every site of the organization, and then off every record a plugin
252
- * keeps about it (AGL-3254).
242
+ * Clears the campaign off every form and screen holding it, on every site of
243
+ * the organization, and then off every record a plugin keeps filed under it
244
+ * (AGL-3254, AGL-3080).
253
245
  *
254
246
  * ## Why the campaign is not simply deleted over the top of them
255
247
  *
@@ -266,36 +258,20 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
266
258
  * may still hold forms and screens filed under it. So the walk covers every
267
259
  * site in the org regardless of which door the deletion came through.
268
260
  *
269
- * ## Leads and contacts are reached on the organization
270
- *
271
- * A lead lives at `orgs/{orgId}/leads` and carries the field at the top of
272
- * its document, like a form. The site `leads` collection is walked as well,
273
- * as part of each site's own collections: records captured before leads
274
- * moved to the org are still read there.
275
- *
276
- * A contact lives at `orgs/{orgId}/contacts` and is shared by every site, so
277
- * the membership sits inside a consent group's facet rather than at the top
278
- * of the document. The field PATH is therefore the scope, and the walk runs
279
- * once per distinct group of the org's sites — two sites declared as one
280
- * sender share a facet, and clearing it twice would find nothing the second
281
- * time. The unscoped collection ref is what the walk runs on precisely
282
- * because the path already carries the boundary — a `visibleTo` filter would
283
- * spend the query's one array-contains slot and leave none for the
284
- * membership.
285
- *
286
261
  * ## A plugin's members are the plugin's to clear
287
262
  *
288
- * A sequence and its enrollments name the campaign from documents under the
289
- * org, in collections this plugin does not know (AGL-3254). Every plugin
290
- * with a membership detacher registered on the core's seam is asked, once
291
- * per site, with the campaign's id and the field it is held in, and a
292
- * detacher that reports records remaining — or threw, which is reported as
293
- * `null` — holds the container exactly as this pass's own `remaining` does.
294
- * A plugin that failed is a plugin whose records may still name the
295
- * campaign, and the deletion has no way to tell that apart from one that has
296
- * more than a request clears.
263
+ * A lead, a contact, a sequence and its enrollments name the campaign from
264
+ * documents another plugin keeps, in collections this plugin does not know:
265
+ * the CRM's leads carry the field at the top of their documents and its
266
+ * contacts inside each consent group's facet, and Outreach's sequences under
267
+ * the org. Every plugin with a membership detacher registered on the core's
268
+ * seam is asked, once per site, with the campaign's id and the field it is
269
+ * held in, and a detacher that reports records remaining — or threw, which is
270
+ * reported as `null` — holds the container exactly as this pass's own
271
+ * `remaining` does. A plugin that failed is a plugin whose records may still
272
+ * name the campaign, and the deletion has no way to tell that apart from one
273
+ * that has more than a request clears.
297
274
  */ async function detachMembers(firestore, orgId, campaignId) {
298
- var _ref;
299
275
  let detached = 0;
300
276
  let remaining = false;
301
277
  const take = (pass)=>{
@@ -310,8 +286,6 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
310
286
  take(await detachMembership(firestore, hostRef.collection(collectionName), CAMPAIGN_MEMBERSHIP_FIELD, campaignId));
311
287
  }
312
288
  }
313
- const orgRef = firestore.collection('orgs').doc(orgId);
314
- take(await detachMembership(firestore, orgRef.collection('leads'), CAMPAIGN_MEMBERSHIP_FIELD, campaignId));
315
289
  /*
316
290
  * An org with no sites still has plugins that may hold the campaign, so
317
291
  * the seam is asked once with no site rather than not at all.
@@ -325,26 +299,11 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
325
299
  id: campaignId
326
300
  });
327
301
  for (const report of Object.values(plugins)){
328
- var _ref1;
329
- detached += (_ref1 = report == null ? void 0 : report.detached) != null ? _ref1 : 0;
302
+ var _ref;
303
+ detached += (_ref = report == null ? void 0 : report.detached) != null ? _ref : 0;
330
304
  remaining = remaining || report === null || report.remaining;
331
305
  }
332
306
  }
333
- /*
334
- * One read of the org document serves every site's group: with it in
335
- * hand, `consentGroupForSite` resolves from the declaration without a
336
- * lookup per site. A missing document answers every site as a group of
337
- * one, which is `consentGroupForSite`'s documented failure direction and
338
- * the safe one here — each site's own facet is cleared, and no other.
339
- */ const org = (_ref = await getOrgDoc(orgId)) != null ? _ref : {};
340
- const groupIds = new Set();
341
- for (const hostId of hostIds){
342
- groupIds.add((await consentGroupForSite(hostId, org)).groupId);
343
- }
344
- const contacts = orgRef.collection('contacts');
345
- for (const groupId of groupIds){
346
- take(await detachMembership(firestore, contacts, contactCampaignFieldPath(groupId), campaignId));
347
- }
348
307
  return {
349
308
  detached,
350
309
  remaining
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-manage.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 * TAKING SOMETHING AWAY: a campaign container, and an abandoned draft.\n *\n * Both are removals, and neither may take a delivered message with it. That\n * is the whole reason this file is small and the comments are long.\n *\n * ## Why these two are not client writes\n *\n * The console creates and edits a campaign CONTAINER with the client SDK —\n * `orgs/{orgId}/emailCampaigns` is client-writable, because a container holds\n * no counter, no consent record and no entitlement input. It is not\n * client-DELETABLE: removing one has to detach everything that names it\n * first, across every site in the organization, which no rule can express.\n *\n * The SEND collection is the opposite: `orgs/{orgId}/campaigns` is excluded\n * from client create, update AND delete, because each document is the record\n * of what a merchant mailed and to whom they were allowed to mail it. Both\n * operations here have to touch it — one to detach sends from the container\n * being removed, the other to remove a draft outright — so both run on the\n * Admin SDK behind a role check.\n *\n * ## Two doors: a site, and the organization\n *\n * A campaign belongs to the organization and may be placed on several of its\n * sites. A request from a SITE hub names the site (`hostId`) and is\n * authorized by the caller's role there, as the send route is — and it acts\n * only on what that site holds: a send sent as another site, or a campaign\n * not placed on this one, answers as if it did not exist. A request from the\n * ORGANIZATION hub names the org (`orgId`) and no site, and is authorized by\n * the caller's org-wide membership; for an email it acts in the name of the\n * site the email is sent as.\n *\n * ## What deleting a campaign MEANS\n *\n * The container goes; every email inside it stays, and reads afterwards as a\n * single send.\n *\n * It cannot mean anything else. A send id is cited by mail already delivered\n * — every unsubscribe footer carries `cid={sendId}` inside its own HMAC, and\n * `/marketing/campaigns/{sendId}` is a URL merchants paste into their own\n * messages — so deleting a container that destroyed its sends would break\n * opt-out links that must go on resolving forever, which is a compliance\n * failure rather than a broken page.\n *\n * Nor may the sends simply be left pointing at a container that is gone.\n * `campaignListRows` groups sends by the container they name and draws one\n * row per container; a send naming a container that no longer exists is in\n * neither half of that — not a container row, and not an orphan the list\n * adopts — so it would vanish from the campaigns table while remaining\n * perfectly deliverable. Detaching is what puts it back in the list, as the\n * \"Single send\" the product already models and the table already draws.\n *\n * Deleting a campaign is therefore NOT a way to stop its mail. A scheduled\n * email inside it keeps its send time and still goes out, because a\n * container is a grouping and cancelling somebody's mail is `cancel`'s job.\n * The console says so before it asks.\n *\n * ## The same rule for everything else the campaign held\n *\n * A campaign also groups forms, screens and contacts, each of which names it\n * from its OWN document. They are detached on exactly the argument the sends\n * are: a form left naming a container nobody can read has not been freed, it\n * has been made unreadable — its page draws a dead id where a campaign name\n * belongs, and the campaigns table can no longer find it. The record itself\n * is never touched. Deleting a campaign deletes a campaign.\n */\n\n/*\n * The MODULE, not the barrel. `@aglyn/tenant-data-admin`'s index reaches\n * `render-cache`, which imports `next/cache` and therefore the whole Next\n * server pipeline — everything this file needs from the admin SDK is one\n * default export, and taking it from the leaf keeps that pipeline out of the\n * graph.\n */\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport { logResourceDuplicated } from '@aglyn/tenant-data-admin/server/duplicate-activity'\nimport { restampCrmListFieldsOf } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { restampFormsInCampaign } from './campaign-form-flags'\nimport {\n DUPLICATE_BUSY_MESSAGE,\n duplicateDisplayName,\n uniqueDuplicateName,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { claimAttempt, createResourceUid } from '@aglyn/aglyn/server'\n/*\n * The MODULES again, for the same reason: the org and membership reads here\n * are what the contact pass and the organization door need, and taking them\n * from `@aglyn/tenant-data-admin`'s index would pull the whole Next server\n * pipeline back into this file's graph.\n */\nimport {\n consentGroupForSite,\n getOrgDoc,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n CAMPAIGN_MEMBER_HOST_COLLECTIONS,\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n isOrgWideMember,\n type AglynOrgMember,\n type PluginApiHandler,\n type PluginApiRequestSubject,\n} from '@aglyn/aglyn/server'\nimport {\n CAMPAIGN_SEND_CONTAINER_FIELD,\n CAMPAIGN_SEND_HOST_FIELD,\n campaignPlacedOnHost,\n campaignSiteIds,\n campaignVisibleTo,\n} from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n campaignSendSiteStamp,\n orgCampaignSends,\n orgEmailCampaigns,\n sendHostId,\n sendIsOnHost,\n} from './campaign-org-refs'\nimport { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n campaignContainerSearchFields,\n campaignSendSearchFields,\n} from '../model/campaign-list-query'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { runPluginMembershipDetachers } from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\n\n/**\n * Sends detached in one write.\n *\n * Under Firestore's 500-operation batch limit with room to spare rather than\n * at it: a batch that is refused for being one over has done nothing, and the\n * margin costs one extra round trip on a campaign of exactly this size.\n */\nconst DETACH_BATCH = 400\n\n/**\n * How many batches one request will run.\n *\n * A ceiling on the REQUEST, not on the campaign. The loop below terminates on\n * its own — each pass clears the very field it queries on, so a detached send\n * is not returned again — but a request that runs unbounded is a request that\n * eventually exceeds the platform's own timeout with no idea how far it got.\n *\n * Stopping early is safe and is why this is a ceiling rather than a refusal\n * up front: detaching is idempotent and partial progress is a consistent\n * state — the sends already detached read as single sends, the rest still\n * read under the campaign — so the answer to hitting it is to ask again.\n */\nconst DETACH_PASSES = 25\n\n/**\n * The most sites one deletion walks for members.\n *\n * A campaign is the organization's, so its forms, screens and consent-group\n * facets may be on any of its sites. An organization past this has more\n * sites than one request should sweep, and the deletion refuses rather than\n * removing a container some sites' records would still name.\n */\nconst ORG_SITES_MAX = 200\n\n/** What one `campaigns/manage` call answered with. */\ninterface ManageResult {\n status: number\n body: Record<string, unknown>\n}\n\n/**\n * Files every send in a campaign under NO campaign: {@link\n * CAMPAIGN_SEND_CONTAINER_FIELD} set to `null`.\n *\n * Equality on one field with a `limit` — no `orderBy` — which Firestore's\n * automatic single-field index serves. Ordering would need a composite index\n * for a walk whose order does not matter: every match is being written, so\n * which order they come back in cannot change the outcome.\n *\n * @returns how many were detached, and whether any were left.\n */\nasync function detachSends(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n campaignId: string,\n): Promise<{ detached: number; remaining: boolean }> {\n let detached = 0\n for (let pass = 0; pass < DETACH_PASSES; pass += 1) {\n const page = await orgCampaignSends(firestore, orgId)\n .where(CAMPAIGN_SEND_CONTAINER_FIELD, '==', campaignId)\n .limit(DETACH_BATCH)\n .get()\n if (page.empty) return { detached, remaining: false }\n const batch = firestore.batch()\n for (const send of page.docs) {\n /*\n * `null`, not an empty string and not a removed field. \"Belongs to no\n * campaign\" has ONE stored shape, because it is a query: the lists ask\n * `emailCampaignId == null` for single sends (AGL-3321), which an\n * empty string would not answer and a missing field cannot — a query\n * never finds a field that is not there. `campaignListRows` adopts any\n * falsy container id, so every reader agrees.\n */\n batch.update(send.ref, {\n [CAMPAIGN_SEND_CONTAINER_FIELD]: null,\n })\n }\n await batch.commit()\n detached += page.size\n if (page.size < DETACH_BATCH) return { detached, remaining: false }\n }\n return { detached, remaining: true }\n}\n\n/**\n * Clears one campaign id out of a membership array, wherever it is held.\n *\n * The same walk {@link detachSends} makes, over an ARRAY field rather than a\n * scalar one: `array-contains` matches on Firestore's automatic single-field\n * index, and `arrayRemove` takes out the one id without touching the other\n * campaigns a form or a screen is in. That is the whole reason the membership\n * is an array here and a `FieldValue.delete()` on a send — a send is in one\n * campaign and a landing page is in several, so removing one must not be\n * removing all of them.\n *\n * `query` rather than a collection ref, because the contact pass hands in a\n * path this file cannot build for itself.\n *\n * @returns how many were detached, and whether any were left.\n */\nasync function detachMembership(\n firestore: FirebaseFirestore.Firestore,\n collectionRef: FirebaseFirestore.CollectionReference,\n fieldPath: string,\n campaignId: string,\n): Promise<{ detached: number; remaining: boolean }> {\n let detached = 0\n for (let pass = 0; pass < DETACH_PASSES; pass += 1) {\n const page = await collectionRef\n .where(fieldPath, 'array-contains', campaignId)\n .limit(DETACH_BATCH)\n .get()\n if (page.empty) return { detached, remaining: false }\n const batch = firestore.batch()\n const isForms = collectionRef.id === 'forms'\n for (const member of page.docs) {\n batch.update(member.ref, {\n [fieldPath]: firebaseAdmin.firestore.FieldValue.arrayRemove(campaignId),\n // Leaving a campaign is an edit to the form, and Updated says when\n // the form was last edited (AGL-3330).\n ...(isForms ? { updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp() } : {}),\n })\n }\n await batch.commit()\n // A lead's campaigns are what the Leads list's Campaign filter reads\n // under a site (AGL-3321): restamped from each lead as it now stands.\n if (collectionRef.id === 'leads') {\n await restampCrmListFieldsOf(page.docs.map((member) => member.ref), 'leads')\n }\n // A form's `inCampaign` is what the Forms list's \"In a campaign\" filter\n // asks (AGL-3330), and `arrayRemove` cannot say whether it emptied the\n // array: restamped from each form as it now stands.\n if (isForms) {\n await restampFormsInCampaign(firestore, page.docs.map((member) => member.ref))\n }\n detached += page.size\n if (page.size < DETACH_BATCH) return { detached, remaining: false }\n }\n return { detached, remaining: true }\n}\n\n/**\n * Every site of the organization, by id — the sites a campaign's members may\n * be on. One equality on the `hostIndex` mirror, the same document\n * `resolveOrgIdForHost` reads to put a site in an org.\n *\n * @returns the ids, and whether the org has more than {@link ORG_SITES_MAX}.\n */\nasync function orgSiteIds(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<{ hostIds: string[]; truncated: boolean }> {\n const page = await firestore\n .collection('hostIndex')\n .where('orgId', '==', orgId)\n .limit(ORG_SITES_MAX + 1)\n .get()\n const hostIds = page.docs.map((doc) => String(doc.id))\n return {\n hostIds: hostIds.slice(0, ORG_SITES_MAX),\n truncated: hostIds.length > ORG_SITES_MAX,\n }\n}\n\n/**\n * Clears the campaign off every form, screen, lead and contact holding it,\n * on every site of the organization, and then off every record a plugin\n * keeps about it (AGL-3254).\n *\n * ## Why the campaign is not simply deleted over the top of them\n *\n * A campaign that went away leaving members naming it is the same failure the\n * send detach exists to prevent, one collection over: the campaign's own page\n * finds its forms and screens with `array-contains`, and a form still naming\n * a container nobody can read is in neither half of that — not a member of\n * any campaign the console can draw, and not free of one either. Its own page\n * would render the dead id as a chip with no name.\n *\n * ## Every site, not the one that asked\n *\n * The campaign is the organization's, and a site it has since been taken off\n * may still hold forms and screens filed under it. So the walk covers every\n * site in the org regardless of which door the deletion came through.\n *\n * ## Leads and contacts are reached on the organization\n *\n * A lead lives at `orgs/{orgId}/leads` and carries the field at the top of\n * its document, like a form. The site `leads` collection is walked as well,\n * as part of each site's own collections: records captured before leads\n * moved to the org are still read there.\n *\n * A contact lives at `orgs/{orgId}/contacts` and is shared by every site, so\n * the membership sits inside a consent group's facet rather than at the top\n * of the document. The field PATH is therefore the scope, and the walk runs\n * once per distinct group of the org's sites — two sites declared as one\n * sender share a facet, and clearing it twice would find nothing the second\n * time. The unscoped collection ref is what the walk runs on precisely\n * because the path already carries the boundary — a `visibleTo` filter would\n * spend the query's one array-contains slot and leave none for the\n * membership.\n *\n * ## A plugin's members are the plugin's to clear\n *\n * A sequence and its enrollments name the campaign from documents under the\n * org, in collections this plugin does not know (AGL-3254). Every plugin\n * with a membership detacher registered on the core's seam is asked, once\n * per site, with the campaign's id and the field it is held in, and a\n * detacher that reports records remaining — or threw, which is reported as\n * `null` — holds the container exactly as this pass's own `remaining` does.\n * A plugin that failed is a plugin whose records may still name the\n * campaign, and the deletion has no way to tell that apart from one that has\n * more than a request clears.\n */\nasync function detachMembers(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n campaignId: string,\n): Promise<{ detached: number; remaining: boolean }> {\n let detached = 0\n let remaining = false\n const take = (pass: { detached: number; remaining: boolean }) => {\n detached += pass.detached\n remaining = remaining || pass.remaining\n }\n const { hostIds, truncated } = await orgSiteIds(firestore, orgId)\n if (truncated) remaining = true\n for (const hostId of hostIds) {\n const hostRef = firestore.collection('hosts').doc(hostId)\n for (const collectionName of CAMPAIGN_MEMBER_HOST_COLLECTIONS) {\n take(\n await detachMembership(\n firestore,\n hostRef.collection(collectionName),\n CAMPAIGN_MEMBERSHIP_FIELD,\n campaignId,\n ),\n )\n }\n }\n const orgRef = firestore.collection('orgs').doc(orgId)\n take(\n await detachMembership(\n firestore,\n orgRef.collection('leads'),\n CAMPAIGN_MEMBERSHIP_FIELD,\n campaignId,\n ),\n )\n /*\n * An org with no sites still has plugins that may hold the campaign, so\n * the seam is asked once with no site rather than not at all.\n */\n for (const hostId of hostIds.length ? hostIds : ['']) {\n const plugins = await runPluginMembershipDetachers({\n hostId,\n orgId,\n field: CAMPAIGN_MEMBERSHIP_FIELD,\n id: campaignId,\n })\n for (const report of Object.values(plugins)) {\n detached += report?.detached ?? 0\n remaining = remaining || report === null || report.remaining\n }\n }\n /*\n * One read of the org document serves every site's group: with it in\n * hand, `consentGroupForSite` resolves from the declaration without a\n * lookup per site. A missing document answers every site as a group of\n * one, which is `consentGroupForSite`'s documented failure direction and\n * the safe one here — each site's own facet is cleared, and no other.\n */\n const org = ((await getOrgDoc(orgId)) ?? {}) as Record<string, unknown>\n const groupIds = new Set<string>()\n for (const hostId of hostIds) {\n groupIds.add((await consentGroupForSite(hostId, org)).groupId)\n }\n const contacts = orgRef.collection('contacts')\n for (const groupId of groupIds) {\n take(\n await detachMembership(\n firestore,\n contacts,\n contactCampaignFieldPath(groupId),\n campaignId,\n ),\n )\n }\n return { detached, remaining }\n}\n\n/** Who is deleting, and through which door. */\ninterface DeleteAccess {\n /** The site hub the request came from, or `''` from the organization hub. */\n hostId: string\n /** The caller, for the site door's reach check. */\n uid: string\n}\n\n/**\n * Removes a campaign container, leaving its emails standing.\n *\n * Detach first, delete second, and the order is the point: stopping between\n * them leaves a container whose emails are already single sends, which the\n * list draws correctly and a second run finishes. Deleting first would leave\n * sends naming a container nobody can read, which is the state that loses\n * them from the table.\n *\n * ## What a site hub may delete\n *\n * A campaign placed on this site and on no other — the one a site hub\n * creates. A campaign that is not placed here is not this site's to see, so\n * it answers 404 as a missing one would. A campaign also placed on other\n * sites is refused unless the caller is an org-wide member: deleting it\n * takes it off those sites too, which is the same reach change the rules\n * reserve for org-wide members when a campaign is edited.\n */\nasync function deleteCampaign(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n campaignId: string,\n access: DeleteAccess,\n): Promise<ManageResult> {\n const ref = orgEmailCampaigns(firestore, orgId).doc(campaignId)\n const snapshot = await ref.get()\n /*\n * A 404 rather than a silent success. The id in a campaign URL may name a\n * container OR a single send — that ambiguity is what keeps every link\n * minted before containers existed resolving — so \"no container here\" is\n * very often \"this is a send\", and answering it as a completed deletion\n * would tell the console it had removed something it never touched.\n */\n const placement = {\n visibleTo: snapshot.exists\n ? (snapshot.get('visibleTo') as string[] | undefined)\n : undefined,\n }\n if (\n !snapshot.exists ||\n (access.hostId && !campaignPlacedOnHost(placement, access.hostId))\n ) {\n return { status: 404, body: { error: 'Unknown campaign' } }\n }\n if (access.hostId) {\n const sites = campaignSiteIds(placement)\n const onlyHere = sites !== null && sites.every((id) => id === access.hostId)\n if (!onlyHere) {\n const membership = await resolveOrgMembership(access.uid, orgId)\n if (!isOrgWideMember(membership?.member)) {\n return {\n status: 403,\n body: {\n error:\n 'This campaign is on other sites too, so deleting it would take ' +\n 'it off them as well. An organization admin can delete it from ' +\n 'the organization’s campaigns.',\n },\n }\n }\n }\n }\n const { detached, remaining } = await detachSends(firestore, orgId, campaignId)\n if (remaining) {\n return {\n status: 409,\n body: {\n detached,\n error:\n `This campaign holds more emails than one request can detach. ` +\n `${detached.toLocaleString()} of them now read as single sends and ` +\n 'the campaign is still here — run the delete again to finish it.',\n },\n }\n }\n /*\n * The MEMBERS come off before the container does, for the reason the sends\n * do: a partial run leaves records that are already out of the campaign,\n * which every surface draws correctly, while deleting first would leave\n * forms, screens and contacts naming an id nothing can resolve.\n *\n * Their own 409 rather than a shared one. \"More emails than one request can\n * detach\" is a sentence about the campaign's mail, and a merchant reading\n * it about a campaign of two emails and four thousand contacts would go\n * looking for emails that are not the problem.\n */\n const members = await detachMembers(firestore, orgId, campaignId)\n if (members.remaining) {\n return {\n status: 409,\n body: {\n detached,\n detachedMembers: members.detached,\n error:\n 'This campaign is on more records than one request can clear. ' +\n `${members.detached.toLocaleString()} of them are already out of ` +\n 'it and the campaign is still here — run the delete again to ' +\n 'finish it.',\n },\n }\n }\n await ref.delete()\n return {\n status: 200,\n body: {\n campaignId,\n detached,\n detachedMembers: members.detached,\n deleted: true,\n },\n }\n}\n\n/**\n * Discards a DRAFT email, and refuses anything else.\n *\n * The state check is inside the transaction as well as being the reason for\n * it. `sendNow` claims a draft by moving it to `sending` in a transaction of\n * its own, so a check made against an earlier read could delete a record\n * while the send path was mailing from it — the merchant would receive a\n * report for an email that no longer exists, and its `cid` would stop\n * resolving with the message already in inboxes.\n *\n * Only `draft`. A scheduled email is withdrawn with `cancel`, which leaves\n * the record and its report standing; a sent one has reached people, and\n * nothing on this surface may remove the evidence of that.\n *\n * `hostId` is the site hub the request came from, and a draft sent as a\n * different site is refused as unknown; from the organization hub it is\n * `''` and any site's draft may be discarded.\n */\nasync function discardDraft(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emailId: string,\n hostId: string,\n): Promise<ManageResult> {\n const ref = orgCampaignSends(firestore, orgId).doc(emailId)\n const refusal = (state: string): string => {\n if (state === 'sent') {\n return 'This email has already been sent, so it cannot be discarded. ' +\n 'Its report and its unsubscribe links have to go on resolving.'\n }\n if (state === 'scheduled') {\n return 'This email is scheduled. Cancel the send first — cancelling ' +\n 'keeps the email and takes it off the clock.'\n }\n if (state === 'sending') {\n return 'This email is being sent right now.'\n }\n return 'Only a draft can be discarded.'\n }\n const outcome = await firestore.runTransaction(\n async (transaction): Promise<ManageResult> => {\n const fresh = await transaction.get(ref)\n if (!fresh.exists || (hostId && !sendIsOnHost(fresh, hostId))) {\n return { status: 404, body: { error: 'Unknown email' } }\n }\n const state = String(fresh.get('status') ?? '')\n if (state !== 'draft') {\n return { status: 409, body: { error: refusal(state) } }\n }\n /*\n * A plain delete, with nothing recursive under it. The one\n * subcollection a message grows is `reports/links`, written by the\n * delivery webhook — an email that has never been sent has no\n * deliveries, so there is nothing beneath a draft to leave behind.\n */\n transaction.delete(ref)\n return { status: 200, body: { emailId, discarded: true } }\n },\n )\n return outcome\n}\n\n/**\n * The fields a copy of a message carries (AGL-2936): what was written and\n * how it is sent — never who it goes to or when. The audience, list,\n * segment, addresses, schedule and experiment are the decisions a person\n * makes about THIS send, and a copy that inherited them would be one click\n * from mailing the same people twice.\n */\nconst DUPLICATED_MESSAGE_FIELDS = [\n 'subject',\n 'body',\n 'fromName',\n 'replyTo',\n 'senderId',\n 'templateScreenId',\n 'plainText',\n 'plainTextVersionId',\n 'topicId',\n CAMPAIGN_SEND_CONTAINER_FIELD,\n] as const\n\n/**\n * Copies a message as a new draft (AGL-2936). Any status can be copied —\n * a sent email is the usual source — and the copy is always `draft`, with\n * the name the person gave it made unique among its site's messages.\n *\n * The copy is sent as the same site as its source: the sender, the design\n * and the consent it would mail under are all that site's. From a site hub\n * that is the requesting site, and a source sent as another site is refused\n * as unknown; from the organization hub (`hostId` `''`) it is whichever site\n * the source records, and a source that records none cannot be copied,\n * because the copy would be an email that can never be sent.\n */\nasync function duplicateMessage(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n hostId: string,\n emailId: string,\n requestedName: string,\n actor: { uid: string; email: string | null },\n): Promise<ManageResult> {\n const messages = orgCampaignSends(firestore, orgId)\n const id = createResourceUid()\n /*\n * The source is read once outside the transaction to learn its site, which\n * decides which siblings the name must be unique among; the transaction\n * reads it again, so a source changed in between is copied as it stands.\n */\n const peek = await messages.doc(emailId).get()\n if (!peek.exists || (hostId && !sendIsOnHost(peek, hostId))) {\n return { status: 404, body: { error: 'Unknown email' } }\n }\n const siteId = hostId || sendHostId(peek)\n if (!siteId) {\n return {\n status: 409,\n body: {\n error:\n 'This email does not record which site it is sent from, so it ' +\n 'cannot be copied from here. Open it from its site instead.',\n },\n }\n }\n const outcome = await firestore.runTransaction(\n async (transaction): Promise<ManageResult & { name?: string; sourceName?: string }> => {\n const [source, siblings] = await Promise.all([\n transaction.get(messages.doc(emailId)),\n transaction.get(\n messages\n .select('displayName', 'subject')\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', siteId),\n ),\n ])\n if (!source.exists) {\n return { status: 404, body: { error: 'Unknown email' } }\n }\n const data = (source.data() ?? {}) as Record<string, unknown>\n const sourceName = String(data['displayName'] ?? data['subject'] ?? '').trim()\n const name = uniqueDuplicateName(\n requestedName.trim() || duplicateDisplayName(sourceName),\n siblings.docs.map((row) => String(row.get('displayName') ?? row.get('subject') ?? '')),\n )\n const copy: Record<string, unknown> = {}\n for (const field of DUPLICATED_MESSAGE_FIELDS) {\n if (data[field] !== undefined && data[field] !== null) copy[field] = data[field]\n }\n const nowMs = Date.now()\n transaction.create(messages.doc(id), {\n ...copy,\n // The list fields a new record carries (AGL-3321): its subject's\n // search tokens, and `null` for a source filed under no campaign.\n ...campaignSendSearchFields(copy['subject']),\n [CAMPAIGN_SEND_CONTAINER_FIELD]: copy[CAMPAIGN_SEND_CONTAINER_FIELD] ?? null,\n ...campaignSendSiteStamp(siteId),\n displayName: name,\n status: 'draft',\n createdAtMs: nowMs,\n draftedAt: new Date(nowMs),\n draftedBy: actor.uid,\n })\n return { status: 200, body: { emailId: id, name }, name, sourceName }\n },\n )\n if (outcome.status === 200) {\n await logResourceDuplicated(\n 'campaign',\n { uid: actor.uid, email: actor.email },\n {\n orgId,\n hostId: siteId,\n source: { id: emailId, name: outcome.sourceName ?? '' },\n target: { id, name: outcome.name ?? '' },\n },\n )\n }\n return { status: outcome.status, body: outcome.body }\n}\n\n/*==========================================\n * A CAMPAIGN THAT EXISTS BEFORE ANYBODY CHOSE WHO GETS IT (AGL-2912).\n *\n * The draft writer this plugin registers for the `campaign` resource on the\n * core's resource-drafts seam. Another plugin that produces a campaign — a\n * generator working from a brief, an importer bringing campaigns over from\n * another tool — asks for it by name, and gets exactly two documents:\n *\n * - the CONTAINER at `orgs/{orgId}/emailCampaigns/{id}`, written as a site\n * hub's create drawer writes one: aimed at no list, placed on the site\n * the draft was asked for;\n * - ONE EMAIL inside it at `orgs/{orgId}/campaigns/{id}-email`,\n * `status: 'draft'`, sent as that site, naming the email design it sends\n * and holding its subject, preheader and the alternatives to them.\n *\n * ## What a drafted email never holds\n *\n * An audience, a list, a segment, addresses, a topic, a sender, a send time or\n * an experiment. Those are the decisions a person makes about a send — the\n * duplicate above strips the same ones for the same reason — and a draft that\n * arrived holding any of them would be one click from mailing people nobody\n * chose. So the writer has no field for them to arrive in.\n *\n * ## Why a draft cannot escape\n *\n * Nothing here reserves an allowance, claims a budget or calls the send path,\n * and the scheduled processor queries `status == 'scheduled'`, which a draft\n * never is. `performCampaignSend` is the only thing that mails a draft, behind\n * the send route's own authorization, when a member asks.\n *\n * The id of the email is derived from the campaign's rather than minted, so a\n * write asked again finds the email it made — and differs from it, because a\n * campaign URL names a container OR a send, and one id naming both would read\n * as either.\n *=========================================*/\n\n/** The resource name the campaign draft writer is registered under. */\nexport const CAMPAIGN_DRAFT_RESOURCE = 'campaign'\n\n/** The id `plugins.config.json` registers this plugin under. */\nconst MARKETING_PLUGIN_ID = 'marketing'\n\n/** A drafted campaign's name when the caller asks for none. */\nexport const CAMPAIGN_DRAFT_DEFAULT_NAME = 'Untitled campaign'\n\n/** The longest subject or preheader a drafted email stores: the send route's header cap. */\nexport const CAMPAIGN_DRAFT_HEADER_MAX_CHARS = 200\n\n/** The most alternative subject lines or preheaders a drafted email keeps. */\nexport const CAMPAIGN_DRAFT_MAX_VARIANTS = 10\n\n/** The site roles that may compose this site's mail. */\nexport const CAMPAIGN_DRAFT_ROLES = ['admin', 'editor'] as const\n/** The send route's refusal for a member who may not compose this site's mail. */\nexport const CAMPAIGN_DRAFT_ROLE_REFUSAL = 'Not a site admin or editor'\n\n/** The id of the one email inside a drafted campaign. */\nexport function campaignDraftEmailId(campaignId: string): string {\n return `${campaignId}-email`\n}\n\n/** Every field a drafted email is written with, and so every field it can hold. */\nexport const CAMPAIGN_DRAFT_EMAIL_FIELDS = [\n 'subject',\n 'subjectTokens',\n 'preheader',\n 'templateScreenId',\n 'subjectVariants',\n 'preheaderVariants',\n CAMPAIGN_SEND_CONTAINER_FIELD,\n CAMPAIGN_SEND_HOST_FIELD,\n 'visibleTo',\n 'status',\n 'createdAtMs',\n 'draftedAt',\n 'draftedBy',\n] as const\n\n/** What a caller sends as `content`. */\nexport interface CampaignDraftContent {\n /** The email design the email sends: a `kind: 'email'` screen on the site. */\n templateScreenId: string\n subject: string\n preheader: string\n subjectVariants: string[]\n preheaderVariants: string[]\n}\n\n/** A header value on one line: controls and runs of space folded to one space. */\nfunction draftHeaderLine(value: unknown): string {\n return String(value ?? '').replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n}\n\nfunction draftVariants(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const lines: string[] = []\n for (const raw of value) {\n const line = draftHeaderLine(raw)\n if (!line || seen.has(line.toLowerCase())) continue\n seen.add(line.toLowerCase())\n lines.push(line)\n }\n return lines.slice(0, CAMPAIGN_DRAFT_MAX_VARIANTS)\n}\n\nexport function readCampaignDraftContent(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CampaignDraftContent } | { ok: false; problems: string[] } {\n const value: CampaignDraftContent = {\n templateScreenId: String(content['templateScreenId'] ?? ''),\n subject: draftHeaderLine(content['subject']),\n preheader: draftHeaderLine(content['preheader']),\n subjectVariants: draftVariants(content['subjectVariants']),\n preheaderVariants: draftVariants(content['preheaderVariants']),\n }\n const problems: string[] = []\n if (!isDocumentId(value.templateScreenId)) {\n problems.push('A drafted campaign needs the email design it sends')\n }\n if (!value.subject) problems.push('A drafted campaign needs a subject line')\n const long = (lines: string[]) =>\n lines.some((line) => line.length > CAMPAIGN_DRAFT_HEADER_MAX_CHARS)\n if (long([value.subject, ...value.subjectVariants])) {\n problems.push(`A subject line is longer than ${CAMPAIGN_DRAFT_HEADER_MAX_CHARS} characters`)\n }\n if (long([value.preheader, ...value.preheaderVariants])) {\n problems.push(`A preheader is longer than ${CAMPAIGN_DRAFT_HEADER_MAX_CHARS} characters`)\n }\n return problems.length ? { ok: false, problems } : { ok: true, value }\n}\n\nfunction campaignDraftRoleRefusal(\n host: FirebaseFirestore.DocumentSnapshot,\n uid: string,\n): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return CAMPAIGN_DRAFT_ROLES.includes(role)\n ? null\n : { status: 403, error: CAMPAIGN_DRAFT_ROLE_REFUSAL }\n}\n\nfunction campaignDraftRecord(campaign: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n return {\n id: campaign.id,\n name: String(campaign.get('name') ?? ''),\n versionId: null,\n facts: { emailId: campaignDraftEmailId(campaign.id) },\n }\n}\n\nexport interface CampaignDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => FirebaseFirestore.Firestore\n /** The site's organization, or null; specs hand in a double. */\n resolveOrgId?: (hostId: string) => Promise<string | null>\n}\n\nexport function createCampaignDraftWriter(\n deps: CampaignDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore =\n deps.firestore ??\n (() => firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore)\n /*\n * The org is resolved from the SITE rather than taken from the request's\n * context: the site is what the role check reads, so the campaign lands in\n * the organization that site belongs to and no other.\n */\n const resolveOrgId = deps.resolveOrgId ?? resolveOrgIdForHost\n return {\n refusal: async ({ hostId, uid }) => {\n const host = await firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return campaignDraftRoleRefusal(host, uid)\n },\n\n check: (content) => {\n const read = readCampaignDraftContent(content)\n return read.ok === false\n ? read\n : { ok: true, facts: { fields: [...CAMPAIGN_DRAFT_EMAIL_FIELDS] } }\n },\n\n read: async ({ hostId, id }) => {\n if (!isDocumentId(id)) return null\n const orgId = await resolveOrgId(hostId)\n if (!orgId) return null\n const campaign = await orgEmailCampaigns(firestore(), orgId).doc(id).get()\n return campaign.exists &&\n campaignPlacedOnHost(\n { visibleTo: campaign.get('visibleTo') as string[] | undefined },\n hostId,\n )\n ? campaignDraftRecord(campaign)\n : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readCampaignDraftContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (!isDocumentId(request.id)) return { ok: false, status: 400, error: 'Invalid campaignId' }\n const db = firestore()\n const orgId = await resolveOrgId(request.hostId)\n if (!orgId) {\n return { ok: false, status: 409, error: 'This site is not part of an organization' }\n }\n const hostRef = db.collection('hosts').doc(request.hostId)\n const campaignRef = orgEmailCampaigns(db, orgId).doc(request.id)\n const emailId = campaignDraftEmailId(request.id)\n const emailRef = orgCampaignSends(db, orgId).doc(emailId)\n const designRef = hostRef.collection('screens').doc(read.value.templateScreenId)\n return db.runTransaction(async (transaction): Promise<PluginDraftWrite> => {\n const [host, campaign, email, design] = await Promise.all([\n transaction.get(hostRef),\n transaction.get(campaignRef),\n transaction.get(emailRef),\n transaction.get(designRef),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (campaign.exists) {\n /*\n * A replay answers only for a campaign this site can see: the id\n * space is the organization's, and one placed on a sibling site is\n * not this request's to report or to write over.\n */\n if (\n !campaignPlacedOnHost(\n { visibleTo: campaign.get('visibleTo') as string[] | undefined },\n request.hostId,\n )\n ) {\n return { ok: false, status: 409, error: 'That campaign already exists' }\n }\n return { ok: true, replayed: true, ...campaignDraftRecord(campaign) }\n }\n const refusal = campaignDraftRoleRefusal(host, request.uid)\n if (refusal) return { ok: false, ...refusal }\n if (\n !design.exists ||\n design.get('deletedAt') != null ||\n design.get('kind') !== SCREEN_KIND_EMAIL\n ) {\n return { ok: false, status: 400, error: 'Unknown email design' }\n }\n if (email.exists) return { ok: false, status: 409, error: 'That email already exists' }\n const name = draftHeaderLine(request.name) || CAMPAIGN_DRAFT_DEFAULT_NAME\n const nowMs = request.now.getTime()\n // A site hub's create drawer's own document: a name, no dates, no\n // lists, placed on the site it was drafted for.\n const visibleTo = campaignVisibleTo([request.hostId])\n transaction.create(campaignRef, {\n name,\n ...campaignContainerSearchFields(name),\n listIds: [],\n visibleTo,\n createdAtMs: nowMs,\n createdBy: request.uid,\n })\n const { value } = read\n transaction.create(emailRef, {\n subject: value.subject,\n ...campaignSendSearchFields(value.subject),\n ...(value.preheader ? { preheader: value.preheader } : {}),\n templateScreenId: value.templateScreenId,\n ...(value.subjectVariants.length ? { subjectVariants: value.subjectVariants } : {}),\n ...(value.preheaderVariants.length ? { preheaderVariants: value.preheaderVariants } : {}),\n [CAMPAIGN_SEND_CONTAINER_FIELD]: request.id,\n ...campaignSendSiteStamp(request.hostId),\n status: 'draft',\n createdAtMs: nowMs,\n draftedAt: request.now,\n draftedBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name,\n versionId: null,\n facts: { emailId },\n }\n })\n },\n }\n}\n\nexport const campaignDraftWriter = createCampaignDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerCampaignDraftWriter(): void {\n registerPluginResourceDraftWriter(CAMPAIGN_DRAFT_RESOURCE, campaignDraftWriter, {\n pluginId: MARKETING_PLUGIN_ID,\n })\n}\n\n/** The org roles that may manage campaigns from the organization hub. */\nconst ORG_MANAGE_ROLES = ['owner', 'admin', 'editor'] as const\n\n/**\n * The organization a `campaigns/manage` request from the organization hub is\n * for, read off its JSON body, so the dispatcher's release gate asks about the\n * right organization. Only consulted for a request that names no site.\n * Unverified, exactly as a `hostId` is: the handler refuses anyone who is not\n * an org-wide member of it.\n */\nexport async function campaignManageSubject(\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n if (request.method !== 'POST') return null\n const body = (await request.json().catch(() => null)) as { orgId?: unknown } | null\n const orgId = typeof body?.orgId === 'string' ? body.orgId : ''\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/**\n * Campaign management API: removing a campaign, discarding a draft, and\n * copying an email.\n *\n * Separate from `campaigns/send` because nothing here sends, reserves\n * allowance or moves a meter, and because the removals are about a record\n * ceasing to exist — which is the one class of change that has to be read\n * against what a merchant has already mailed.\n *\n * Reached through two doors — see the header. A site request carries\n * `hostId` and is authorized by the caller's admin or editor role on that\n * site; an organization request carries `orgId` and no `hostId`, and is\n * authorized by an org-wide membership as owner, admin or editor.\n *\n * Editing a campaign is deliberately NOT here. The container is client-\n * writable, its create already runs on the client SDK, and a second door to\n * the same document would be a second place for the two to disagree about\n * what a campaign may hold.\n */\nexport const campaignManageHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const requestedOrgId = String(req.body?.orgId ?? '')\n const action = String(req.body?.action ?? '')\n const targetId = String(req.body?.campaignId ?? '')\n if (!hostId && !requestedOrgId) {\n return res.status(400).json({ error: 'Missing hostId' })\n }\n if (hostId && !isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (requestedOrgId && !isDocumentId(requestedOrgId)) {\n return res.status(400).json({ error: 'Invalid orgId' })\n }\n if (!isDocumentId(targetId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n if (\n action !== 'deleteCampaign' &&\n action !== 'discardEmail' &&\n action !== 'duplicate'\n ) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n let orgId: string\n if (hostId) {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n orgId = (await resolveOrgIdForHost(hostId)) ?? ''\n if (!orgId) {\n return res.status(409).json({\n error: 'This site is not part of an organization, so it holds no campaigns.',\n })\n }\n /*\n * A request naming both is answered for the site, and only when the\n * two agree: an org id the site does not belong to is a confused\n * caller, and acting on the site's own org instead would be a guess.\n */\n if (requestedOrgId && requestedOrgId !== orgId) {\n return res.status(400).json({ error: 'That site is not in this organization' })\n }\n } else {\n const membership = await resolveOrgMembership(decoded.uid, requestedOrgId)\n const member = membership?.member as Partial<AglynOrgMember> | undefined\n if (\n !member ||\n !isOrgWideMember(member) ||\n !(ORG_MANAGE_ROLES as readonly string[]).includes(String(member.role ?? ''))\n ) {\n return res\n .status(403)\n .json({ error: 'Not an organization owner, admin or editor' })\n }\n /*\n * The site door is refused by the dispatcher when Marketing is off for\n * the site; a request naming no site reaches here past only the release\n * gate, so the organization's own switch is asked here.\n */\n if (!isPluginEnabled(await getOrgDoc(requestedOrgId), MARKETING_PLUGIN_ID)) {\n return res.status(404).json({ error: 'Not found' })\n }\n orgId = requestedOrgId\n }\n\n if (action === 'duplicate') {\n // One attempt key, one copy (AGL-2936): a double click on Duplicate\n // replays the first answer rather than drafting the message twice.\n const attemptKey = String(req.body?.attemptKey ?? '').trim().slice(0, 200)\n const claimed = attemptKey\n ? await claimAttempt(firestore as never, {\n kind: 'duplicate-campaign',\n scopeId: `${hostId || `org:${orgId}`}:${targetId}`,\n orgId,\n key: attemptKey,\n busyMessage: DUPLICATE_BUSY_MESSAGE,\n })\n : null\n if (claimed && 'replay' in claimed) {\n return res.status(claimed.replay.status).json(claimed.replay.body)\n }\n const claim = claimed && 'claim' in claimed ? claimed.claim : null\n const copied = await duplicateMessage(\n firestore,\n orgId,\n hostId,\n targetId,\n String(req.body?.name ?? ''),\n { uid: decoded.uid, email: decoded.email ?? null },\n )\n if (copied.status === 200) await claim?.record(copied.status, copied.body)\n else await claim?.release()\n return res.status(copied.status).json(copied.body)\n }\n const result =\n action === 'deleteCampaign'\n ? await deleteCampaign(firestore, orgId, targetId, {\n hostId,\n uid: decoded.uid,\n })\n : await discardDraft(firestore, orgId, targetId, hostId)\n return res.status(result.status).json(result.body)\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'The request could not be completed' })\n }\n}\n"],"names":["firebaseAdmin","isDocumentId","logResourceDuplicated","restampCrmListFieldsOf","restampFormsInCampaign","DUPLICATE_BUSY_MESSAGE","duplicateDisplayName","uniqueDuplicateName","claimAttempt","createResourceUid","consentGroupForSite","getOrgDoc","resolveOrgIdForHost","resolveOrgMembership","CAMPAIGN_MEMBER_HOST_COLLECTIONS","CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","isOrgWideMember","CAMPAIGN_SEND_CONTAINER_FIELD","CAMPAIGN_SEND_HOST_FIELD","campaignPlacedOnHost","campaignSiteIds","campaignVisibleTo","campaignSendSiteStamp","orgCampaignSends","orgEmailCampaigns","sendHostId","sendIsOnHost","SCREEN_KIND_EMAIL","campaignContainerSearchFields","campaignSendSearchFields","registerPluginResourceDraftWriter","runPluginMembershipDetachers","isPluginEnabled","DETACH_BATCH","DETACH_PASSES","ORG_SITES_MAX","detachSends","firestore","orgId","campaignId","detached","pass","page","where","limit","get","empty","remaining","batch","send","docs","update","ref","commit","size","detachMembership","collectionRef","fieldPath","isForms","id","member","FieldValue","arrayRemove","updatedAt","serverTimestamp","map","orgSiteIds","collection","hostIds","doc","String","slice","truncated","length","detachMembers","take","hostId","hostRef","collectionName","orgRef","plugins","field","report","Object","values","org","groupIds","Set","add","groupId","contacts","deleteCampaign","access","snapshot","placement","visibleTo","exists","undefined","status","body","error","sites","onlyHere","every","membership","uid","toLocaleString","members","detachedMembers","delete","deleted","discardDraft","emailId","refusal","state","outcome","runTransaction","transaction","fresh","discarded","DUPLICATED_MESSAGE_FIELDS","duplicateMessage","requestedName","actor","messages","peek","siteId","source","data","copy","siblings","Promise","all","select","sourceName","trim","name","row","nowMs","Date","now","create","displayName","createdAtMs","draftedAt","draftedBy","email","target","CAMPAIGN_DRAFT_RESOURCE","MARKETING_PLUGIN_ID","CAMPAIGN_DRAFT_DEFAULT_NAME","CAMPAIGN_DRAFT_HEADER_MAX_CHARS","CAMPAIGN_DRAFT_MAX_VARIANTS","CAMPAIGN_DRAFT_ROLES","CAMPAIGN_DRAFT_ROLE_REFUSAL","campaignDraftEmailId","CAMPAIGN_DRAFT_EMAIL_FIELDS","draftHeaderLine","value","replace","draftVariants","Array","isArray","seen","lines","raw","line","has","toLowerCase","push","readCampaignDraftContent","content","templateScreenId","subject","preheader","subjectVariants","preheaderVariants","problems","long","some","ok","campaignDraftRoleRefusal","host","role","includes","campaignDraftRecord","campaign","versionId","facts","createCampaignDraftWriter","deps","app","resolveOrgId","check","read","fields","write","request","db","campaignRef","emailRef","designRef","design","replayed","getTime","listIds","createdBy","campaignDraftWriter","registerCampaignDraftWriter","pluginId","ORG_MANAGE_ROLES","campaignManageSubject","method","json","catch","campaignManageHandler","req","res","requestedOrgId","action","targetId","authorization","headers","idToken","startsWith","decoded","auth","verifyIdToken","hostSnapshot","memberRole","attemptKey","claimed","kind","scopeId","key","busyMessage","replay","claim","copied","record","release","result","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-manage.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 * TAKING SOMETHING AWAY: a campaign container, and an abandoned draft.\n *\n * Both are removals, and neither may take a delivered message with it. That\n * is the whole reason this file is small and the comments are long.\n *\n * ## Why these two are not client writes\n *\n * The console creates and edits a campaign CONTAINER with the client SDK —\n * `orgs/{orgId}/emailCampaigns` is client-writable, because a container holds\n * no counter, no consent record and no entitlement input. It is not\n * client-DELETABLE: removing one has to detach everything that names it\n * first, across every site in the organization, which no rule can express.\n *\n * The SEND collection is the opposite: `orgs/{orgId}/campaigns` is excluded\n * from client create, update AND delete, because each document is the record\n * of what a merchant mailed and to whom they were allowed to mail it. Both\n * operations here have to touch it — one to detach sends from the container\n * being removed, the other to remove a draft outright — so both run on the\n * Admin SDK behind a role check.\n *\n * ## Two doors: a site, and the organization\n *\n * A campaign belongs to the organization and may be placed on several of its\n * sites. A request from a SITE hub names the site (`hostId`) and is\n * authorized by the caller's role there, as the send route is — and it acts\n * only on what that site holds: a send sent as another site, or a campaign\n * not placed on this one, answers as if it did not exist. A request from the\n * ORGANIZATION hub names the org (`orgId`) and no site, and is authorized by\n * the caller's org-wide membership; for an email it acts in the name of the\n * site the email is sent as.\n *\n * ## What deleting a campaign MEANS\n *\n * The container goes; every email inside it stays, and reads afterwards as a\n * single send.\n *\n * It cannot mean anything else. A send id is cited by mail already delivered\n * — every unsubscribe footer carries `cid={sendId}` inside its own HMAC, and\n * `/marketing/campaigns/{sendId}` is a URL merchants paste into their own\n * messages — so deleting a container that destroyed its sends would break\n * opt-out links that must go on resolving forever, which is a compliance\n * failure rather than a broken page.\n *\n * Nor may the sends simply be left pointing at a container that is gone.\n * `campaignListRows` groups sends by the container they name and draws one\n * row per container; a send naming a container that no longer exists is in\n * neither half of that — not a container row, and not an orphan the list\n * adopts — so it would vanish from the campaigns table while remaining\n * perfectly deliverable. Detaching is what puts it back in the list, as the\n * \"Single send\" the product already models and the table already draws.\n *\n * Deleting a campaign is therefore NOT a way to stop its mail. A scheduled\n * email inside it keeps its send time and still goes out, because a\n * container is a grouping and cancelling somebody's mail is `cancel`'s job.\n * The console says so before it asks.\n *\n * ## The same rule for everything else the campaign held\n *\n * A campaign also groups forms, screens and contacts, each of which names it\n * from its OWN document. They are detached on exactly the argument the sends\n * are: a form left naming a container nobody can read has not been freed, it\n * has been made unreadable — its page draws a dead id where a campaign name\n * belongs, and the campaigns table can no longer find it. The record itself\n * is never touched. Deleting a campaign deletes a campaign.\n */\n\n/*\n * The MODULE, not the barrel. `@aglyn/tenant-data-admin`'s index reaches\n * `render-cache`, which imports `next/cache` and therefore the whole Next\n * server pipeline — everything this file needs from the admin SDK is one\n * default export, and taking it from the leaf keeps that pipeline out of the\n * graph.\n */\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport { logResourceDuplicated } from '@aglyn/tenant-data-admin/server/duplicate-activity'\nimport { restampFormsInCampaign } from './campaign-form-flags'\nimport {\n DUPLICATE_BUSY_MESSAGE,\n duplicateDisplayName,\n uniqueDuplicateName,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { claimAttempt, createResourceUid } from '@aglyn/aglyn/server'\n/*\n * The MODULES again, for the same reason: the org and membership reads here\n * are what the contact pass and the organization door need, and taking them\n * from `@aglyn/tenant-data-admin`'s index would pull the whole Next server\n * pipeline back into this file's graph.\n */\nimport {\n getOrgDoc,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n isOrgWideMember,\n type AglynOrgMember,\n type PluginApiHandler,\n type PluginApiRequestSubject,\n} from '@aglyn/aglyn/server'\nimport {\n CAMPAIGN_SEND_CONTAINER_FIELD,\n CAMPAIGN_SEND_HOST_FIELD,\n campaignPlacedOnHost,\n campaignSiteIds,\n campaignVisibleTo,\n} from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n campaignSendSiteStamp,\n orgCampaignSends,\n orgEmailCampaigns,\n sendHostId,\n sendIsOnHost,\n} from './campaign-org-refs'\nimport { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n CAMPAIGN_MEMBER_HOST_COLLECTIONS,\n CAMPAIGN_MEMBERSHIP_FIELD,\n} from '../model/campaign-kind'\nimport {\n campaignContainerSearchFields,\n campaignSendSearchFields,\n} from '../model/campaign-list-query'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { runPluginMembershipDetachers } from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\n\n/**\n * Sends detached in one write.\n *\n * Under Firestore's 500-operation batch limit with room to spare rather than\n * at it: a batch that is refused for being one over has done nothing, and the\n * margin costs one extra round trip on a campaign of exactly this size.\n */\nconst DETACH_BATCH = 400\n\n/**\n * How many batches one request will run.\n *\n * A ceiling on the REQUEST, not on the campaign. The loop below terminates on\n * its own — each pass clears the very field it queries on, so a detached send\n * is not returned again — but a request that runs unbounded is a request that\n * eventually exceeds the platform's own timeout with no idea how far it got.\n *\n * Stopping early is safe and is why this is a ceiling rather than a refusal\n * up front: detaching is idempotent and partial progress is a consistent\n * state — the sends already detached read as single sends, the rest still\n * read under the campaign — so the answer to hitting it is to ask again.\n */\nconst DETACH_PASSES = 25\n\n/**\n * The most sites one deletion walks for members.\n *\n * A campaign is the organization's, so its forms, screens and consent-group\n * facets may be on any of its sites. An organization past this has more\n * sites than one request should sweep, and the deletion refuses rather than\n * removing a container some sites' records would still name.\n */\nconst ORG_SITES_MAX = 200\n\n/** What one `campaigns/manage` call answered with. */\ninterface ManageResult {\n status: number\n body: Record<string, unknown>\n}\n\n/**\n * Files every send in a campaign under NO campaign: {@link\n * CAMPAIGN_SEND_CONTAINER_FIELD} set to `null`.\n *\n * Equality on one field with a `limit` — no `orderBy` — which Firestore's\n * automatic single-field index serves. Ordering would need a composite index\n * for a walk whose order does not matter: every match is being written, so\n * which order they come back in cannot change the outcome.\n *\n * @returns how many were detached, and whether any were left.\n */\nasync function detachSends(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n campaignId: string,\n): Promise<{ detached: number; remaining: boolean }> {\n let detached = 0\n for (let pass = 0; pass < DETACH_PASSES; pass += 1) {\n const page = await orgCampaignSends(firestore, orgId)\n .where(CAMPAIGN_SEND_CONTAINER_FIELD, '==', campaignId)\n .limit(DETACH_BATCH)\n .get()\n if (page.empty) return { detached, remaining: false }\n const batch = firestore.batch()\n for (const send of page.docs) {\n /*\n * `null`, not an empty string and not a removed field. \"Belongs to no\n * campaign\" has ONE stored shape, because it is a query: the lists ask\n * `emailCampaignId == null` for single sends (AGL-3321), which an\n * empty string would not answer and a missing field cannot — a query\n * never finds a field that is not there. `campaignListRows` adopts any\n * falsy container id, so every reader agrees.\n */\n batch.update(send.ref, {\n [CAMPAIGN_SEND_CONTAINER_FIELD]: null,\n })\n }\n await batch.commit()\n detached += page.size\n if (page.size < DETACH_BATCH) return { detached, remaining: false }\n }\n return { detached, remaining: true }\n}\n\n/**\n * Clears one campaign id out of a membership array, wherever it is held.\n *\n * The same walk {@link detachSends} makes, over an ARRAY field rather than a\n * scalar one: `array-contains` matches on Firestore's automatic single-field\n * index, and `arrayRemove` takes out the one id without touching the other\n * campaigns a form or a screen is in. That is the whole reason the membership\n * is an array here and a `FieldValue.delete()` on a send — a send is in one\n * campaign and a landing page is in several, so removing one must not be\n * removing all of them.\n *\n * @returns how many were detached, and whether any were left.\n */\nasync function detachMembership(\n firestore: FirebaseFirestore.Firestore,\n collectionRef: FirebaseFirestore.CollectionReference,\n fieldPath: string,\n campaignId: string,\n): Promise<{ detached: number; remaining: boolean }> {\n let detached = 0\n for (let pass = 0; pass < DETACH_PASSES; pass += 1) {\n const page = await collectionRef\n .where(fieldPath, 'array-contains', campaignId)\n .limit(DETACH_BATCH)\n .get()\n if (page.empty) return { detached, remaining: false }\n const batch = firestore.batch()\n const isForms = collectionRef.id === 'forms'\n for (const member of page.docs) {\n batch.update(member.ref, {\n [fieldPath]: firebaseAdmin.firestore.FieldValue.arrayRemove(campaignId),\n // Leaving a campaign is an edit to the form, and Updated says when\n // the form was last edited (AGL-3330).\n ...(isForms ? { updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp() } : {}),\n })\n }\n await batch.commit()\n // A form's `inCampaign` is what the Forms list's \"In a campaign\" filter\n // asks (AGL-3330), and `arrayRemove` cannot say whether it emptied the\n // array: restamped from each form as it now stands.\n if (isForms) {\n await restampFormsInCampaign(firestore, page.docs.map((member) => member.ref))\n }\n detached += page.size\n if (page.size < DETACH_BATCH) return { detached, remaining: false }\n }\n return { detached, remaining: true }\n}\n\n/**\n * Every site of the organization, by id — the sites a campaign's members may\n * be on. One equality on the `hostIndex` mirror, the same document\n * `resolveOrgIdForHost` reads to put a site in an org.\n *\n * @returns the ids, and whether the org has more than {@link ORG_SITES_MAX}.\n */\nasync function orgSiteIds(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<{ hostIds: string[]; truncated: boolean }> {\n const page = await firestore\n .collection('hostIndex')\n .where('orgId', '==', orgId)\n .limit(ORG_SITES_MAX + 1)\n .get()\n const hostIds = page.docs.map((doc) => String(doc.id))\n return {\n hostIds: hostIds.slice(0, ORG_SITES_MAX),\n truncated: hostIds.length > ORG_SITES_MAX,\n }\n}\n\n/**\n * Clears the campaign off every form and screen holding it, on every site of\n * the organization, and then off every record a plugin keeps filed under it\n * (AGL-3254, AGL-3080).\n *\n * ## Why the campaign is not simply deleted over the top of them\n *\n * A campaign that went away leaving members naming it is the same failure the\n * send detach exists to prevent, one collection over: the campaign's own page\n * finds its forms and screens with `array-contains`, and a form still naming\n * a container nobody can read is in neither half of that — not a member of\n * any campaign the console can draw, and not free of one either. Its own page\n * would render the dead id as a chip with no name.\n *\n * ## Every site, not the one that asked\n *\n * The campaign is the organization's, and a site it has since been taken off\n * may still hold forms and screens filed under it. So the walk covers every\n * site in the org regardless of which door the deletion came through.\n *\n * ## A plugin's members are the plugin's to clear\n *\n * A lead, a contact, a sequence and its enrollments name the campaign from\n * documents another plugin keeps, in collections this plugin does not know:\n * the CRM's leads carry the field at the top of their documents and its\n * contacts inside each consent group's facet, and Outreach's sequences under\n * the org. Every plugin with a membership detacher registered on the core's\n * seam is asked, once per site, with the campaign's id and the field it is\n * held in, and a detacher that reports records remaining — or threw, which is\n * reported as `null` — holds the container exactly as this pass's own\n * `remaining` does. A plugin that failed is a plugin whose records may still\n * name the campaign, and the deletion has no way to tell that apart from one\n * that has more than a request clears.\n */\nasync function detachMembers(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n campaignId: string,\n): Promise<{ detached: number; remaining: boolean }> {\n let detached = 0\n let remaining = false\n const take = (pass: { detached: number; remaining: boolean }) => {\n detached += pass.detached\n remaining = remaining || pass.remaining\n }\n const { hostIds, truncated } = await orgSiteIds(firestore, orgId)\n if (truncated) remaining = true\n for (const hostId of hostIds) {\n const hostRef = firestore.collection('hosts').doc(hostId)\n for (const collectionName of CAMPAIGN_MEMBER_HOST_COLLECTIONS) {\n take(\n await detachMembership(\n firestore,\n hostRef.collection(collectionName),\n CAMPAIGN_MEMBERSHIP_FIELD,\n campaignId,\n ),\n )\n }\n }\n /*\n * An org with no sites still has plugins that may hold the campaign, so\n * the seam is asked once with no site rather than not at all.\n */\n for (const hostId of hostIds.length ? hostIds : ['']) {\n const plugins = await runPluginMembershipDetachers({\n hostId,\n orgId,\n field: CAMPAIGN_MEMBERSHIP_FIELD,\n id: campaignId,\n })\n for (const report of Object.values(plugins)) {\n detached += report?.detached ?? 0\n remaining = remaining || report === null || report.remaining\n }\n }\n return { detached, remaining }\n}\n\n/** Who is deleting, and through which door. */\ninterface DeleteAccess {\n /** The site hub the request came from, or `''` from the organization hub. */\n hostId: string\n /** The caller, for the site door's reach check. */\n uid: string\n}\n\n/**\n * Removes a campaign container, leaving its emails standing.\n *\n * Detach first, delete second, and the order is the point: stopping between\n * them leaves a container whose emails are already single sends, which the\n * list draws correctly and a second run finishes. Deleting first would leave\n * sends naming a container nobody can read, which is the state that loses\n * them from the table.\n *\n * ## What a site hub may delete\n *\n * A campaign placed on this site and on no other — the one a site hub\n * creates. A campaign that is not placed here is not this site's to see, so\n * it answers 404 as a missing one would. A campaign also placed on other\n * sites is refused unless the caller is an org-wide member: deleting it\n * takes it off those sites too, which is the same reach change the rules\n * reserve for org-wide members when a campaign is edited.\n */\nasync function deleteCampaign(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n campaignId: string,\n access: DeleteAccess,\n): Promise<ManageResult> {\n const ref = orgEmailCampaigns(firestore, orgId).doc(campaignId)\n const snapshot = await ref.get()\n /*\n * A 404 rather than a silent success. The id in a campaign URL may name a\n * container OR a single send — that ambiguity is what keeps every link\n * minted before containers existed resolving — so \"no container here\" is\n * very often \"this is a send\", and answering it as a completed deletion\n * would tell the console it had removed something it never touched.\n */\n const placement = {\n visibleTo: snapshot.exists\n ? (snapshot.get('visibleTo') as string[] | undefined)\n : undefined,\n }\n if (\n !snapshot.exists ||\n (access.hostId && !campaignPlacedOnHost(placement, access.hostId))\n ) {\n return { status: 404, body: { error: 'Unknown campaign' } }\n }\n if (access.hostId) {\n const sites = campaignSiteIds(placement)\n const onlyHere = sites !== null && sites.every((id) => id === access.hostId)\n if (!onlyHere) {\n const membership = await resolveOrgMembership(access.uid, orgId)\n if (!isOrgWideMember(membership?.member)) {\n return {\n status: 403,\n body: {\n error:\n 'This campaign is on other sites too, so deleting it would take ' +\n 'it off them as well. An organization admin can delete it from ' +\n 'the organization’s campaigns.',\n },\n }\n }\n }\n }\n const { detached, remaining } = await detachSends(firestore, orgId, campaignId)\n if (remaining) {\n return {\n status: 409,\n body: {\n detached,\n error:\n `This campaign holds more emails than one request can detach. ` +\n `${detached.toLocaleString()} of them now read as single sends and ` +\n 'the campaign is still here — run the delete again to finish it.',\n },\n }\n }\n /*\n * The MEMBERS come off before the container does, for the reason the sends\n * do: a partial run leaves records that are already out of the campaign,\n * which every surface draws correctly, while deleting first would leave\n * forms, screens and contacts naming an id nothing can resolve.\n *\n * Their own 409 rather than a shared one. \"More emails than one request can\n * detach\" is a sentence about the campaign's mail, and a merchant reading\n * it about a campaign of two emails and four thousand contacts would go\n * looking for emails that are not the problem.\n */\n const members = await detachMembers(firestore, orgId, campaignId)\n if (members.remaining) {\n return {\n status: 409,\n body: {\n detached,\n detachedMembers: members.detached,\n error:\n 'This campaign is on more records than one request can clear. ' +\n `${members.detached.toLocaleString()} of them are already out of ` +\n 'it and the campaign is still here — run the delete again to ' +\n 'finish it.',\n },\n }\n }\n await ref.delete()\n return {\n status: 200,\n body: {\n campaignId,\n detached,\n detachedMembers: members.detached,\n deleted: true,\n },\n }\n}\n\n/**\n * Discards a DRAFT email, and refuses anything else.\n *\n * The state check is inside the transaction as well as being the reason for\n * it. `sendNow` claims a draft by moving it to `sending` in a transaction of\n * its own, so a check made against an earlier read could delete a record\n * while the send path was mailing from it — the merchant would receive a\n * report for an email that no longer exists, and its `cid` would stop\n * resolving with the message already in inboxes.\n *\n * Only `draft`. A scheduled email is withdrawn with `cancel`, which leaves\n * the record and its report standing; a sent one has reached people, and\n * nothing on this surface may remove the evidence of that.\n *\n * `hostId` is the site hub the request came from, and a draft sent as a\n * different site is refused as unknown; from the organization hub it is\n * `''` and any site's draft may be discarded.\n */\nasync function discardDraft(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emailId: string,\n hostId: string,\n): Promise<ManageResult> {\n const ref = orgCampaignSends(firestore, orgId).doc(emailId)\n const refusal = (state: string): string => {\n if (state === 'sent') {\n return 'This email has already been sent, so it cannot be discarded. ' +\n 'Its report and its unsubscribe links have to go on resolving.'\n }\n if (state === 'scheduled') {\n return 'This email is scheduled. Cancel the send first — cancelling ' +\n 'keeps the email and takes it off the clock.'\n }\n if (state === 'sending') {\n return 'This email is being sent right now.'\n }\n return 'Only a draft can be discarded.'\n }\n const outcome = await firestore.runTransaction(\n async (transaction): Promise<ManageResult> => {\n const fresh = await transaction.get(ref)\n if (!fresh.exists || (hostId && !sendIsOnHost(fresh, hostId))) {\n return { status: 404, body: { error: 'Unknown email' } }\n }\n const state = String(fresh.get('status') ?? '')\n if (state !== 'draft') {\n return { status: 409, body: { error: refusal(state) } }\n }\n /*\n * A plain delete, with nothing recursive under it. The one\n * subcollection a message grows is `reports/links`, written by the\n * delivery webhook — an email that has never been sent has no\n * deliveries, so there is nothing beneath a draft to leave behind.\n */\n transaction.delete(ref)\n return { status: 200, body: { emailId, discarded: true } }\n },\n )\n return outcome\n}\n\n/**\n * The fields a copy of a message carries (AGL-2936): what was written and\n * how it is sent — never who it goes to or when. The audience, list,\n * segment, addresses, schedule and experiment are the decisions a person\n * makes about THIS send, and a copy that inherited them would be one click\n * from mailing the same people twice.\n */\nconst DUPLICATED_MESSAGE_FIELDS = [\n 'subject',\n 'body',\n 'fromName',\n 'replyTo',\n 'senderId',\n 'templateScreenId',\n 'plainText',\n 'plainTextVersionId',\n 'topicId',\n CAMPAIGN_SEND_CONTAINER_FIELD,\n] as const\n\n/**\n * Copies a message as a new draft (AGL-2936). Any status can be copied —\n * a sent email is the usual source — and the copy is always `draft`, with\n * the name the person gave it made unique among its site's messages.\n *\n * The copy is sent as the same site as its source: the sender, the design\n * and the consent it would mail under are all that site's. From a site hub\n * that is the requesting site, and a source sent as another site is refused\n * as unknown; from the organization hub (`hostId` `''`) it is whichever site\n * the source records, and a source that records none cannot be copied,\n * because the copy would be an email that can never be sent.\n */\nasync function duplicateMessage(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n hostId: string,\n emailId: string,\n requestedName: string,\n actor: { uid: string; email: string | null },\n): Promise<ManageResult> {\n const messages = orgCampaignSends(firestore, orgId)\n const id = createResourceUid()\n /*\n * The source is read once outside the transaction to learn its site, which\n * decides which siblings the name must be unique among; the transaction\n * reads it again, so a source changed in between is copied as it stands.\n */\n const peek = await messages.doc(emailId).get()\n if (!peek.exists || (hostId && !sendIsOnHost(peek, hostId))) {\n return { status: 404, body: { error: 'Unknown email' } }\n }\n const siteId = hostId || sendHostId(peek)\n if (!siteId) {\n return {\n status: 409,\n body: {\n error:\n 'This email does not record which site it is sent from, so it ' +\n 'cannot be copied from here. Open it from its site instead.',\n },\n }\n }\n const outcome = await firestore.runTransaction(\n async (transaction): Promise<ManageResult & { name?: string; sourceName?: string }> => {\n const [source, siblings] = await Promise.all([\n transaction.get(messages.doc(emailId)),\n transaction.get(\n messages\n .select('displayName', 'subject')\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', siteId),\n ),\n ])\n if (!source.exists) {\n return { status: 404, body: { error: 'Unknown email' } }\n }\n const data = (source.data() ?? {}) as Record<string, unknown>\n const sourceName = String(data['displayName'] ?? data['subject'] ?? '').trim()\n const name = uniqueDuplicateName(\n requestedName.trim() || duplicateDisplayName(sourceName),\n siblings.docs.map((row) => String(row.get('displayName') ?? row.get('subject') ?? '')),\n )\n const copy: Record<string, unknown> = {}\n for (const field of DUPLICATED_MESSAGE_FIELDS) {\n if (data[field] !== undefined && data[field] !== null) copy[field] = data[field]\n }\n const nowMs = Date.now()\n transaction.create(messages.doc(id), {\n ...copy,\n // The list fields a new record carries (AGL-3321): its subject's\n // search tokens, and `null` for a source filed under no campaign.\n ...campaignSendSearchFields(copy['subject']),\n [CAMPAIGN_SEND_CONTAINER_FIELD]: copy[CAMPAIGN_SEND_CONTAINER_FIELD] ?? null,\n ...campaignSendSiteStamp(siteId),\n displayName: name,\n status: 'draft',\n createdAtMs: nowMs,\n draftedAt: new Date(nowMs),\n draftedBy: actor.uid,\n })\n return { status: 200, body: { emailId: id, name }, name, sourceName }\n },\n )\n if (outcome.status === 200) {\n await logResourceDuplicated(\n 'campaign',\n { uid: actor.uid, email: actor.email },\n {\n orgId,\n hostId: siteId,\n source: { id: emailId, name: outcome.sourceName ?? '' },\n target: { id, name: outcome.name ?? '' },\n },\n )\n }\n return { status: outcome.status, body: outcome.body }\n}\n\n/*==========================================\n * A CAMPAIGN THAT EXISTS BEFORE ANYBODY CHOSE WHO GETS IT (AGL-2912).\n *\n * The draft writer this plugin registers for the `campaign` resource on the\n * core's resource-drafts seam. Another plugin that produces a campaign — a\n * generator working from a brief, an importer bringing campaigns over from\n * another tool — asks for it by name, and gets exactly two documents:\n *\n * - the CONTAINER at `orgs/{orgId}/emailCampaigns/{id}`, written as a site\n * hub's create drawer writes one: aimed at no list, placed on the site\n * the draft was asked for;\n * - ONE EMAIL inside it at `orgs/{orgId}/campaigns/{id}-email`,\n * `status: 'draft'`, sent as that site, naming the email design it sends\n * and holding its subject, preheader and the alternatives to them.\n *\n * ## What a drafted email never holds\n *\n * An audience, a list, a segment, addresses, a topic, a sender, a send time or\n * an experiment. Those are the decisions a person makes about a send — the\n * duplicate above strips the same ones for the same reason — and a draft that\n * arrived holding any of them would be one click from mailing people nobody\n * chose. So the writer has no field for them to arrive in.\n *\n * ## Why a draft cannot escape\n *\n * Nothing here reserves an allowance, claims a budget or calls the send path,\n * and the scheduled processor queries `status == 'scheduled'`, which a draft\n * never is. `performCampaignSend` is the only thing that mails a draft, behind\n * the send route's own authorization, when a member asks.\n *\n * The id of the email is derived from the campaign's rather than minted, so a\n * write asked again finds the email it made — and differs from it, because a\n * campaign URL names a container OR a send, and one id naming both would read\n * as either.\n *=========================================*/\n\n/** The resource name the campaign draft writer is registered under. */\nexport const CAMPAIGN_DRAFT_RESOURCE = 'campaign'\n\n/** The id `plugins.config.json` registers this plugin under. */\nconst MARKETING_PLUGIN_ID = 'marketing'\n\n/** A drafted campaign's name when the caller asks for none. */\nexport const CAMPAIGN_DRAFT_DEFAULT_NAME = 'Untitled campaign'\n\n/** The longest subject or preheader a drafted email stores: the send route's header cap. */\nexport const CAMPAIGN_DRAFT_HEADER_MAX_CHARS = 200\n\n/** The most alternative subject lines or preheaders a drafted email keeps. */\nexport const CAMPAIGN_DRAFT_MAX_VARIANTS = 10\n\n/** The site roles that may compose this site's mail. */\nexport const CAMPAIGN_DRAFT_ROLES = ['admin', 'editor'] as const\n/** The send route's refusal for a member who may not compose this site's mail. */\nexport const CAMPAIGN_DRAFT_ROLE_REFUSAL = 'Not a site admin or editor'\n\n/** The id of the one email inside a drafted campaign. */\nexport function campaignDraftEmailId(campaignId: string): string {\n return `${campaignId}-email`\n}\n\n/** Every field a drafted email is written with, and so every field it can hold. */\nexport const CAMPAIGN_DRAFT_EMAIL_FIELDS = [\n 'subject',\n 'subjectTokens',\n 'preheader',\n 'templateScreenId',\n 'subjectVariants',\n 'preheaderVariants',\n CAMPAIGN_SEND_CONTAINER_FIELD,\n CAMPAIGN_SEND_HOST_FIELD,\n 'visibleTo',\n 'status',\n 'createdAtMs',\n 'draftedAt',\n 'draftedBy',\n] as const\n\n/** What a caller sends as `content`. */\nexport interface CampaignDraftContent {\n /** The email design the email sends: a `kind: 'email'` screen on the site. */\n templateScreenId: string\n subject: string\n preheader: string\n subjectVariants: string[]\n preheaderVariants: string[]\n}\n\n/** A header value on one line: controls and runs of space folded to one space. */\nfunction draftHeaderLine(value: unknown): string {\n return String(value ?? '').replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n}\n\nfunction draftVariants(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const lines: string[] = []\n for (const raw of value) {\n const line = draftHeaderLine(raw)\n if (!line || seen.has(line.toLowerCase())) continue\n seen.add(line.toLowerCase())\n lines.push(line)\n }\n return lines.slice(0, CAMPAIGN_DRAFT_MAX_VARIANTS)\n}\n\nexport function readCampaignDraftContent(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CampaignDraftContent } | { ok: false; problems: string[] } {\n const value: CampaignDraftContent = {\n templateScreenId: String(content['templateScreenId'] ?? ''),\n subject: draftHeaderLine(content['subject']),\n preheader: draftHeaderLine(content['preheader']),\n subjectVariants: draftVariants(content['subjectVariants']),\n preheaderVariants: draftVariants(content['preheaderVariants']),\n }\n const problems: string[] = []\n if (!isDocumentId(value.templateScreenId)) {\n problems.push('A drafted campaign needs the email design it sends')\n }\n if (!value.subject) problems.push('A drafted campaign needs a subject line')\n const long = (lines: string[]) =>\n lines.some((line) => line.length > CAMPAIGN_DRAFT_HEADER_MAX_CHARS)\n if (long([value.subject, ...value.subjectVariants])) {\n problems.push(`A subject line is longer than ${CAMPAIGN_DRAFT_HEADER_MAX_CHARS} characters`)\n }\n if (long([value.preheader, ...value.preheaderVariants])) {\n problems.push(`A preheader is longer than ${CAMPAIGN_DRAFT_HEADER_MAX_CHARS} characters`)\n }\n return problems.length ? { ok: false, problems } : { ok: true, value }\n}\n\nfunction campaignDraftRoleRefusal(\n host: FirebaseFirestore.DocumentSnapshot,\n uid: string,\n): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return CAMPAIGN_DRAFT_ROLES.includes(role)\n ? null\n : { status: 403, error: CAMPAIGN_DRAFT_ROLE_REFUSAL }\n}\n\nfunction campaignDraftRecord(campaign: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n return {\n id: campaign.id,\n name: String(campaign.get('name') ?? ''),\n versionId: null,\n facts: { emailId: campaignDraftEmailId(campaign.id) },\n }\n}\n\nexport interface CampaignDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => FirebaseFirestore.Firestore\n /** The site's organization, or null; specs hand in a double. */\n resolveOrgId?: (hostId: string) => Promise<string | null>\n}\n\nexport function createCampaignDraftWriter(\n deps: CampaignDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore =\n deps.firestore ??\n (() => firebaseAdmin.app().firestore() as unknown as FirebaseFirestore.Firestore)\n /*\n * The org is resolved from the SITE rather than taken from the request's\n * context: the site is what the role check reads, so the campaign lands in\n * the organization that site belongs to and no other.\n */\n const resolveOrgId = deps.resolveOrgId ?? resolveOrgIdForHost\n return {\n refusal: async ({ hostId, uid }) => {\n const host = await firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return campaignDraftRoleRefusal(host, uid)\n },\n\n check: (content) => {\n const read = readCampaignDraftContent(content)\n return read.ok === false\n ? read\n : { ok: true, facts: { fields: [...CAMPAIGN_DRAFT_EMAIL_FIELDS] } }\n },\n\n read: async ({ hostId, id }) => {\n if (!isDocumentId(id)) return null\n const orgId = await resolveOrgId(hostId)\n if (!orgId) return null\n const campaign = await orgEmailCampaigns(firestore(), orgId).doc(id).get()\n return campaign.exists &&\n campaignPlacedOnHost(\n { visibleTo: campaign.get('visibleTo') as string[] | undefined },\n hostId,\n )\n ? campaignDraftRecord(campaign)\n : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readCampaignDraftContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (!isDocumentId(request.id)) return { ok: false, status: 400, error: 'Invalid campaignId' }\n const db = firestore()\n const orgId = await resolveOrgId(request.hostId)\n if (!orgId) {\n return { ok: false, status: 409, error: 'This site is not part of an organization' }\n }\n const hostRef = db.collection('hosts').doc(request.hostId)\n const campaignRef = orgEmailCampaigns(db, orgId).doc(request.id)\n const emailId = campaignDraftEmailId(request.id)\n const emailRef = orgCampaignSends(db, orgId).doc(emailId)\n const designRef = hostRef.collection('screens').doc(read.value.templateScreenId)\n return db.runTransaction(async (transaction): Promise<PluginDraftWrite> => {\n const [host, campaign, email, design] = await Promise.all([\n transaction.get(hostRef),\n transaction.get(campaignRef),\n transaction.get(emailRef),\n transaction.get(designRef),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (campaign.exists) {\n /*\n * A replay answers only for a campaign this site can see: the id\n * space is the organization's, and one placed on a sibling site is\n * not this request's to report or to write over.\n */\n if (\n !campaignPlacedOnHost(\n { visibleTo: campaign.get('visibleTo') as string[] | undefined },\n request.hostId,\n )\n ) {\n return { ok: false, status: 409, error: 'That campaign already exists' }\n }\n return { ok: true, replayed: true, ...campaignDraftRecord(campaign) }\n }\n const refusal = campaignDraftRoleRefusal(host, request.uid)\n if (refusal) return { ok: false, ...refusal }\n if (\n !design.exists ||\n design.get('deletedAt') != null ||\n design.get('kind') !== SCREEN_KIND_EMAIL\n ) {\n return { ok: false, status: 400, error: 'Unknown email design' }\n }\n if (email.exists) return { ok: false, status: 409, error: 'That email already exists' }\n const name = draftHeaderLine(request.name) || CAMPAIGN_DRAFT_DEFAULT_NAME\n const nowMs = request.now.getTime()\n // A site hub's create drawer's own document: a name, no dates, no\n // lists, placed on the site it was drafted for.\n const visibleTo = campaignVisibleTo([request.hostId])\n transaction.create(campaignRef, {\n name,\n ...campaignContainerSearchFields(name),\n listIds: [],\n visibleTo,\n createdAtMs: nowMs,\n createdBy: request.uid,\n })\n const { value } = read\n transaction.create(emailRef, {\n subject: value.subject,\n ...campaignSendSearchFields(value.subject),\n ...(value.preheader ? { preheader: value.preheader } : {}),\n templateScreenId: value.templateScreenId,\n ...(value.subjectVariants.length ? { subjectVariants: value.subjectVariants } : {}),\n ...(value.preheaderVariants.length ? { preheaderVariants: value.preheaderVariants } : {}),\n [CAMPAIGN_SEND_CONTAINER_FIELD]: request.id,\n ...campaignSendSiteStamp(request.hostId),\n status: 'draft',\n createdAtMs: nowMs,\n draftedAt: request.now,\n draftedBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name,\n versionId: null,\n facts: { emailId },\n }\n })\n },\n }\n}\n\nexport const campaignDraftWriter = createCampaignDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerCampaignDraftWriter(): void {\n registerPluginResourceDraftWriter(CAMPAIGN_DRAFT_RESOURCE, campaignDraftWriter, {\n pluginId: MARKETING_PLUGIN_ID,\n })\n}\n\n/** The org roles that may manage campaigns from the organization hub. */\nconst ORG_MANAGE_ROLES = ['owner', 'admin', 'editor'] as const\n\n/**\n * The organization a `campaigns/manage` request from the organization hub is\n * for, read off its JSON body, so the dispatcher's release gate asks about the\n * right organization. Only consulted for a request that names no site.\n * Unverified, exactly as a `hostId` is: the handler refuses anyone who is not\n * an org-wide member of it.\n */\nexport async function campaignManageSubject(\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n if (request.method !== 'POST') return null\n const body = (await request.json().catch(() => null)) as { orgId?: unknown } | null\n const orgId = typeof body?.orgId === 'string' ? body.orgId : ''\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/**\n * Campaign management API: removing a campaign, discarding a draft, and\n * copying an email.\n *\n * Separate from `campaigns/send` because nothing here sends, reserves\n * allowance or moves a meter, and because the removals are about a record\n * ceasing to exist — which is the one class of change that has to be read\n * against what a merchant has already mailed.\n *\n * Reached through two doors — see the header. A site request carries\n * `hostId` and is authorized by the caller's admin or editor role on that\n * site; an organization request carries `orgId` and no `hostId`, and is\n * authorized by an org-wide membership as owner, admin or editor.\n *\n * Editing a campaign is deliberately NOT here. The container is client-\n * writable, its create already runs on the client SDK, and a second door to\n * the same document would be a second place for the two to disagree about\n * what a campaign may hold.\n */\nexport const campaignManageHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const requestedOrgId = String(req.body?.orgId ?? '')\n const action = String(req.body?.action ?? '')\n const targetId = String(req.body?.campaignId ?? '')\n if (!hostId && !requestedOrgId) {\n return res.status(400).json({ error: 'Missing hostId' })\n }\n if (hostId && !isDocumentId(hostId)) {\n return res.status(400).json({ error: 'Invalid hostId' })\n }\n if (requestedOrgId && !isDocumentId(requestedOrgId)) {\n return res.status(400).json({ error: 'Invalid orgId' })\n }\n if (!isDocumentId(targetId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n if (\n action !== 'deleteCampaign' &&\n action !== 'discardEmail' &&\n action !== 'duplicate'\n ) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n let orgId: string\n if (hostId) {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n orgId = (await resolveOrgIdForHost(hostId)) ?? ''\n if (!orgId) {\n return res.status(409).json({\n error: 'This site is not part of an organization, so it holds no campaigns.',\n })\n }\n /*\n * A request naming both is answered for the site, and only when the\n * two agree: an org id the site does not belong to is a confused\n * caller, and acting on the site's own org instead would be a guess.\n */\n if (requestedOrgId && requestedOrgId !== orgId) {\n return res.status(400).json({ error: 'That site is not in this organization' })\n }\n } else {\n const membership = await resolveOrgMembership(decoded.uid, requestedOrgId)\n const member = membership?.member as Partial<AglynOrgMember> | undefined\n if (\n !member ||\n !isOrgWideMember(member) ||\n !(ORG_MANAGE_ROLES as readonly string[]).includes(String(member.role ?? ''))\n ) {\n return res\n .status(403)\n .json({ error: 'Not an organization owner, admin or editor' })\n }\n /*\n * The site door is refused by the dispatcher when Marketing is off for\n * the site; a request naming no site reaches here past only the release\n * gate, so the organization's own switch is asked here.\n */\n if (!isPluginEnabled(await getOrgDoc(requestedOrgId), MARKETING_PLUGIN_ID)) {\n return res.status(404).json({ error: 'Not found' })\n }\n orgId = requestedOrgId\n }\n\n if (action === 'duplicate') {\n // One attempt key, one copy (AGL-2936): a double click on Duplicate\n // replays the first answer rather than drafting the message twice.\n const attemptKey = String(req.body?.attemptKey ?? '').trim().slice(0, 200)\n const claimed = attemptKey\n ? await claimAttempt(firestore as never, {\n kind: 'duplicate-campaign',\n scopeId: `${hostId || `org:${orgId}`}:${targetId}`,\n orgId,\n key: attemptKey,\n busyMessage: DUPLICATE_BUSY_MESSAGE,\n })\n : null\n if (claimed && 'replay' in claimed) {\n return res.status(claimed.replay.status).json(claimed.replay.body)\n }\n const claim = claimed && 'claim' in claimed ? claimed.claim : null\n const copied = await duplicateMessage(\n firestore,\n orgId,\n hostId,\n targetId,\n String(req.body?.name ?? ''),\n { uid: decoded.uid, email: decoded.email ?? null },\n )\n if (copied.status === 200) await claim?.record(copied.status, copied.body)\n else await claim?.release()\n return res.status(copied.status).json(copied.body)\n }\n const result =\n action === 'deleteCampaign'\n ? await deleteCampaign(firestore, orgId, targetId, {\n hostId,\n uid: decoded.uid,\n })\n : await discardDraft(firestore, orgId, targetId, hostId)\n return res.status(result.status).json(result.body)\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'The request could not be completed' })\n }\n}\n"],"names":["firebaseAdmin","isDocumentId","logResourceDuplicated","restampFormsInCampaign","DUPLICATE_BUSY_MESSAGE","duplicateDisplayName","uniqueDuplicateName","claimAttempt","createResourceUid","getOrgDoc","resolveOrgIdForHost","resolveOrgMembership","isOrgWideMember","CAMPAIGN_SEND_CONTAINER_FIELD","CAMPAIGN_SEND_HOST_FIELD","campaignPlacedOnHost","campaignSiteIds","campaignVisibleTo","campaignSendSiteStamp","orgCampaignSends","orgEmailCampaigns","sendHostId","sendIsOnHost","SCREEN_KIND_EMAIL","CAMPAIGN_MEMBER_HOST_COLLECTIONS","CAMPAIGN_MEMBERSHIP_FIELD","campaignContainerSearchFields","campaignSendSearchFields","registerPluginResourceDraftWriter","runPluginMembershipDetachers","isPluginEnabled","DETACH_BATCH","DETACH_PASSES","ORG_SITES_MAX","detachSends","firestore","orgId","campaignId","detached","pass","page","where","limit","get","empty","remaining","batch","send","docs","update","ref","commit","size","detachMembership","collectionRef","fieldPath","isForms","id","member","FieldValue","arrayRemove","updatedAt","serverTimestamp","map","orgSiteIds","collection","hostIds","doc","String","slice","truncated","length","detachMembers","take","hostId","hostRef","collectionName","plugins","field","report","Object","values","deleteCampaign","access","snapshot","placement","visibleTo","exists","undefined","status","body","error","sites","onlyHere","every","membership","uid","toLocaleString","members","detachedMembers","delete","deleted","discardDraft","emailId","refusal","state","outcome","runTransaction","transaction","fresh","discarded","DUPLICATED_MESSAGE_FIELDS","duplicateMessage","requestedName","actor","messages","peek","siteId","source","data","copy","siblings","Promise","all","select","sourceName","trim","name","row","nowMs","Date","now","create","displayName","createdAtMs","draftedAt","draftedBy","email","target","CAMPAIGN_DRAFT_RESOURCE","MARKETING_PLUGIN_ID","CAMPAIGN_DRAFT_DEFAULT_NAME","CAMPAIGN_DRAFT_HEADER_MAX_CHARS","CAMPAIGN_DRAFT_MAX_VARIANTS","CAMPAIGN_DRAFT_ROLES","CAMPAIGN_DRAFT_ROLE_REFUSAL","campaignDraftEmailId","CAMPAIGN_DRAFT_EMAIL_FIELDS","draftHeaderLine","value","replace","draftVariants","Array","isArray","seen","Set","lines","raw","line","has","toLowerCase","add","push","readCampaignDraftContent","content","templateScreenId","subject","preheader","subjectVariants","preheaderVariants","problems","long","some","ok","campaignDraftRoleRefusal","host","role","includes","campaignDraftRecord","campaign","versionId","facts","createCampaignDraftWriter","deps","app","resolveOrgId","check","read","fields","write","request","db","campaignRef","emailRef","designRef","design","replayed","getTime","listIds","createdBy","campaignDraftWriter","registerCampaignDraftWriter","pluginId","ORG_MANAGE_ROLES","campaignManageSubject","method","json","catch","campaignManageHandler","req","res","requestedOrgId","action","targetId","authorization","headers","idToken","startsWith","decoded","auth","verifyIdToken","hostSnapshot","memberRole","attemptKey","claimed","kind","scopeId","key","busyMessage","replay","claim","copied","record","release","result","console"],"mappings":"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"}
@@ -97,7 +97,7 @@ export interface CampaignSendOptions {
97
97
  /**
98
98
  * The stream this campaign belongs to, chosen in the composer.
99
99
  *
100
- * Resolved to {@link DEFAULT_CAMPAIGN_TOPIC_ID} when absent, so every send
100
+ * Resolved to {@link DEFAULT_SUBSCRIPTION_TOPIC_ID} when absent, so every send
101
101
  * belongs to some topic: a campaign with none would mint an unsubscribe link
102
102
  * the preference page can render but not place, offering the recipient a
103
103
  * catalog without saying which entry the message in front of them was.