@aglyn/plugins-marketing 1.0.0-beta.165 → 1.0.0-beta.167
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-send.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkQuota,\n contactMatchesSegment,\n isEmailTopicId,\n DEFAULT_CAMPAIGN_TOPIC_ID,\n readMarketingBasis,\n resolveMarketingConsentPolicy,\n splitByMarketingConsent,\n type MarketingConsentRecord,\n createResourceUid,\n decodeStoredNodes,\n emailStarterSendBlock,\n resolveBrandingProfile,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport type { PluginRevocation } from '@aglyn/aglyn/server'\nimport { renderRecipientEmail } from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport type { EmailRenderProduct } from '@aglyn/shared-util-email'\nimport { assignExperimentVariant, type HostExperiment } from '../model'\nimport { readPluginRecordCard } from '@aglyn/aglyn/plugin-manager/plugin-record-cards'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n orgDataCollectionForHost,\n orgDataQueryForHost,\n filterSendableForHost,\n filterTopicSendable,\n firebaseAdmin,\n getOrgForHost,\n meterHostEmail,\n claimOrgEmailSendBudget,\n orgCampaignEmailSendsForMonth,\n readEmailSendRateConfig,\n readEmailSendRateWindow,\n reconcileCampaignSendReservation,\n reserveCampaignEmailSends,\n type CampaignSendReservation,\n resolveHostSendingIdentity,\n} from '@aglyn/tenant-data-admin'\n// The leaf, not the barrel: this plugin's specs substitute the barrel\n// wholesale, and the lookup must reach the real index logic under them.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\n/*\n * The LEAF module for the impersonation exemption too, and for a sharper\n * version of the reason the notes below give: every spec that reaches this\n * file mocks the `@aglyn/tenant-data-admin` barrel, and a factory that\n * omitted this name would make the verification gate THROW rather than\n * refuse. A control that fails by exception is one nobody can reason about.\n * Resolved from the module itself, the real predicate runs under every\n * harness.\n */\nimport { isImpersonationSession } from '@aglyn/tenant-data-admin/server/firebase-admin'\n/*\n * The LEAF module, not the barrel, for the reason `document-id` is imported\n * the same way: a barrel import resolves to whatever a spec's `jest.mock` of\n * `@aglyn/tenant-data-admin` happens to contain, and nearly every spec that\n * reaches this file mocks it.\n */\nimport {\n buildUnsubscribeUrl,\n unsubscribeSignature as sharedUnsubscribeSignature,\n} from '@aglyn/tenant-data-admin/server/email-unsubscribe-link'\nimport {\n filterCadenceSendable,\n recordMarketingSends,\n} from '@aglyn/tenant-data-admin/server/email-marketing-gate'\n/*\n * The LEAF module again, and for the third reason listed above `document-id`:\n * the specs that reach this file mock the `@aglyn/tenant-data-admin` barrel,\n * and a reach helper resolved through it would be whatever their factory\n * happens to contain. The one function here that may not be wrong — the read\n * that decides who has already had this email — is the one that must come\n * from the real module.\n */\nimport {\n CAMPAIGN_REACH_CEILING,\n campaignReachCovers,\n campaignSettledSize,\n partitionByCampaignReach,\n readCampaignReach,\n readCampaignSettled,\n recordCampaignReach,\n recordCampaignSkipped,\n} from './email-campaign-reach'\n/*\n * The LEAF module for the reputation controls too, and for the same reason\n * as the three above it: every spec that reaches this file mocks the\n * `@aglyn/tenant-data-admin` barrel, so a breaker resolved through it would\n * be whatever a factory happened to contain — which for a control that\n * REFUSES a send means a test could pass against a breaker that is not there.\n * Resolved from the module itself, the real control runs, and it fails open\n * against a harness that cannot serve it.\n */\nimport {\n claimOrgEmailSendDay,\n orgAgeDays,\n readSenderReputation,\n reconcileOrgEmailSendDay,\n recordCampaignAccepted,\n resolveOrgEmailRamp,\n type OrgEmailSendDayReservation,\n type SenderReputationRead,\n} from '@aglyn/tenant-data-admin/server/email-sender-reputation'\nimport { createHash, createHmac } from 'crypto'\nimport {\n EMAIL_MAX_AUDIENCE_PER_SEND,\n EMAIL_MAX_RECIPIENTS_PER_SEND,\n campaignBatchPlan,\n createProviderRequestPacer,\n effectiveReputationPolicy,\n HOST_SENDERS_COLLECTION,\n isEmailConfigured,\n rateLimitedRetryAtMs,\n readHostSender,\n sendEmail,\n sendingIdentityRefusal,\n sentAsStamp,\n type SendingIdentitySource,\n type EmailRampVerdict,\n} from '@aglyn/shared-util-email'\n\n/**\n * Recipients one send may address.\n *\n * The number lives in `send-ceilings.ts` with the other two email ceilings\n * rather than here, because it only means anything in relation to them: it has\n * to fit inside a workspace's share of the platform hour, which in turn has to\n * fit inside the platform hour. Held privately here it was a third number\n * nobody could check against the other two.\n */\nconst MAX_RECIPIENTS_PER_SEND = EMAIL_MAX_RECIPIENTS_PER_SEND\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** How many audience documents one Firestore round trip fetches. */\nconst AUDIENCE_PAGE_SIZE = 500\n\n/**\n * The read budget for resolving one audience — a ceiling on the SCAN, not a\n * silent cap on the answer.\n *\n * It is deliberately the largest window this file already spent (`contacts`\n * and list members read 5,000), so no audience costs more to resolve than it\n * did before; `leads` and `siteMembers` read 1,000 and now share the same\n * number, which is the point — a site with 3,000 leads was told its audience\n * was 1,000.\n *\n * Reaching it does not truncate anything silently. The resolution reports\n * {@link CampaignSendResult.audienceTruncated}, `audienceSize` becomes a\n * floor rather than a total, and the composer and the History row both say so.\n * An audience larger than one send may carry is delivered across several\n * batches; an audience larger than THIS is one the sender cannot resolve at\n * all, and batching does not change that. It is the same number as\n * `CAMPAIGN_REACH_CEILING`, taken from the one place both are stated.\n */\nconst AUDIENCE_SCAN_CEILING = EMAIL_MAX_AUDIENCE_PER_SEND\n\n/**\n * Page a query to exhaustion in document-name order.\n *\n * ## Why the read has to be ordered at all\n *\n * Firestore answers a `limit()` with no `orderBy` in document-id order, and\n * these ids are generated — so the old bare `limit(1000)` / `limit(5000)`\n * picked an arbitrary slice of any audience larger than the window, and the\n * merchant was told that slice was the whole audience. A cursor needs an\n * ordering regardless; this makes the selection explicable (\"the first N by\n * document name\") and stable across sends instead of merely bounded.\n *\n * ## Why the document NAME and not a date\n *\n * `orderBy(field)` drops every document that lacks that field, so ordering an\n * audience newest-first would silently un-invite people. There is no field\n * every writer of these four collections sets: list members carry `addedAt`\n * only when `enrollListMember` CREATED the row, and the newsletter handler\n * that wrote the collection before it stored `{ email, name, source }` and no\n * date at all — so `orderBy('addedAt')` would drop every newsletter\n * subscriber from every list campaign. `__name__` is the one key every\n * document has, and an unfiltered collection ordered by it needs no index.\n *\n * `startAfter` takes the SNAPSHOT rather than its id, so the cursor keeps\n * working if a filter is ever added ahead of the ordering.\n *\n * @returns the documents, and whether {@link AUDIENCE_SCAN_CEILING} — rather\n * than the end of the collection — is what stopped the sweep. The\n * flag is settled by one extra single-document read, so \"more than\n * 5,000\" is never claimed of a collection holding exactly 5,000.\n */\nasync function sweepAudience(base: FirebaseFirestore.Query): Promise<{\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n truncated: boolean\n}> {\n const ordered = base.orderBy(\n firebaseAdmin.firestore.FieldPath.documentId(),\n )\n const docs: FirebaseFirestore.QueryDocumentSnapshot[] = []\n let cursor: FirebaseFirestore.QueryDocumentSnapshot | undefined\n for (;;) {\n const page = await (cursor ? ordered.startAfter(cursor) : ordered)\n .limit(AUDIENCE_PAGE_SIZE)\n .get()\n docs.push(...page.docs)\n cursor = page.docs[page.docs.length - 1]\n if (!cursor || page.docs.length < AUDIENCE_PAGE_SIZE) {\n return { docs, truncated: false }\n }\n if (docs.length >= AUDIENCE_SCAN_CEILING) {\n const probe = await ordered.startAfter(cursor).limit(1).get()\n return { docs, truncated: probe.docs.length > 0 }\n }\n }\n}\n\n/**\n * Stable doc id for a suppression entry (emails are PII — hash them).\n *\n * A WRITER's derivation now — `email-events.ts` files bounces and complaints\n * under it. The send path reads through `emailSuppressionKey`, which hashes\n * the same trimmed, lowercased form and additionally refuses to guess an id\n * for a value that is not an address.\n */\nexport function suppressionId(email: string): string {\n return createHash('sha256').update(email.toLowerCase()).digest('hex')\n}\n\n/** HMAC for unsubscribe links; env-gated on the shared secret. */\nexport function unsubscribeSignature(\n hostId: string,\n email: string,\n secret: string,\n campaignId?: string,\n topicId?: string,\n): string {\n /*\n * Delegated to the module that owns the signed subject.\n *\n * The campaign sender mints links, the unsubscribe handler verifies them,\n * and the marketing gate mints them for every other audience path — three\n * parties to one HMAC subject, which stays correct only while there is one\n * implementation of it. The name stays exported because it is this\n * module's published surface.\n */\n return sharedUnsubscribeSignature(hostId, email, secret, campaignId, topicId)\n}\n\n/** Send failures carry the HTTP status the API route should answer. */\nexport class CampaignSendError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n ) {\n super(message)\n }\n}\n\n/**\n * The platform send rate had no room for this campaign THIS HOUR (AGL-2409).\n *\n * A subclass rather than a status code the caller has to recognise, because\n * the difference it carries is not \"which HTTP number\" — it is **retry, do not\n * fail**. A scheduled campaign that trips the hourly ceiling must go back to\n * `scheduled` and be picked up by the next 15-minute run; marking it `failed`,\n * which is what every other `CampaignSendError` correctly does, would turn a\n * ramp into a lost campaign that a merchant has to notice and re-create.\n *\n * Thrown ONLY before anything has been sent, so a caller that retries cannot\n * double-send.\n */\nexport class CampaignSendDeferredError extends CampaignSendError {\n constructor(\n message: string,\n /** When the hourly window rolls. */\n public readonly retryAtMs: number,\n ) {\n super(message, 429)\n }\n}\n\nexport interface CampaignSendOptions {\n hostId: string\n subject: string\n /**\n * The MESSAGE of a plain-text email — what recipients read, and what the\n * HTML part is synthesized from.\n *\n * Not read on a designed send, where the nodes are the message. The route\n * refuses a request carrying both rather than accepting one and dropping it,\n * which is what it used to do.\n */\n body: string\n audience: string\n segmentId?: string\n listId?: string\n /**\n * The stream this campaign belongs to, chosen in the composer.\n *\n * Resolved to {@link DEFAULT_CAMPAIGN_TOPIC_ID} when absent, so every send\n * belongs to some topic: a campaign with none would mint an unsubscribe link\n * the preference page can render but not place, offering the recipient a\n * catalog without saying which entry the message in front of them was.\n */\n topicId?: string\n /**\n * Resolve the audience and return the count WITHOUT sending anything\n * (AGL-2178). Returns before the first write, so it mints no campaign\n * id and touches no counter.\n */\n dryRun?: boolean\n emails?: string[]\n campaignId?: string\n experimentId?: string\n /**\n * Designed email template (AGL-349): screen id of a besigner email\n * document, which is what makes this a DESIGNED send.\n *\n * The nodes produce both parts an inbox receives — the HTML and a plain-text\n * rendering of the same design — so `body` has no job here and is not read.\n * `plainText` below is the one thing an author may substitute.\n */\n templateScreenId?: string\n /**\n * The author's own PLAIN-TEXT PART for a designed email.\n *\n * Empty means the design generates it, which is the default and what every\n * designed campaign did before this existed. Read only on the designed\n * path: a plain-text campaign's text part IS its body, and a second string\n * claiming to be the text part beside it would be the two-sources problem\n * again under a new name.\n */\n plainText?: string\n /**\n * The sender's DISPLAY NAME for this campaign, overriding the org's\n * branding default.\n *\n * A display name and nothing more: `applyFromName` keeps the verified\n * address it is applied to, so this cannot move the mail onto a domain the\n * org has not proved. The route strips control characters before it gets\n * here, because the value is merchant-typed and lands in a header.\n */\n fromName?: string\n /** Where replies go, when it is not the sending address. */\n replyTo?: string\n /**\n * WHICH OF THE SITE'S SENDERS THIS EMAIL LEAVES AS —\n * `hosts/{hostId}/senders/{senderId}`.\n *\n * The mailbox, and only the mailbox. A sender also carries a display name\n * and a reply address, and those stay per-send fields resolved from\n * {@link fromName} and {@link replyTo}: the composer seeds them from the\n * chosen sender so a merchant sees what will go out, and the send records\n * what was submitted, which is what keeps the stored report and the message\n * one answer rather than two.\n *\n * ## An id, which is not the input path that was closed\n *\n * `req.body.sendingIdentity` is read by nothing, and a per-send local part\n * would reopen it: a mailbox is where a bounce returns and where a client\n * ignoring `Reply-To:` answers, so one that exists in a single campaign's\n * headers is an address nobody serves. A sender id names a mailbox that was\n * configured once, validated once and stored on this site under an\n * `org.settings` gate. The set of addresses this field can reach is the set\n * an org admin already approved.\n *\n * Absent means the site's DEFAULT sender, which is the projection the host\n * document has always carried. Naming a sender this site does not hold is\n * REFUSED — see the resolution below — rather than falling back to it.\n */\n senderId?: string\n /**\n * The CAMPAIGN this send belongs to — `hosts/{hostId}/emailCampaigns/{id}`.\n *\n * Not the send's own id, which is what `campaignId` means here and what\n * every delivered unsubscribe link carries as `cid`. Absent on a send\n * composed outside a campaign, and on every send that predates containers;\n * the campaigns list adopts those as a campaign of one at read time rather\n * than rewriting them.\n */\n emailCampaignId?: string\n /**\n * The preview line inboxes show after the subject. Overrides a designed\n * template's own, and gives a plain-text campaign one at all.\n */\n preheader?: string\n /**\n * What the merchant called this email, for the record rather than for the\n * recipient.\n *\n * Never leaves the console — it is not the subject and reaches no header,\n * so it is stored as typed apart from a length cap. Absent on every send\n * composed from a campaign, where the campaign is what carries the name.\n */\n displayName?: string\n /** Test sends (AGL-349) skip the campaign record and stats. */\n recordCampaign?: boolean\n /**\n * SEND THIS EMAIL AGAIN, TO PEOPLE IT HAS NOT REACHED.\n *\n * Set with a `campaignId` naming a send that has already gone out. The send\n * is not copied and no second document is made: this send addresses the\n * same audience under the same id, minus everyone the earlier sends\n * reached, and ADDS to the counters already on the record.\n *\n * ## Why the same document, rather than a new send\n *\n * Two reasons, and both are properties a copy would break.\n *\n * The unsubscribe link is the first. Every message this email has already\n * delivered carries `cid={campaignId}` inside its own HMAC, and those\n * messages sit in inboxes forever. A follow-up under a new id would be a\n * second `cid` for one email — two opt-out scopes for one mailing, and an\n * unsubscribe rate split across two records neither of which is the answer.\n *\n * The report is the second. Opens and clicks are attributed by the\n * `campaignId` tag on the delivered message, so a copy would collect the\n * follow-up's engagement on a document whose `sent` counts only the\n * follow-up. Keeping one document keeps every rate over a denominator that\n * covers the same mail the numerator does — see the additive write at the\n * bottom of this function for the arithmetic that holds it.\n *\n * ## What it does NOT relax\n *\n * Nothing. The follow-up runs the whole of this function: the consent\n * split, both suppression lists, the topic filter, the monthly pre-check\n * and reservation, the platform and per-workspace hourly ceilings, and the\n * per-message governor. Its only additions are the subtraction below and\n * the shape of the write at the end.\n */\n followUp?: boolean\n /**\n * THE NEXT BATCH OF AN EMAIL THAT IS STILL GOING OUT.\n *\n * Set by the scheduled-campaign processor, never by a request. An audience\n * larger than {@link EMAIL_MAX_RECIPIENTS_PER_SEND} is delivered across\n * several invocations; each one addresses the people the earlier ones did\n * not, and the campaign is written back as `scheduled` until nothing is\n * left. See the batch plan at the bottom of this function.\n *\n * ## Why it is not {@link followUp}\n *\n * They share the subtraction and the additive write, and they differ on the\n * two things that matter. A follow-up is a MERCHANT's act on an email that\n * has finished — it subtracts who was reached, and somebody suppressed at\n * the time who has since been released is entitled to get it. A batch is\n * one email still in flight — it subtracts everyone the email has SETTLED,\n * reached or refused, because a refused address sitting at the head of a\n * stable order would consume a slot in every remaining batch and, in\n * enough numbers, stop the campaign dead having mailed nobody.\n *\n * They also record differently. A follow-up measures a NEW population, so\n * the audience size and the consent split add. A batch measures a slice of\n * the population the first batch already measured, so those figures are\n * left exactly as the first batch wrote them.\n */\n continuation?: boolean\n /**\n * ONE address the caller has been proved to be entitled to proof to, for\n * the composer's test send. Exempts exactly this address from the\n * marketing-consent rule.\n *\n * The route decides entitlement and this file decides what the exemption\n * can do. See {@link eligibleProofAddress} for the first half and the\n * carve-out at the consent join for the second — including the two\n * properties that keep the exemption one address wide however the caller\n * arrived at it.\n */\n proofFor?: string\n /**\n * Test send only: render every message as if it were addressed to this\n * person, while DELIVERING to the recipient resolved in the ordinary way.\n *\n * What a merge tag resolves to is the whole question a proof answers. A\n * test rendered against the tester shows `{{firstName|there}}` falling back\n * for an audience whose contacts all have names, which reports the merge\n * tags as broken when they work — and, more expensively, hides the reverse.\n *\n * It changes the RENDER and nothing else. The unsubscribe link, the\n * `List-Unsubscribe` header and the suppression key are all minted from the\n * delivery address, deliberately: a proof carrying the persona's opt-out\n * link would let the person testing the email unsubscribe a real contact by\n * clicking a link in their own inbox.\n */\n proofPersona?: { email: string; name?: string }\n /*\n * THERE IS NO `sendingIdentity` OPTION.\n *\n * There was one, with exactly two values: empty for the site's standing\n * selection, and `platform` for the shared Aglyn domain. The second value\n * is gone, and with it the whole option, because a campaign is a site\n * talking to its own audience and `aglyn.com` is where Aglyn's billing and\n * account mail leaves from. A merchant choosing to put their campaign there\n * is choosing to charge their list's complaint rate against every other\n * customer's password reset — which is not a decision that is theirs to\n * make, however deliberately they make it.\n *\n * Deleting it rather than validating it against a plan is what makes that a\n * property instead of a rule. The address a campaign leaves on is now\n * resolved from the host document alone, in one place, and there is no\n * field a request can carry that changes it.\n */\n /** Recorded as `sentBy`; the scheduler passes the scheduling user. */\n senderUid: string\n}\n\n/**\n * Loads a designed email template's nodes + referenced products for the\n * render pipeline. Throws 400 when the screen isn't an email document.\n */\nasync function loadEmailTemplate(hostId: string, screenId: string) {\n const firestore = firebaseAdmin.app().firestore()\n const screenRef = firestore\n .collection('hosts')\n .doc(hostId)\n .collection('screens')\n .doc(screenId)\n const screenSnapshot = await screenRef.get()\n if (!screenSnapshot.exists) {\n throw new CampaignSendError('Unknown email template', 400)\n }\n const versionId = screenSnapshot.get('versionId')\n const versionSnapshot = versionId\n ? await screenRef.collection('versions').doc(String(versionId)).get()\n : null\n /**\n * Decoded, because a `kind: 'email'` screen is a SCREEN document and its\n * versions are compressed msgpack `Bytes` from the first designer save\n * onward (AGL-1394). `createEmailScreen` writes it under\n * `hosts/{h}/screens/{id}` and the Emails list opens it in the SCREEN\n * besigner, which saves through `use-screen-version`'s converter —\n * `Bytes.fromUint8Array(compress(nodes))`. Only the very first version,\n * seeded from a JSON body through `/api/hosts/versions`, is a plain map.\n *\n * `publish-email-template.ts` reading `nodes` raw is not evidence that this\n * one may: that reads `emailTemplates`, a different collection whose\n * besigner saves with a bare `setDoc` and no converter.\n *\n * The guard below is why this was silent rather than loud. `Object.keys` of\n * a Buffer yields the byte INDICES, so a compressed version read as a\n * populated template, `Object.values` found no `emailProduct` node, and a\n * designed campaign went out to real customer inboxes with its product\n * blocks missing — discovered by the recipients. Decoding first is also what\n * makes the guard mean something: `decodeStoredNodes` returns null for an\n * undecodable payload, so the send is refused instead of mailed empty.\n */\n const nodes = (decodeStoredNodes(versionSnapshot?.get('nodes')) ??\n {}) as Record<string, any>\n if (!Object.keys(nodes).length) {\n throw new CampaignSendError('The email template is empty', 400)\n }\n /**\n * The marketplace kill switch, reaching an email somebody already installed\n * (AGL-657's copy-on-install is what makes this necessary).\n *\n * An installed starter is a copy in this site's own screens, so every other\n * marketplace lever — unpublish, takedown, a rejected version — stops at the\n * storefront and is felt by nobody who already has the design. The kill is\n * the one that has to reach a tenant who installed last week and is sending\n * today, and this is the only chokepoint every campaign passes through.\n *\n * It refuses the SEND and leaves the document alone. The tenant keeps the\n * design, keeps editing it, keeps previewing it; what they cannot do is put\n * it on the sending domain every other tenant shares. Reaching into somebody\n * else's content to enforce a decision about a third party's artifact would\n * take the wrong thing away.\n *\n * Read off the VERSION first, then the screen: the version is the document\n * these bytes came out of, and a screen whose design was later replaced\n * wholesale should be judged on what it is now. Costs one document read on a\n * design that carries no marketplace provenance at all, which is every email\n * a site wrote itself — the `listingId` guard inside `emailStarterSendBlock`\n * is what keeps that read from happening.\n */\n const installedFrom = (versionSnapshot?.get('installedFrom') ??\n screenSnapshot.get('installedFrom')) as\n | { listingId?: string | null; version?: string | null }\n | undefined\n if (installedFrom?.listingId) {\n const revocation = (\n await firestore.collection('revocations').doc(installedFrom.listingId).get()\n ).data() as PluginRevocation | undefined\n const block = emailStarterSendBlock({ installedFrom, revocation })\n if (block) throw new CampaignSendError(block.reason, 409)\n }\n // Resolve emailProduct references (by id — rename-safe, AGL-343).\n const productIds = [\n ...new Set(\n Object.values(nodes)\n .filter((node: any) => node?.componentId === 'emailProduct')\n .map((node: any) => String(node?.props?.productId ?? ''))\n // AGL-1771: a besigner node prop is merchant-authored and reaches\n // `.doc()` below, where a slash-bearing value throws and turns the\n // whole send into a 500. Dropped rather than refused: the block simply\n // resolves to no product, exactly as it does for a deleted one, and a\n // designed campaign is not worth blocking over one bad reference.\n .filter(isDocumentId),\n ),\n ].slice(0, 20)\n const products: Record<string, EmailRenderProduct> = {}\n await Promise.all(\n productIds.map(async (productId) => {\n // Asked of whichever plugin keeps products: its name, its \"from\" price\n // as that plugin words it, its image and where it is read. A site with\n // no such plugin, and a product since deleted, both resolve to no card —\n // the block draws nothing, as it always has for a missing product.\n const card = await readPluginRecordCard('product', { hostId, id: productId })\n if (!card) return\n products[productId] = {\n name: card.title,\n priceLabel: card.caption,\n imageUrl: card.imageUrl,\n url: card.path,\n }\n }),\n )\n return {\n nodes,\n products,\n subject: String(screenSnapshot.get('emailSubject') ?? ''),\n preheader: String(screenSnapshot.get('emailPreheader') ?? ''),\n }\n}\n\n/**\n * Campaign delivery core (AGL-161, extracted for AGL-272): resolves the\n * audience server-side, drops suppressed addresses, enforces the plan's\n * monthly send cap, personalizes merge tags per recipient, and delivers\n * through Resend with a signed unsubscribe link. Shared by the\n * authenticated send route and the scheduled-campaign processor. The\n * caller owns authorization.\n */\nexport interface CampaignSendResult {\n campaignId: string\n /** Addresses this send ADDRESSED — the audience after the per-send cap. */\n recipients: number\n sent: number\n /**\n * The whole audience, deduplicated and validated, BEFORE the per-send cap.\n *\n * Reported separately from `recipients` because the two differ whenever an\n * audience is larger than one send may carry, and a merchant who is only\n * shown the smaller number has no way to learn that the rest were never\n * mailed. `recipients` of 500 against an `audienceSize` of 3,000 is the\n * whole point of the field.\n *\n * Named for the SIZE because `audience` on the options and on the stored\n * campaign is the audience KIND — `'leads'`, `'list'` — and one word\n * meaning both a name and a count on the same send path is how the two get\n * read into each other.\n */\n audienceSize: number\n /**\n * `audienceSize` is a FLOOR, not a total: the resolution stopped at its\n * read ceiling with documents still unread. Absent means it is exact.\n */\n audienceTruncated?: boolean\n /**\n * Dry run only (AGL-2178): recipients that will actually be mailed —\n * after the consent join, the per-send cap and both suppression lists.\n */\n sendable?: number\n /** Dry run only: of `audienceSize`, how many carry a recorded consent basis. */\n consented?: number\n /**\n * Dry run only: of `consented`, how many hold a basis an OPERATOR asserted\n * on their behalf rather than one they gave — a backfill over seed data,\n * say. A subset of `consented` and not a fourth population.\n */\n consentedByOperator?: number\n /**\n * Dry run only: of `audienceSize`, how many are reachable only because\n * consent enforcement is not retroactive. This is the population a strict\n * policy would remove.\n */\n grandfathered?: number\n /** Dry run only: how many of `audienceSize` the consent rule refused. */\n consentWithheld?: number\n /**\n * Dry run only: how many of `recipients` are suppressed or off-topic.\n *\n * Not netted with {@link cadenceHeld}: leaving this site and asking it for\n * less mail are different statements, and only one of them is a list a\n * merchant has to rebuild.\n */\n suppressed?: number\n /**\n * Dry run only: how many of `recipients` asked this site for mail less\n * often than this send would arrive.\n *\n * They stay on every audience they were on and are mailed by the next\n * campaign that falls outside their interval — the filter refuses a SEND,\n * never a person.\n */\n cadenceHeld?: number\n /** Dry run only: which sending identity this campaign would leave on. */\n identity?: string\n /**\n * Dry run only: which KIND of identity this campaign would leave on.\n *\n * The union from the resolver rather than a local copy of it, so a new arm\n * cannot be added there and silently narrowed here. `'shared'` never reaches\n * a real campaign — the resolution above declares `purpose: 'marketing'`, and\n * a pooled identity refuses before this is assembled — but the type says what\n * the resolver can return, not what this path expects to see.\n */\n identitySource?: SendingIdentitySource | null\n dryRun?: boolean\n /** Recipients the hourly governor refused mid-batch (AGL-2409). */\n deferred?: number\n /**\n * Follow-up only: people in the audience this email had already reached, and\n * which this send therefore did not address.\n *\n * Reported rather than netted away, because it is the number that explains\n * the other ones. A follow-up over a 3,000-person list that addresses 40\n * people has not failed — 2,960 of them already have the email — and\n * without this figure the merchant cannot tell that from a broken audience.\n */\n alreadyReached?: number\n /** Whether this send added to an existing email rather than starting one. */\n followUp?: boolean\n /**\n * People this email has resolved and not yet addressed.\n *\n * Non-zero on a send that is still going out, and the figure that makes\n * \"reached 500 of 3,000\" a sentence rather than a truncation. Zero when the\n * email is finished.\n */\n remaining?: number\n /** True when another batch of this email will run on its own. */\n resuming?: boolean\n /** When the next batch may go, ms. Present only while {@link resuming}. */\n nextAtMs?: number\n /** Batches this email has run, including this one. */\n batch?: number\n}\n\nexport async function performCampaignSend(\n options: CampaignSendOptions,\n): Promise<CampaignSendResult> {\n const unsubscribeSecret =\n process.env.EMAIL_UNSUBSCRIBE_SECRET || process.env.CRON_SECRET\n if (!isEmailConfigured() || !unsubscribeSecret) {\n throw new CampaignSendError(\n 'Campaigns are not configured (RESEND_API_KEY, USAGE_EMAIL_FROM, ' +\n 'EMAIL_UNSUBSCRIBE_SECRET).',\n 501,\n )\n }\n const { hostId, subject, body, audience } = options\n\n // AGL-1771: every optional id on `options` becomes a `.doc()` argument\n // below, and `.doc()` appends a SLASH-SEPARATED path rather than taking one\n // opaque id — so an unvalidated one names the nesting as well as the\n // document. `campaignId` is the one that matters most: it is WRITTEN at the\n // bottom of this function, so `a/b/c` filed the campaign at\n // `campaigns/a/b/c`, beneath a document that does not exist and therefore\n // invisible to the merchant's own campaigns list — and it is the same value\n // that comes back on every Resend tag days later (AGL-1768), which is why\n // tracing where an id was MINTED matters more than where it was last\n // handled. The rest are read-only, where the cost is a 500 dressed up as an\n // outage rather than a stray document; refused here so the caller is told\n // which id was wrong.\n //\n // `hostId` is deliberately NOT guarded here, and that is measured rather\n // than overlooked: both callers prove it first — the handler resolves the\n // host document and checks the caller's role on it before calling in, and\n // the scheduled processor passes `hostRef.id` off a document it just read. A\n // guard here could not fail today. A third caller would need to earn that.\n for (const [name, value] of [\n ['campaignId', options.campaignId],\n ['experimentId', options.experimentId],\n ['templateScreenId', options.templateScreenId],\n ['segmentId', options.segmentId],\n ['listId', options.listId],\n ] as const) {\n if (value && !isDocumentId(value)) {\n throw new CampaignSendError(`Invalid ${name}`, 400)\n }\n }\n /*\n * `topicId` is checked against its OWN predicate, not `isDocumentId`.\n *\n * It is a path component like the others, but it is also a colon-joined\n * component of the unsubscribe link's signed subject, and `isDocumentId`\n * permits a colon. Signing one would let a single subject string be read as\n * two different parameter tuples — see `signedSubject` in the email plugin's\n * `unsubscribe-link.ts`. Refused at the point the topic ENTERS the send, so\n * the link that leaves it is unambiguous by construction.\n */\n if (options.topicId && !isEmailTopicId(options.topicId)) {\n throw new CampaignSendError('Invalid topicId', 400)\n }\n const topicId = options.topicId || DEFAULT_CAMPAIGN_TOPIC_ID\n /**\n * Whether this send ADDS to an email that already exists, rather than\n * starting one.\n *\n * True for a merchant's follow-up and for an automatic batch. The two\n * differ in what they subtract and in what they record — see\n * `CampaignSendOptions.continuation` — but they agree on the one thing this\n * flag decides: every counter on the campaign is an increment rather than a\n * replacement, so no rate can come out over a denominator that counts less\n * mail than its numerator.\n */\n const addsToExistingSend = Boolean(options.followUp || options.continuation)\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n throw new CampaignSendError('Unknown site', 404)\n }\n\n /*==========================================\n * THE FOLLOW-UP'S ADMISSION CHECKS.\n *\n * Here rather than in the route, and re-run even though the route has\n * already loaded the same document to build these options: the properties\n * below are the ones that decide whether somebody gets a second copy of an\n * email, and a check a caller performs is a check the next caller forgets.\n * Two reads of one small document on a deliberate button press is what that\n * costs.\n *=========================================*/\n let reachedKeys: ReadonlySet<string> | null = null\n /**\n * Batches this email has already run, so the plan below can tell the second\n * from the twentieth. Zero on every first send.\n */\n let batchesSoFar = 0\n /*==========================================\n * THE NEXT BATCH'S ADMISSION CHECKS.\n *\n * The same two properties the follow-up checks below, read off the same\n * record and refused for the same reasons — with `skipped` folded into the\n * subtraction, which is the whole difference between the two. See\n * `CampaignSendOptions.continuation`.\n *=========================================*/\n if (options.continuation) {\n if (!options.campaignId) {\n throw new CampaignSendError(\n 'A batch has to name the email it is continuing',\n 400,\n )\n }\n const sendSnapshot = await hostRef\n .collection('campaigns')\n .doc(options.campaignId)\n .get()\n if (!sendSnapshot.exists) {\n throw new CampaignSendError('Unknown email', 404)\n }\n /*\n * A batch continues an email the processor has CLAIMED. `sending` is what\n * that claim writes; `scheduled` is accepted beside it so a campaign that\n * was written back by a deferral and is picked up again is not refused by\n * its own retry.\n */\n const status = String(sendSnapshot.get('status') ?? '')\n if (status !== 'sending' && status !== 'scheduled') {\n throw new CampaignSendError(\n 'This email is not in the middle of being sent',\n 400,\n )\n }\n /*\n * FAILS CLOSED, exactly as the follow-up does. The record of who this\n * email has already mailed is the only thing standing between a resumed\n * send and a second copy in somebody's inbox, and a batch that cannot\n * read it must not run.\n */\n const settled = await readCampaignSettled(\n hostId,\n options.campaignId,\n firestore,\n )\n const sentSoFar = Number(sendSnapshot.get('stats')?.sent ?? 0)\n if (!campaignReachCovers(settled.reached, sentSoFar)) {\n throw new CampaignSendError(\n 'This email does not have a complete record of who it reached, so ' +\n 'the rest of it cannot be sent without risking a second copy for ' +\n 'somebody who already has it.',\n 409,\n )\n }\n if (campaignSettledSize(settled) >= CAMPAIGN_REACH_CEILING) {\n throw new CampaignSendError(\n `This email has already addressed ${CAMPAIGN_REACH_CEILING.toLocaleString()} ` +\n 'people, which is the most one email may reach. Compose a new ' +\n 'email for the rest of the audience.',\n 409,\n )\n }\n reachedKeys = new Set([...settled.reached, ...settled.skipped])\n batchesSoFar = Math.max(\n 0,\n Math.floor(Number(sendSnapshot.get('resume')?.batch ?? 0)) || 0,\n )\n }\n /**\n * A batch that found nothing left to do: the email is FINISHED, not broken.\n *\n * Every \"there is nobody to send to\" refusal below is a 400 that tells a\n * merchant their audience is empty — which is the right answer to a send\n * they just pressed, and the wrong one to a batch of an email that has\n * already delivered two thousand messages. The processor would mark it\n * `failed`, and a campaign that reached most of its list would be filed\n * under the same word as one that never left.\n *\n * So a continuation closes the email out instead, with the counters the\n * earlier batches wrote left exactly as they are.\n */\n const finishContinuation = async (): Promise<CampaignSendResult> => {\n const sendId = String(options.campaignId ?? '')\n await hostRef\n .collection('campaigns')\n .doc(sendId)\n .set(\n {\n status: 'sent',\n resume: { remaining: 0, batch: batchesSoFar + 1, nextAtMs: 0 },\n lastSentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n return {\n campaignId: sendId,\n recipients: 0,\n audienceSize: 0,\n sent: 0,\n remaining: 0,\n resuming: false,\n batch: batchesSoFar + 1,\n }\n }\n\n if (options.followUp) {\n if (!options.campaignId) {\n throw new CampaignSendError(\n 'A follow-up has to name the email it is adding to',\n 400,\n )\n }\n const sendSnapshot = await hostRef\n .collection('campaigns')\n .doc(options.campaignId)\n .get()\n if (!sendSnapshot.exists) {\n throw new CampaignSendError('Unknown email', 404)\n }\n /*\n * Only a SENT email has anybody to add to. A scheduled one has not gone\n * out — sending it now is what the scheduler is for, and doing it here\n * would deliver it twice; a canceled one was withdrawn on purpose.\n */\n if (sendSnapshot.get('status') !== 'sent') {\n throw new CampaignSendError(\n 'Only an email that has already been sent can go to more people',\n 400,\n )\n }\n /*\n * FAILS CLOSED. `readCampaignReach` throws rather than answering\n * \"nobody\", and that throw is deliberately not caught: the whole feature\n * rests on being able to name who already has this email, and a send that\n * cannot is a send that must not happen.\n */\n const keys = await readCampaignReach(hostId, options.campaignId, firestore)\n const sentSoFar = Number(sendSnapshot.get('stats')?.sent ?? 0)\n if (!campaignReachCovers(keys, sentSoFar)) {\n throw new CampaignSendError(\n 'This email does not have a complete record of who it reached, so ' +\n 'it cannot be sent to more people without risking a second copy ' +\n 'for somebody who already has it. Compose a new email to the ' +\n 'people you want to add.',\n 409,\n )\n }\n if (keys.size >= CAMPAIGN_REACH_CEILING) {\n throw new CampaignSendError(\n `This email has already reached ${keys.size.toLocaleString()} ` +\n `people, which is the most one email may reach ` +\n `(${CAMPAIGN_REACH_CEILING.toLocaleString()}). Compose a new email ` +\n 'for the rest of the audience.',\n 409,\n )\n }\n reachedKeys = keys\n }\n\n /*\n Audience resolution. Names ride along for merge tags (AGL-272).\n\n ⚠️ THE FIELD NAME IS PER COLLECTION AND THEY DO NOT AGREE (AGL-2303).\n `contacts` and `leads` store `name`; `siteMembers` stores `displayName`.\n A merge tag whose source field does not exist does not error — it\n substitutes an empty string into mail that has already been sent. Whenever\n an audience is added here, check what its collection actually writes.\n */\n let recipients: string[]\n /**\n * Set when {@link AUDIENCE_SCAN_CEILING} stopped the resolution, so\n * `audience` below is a floor and every number derived from it says so.\n * A `manual` audience arrives whole in the request and can never be one.\n */\n let audienceTruncated = false\n /**\n * The list's name as it stands at the moment of the send.\n *\n * Recorded on the campaign rather than looked up when a report is read, for\n * the reason every other send-time figure is recorded: a list can be\n * renamed or deleted, and resolving the name later either rewrites the\n * history of a campaign that went out months ago or loses it entirely. The\n * campaign went to the list under this name, and that stays true.\n */\n let listName = ''\n const names = new Map<string, string>()\n /*\n * The consent facts ride out of the audience sweep with the names\n * (`docs/specs/email-overhaul.md` §3f), because paging the audience is the\n * only point in the send where the person's DOCUMENT is in hand.\n *\n * That placement is the correction §3f carries: consent is a property of\n * the PERSON, knowable while the sweep is already reading them, where\n * suppression is a per-address keyed lookup deliberately deferred until\n * after the cap. Reading consent back later would be a second pass over\n * every silo, per campaign, to recover data this loop already had.\n *\n * An audience whose members carry no consent field records an `unrecorded`\n * basis — a THIRD state, handled by the policy, and never a quiet `true`.\n */\n /*\n * THE CONTROLLER THIS SEND IS MADE BY — the declared group of sites that\n * are one sender, or this site alone.\n *\n * Resolved before the sweep because every consent read below is about a\n * controller and not about a site: three sites a business declared as one\n * sender share a basis, and twelve unrelated client brands in an agency's\n * account share nothing. The org read is deduped per request, so the policy\n * lookup further down pays nothing for this.\n */\n const consentGroup = await consentGroupForSite(hostId)\n const consent = new Map<string, MarketingConsentRecord>()\n const collectConsent = (email: string, data: unknown) => {\n const cleaned = email.trim().toLowerCase()\n if (!cleaned) return\n consent.set(\n cleaned,\n readMarketingBasis(\n data as Record<string, unknown> | null | undefined,\n consentGroup,\n ),\n )\n }\n const collectName = (email: string, name: unknown) => {\n const cleaned = email.trim().toLowerCase()\n if (cleaned && typeof name === 'string' && name.trim()) {\n names.set(cleaned, name.trim())\n }\n }\n if (audience === 'leads') {\n const leads = await sweepAudience(hostRef.collection('leads'))\n audienceTruncated = leads.truncated\n recipients = leads.docs.map((doc) => {\n const email = String(doc.get('email') ?? '')\n collectName(email, doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else if (audience === 'members') {\n const members = await sweepAudience(hostRef.collection('siteMembers'))\n audienceTruncated = members.truncated\n recipients = members.docs.map((doc) => {\n const email = String(doc.get('email') ?? '')\n /*==========================================\n * `displayName`, NOT `name` (AGL-2303).\n *\n * `siteMembers` has never had a `name` field — sign-up, the account\n * page and the admin password route all write `displayName`. So this\n * read matched nothing on every member campaign ever sent, `names` was\n * empty for the whole audience, and `{{contact.name}}` and\n * `{{contact.firstName}}` rendered as EMPTY STRINGS in mail that went\n * out to real people. `resolveMergeTags` substitutes rather than\n * failing, so nothing errored and nothing looked wrong here.\n *\n * `name` is kept as a fallback and read second: a lead promoted to a\n * member, or a future writer, may carry either.\n *=========================================*/\n collectName(email, doc.get('displayName') ?? doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else if (audience === 'segment') {\n // Contact segments (AGL-199): resolve the saved filter against the\n // contacts collection server-side.\n const segmentId = String(options.segmentId ?? '')\n const segmentSnapshot = segmentId\n ? await (await orgDataCollectionForHost(hostId, 'contactSegments')).doc(segmentId).get()\n : null\n // A doc get cannot carry the scope filter, so check after the read\n // (AGL-1039). Reported as \"unknown\" rather than \"forbidden\": whether a\n // segment exists in another site's scope is not this caller's business.\n if (\n !segmentSnapshot?.exists ||\n !visibleToHost(segmentSnapshot.get('visibleTo'), hostId)\n ) {\n throw new CampaignSendError('Unknown segment', 400)\n }\n const segment = {\n tags: segmentSnapshot.get('tags') ?? [],\n sources: segmentSnapshot.get('sources') ?? [],\n }\n // Scoped (AGL-1039): a campaign sent from one site must not reach\n // another site's audience — the agency case is a client's campaign\n // blasting the whole org's contact list.\n /*\n * The only audience whose sweep carries a FILTER, and the reason the\n * ordering is `__name__` rather than a field: Firestore's automatic\n * single-field index for an array member is keyed on that value and the\n * document name, so `array-contains-any` plus `orderBy(__name__)` is\n * served by it. Ordering on any other field would need a composite index\n * per audience — `cloud/firebase-firestore.indexes.json` carries exactly\n * that shape for `media`, and a missing one fails the whole send.\n */\n const contacts = await sweepAudience(\n (await orgDataQueryForHost(hostId, 'contacts')).query,\n )\n /*\n * A segment's membership is decided HERE rather than by the query — the\n * tag and source rules are evaluated in `contactMatchesSegment` against\n * documents the scan already fetched — so the ceiling bounds CONTACTS\n * READ, not contacts matched. A narrow segment over a large org therefore\n * reports a small `audienceSize` with `audienceTruncated` set, reading as\n * \"at least this many, we stopped counting\" and not as a total.\n */\n audienceTruncated = contacts.truncated\n recipients = contacts.docs\n .filter((doc) =>\n contactMatchesSegment(\n { tags: doc.get('tags') ?? [], sources: doc.get('sources') ?? {} },\n segment,\n ),\n )\n .map((doc) => {\n const email = String(doc.get('email') ?? '')\n collectName(email, doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else if (audience === 'list') {\n // Org lists (AGL-254): static audiences enrolled manually or by the\n // enrollList automation step.\n const listId = String(options.listId ?? '')\n const listRef = listId\n ? (await orgDataCollectionForHost(hostId, 'contacts')).parent\n ?.collection('lists')\n .doc(listId)\n : null\n if (!listRef) throw new CampaignSendError('Unknown list', 400)\n // One document beside a sweep that reads up to the audience ceiling, and\n // the only place the name is knowable without a second round trip later.\n listName = String((await listRef.get()).get('name') ?? '')\n const members = await sweepAudience(listRef.collection('members'))\n audienceTruncated = members.truncated\n recipients = members.docs.map((doc) => {\n const email = String(doc.get('email') ?? '')\n collectName(email, doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else {\n recipients = Array.isArray(options.emails)\n ? options.emails.map((value: unknown) => String(value))\n : []\n }\n /*\n * The AUDIENCE — deduplicated and validated, and deliberately measured\n * BEFORE the per-send cap.\n *\n * The cap and the audience are two different numbers and the composer has\n * always shown only the smaller one, which is how a site with 3,000 leads\n * was told its audience was 500 and never found out the other 2,500 were\n * not being mailed. Every result from here down carries both, so a send can\n * report \"reached N of M\" rather than reporting N as if it were M.\n */\n const addressable = [\n ...new Set(\n recipients\n .map((email) => email.trim().toLowerCase())\n .filter((email) => EMAIL_PATTERN.test(email)),\n ),\n ]\n if (!addressable.length) {\n if (options.continuation) return finishContinuation()\n throw new CampaignSendError('The audience is empty', 400)\n }\n\n /*==========================================\n * THE SUBTRACTION: NOBODY GETS THIS EMAIL TWICE.\n *\n * ## Why it is HERE, above the per-send cap\n *\n * Not a preference — the only position that works. The cap a few lines\n * below takes \"the FIRST N of a stable order\", and that stability is what\n * makes it defensible: two sends of an unchanged audience address the same\n * people. Which means a follow-up that subtracted AFTER the cap would be\n * handed the same first 500 addresses the original send took, discover that\n * all 500 have had the email, and mail nobody — every time, forever, for\n * any audience larger than one send.\n *\n * So the subtraction runs on the whole resolved audience and the cap then\n * takes the first N of what is LEFT, which is the same rule pointed at the\n * remainder.\n *\n * ## What it costs, and why that is the right trade\n *\n * Nothing per address: the reach record is one document, read once above,\n * and the test is a hash and a set lookup. This is the one filter in the\n * send that can afford to run over the whole audience rather than the\n * capped list, which is why suppression stays where it is.\n *\n * ## What `resolved` means from here down\n *\n * The people this send may address — so on a follow-up it is the NEW part\n * of the audience, and every figure derived from it (the consent split,\n * `audienceSize`) describes that part. The write at the bottom adds those\n * figures to the ones already recorded, so the totals on the email cover\n * both sends over two populations that cannot overlap.\n *=========================================*/\n const partitioned = reachedKeys\n ? partitionByCampaignReach(addressable, reachedKeys)\n : { unreached: addressable, alreadyReached: 0 }\n const alreadyReached = partitioned.alreadyReached\n const resolved = partitioned.unreached\n if (!resolved.length) {\n if (options.continuation) return finishContinuation()\n throw new CampaignSendError(\n 'Everyone in this audience has already had this email, so nothing has ' +\n 'been sent.',\n 400,\n )\n }\n\n /*\n * THE CONSENT JOIN (`docs/specs/email-overhaul.md` §3f).\n *\n * `marketingConsent` had seven writers and no reader on any send path, so a\n * recorded opt-OUT reached the same inbox as a recorded opt-in.\n *\n * ## Why HERE, above the cap, and not beside suppression\n *\n * §3f used to say \"after suppression and before the cap\", which is not an\n * order this file has ever had — the cap runs first and suppression after\n * it. Consent belongs at the SWEEP: it is a property of the person, already\n * read into `consent` by the loop above at no extra cost, so filtering on\n * it before the cap means the 500 slots go to people who may actually be\n * mailed. Suppression stays where it is, because it is a keyed lookup per\n * address and moving it up would cost the whole audience in reads on every\n * debounced preview.\n *\n * It is also, necessarily, before the meter claim: a recipient the rule\n * withholds is never counted against the org's monthly allowance. Being\n * charged for mail that policy forbids sending would make the consent rule\n * cost the merchant money as well as reach.\n *\n * ## What it does to an audience that exists today\n *\n * NOT retroactive by default. `resolveMarketingConsentPolicy` answers\n * `mode: 'forward'` for an org that has configured nothing, which keeps\n * every address captured before the cutoff reachable and reports it as\n * grandfathered rather than mailing it silently. The one thing enforced\n * unconditionally is a STORED refusal, which no policy may mail.\n *\n * The retroactive mode can shrink an audience sharply, so it is a stored\n * per-org setting and never a default. The split below is what makes that\n * decision informed — it rides the same readout as `audienceSize`, so a\n * merchant sees which population is which before sending.\n *\n * The org is resolved here rather than at the quota block below because the\n * policy lives on it and this is the first thing that needs it; the quota\n * lines further down reuse the same read rather than taking a second one.\n */\n const orgForHost = await getOrgForHost(hostId).catch(() => null)\n const orgId = String(orgForHost?.orgId ?? '')\n\n /*\n * THE SENDING IDENTITY, and the refusal when it is not usable.\n *\n * Resolved ABOVE the dry run on purpose. `preview` is where a merchant finds\n * out what a send will do before writing copy, so it must answer the same\n * question a real send would — both which identity the mail leaves on, and\n * whether it may leave at all. Resolving after this point would let\n * `preview` report a healthy dry run for a campaign that Send then refuses.\n *\n * The address comes from the org document by way of the host's selection,\n * never from `options`. A `From:` assembled from request input is the\n * spoofing path the verified-identity rule exists to close.\n *\n * A refusal is a 409 rather than a silent no-op because that is the whole\n * point: `USAGE_EMAIL_FROM` was empty in production for weeks and no surface\n * ever said so, since every sender treats mail as best-effort. A tenant\n * whose DNS is unfinished has to be told, by name, at the composer.\n */\n /*\n * The DOMAIN is the site's standing selection, and nothing a request says\n * moves it.\n *\n * No option reaches it. It is read from the host document, so a request\n * cannot name a domain to send as — which is the spoofing path — and cannot\n * drop the selection to reach the shared platform domain either, which is\n * the reputation path. Both used to be one field.\n *\n * The MAILBOX in front of it is chosen per send, and by id: `senderId` names\n * a row in `hosts/{hostId}/senders` that an org admin configured, so it can\n * only reach an address this site was already set up to send as. That is the\n * whole difference between it and the free `sendingIdentity` the route reads\n * from nobody.\n */\n /*\n * WHICH SENDER, and the refusal when the campaign names one this site does\n * not hold.\n *\n * The one line that decides the mailbox a campaign leaves on. A site's\n * senders are `hosts/{hostId}/senders/{senderId}`, and the host's\n * `sendingLocalPart` is the DEFAULT sender's projection — so a send that\n * names nobody resolves exactly as it did before the collection existed,\n * including on a site that has never written to it.\n *\n * An unknown id is REFUSED rather than defaulted, and that is the whole\n * reason this is a read and not a `??`. Quietly sending as the default is\n * the same class of failure as the mailbox validation that used to answer\n * `hello` to a name it could not parse: a merchant is told their campaign\n * went out as the sender they picked, and it did not.\n *\n * Refused HERE, above the dry run, so `preview` answers it too — the\n * composer finds out at the picker rather than from the Send button.\n */\n const senderId = String(options.senderId ?? '').trim()\n const senderSnapshot = senderId\n ? await hostRef.collection(HOST_SENDERS_COLLECTION).doc(senderId).get()\n : null\n if (senderId && !senderSnapshot?.exists) {\n throw new CampaignSendError(\n 'The sender this email is set to go out as is no longer one this site ' +\n 'holds. Pick a sender in the composer, or add it back under ' +\n 'Emails → Sending.',\n 404,\n )\n }\n const chosenSender = senderSnapshot?.exists\n ? readHostSender({\n id: senderId,\n data: senderSnapshot.data() as Record<string, unknown>,\n })\n : null\n\n /*\n * `purpose: 'marketing'` is what makes the SUMMARY name the arrangement. A\n * campaign is the one send site that knows for certain what it is carrying,\n * so it says so, and a site on the pool is told in the composer that its\n * reputation is shared and graded more tightly for it.\n */\n const sendingIdentity = await resolveHostSendingIdentity({\n orgId,\n hostId,\n selectedDomain: hostSnapshot.get('sendingDomain'),\n selectedLocalPart:\n chosenSender?.localPart || hostSnapshot.get('sendingLocalPart'),\n poolMember: hostSnapshot.get('sendingPoolMember'),\n purpose: 'marketing',\n })\n const identityRefusal = sendingIdentityRefusal(sendingIdentity)\n if (identityRefusal) {\n const missing = identityRefusal.missing?.length\n ? ` Missing: ${identityRefusal.missing.join(', ')}.`\n : ''\n throw new CampaignSendError(`${identityRefusal.message}${missing}`, 409)\n }\n /**\n * WHOSE REPUTATION THIS CAMPAIGN SPENDS.\n *\n * A pooled sender shares one domain with every other site that has no domain\n * of its own, so a complaint it earns is charged to their receipts as much as\n * to its own. That asymmetry is the whole reason the pool used to refuse\n * marketing outright, and it is answered by grading rather than by\n * prohibition: on the pool a campaign is held to `strict`, which stops it on\n * the WATCH thresholds — Google's \"keep under\" 0.10% complaint rate and a 5%\n * bounce rate — instead of waiting for the trip levels three and two times\n * higher.\n *\n * On a domain the merchant owns, the org's own setting stands. There the\n * reputation being spent is theirs alone, and how fast they spend it is\n * theirs to decide.\n *\n * It overrides `none` as well, which is the point rather than an oversight:\n * a workspace that has switched its own breaker off must not thereby switch\n * off the one protecting the other sites on its pool member. Same posture as\n * the platform frequency ceiling, which is the same number on every plan for\n * the same reason.\n */\n const reputationPolicy = effectiveReputationPolicy(\n sendingIdentity.source,\n (orgForHost?.org as Record<string, unknown> | undefined)?.[\n 'emailReputationPolicy'\n ],\n )\n const consentPolicy = resolveMarketingConsentPolicy(\n (orgForHost?.org as Record<string, unknown> | undefined)?.[\n 'marketingConsentPolicy'\n ],\n )\n /*\n * THE PROOF CARVE-OUT.\n *\n * A proof delivered to an account holder on this workspace is not a\n * marketing send. The recipient is the person who pressed the button, or a\n * colleague of theirs who already receives this workspace's console mail,\n * and the consent rule exists to protect somebody from mail they did not\n * ask for — which is not what proofing a draft to your own team is.\n *\n * Without this the composer's test send is dead under `strict`: it delivers\n * through the `manual` audience, a hand-typed address is backed by no\n * document, and `unrecorded` is withheld before reaching the clause that\n * grandfathers a record carrying no capture date. Proofing your own email\n * would be refused on consent grounds.\n *\n * ⚠️ TWO PROPERTIES KEEP THE EXEMPTION ONE ADDRESS WIDE, and both are here\n * rather than at the call site, because a caller that could widen it is\n * exactly what this must not be:\n *\n * 1. The address must ALREADY be in the resolved audience. The option can\n * therefore only exempt a recipient, never introduce one — passing an\n * address that is not being sent to does nothing at all.\n * 2. A stored `declined` is still refused, below. A refusal is the one\n * thing no policy may mail, and a proof is not the first exception to\n * it: somebody who declined marketing on this site un-declines rather\n * than being quietly overridden.\n *\n * Neither of them is what stops a merchant proofing to a stranger. That is\n * {@link eligibleProofAddress}, at the route, because it is a question\n * about WHO an address belongs to and this function has no way to ask it.\n * The two here are what hold when that check is wrong.\n */\n const proofFor = String(options.proofFor ?? '')\n .trim()\n .toLowerCase()\n const proofAddress = proofFor && resolved.includes(proofFor) ? proofFor : ''\n /*\n * THE ONE READ THAT MAKES THE `declined` GUARANTEE TRUE.\n *\n * A proof is delivered through the `manual` audience, and a manual audience\n * is a list of ADDRESSES — no documents are swept, so `consent` is empty\n * and every proof address arrives as `unrecorded`. The refusal below was\n * therefore unreachable on the only path that can reach it: the carve-out\n * promised that a stored opt-out still refuses, and nothing ever looked one\n * up.\n *\n * Keyed on the single address and only when a proof is in flight, so the\n * cost is one small lookup on an explicit click rather than anything on the\n * campaign path.\n */\n if (proofAddress && !consent.has(proofAddress)) {\n const stored = await readStoredConsent(hostId, proofAddress)\n if (stored) {\n consent.set(proofAddress, readMarketingBasis(stored, consentGroup))\n }\n }\n if (proofAddress && consent.get(proofAddress)?.basis === 'declined') {\n throw new CampaignSendError(\n `${proofAddress} has a recorded marketing opt-out on this site, so the ` +\n 'test send was not delivered. Proof to an address that has not opted ' +\n 'out, or opt that one back in.',\n 400,\n )\n }\n const consentSplit = splitByMarketingConsent(\n proofAddress ? resolved.filter((one) => one !== proofAddress) : resolved,\n consent,\n consentPolicy,\n consentGroup,\n )\n if (proofAddress) consentSplit.mailable.unshift(proofAddress)\n if (!consentSplit.mailable.length) {\n if (options.continuation) return finishContinuation()\n throw new CampaignSendError(\n 'No recipient in this audience has a marketing consent record, so ' +\n 'nothing has been sent. Add an opt-in checkbox to the form or sign-up ' +\n 'this audience comes from, or send to an audience that has one.',\n 400,\n )\n }\n\n /*==========================================\n * THE TWO PLATFORM CONTROLS THAT SIZE THIS BATCH.\n *\n * Both read the same seven-day window, so they are resolved together and\n * the window is read once. Both are campaign-only by construction — this\n * function is the only caller, and transactional mail cannot reach it.\n *\n * A TEST SEND is exempt from both. It delivers one message to the address\n * of the person who pressed the button, it writes no campaign record, and\n * refusing it would leave a merchant whose list has a problem unable to\n * even look at the email they are trying to fix.\n *=========================================*/\n const platformRate = await readEmailSendRateConfig()\n const proofOnly = options.recordCampaign === false\n /**\n * How many people this batch may address.\n *\n * The per-send cap unless the new-sender ramp is lower, in which case the\n * ramp is what the batch takes and the rest of the audience goes out on the\n * following days. Shrinking rather than deferring is the only shape that\n * works: a workspace on a 200-a-day step would defer a 500-recipient batch\n * every single day and never send anything at all.\n */\n let batchCap = MAX_RECIPIENTS_PER_SEND\n /** Today's ramp, resolved once and claimed against below. */\n let ramp: EmailRampVerdict | null = null\n if (!proofOnly) {\n /** This workspace's seven-day grade, and the window both controls read. */\n const reputation: SenderReputationRead = await readSenderReputation({\n orgId,\n policy: reputationPolicy,\n reinstatedUntilMs: (\n orgForHost?.org as Record<string, unknown> | undefined\n )?.['emailReputationReinstatedUntilMs'],\n })\n /*\n * THE CIRCUIT BREAKER.\n *\n * A 409 rather than a deferral, and that difference is the point. A\n * deferral says \"not this hour\" and retries itself; this says \"not until\n * something changes\", and the thing that has to change is the list. A\n * campaign that rescheduled itself against a tripped breaker would mail\n * the same bad addresses on a timer.\n *\n * NOTHING IS REMOVED. No contact is deleted, no audience is trimmed,\n * nobody is unsubscribed and no list membership moves — the refusal is on\n * the SEND, which is a flow, and refusing a flow strands nobody's data.\n * That is the enforce-at-the-reduction rule (`over-limit.ts`) applied to\n * the one control in this file that could be tempted to break it.\n *\n * The message carries the numbers and what to do about them, because a\n * merchant who cannot send and cannot find out why will open a ticket\n * that says the product is broken.\n */\n if (reputation.blocked) {\n throw new CampaignSendError(reputation.reason, 409)\n }\n /*\n * THE NEW-SENDER RAMP.\n *\n * A workspace created today may not put its whole first import onto the\n * domain every other tenant's receipts leave on. The step it is on is\n * earned by clean volume as well as reached by age, and a workspace past\n * its first week — which is every existing customer, and every org whose\n * record predates the creation timestamp — is not ramped at all.\n */\n ramp = resolveOrgEmailRamp({\n ageDays: orgAgeDays(\n (orgForHost?.org as Record<string, unknown> | undefined)?.['createdAt'],\n ),\n deliveredLifetime: reputation.window.accepted,\n platformPerHour: platformRate.perHour,\n })\n if (!ramp.graduated && platformRate.enabled) {\n const dayRemaining = Math.max(\n 0,\n ramp.perDay - reputation.window.claimedToday,\n )\n if (dayRemaining <= 0) {\n throw new CampaignSendDeferredError(\n `${ramp.detail} It has already sent ` +\n `${reputation.window.claimedToday.toLocaleString()} today, so ` +\n 'this campaign has not been sent and nothing has been counted — ' +\n 'it goes out automatically tomorrow. Transactional mail — ' +\n 'receipts, booking reminders, password resets — keeps sending.',\n Math.floor(Date.now() / 86_400_000) * 86_400_000 + 86_400_000,\n )\n }\n batchCap = Math.min(batchCap, dayRemaining)\n }\n }\n\n /*\n * The cap takes the FIRST N of a stable order, which is what makes taking\n * some of the audience defensible at all: two sends of the same unchanged\n * audience now address the same people, and which people is answerable\n * (\"the first N by document name\"). It was previously whichever slice\n * Firestore happened to return.\n *\n * What is left over is not lost. The plan at the bottom of this function\n * writes the email back as `scheduled` with a record of how far it got, and\n * the next run addresses the first N of the REMAINDER — the same rule\n * pointed at what is left, which is the same move the follow-up's\n * subtraction makes one block above.\n */\n recipients = consentSplit.mailable.slice(0, batchCap)\n\n /*\n * BOTH suppression lists, on one derivation (D6 of\n * `docs/specs/email-overhaul.md`).\n *\n * This read used to be the site's own list alone, so an address that hard\n * bounced or reported spam on any OTHER send — another site in the org, or\n * transactional mail carrying no site tag, which is where most of the\n * platform list comes from — was mailed anyway. Every tenant's campaigns\n * leave by one sending domain under `p=reject`, so that is not one\n * merchant's deliverability, it is everyone's.\n *\n * Checked on the capped list rather than the whole audience on purpose: it\n * is a keyed lookup per address, so its cost is the size of what is being\n * mailed, and asking about people this send will not reach would buy a\n * larger read for a number nobody acts on.\n */\n const notSuppressed = await filterSendableForHost(\n hostId,\n recipients,\n firestore,\n )\n /*\n * The THIRD list, and the narrowest: who has left THIS stream.\n *\n * After the two suppression lists rather than before them, because it is the\n * weaker fact and the weaker fact should never be the one that decides. A\n * person who unticked \"Promotions and offers\" is still a subscriber; a\n * person on either suppression list is not, and asking about their topic\n * preferences would be a read taken on a question already answered.\n */\n const onTopic = await filterTopicSendable(\n hostId,\n topicId,\n notSuppressed,\n firestore,\n )\n /*\n * THE FOURTH FILTER: how often the recipient asked to hear from this site.\n *\n * The one pace control a campaign is bound by, and the reason it is bound\n * is the reason the other two do not bind it. A ceiling and a sunset are\n * conclusions the platform draws about a person; this is a request the\n * person made, on a page this product built for them to make it. A\n * campaign that overrode it would make the preference center a form that\n * records a choice nobody honors — the same failure as ignoring an\n * unsubscribe, one notch quieter.\n *\n * HERE rather than at the per-message gate, and that placement is the whole\n * point. `sendEmail`'s marketing gate is not on this path: a campaign\n * carries no `marketing` context, because it mints its own unsubscribe URL\n * upstream and needs it as a merge value long before the message exists.\n * Enforcing a pace one message at a time would also refuse people AFTER the\n * merchant read a number that counted them, which is the thing every filter\n * on this path exists not to do. Subtracted where the other three are\n * subtracted, the count on screen is true before Send is pressed.\n *\n * Fails OPEN on an unreadable counter — see `filterCadenceSendable`. A\n * pace is not a stop, and the two suppression lists above have already\n * removed everybody who asked us to stop entirely.\n */\n const sendable = await filterCadenceSendable(hostId, onTopic, { firestore })\n /*\n * NOBODY IN THIS BATCH, BUT SOMEBODY AFTER IT.\n *\n * A send whose whole audience is suppressed is a 400 a merchant needs to\n * see. A BATCH whose five hundred are all suppressed is not — there are\n * two and a half thousand people behind them, and refusing here would end\n * the campaign at the first block of bad addresses in the list.\n *\n * The batch falls through instead: it addresses nobody, records the\n * addresses it refused so the next batch does not spend its slots on them\n * again, and the plan at the bottom schedules the remainder.\n */\n if (!sendable.length && consentSplit.mailable.length <= recipients.length) {\n if (options.continuation) return finishContinuation()\n /*\n * Which of the four filters emptied the batch, because the answer changes\n * what the merchant should do. An audience that unsubscribed is one they\n * have to rebuild; an audience holding for its own cadence is one that\n * becomes mailable on its own, and telling them it \"unsubscribed\" would\n * send them looking for a problem that is not there.\n */\n throw new CampaignSendError(\n onTopic.length\n ? 'Every recipient has asked this site for mail less often than this'\n : 'Every recipient has unsubscribed or been suppressed',\n 400,\n )\n }\n\n // Monthly cap by the owning org's plan (dark-launch rule, AGL-238).\n //\n // A campaign is the ONLY send a quota may refuse (AGL-1438). It is\n // discretionary — the customer sees a clear message, and upgrades or waits —\n // where refusing a receipt or a password reset would convert a billing event\n // into an outage on their business. So this is measured against\n // `campaignEmailSends` and NOT against `emailSends`, which since AGL-1438\n // also carries every order confirmation, booking reminder and workflow\n // notification the site sent. Enforcing the campaign cap against that total\n // would refuse a campaign because the store had a busy week of orders.\n //\n // SINCE AGL-2267 THE COUNTER IS PER ORG AND THE CLAIM IS ATOMIC. The cap was\n // enforced against `hosts/{hostId}/counters/campaignEmailSends` — per SITE —\n // while `emailSendsPerMonth` is an ORG entitlement, so an org with N sites\n // got N × the cap it bought. And it was read here and incremented after\n // delivery, so two concurrent campaigns both passed the same reading. See\n // `email-metering.ts` for the counter, the transition, and why the existing\n // per-site counters were NOT folded in.\n const monthKey = new Date().toISOString().slice(0, 7)\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs. The org\n // document is read ONCE, at the consent join above, and reused here and for\n // branding below; both used to re-fetch it.\n // The limit itself, read through the one shared resolver. `checkQuota` with\n // a usage of 0 is how a plain limit is read; the ALLOW/REFUSE decision is\n // not made here — it is made by the atomic reservation below.\n const campaignSendLimit = checkQuota(\n orgForHost?.org as any,\n 'emailSendsPerMonth',\n 0,\n ).limit\n const overCapError = () =>\n new CampaignSendError(\n `Monthly campaign email limit reached (${campaignSendLimit}) — upgrade ` +\n 'in Billing or shrink the audience. Transactional mail — receipts, ' +\n 'booking reminders, password resets — keeps sending.',\n 403,\n )\n {\n // A cheap read-only pre-check, so an over-cap campaign is refused before\n // the template load, the experiment read and the campaign id — and so the\n // DRY RUN has an answer without writing anything (AGL-2178: \"nothing has\n // been written above this line\"). It is not the enforcement; it cannot be,\n // because a read is not a claim.\n const used = await orgCampaignEmailSendsForMonth(orgId, monthKey)\n if (used + sendable.length > campaignSendLimit) throw overCapError()\n }\n\n /*\n * Recipient PREVIEW (AGL-2178). The campaign composer mockup shows\n * `Recipients 1,240` beside the audience picker, and the console had no\n * count before a send at all — the number appeared afterwards, in a\n * snackbar.\n *\n * It returns from HERE rather than from a counting function of its own,\n * and that is the whole point: the figure has already been through\n * audience resolution, normalisation, de-duplication, the\n * `MAX_RECIPIENTS_PER_SEND` cap, both suppression lists and the monthly\n * quota. A second implementation would be a second set of rules to\n * drift, and the one number a merchant checks before pressing Send is\n * the worst possible place for an estimate that disagrees with what\n * happens.\n *\n * `audience` rides along so the composer can show the SHORTFALL rather\n * than only the send size. The preview is the surface a merchant reads\n * before deciding, so it is the surface on which \"your audience is 3,000\n * and this send reaches 500\" has to appear.\n *\n * Nothing has been written above this line — every step so far is a\n * read — so an early return here leaves no campaign document, no\n * counter and no id behind.\n */\n if (options.dryRun) {\n return {\n campaignId: '',\n recipients: recipients.length,\n audienceSize: resolved.length,\n ...(audienceTruncated ? { audienceTruncated: true } : {}),\n ...(options.followUp ? { followUp: true, alreadyReached } : {}),\n sendable: sendable.length,\n /*\n * Measured to `onTopic` rather than to `sendable`, so the cadence\n * filter below does not silently enlarge a number labelled\n * \"unsubscribed or suppressed\". Somebody who asked for monthly mail did\n * not unsubscribe, and reporting them under a heading that says they\n * did is the netting this readout has refused everywhere else.\n */\n suppressed: recipients.length - onTopic.length,\n cadenceHeld: onTopic.length - sendable.length,\n /*\n * What this send will NOT reach on its first pass, so the composer can\n * say \"3,000 people, 500 in the first batch, the rest over the next few\n * runs\" instead of showing 500 beside an audience of 3,000 and leaving\n * a merchant to guess which number is the promise.\n */\n remaining: Math.max(0, consentSplit.mailable.length - recipients.length),\n /*\n * The consent split, measured over the WHOLE audience and named rather\n * than netted (§3f).\n *\n * Over the whole audience, not over the capped 500, because it rides\n * the same readout as `audienceSize` and answers a question about the\n * audience: of the 3,200 people this list holds, how many asked for\n * this mail? Reporting it over the capped set would make the figures\n * move whenever the cap bit, for reasons that have nothing to do with\n * consent.\n *\n * Three numbers because one would hide the thing a merchant has to\n * decide about. `consented` is who has a basis; `grandfathered` is who\n * is reachable only because enforcement is not retroactive, and is\n * therefore exactly the population that disappears the day the org\n * turns the strict mode on; `consentWithheld` is who the rule already\n * refuses.\n *\n * `consentedByOperator` splits the first of those, because \"who has a\n * basis\" and \"who asked\" stopped being the same question once a basis\n * could be asserted on somebody's behalf. Reporting only the total\n * would present an operator backfill as that many opt-ins, which is\n * the one thing the provenance field exists to prevent.\n */\n consented: consentSplit.consented,\n consentedByOperator: consentSplit.consentedByOperator,\n grandfathered: consentSplit.grandfathered,\n consentWithheld: consentSplit.withheld,\n // Which identity this campaign would leave on, so the composer can say\n // so rather than leaving a merchant to assume.\n identity: sendingIdentity.summary,\n identitySource: sendingIdentity.source,\n sent: 0,\n dryRun: true,\n }\n }\n\n // `hostPublicOrigin`, not a hand-rolled apex (AGL-2195). Campaign links are\n // mailed out and clicked days later; a wrong apex sends the operator's whole\n // audience to a domain the operator does not control.\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n\n // White-label sender identity (White-Label Phase 3): a campaign sent from a\n // white-label store reads as that store's brand. Resolved once for the whole\n // batch from the owning org doc through the one shared resolver.\n const branding = resolveBrandingProfile(orgForHost?.org as never)\n\n const campaignId = options.campaignId || createResourceUid()\n /*\n * Whether this send MINTS the record or writes onto one that already\n * exists.\n *\n * A caller naming a `campaignId` is addressing a record somebody else\n * created — a draft being sent now, a sent email taking a follow-up — and\n * that record already carries its own creation stamp. Re-stamping it would\n * move an email's creation date forward every time it was sent again, and\n * the emails list orders drafts on exactly that field.\n */\n const mintsRecord = !options.campaignId\n\n // Designed email template (AGL-349): loaded once; rendered per\n // recipient with their merge values.\n const template = options.templateScreenId\n ? await loadEmailTemplate(hostId, options.templateScreenId)\n : null\n /**\n * The message's SOURCE, settled once for the whole send.\n *\n * A designed email supplies both parts an inbox receives from the same\n * nodes, so `options.body` has nowhere to go on this branch and the union\n * gives it nowhere to be put. Deciding it here rather than inside the\n * per-recipient loop is also what stops the two ever disagreeing across a\n * batch.\n */\n const designedContent = template\n ? ({\n mode: 'design',\n template,\n ...(options.plainText ? { plainText: options.plainText } : {}),\n } as const)\n : null\n\n // Email A/B (AGL-255): each recipient deterministically lands in a\n // variant whose subject/body overrides apply; sends count as that\n // variant's exposures. A finished experiment sends the winner copy.\n const experimentId = String(options.experimentId ?? '')\n let experiment: (HostExperiment & { $id: string }) | null = null\n if (experimentId) {\n const experimentSnapshot = await hostRef\n .collection('experiments')\n .doc(experimentId)\n .get()\n const data = experimentSnapshot.data() as HostExperiment | undefined\n if (\n !experimentSnapshot.exists ||\n !data ||\n data.target !== 'email' ||\n (data.status !== 'running' && !data.winnerVariantId)\n ) {\n throw new CampaignSendError('Pick a running email experiment', 400)\n }\n experiment = { $id: experimentSnapshot.id, ...data }\n }\n /*\n * PLATFORM SEND-RATE ADMISSION CONTROL (AGL-2409).\n *\n * `sendEmail` governs every message individually and is the hard ceiling.\n * This is the admission check in front of it, and it exists for one reason:\n * without it, a campaign that does not fit in the current hour would deliver\n * to the first N addresses and stop, and a scheduled campaign in that state\n * cannot be retried without double-sending the N that already went.\n *\n * Asking for room for the WHOLE batch up front turns the ordinary case into\n * \"all of it, or none of it and try again next run\". A read, not a claim —\n * two campaigns can still both pass this and then contend at the per-message\n * governor, which is why the loop below also handles a mid-batch refusal\n * rather than assuming this settled it.\n */\n {\n // The configuration was read above, where it sized this batch against the\n // new-sender ramp. One read, two controls: a second one here could answer\n // differently inside one send, which would let a campaign be sized\n // against one ceiling and admitted against another.\n const config = platformRate\n const window = await readEmailSendRateWindow()\n if (config.enabled && window.used + sendable.length > config.perHour) {\n throw new CampaignSendDeferredError(\n `The platform is sending at its hourly limit (${config.perHour}/hour). ` +\n 'This campaign has not been sent and nothing has been counted — it ' +\n 'will go out automatically on the next run, or you can send it again ' +\n 'after the hour rolls.',\n window.resetMs,\n )\n }\n\n /*\n * THIS WORKSPACE'S SHARE OF THAT HOUR.\n *\n * The check above bounds the platform; it does not bound how much of the\n * platform one tenant may take. Without this, an org with a large audience\n * occupies the whole hour and every other customer's campaigns are refused\n * by a ceiling they did nothing to reach.\n *\n * The ceiling is DERIVED from the live platform ceiling\n * (`orgHourlyCampaignCeiling`), so a staff ramp moves both together and\n * the two can never contradict each other. See `send-ceilings.ts` for the\n * arithmetic tying this to the per-send cap and the plan allowance.\n *\n * A deferral, not a refusal: the campaign stays a draft, the audience is\n * untouched, no list membership changes and no suppression or delivery\n * record is affected. A send is a flow rather than a holding, which is the\n * one place the enforce-at-the-reduction rule does not reach — refusing a\n * flow strands nobody's data.\n *\n * Taken BEFORE the monthly claim so that a workspace deferred for the hour\n * has not spent a month's allowance on a campaign that did not go.\n */\n const hourly = await claimOrgEmailSendBudget({\n orgId,\n count: sendable.length,\n platformPerHour: config.perHour,\n enabled: config.enabled,\n })\n if (!hourly.allowed) {\n throw new CampaignSendDeferredError(\n `This workspace may send ${hourly.ceiling.toLocaleString()} campaign ` +\n `emails an hour and has sent ${hourly.used.toLocaleString()} this ` +\n `hour, so there is room for ${hourly.remaining.toLocaleString()} ` +\n `and this campaign needs ${sendable.length.toLocaleString()}. ` +\n 'Nothing has been sent and nothing has been counted — the campaign ' +\n 'is unchanged and will go out automatically on the next run, or you ' +\n 'can send it again after the hour rolls. Transactional mail — ' +\n 'receipts, booking reminders, password resets — keeps sending.',\n hourly.retryAtMs,\n )\n }\n }\n\n /*\n * THE DAY'S RAMP, CLAIMED.\n *\n * Between the hourly claim and the monthly reservation, and the ordering is\n * the same argument both of its neighbours make. AFTER the hourly one, so a\n * workspace deferred for the hour has not spent a day's budget. BEFORE the\n * monthly one, because an unreconciled claim costs whatever its window is\n * and a day is cheaper to leak than a month.\n *\n * A graduated workspace claims nothing and pays no read; see\n * `claimOrgEmailSendDay`.\n */\n const dayClaim = await claimOrgEmailSendDay({\n orgId,\n count: sendable.length,\n ramp:\n ramp ??\n resolveOrgEmailRamp({\n ageDays: null,\n deliveredLifetime: 0,\n platformPerHour: platformRate.perHour,\n }),\n enabled: platformRate.enabled,\n })\n if (!dayClaim.allowed) {\n throw new CampaignSendDeferredError(\n `This workspace may send ${dayClaim.ceiling.toLocaleString()} campaign ` +\n `emails a day while it establishes a sending history, and has sent ` +\n `${dayClaim.used.toLocaleString()} today. Nothing has been sent and ` +\n 'nothing has been counted — the rest goes out automatically ' +\n 'tomorrow. Transactional mail — receipts, booking reminders, ' +\n 'password resets — keeps sending.',\n dayClaim.retryAtMs,\n )\n }\n\n /*\n * THE MONTHLY CLAIM (AGL-2267), taken here and not at the pre-check above.\n *\n * As late as possible on purpose: everything between the pre-check and this\n * line can throw (an unknown template, a stopped experiment), and a claim\n * taken before them would leak the org's allowance for the rest of the month\n * on a campaign that never existed. From here to the `finally` below there\n * is nothing that can throw before the reconcile runs.\n */\n const claim = await reserveCampaignEmailSends({\n orgId,\n month: monthKey,\n count: sendable.length,\n limit: campaignSendLimit,\n })\n if (!claim.ok) {\n // The day's claim was taken a few lines above and this campaign is not\n // going out, so it is given back before the throw. The `finally` below\n // has not been entered yet, which is exactly why this cannot be left to\n // it.\n await reconcileOrgEmailSendDay(dayClaim.reservation, 0)\n throw overCapError()\n }\n const reservation: CampaignSendReservation = claim.reservation\n const dayReservation: OrgEmailSendDayReservation | null = dayClaim.reservation\n\n const variantSends: Record<string, number> = {}\n /**\n * Who this campaign actually reached, for the marketing frequency window.\n *\n * A campaign is exempt from the frequency REFUSAL — it is a merchant's\n * reviewed, one-shot act with a recipient count on screen before they press\n * Send, and a cap that silently removed people from it would make that\n * number a lie — but it is most of the mail a person receives from a site,\n * so a ceiling that did not count it would describe nothing. Collected here\n * and written once below rather than per recipient, because this loop is\n * already one awaited HTTP POST per person.\n *\n * ## The ENGAGEMENT SUNSET is exempt too, and for a stronger reason\n *\n * `marketingSunsetVerdict` refuses a site that has been mailing somebody\n * past the sunset window with nothing in it to say they are still\n * listening. Nothing on this path consults it, deliberately, and the\n * reasoning is the frequency argument plus two things that only apply here.\n *\n * The obvious objection is that a ceiling guards a merchant's volume while\n * a sunset guards a shared sending domain's reputation, so the platform's\n * interest should outrank a merchant's reviewed act. That distinction does\n * not survive contact with the two controls: `marketingFrequencyCap` is\n * itself \"a deliverability control on a shared sending domain\" that \"cannot\n * be something one plan buys its way past\". Both serve the platform. The\n * exemption was never about whose interest a control serves — it is about\n * whether an invisible subtraction may change a number a person already\n * read and approved.\n *\n * What separates the two is what a refusal is made of, and it separates\n * them in the direction of exempting the sunset MORE readily:\n *\n * 1. **A sunset refuses on an inference, not on a stated fact.**\n * Suppression carries \"stop\", the cadence filter above carries \"less\n * often\" — both said by the recipient. A ceiling counts messages that\n * demonstrably arrived. A sunset concludes from silence that somebody\n * has gone, which is the one refusal on this path that can be wrong\n * about a person who is still reading.\n * 2. **Its refusal is terminal where a ceiling's is retryable.** An\n * automated sweep defers a capped message and sends it later; a\n * sunsetted address is refused every time until they engage with mail\n * they are no longer being sent. Applied to campaigns that is the\n * largest silent subtraction in the system, made on the weakest\n * evidence.\n *\n * And it would break the one campaign written for exactly these people. A\n * win-back is a reviewed marketing act whose audience is, by definition,\n * everybody a sunset would refuse — so binding campaigns to it would make\n * the message that exists to end a disengagement the message that cannot be\n * sent. No automated path has that case: nobody writes a cart reminder\n * aimed at people who stopped reading.\n *\n * The reputation this leaves unguarded on the campaign path is guarded by\n * the four filters that DO run before this loop — both suppression lists,\n * the topic opt-outs and the recipient's own cadence — plus the sender\n * ramp and the hourly governor. The sunset governs the automated paths,\n * which fire with no human present. `MarketingSendContext.capped` carries\n * the same decision for the paths that reach the gate.\n */\n const reached: string[] = []\n /**\n * WHO THIS BATCH CONSIDERED AND WILL NOT MAIL, so the next one does not\n * consider them again.\n *\n * Two populations, and the reason they belong together is what the next\n * batch does with them. The suppression and topic filters above removed\n * people from `sendable`; the loop below removes any address the provider\n * would not take. Both stay at the head of a stable order, so a batch that\n * did not record them would re-select them, spend a slot on each, and — at\n * enough of them — address five hundred people it cannot mail and make no\n * progress at all.\n *\n * A message that failed for a transient reason is settled out with the\n * rest. That is not a new loss: before batching, a failed recipient was\n * never retried either, because there was no second pass. What it buys is\n * that one unreachable address cannot stall the two thousand behind it.\n *\n * The one failure that is NOT settled here is a refusal about the rate —\n * the hourly governor's, or the provider's 429. Those are answers about\n * when, not about whom, and settling a recipient on one would drop somebody\n * the provider never even looked at while the campaign closed as complete.\n * They break the loop into `deferred` instead; see the branch below.\n */\n const sendableSet = new Set(sendable)\n const settledOut: string[] = recipients.filter(\n (email) => !sendableSet.has(email),\n )\n let sent = 0\n /** Recipients a rate refusal left untouched mid-batch, if any. */\n let deferred = 0\n /**\n * THE PACE INSIDE ONE BATCH.\n *\n * The hourly governor above decides HOW MANY messages this workspace may\n * put on the domain in an hour; it says nothing about how closely together\n * they arrive, and the provider only counts the second. A batch of five\n * hundred is the one place in this codebase that issues provider requests\n * in a tight loop, so it is the one place that has to hold the request\n * rate — everything else here sends one message and returns.\n *\n * Spreading rather than concurrency: a sequential `await` already caps this\n * loop at one request in flight, and the defect is that its rate is\n * whatever the round trip happens to be. See\n * {@link createProviderRequestPacer} for why waiting out the REMAINDER of\n * the interval costs a real send nothing.\n */\n const paceProviderRequest = createProviderRequestPacer()\n try {\n for (let index = 0; index < sendable.length; index += 1) {\n const email = sendable[index]\n // `cid` is what lets an unsubscribe be attributed to the campaign that\n // caused it. Without it the suppression list records that somebody left\n // and nothing about which mailing they left over, which is the one\n // question an unsubscribe rate exists to answer.\n const link = {\n siteBase,\n hostId,\n email,\n campaignId,\n topicId,\n secret: unsubscribeSecret,\n }\n /*\n * TWO URLS OVER ONE SIGNATURE, and which one goes where is the whole\n * RFC 8058 story.\n *\n * `oneClickUrl` is what the `List-Unsubscribe` header names. A mailbox\n * provider POSTs it with no human present and expects the act to have\n * happened when it reads the 200 — so it points at the route whose POST\n * writes immediately, and it must never point at a page of checkboxes\n * that has to be submitted by somebody.\n *\n * `unsubscribeUrl` is the link a PERSON clicks in the footer, and it\n * points at the preference center, where the topic this message\n * belonged to is one of the things they can stop instead of all of it.\n * The merge token keeps its name because designed templates in the wild\n * reference `{{unsubscribeUrl}}`, and because the page it opens is still\n * where you go to unsubscribe — with \"Unsubscribe from everything\" on\n * it, one button away.\n */\n const oneClickUrl = buildUnsubscribeUrl({ ...link, surface: 'one-click' })\n const unsubscribeUrl = buildUnsubscribeUrl({\n ...link,\n surface: 'preferences',\n })\n // Variant assignment keys on the recipient address (AGL-255) so a\n // re-send reaches the same variant.\n const variant = experiment\n ? assignExperimentVariant(experiment, experiment.$id, email)\n : null\n /*\n * THIS RECIPIENT'S MESSAGE, through the renderer the composer previews\n * with (`@aglyn/aglyn/app-utils/recipient-email-render`).\n *\n * Merge tags resolve after the variant override so variant copy can use\n * tags too, a designed template renders per recipient, and a plain-text\n * body gets the HTML part `sendEmail` would otherwise synthesize for it.\n * Shared rather than inlined because a preview rendered by a second\n * implementation is a preview of something else — the two defects this\n * send path has already shipped, product blocks silently dropped and\n * merge tags resolving to empty strings for a whole audience, are both\n * invisible to a preview that does not run this exact code.\n */\n const message = renderRecipientEmail({\n subject: variant?.subject?.trim() || subject,\n preheader: options.preheader,\n /*\n * ONE SOURCE, chosen once for the whole send.\n *\n * `designedContent` is resolved above the loop because the mode is a\n * property of the EMAIL, not of the recipient. A designed message\n * carries no body at all — the union has no field for one — so the\n * variant's body override is only reachable on the text branch, which\n * is the only branch where a body is the message.\n */\n content: designedContent ?? {\n mode: 'text',\n body: variant?.body?.trim() || body,\n },\n /*\n * The persona where a proof named one, and the actual recipient\n * everywhere else.\n *\n * Only the RENDER moves. `email` above still decides who the message\n * is delivered to, which unsubscribe link is signed, and which\n * address a suppression would be recorded against — so a proof shows\n * a real contact's merge values without putting that contact's\n * opt-out link in somebody else's inbox.\n */\n recipient: options.proofPersona ?? { email, name: names.get(email) },\n siteBase,\n hostId,\n unsubscribeUrl,\n })\n await paceProviderRequest()\n const result = await sendEmail({\n to: email,\n subject: message.subject,\n ...(message.html ? { html: message.html } : {}),\n // The plain-text footer names what the link actually opens. \"Choose\n // which emails you get\" in front of \"or unsubscribe\" is the only place\n // a text-only reader learns that leaving one stream is an option at\n // all, and the word \"unsubscribe\" stays in the line because that is\n // what a recipient scans the footer for. It is written by\n // `renderRecipientEmail`, so the composer's preview shows the footer\n // that is actually mailed.\n text: message.text,\n // RFC 8058 one-click (AGL-2408). `List-Unsubscribe` alone does NOT\n // satisfy Gmail's and Yahoo's bulk-sender rules — the pair does, and\n // Gmail is where most of a merchant's list lives. A client honoring\n // the pair POSTs `List-Unsubscribe=One-Click` to the URL, which is\n // why the handler had to accept POST first: advertising one-click\n // against a GET-only handler would promise a verb nothing served.\n //\n // `oneClickUrl`, NOT the preference center. Topics narrow what a\n // person can choose on a page; they change nothing about what a\n // machine POSTing this header is promised, which is that the\n // recipient stops hearing from this site.\n headers: {\n 'List-Unsubscribe': `<${oneClickUrl}>`,\n 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',\n },\n // The campaign's own display name where the composer set one, and the\n // org's branding default otherwise. Either way the ADDRESS is the\n // resolved identity's — `applyFromName` replaces the display name in\n // front of it and nothing else.\n fromName: options.fromName || branding.fromName,\n ...(options.replyTo ? { replyTo: options.replyTo } : {}),\n // The server's answer to which verified address this leaves on. The\n // send path re-checks it, so a refusal holds even here where the\n // route has already passed one.\n sendingIdentity,\n // A campaign is a site talking to its own audience, so the shared\n // platform address is not reachable from it even if the resolution\n // above somehow yielded nothing.\n audience: 'tenant',\n // Event attribution (AGL-268): the opens/clicks webhook maps\n // deliveries back to the campaign (and experiment) via tags.\n tags: [\n { name: 'hostId', value: hostId },\n { name: 'campaignId', value: campaignId },\n ...(experiment\n ? [{ name: 'experimentId', value: experiment.$id }]\n : []),\n ],\n context: 'campaign',\n })\n if (result.sent) {\n sent += 1\n reached.push(email)\n if (variant) {\n variantSends[variant.id] = (variantSends[variant.id] ?? 0) + 1\n }\n continue\n }\n /*\n * A REFUSAL ABOUT THE RATE STOPS THE BATCH, whichever control produced\n * it.\n *\n * The hourly governor refuses because a counter that only goes up has\n * reached its ceiling, so every message after this one in the window\n * gets the same answer. The provider refuses with a 429 because\n * requests arrived too close together, and the next one is closer\n * still. Neither says anything about the recipient in hand.\n *\n * Not a throw: some of this batch has already been delivered, and a throw\n * here would lose the delivered count, skip the meters and (on the\n * scheduled path) re-queue a campaign that would double-send. The\n * remainder is reported instead, the reservation is reconciled to what\n * actually went, and the merchant sees a number that is short.\n *\n * Any OTHER failure — a rejection, a network error — is per-recipient and\n * the loop continues.\n */\n if (rateLimitedRetryAtMs(result) !== null) {\n /*\n * Everything from HERE ON is untouched and retryable, which is what\n * makes it the remainder rather than a loss: the campaign schedules\n * itself for the next window and addresses these people then.\n * Counted from the index rather than from `sent`, so a rejection\n * earlier in the batch is not counted twice — once as settled and\n * again as deferred.\n */\n deferred = sendable.length - index\n break\n }\n /*\n * The provider would not take this address. Settled rather than left\n * for the next batch — see `settledOut` above for why an address that\n * keeps failing must not keep consuming a slot.\n */\n settledOut.push(email)\n }\n } finally {\n /*\n * Give back what did not go out (AGL-2267).\n *\n * In a `finally` so it runs on the throw paths too. The claim was taken\n * for the whole batch — that is what makes it a cap — and a campaign that\n * delivered 300 of 500 must not spend 500 of the org's allowance.\n * `reconcileCampaignSendReservation` never throws.\n */\n await reconcileCampaignSendReservation(reservation, sent)\n /*\n * And the day's, for the same reason one line up. The hourly claim is\n * deliberately NOT reconciled — its window is an hour — but a day is long\n * enough that a failed batch would cost a new workspace the rest of it.\n */\n await reconcileOrgEmailSendDay(dayReservation, sent)\n }\n // Both meters, from one call, on the DELIVERED count (AGL-1438). Ahead of\n // the `recordCampaign` early return below, because a test send is a real\n // email with a real cost even though it writes no campaign record — and\n // ahead of nothing else that writes `emailSends`, so a campaign reaches the\n // cost meter exactly once. This sender used to increment that counter\n // itself, which is how a counter named for all email came to hold campaign\n // sends alone.\n // The frequency window, for the messages that left. After the reservation\n // reconcile and never in front of it: this is a deliverability counter and\n // the reconcile is a merchant's allowance, so the allowance is settled\n // first. `recordMarketingSends` never throws.\n await recordMarketingSends(hostId, reached)\n await meterHostEmail(hostId, sent, 'campaign')\n /*\n * The DENOMINATOR every per-tenant rate divides by.\n *\n * Recorded on the DELIVERED count and not on what was attempted, so a\n * bounce rate is bounces over messages that actually left. It is also the\n * volume the new-sender ramp reads to decide which step a workspace has\n * earned, which is why it is written from the send rather than inferred\n * from the delivery webhook: a ramp that only moved when a provider\n * reported back would stall a new tenant on the day the webhook was slow.\n *\n * Never throws, like both meters above it.\n */\n await recordCampaignAccepted(orgId, sent)\n\n /*\n * WHO THIS EMAIL HAS NOW REACHED, so a later send can subtract them.\n *\n * Written for every real send and not only for follow-ups, because the\n * record has to exist BEFORE anybody asks for one — an email whose first\n * send kept no account of itself can never be sent to more people, which is\n * exactly what `campaignReachCovers` refuses above.\n *\n * A test send is excluded with the campaign record it also skips: it\n * delivers to the requester's own address under an id nothing will ever\n * follow up, and counting it would file a real person's address against a\n * mailing that does not exist.\n *\n * Ahead of the campaign write below rather than after it, so a failure\n * between the two leaves an email whose reach record is AHEAD of its\n * recorded `sent`. `campaignReachCovers` reads that as covered, which is\n * the safe direction: a follow-up subtracts more people than it strictly\n * has to. The other order would leave the record short and refuse the\n * follow-up, which is also safe but loses the feature over a transient.\n */\n if (options.recordCampaign !== false) {\n await recordCampaignReach(hostId, campaignId, reached, firestore)\n /*\n * And who it decided NOT to mail, under a field of its own.\n *\n * Written for every real send rather than only for a batched one, for the\n * reason the reach record itself is: the record has to exist before\n * anybody asks for one, and the batch that asks is the NEXT invocation of\n * this function, which will have no way to know that these five hundred\n * addresses were already considered.\n */\n await recordCampaignSkipped(hostId, campaignId, settledOut, firestore)\n }\n\n // Sends are the email variant's exposures (AGL-255).\n if (experiment && experiment.status === 'running') {\n for (const [variantId, count] of Object.entries(variantSends)) {\n // AGL-1771: `variant.id` is MERCHANT-AUTHORED — `validateExperiment`\n // checks the ids are unique and nothing about their shape — and it is a\n // path component here. The same third instance `d51e23df4` found on the\n // conversion write in `email-events.ts`, on the exposure write that\n // pairs with it. This one stays a merge-set and stays a create: the\n // first send for a variant has no stats document, the experiment was\n // just read, and the emails really went out.\n if (!isDocumentId(variantId)) continue\n await hostRef\n .collection('experiments')\n .doc(experiment.$id)\n .collection('stats')\n .doc(variantId)\n .set(\n {\n exposures: firebaseAdmin.firestore.FieldValue.increment(count),\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n\n /*==========================================\n * WHAT HAPPENS TO THE REST OF THE AUDIENCE.\n *\n * The cap took the first N of the people this send may mail. Everyone past\n * it, plus anybody an hourly cut left untouched, is the REMAINDER — and\n * until now the remainder was simply not mailed, and a merchant with three\n * thousand contacts pressed Send six times to reach them.\n *\n * The plan is pure and lives in `send-ceilings.ts` with the numbers it\n * reasons about. It answers two things: how many are left, and whether\n * another batch runs. The second is the one that matters, because a job\n * that reschedules itself has exactly one interesting failure — doing it\n * forever — and there are three ways this one stops: nothing left, the\n * batch guard, and a batch that settled nobody.\n *=========================================*/\n const plan = campaignBatchPlan({\n mailable: consentSplit.mailable.length,\n addressed: recipients.length,\n retryable: deferred,\n settled: sent + settledOut.length,\n batchesSoFar,\n })\n /**\n * When the next batch may go.\n *\n * Now, in the ordinary case: the scheduled-campaign processor claims\n * anything `scheduled` and due, and its next run is what continues this\n * email. A window the send was paced by moves it out — but a send paced by\n * the hour or the day THREW rather than reaching here, so the only reason\n * this is not immediate is a batch that ran to the end of its own cap.\n */\n const nextAtMs = plan.resuming ? Date.now() : 0\n\n if (options.recordCampaign === false) {\n return {\n campaignId,\n recipients: sendable.length,\n audienceSize: resolved.length,\n ...(audienceTruncated ? { audienceTruncated: true } : {}),\n sent,\n ...(deferred ? { deferred } : {}),\n }\n }\n /*==========================================\n * ADDING TO A COUNTER, AS AGAINST REPLACING IT.\n *\n * `set(..., {merge: true})` merges a nested map FIELD BY FIELD, so a\n * follow-up writing a plain `stats.recipients` would overwrite the original\n * send's figure with its own — and every rate on the report divides by\n * `sent` or by `delivered`. `delivered`, `opens`, `bounced` and the rest are\n * incremented by the delivery webhook keyed on this same `campaignId`, so\n * they already cover BOTH sends. Replacing `sent` with the follow-up's\n * smaller number would leave a numerator counting two sends over a\n * denominator counting one, and a delivery rate of 300%.\n *\n * So every send-recorded counter goes through here: a plain number the\n * first time, an increment on a follow-up. The two populations a follow-up\n * measures are disjoint by construction — it addressed nobody the earlier\n * sends reached — so the sums are totals rather than double counts.\n *=========================================*/\n const additive = (value: number) =>\n addsToExistingSend\n ? (firebaseAdmin.firestore.FieldValue.increment(value) as never)\n : value\n /**\n * The figures a BATCH must not add to, because it did not measure a second\n * population — it measured a slice of the one the first batch already\n * counted.\n *\n * `audienceSize` is the arithmetic that makes this concrete. A first batch\n * over three thousand people records 3,000; a second batch sees the 2,500\n * that are left and, adding, would record 5,500 — an audience that does not\n * exist, under every rate on the report. The same holds for the consent\n * split, which is measured over the whole remaining audience rather than\n * over the capped slice.\n *\n * So a continuation omits them entirely and the first batch's figures\n * stand, which is the same \"recorded, not recomputed\" rule the populations\n * below are stored under: they are true of the send that happened.\n */\n const measuresTheAudience = !options.continuation\n await hostRef.collection('campaigns').doc(campaignId).set(\n {\n subject,\n body,\n audience,\n /*\n * The one date every message carries — see `email-record.ts`. Written\n * only where this send mints the document, because a record addressed\n * by id was created by whoever minted it and keeps that date.\n */\n ...(mintsRecord ? { createdAtMs: Date.now() } : {}),\n /*\n * WHICH audience, not only which KIND.\n *\n * `audience` is `'list'` or `'segment'` — a kind — and on its own it\n * cannot answer \"which lists has this design been sent to\", because\n * every list send looks identical to every other. The scheduled branch\n * of the handler has always recorded these; the immediate send dropped\n * them, so a campaign's own document could not say where it went.\n */\n ...(options.listId ? { listId: options.listId } : {}),\n ...(listName ? { listName } : {}),\n ...(options.segmentId ? { segmentId: options.segmentId } : {}),\n // The RESOLVED topic, not `options.topicId`. Recording the default\n // explicitly is what lets the campaign report and the preference page\n // agree about which stream this send belonged to, without either of them\n // re-deriving a default that could drift from the other's.\n topicId,\n ...(options.templateScreenId\n ? { templateScreenId: options.templateScreenId }\n : {}),\n /*\n * The plain-text part somebody wrote, recorded beside the design it\n * belongs to. `storedSendOptionsFrom` reads it back, so a follow-up\n * mails the same text half the first batch did rather than regenerating\n * one from a design that may have moved since.\n */\n ...(options.plainText ? { plainText: options.plainText } : {}),\n // What this send actually left as, recorded beside the copy: the report\n // is read months later, by which time the org's branding default may be\n // a different name than the one this campaign went out under.\n ...(options.fromName ? { fromName: options.fromName } : {}),\n ...(options.replyTo ? { replyTo: options.replyTo } : {}),\n /*\n * WHICH SENDER was chosen, beside the address it resolved to.\n *\n * Not a duplicate of `sentAs.from`: that records what left, and this\n * records what was picked. A follow-up re-sends under the same sender —\n * `storedSendOptionsFrom` reads this field — so an email that went out\n * as `jamie@` reaches the rest of its audience as `jamie@` even after\n * the site's default has moved to somebody else.\n */\n ...(senderId ? { senderId } : {}),\n /*\n * THE ADDRESS, beside the name and for the same reason.\n *\n * The two fields above are what the COMPOSER submitted; this is what\n * the recipient saw. They part company on a campaign that named no\n * display name, which goes out under the org's branding default — and\n * on every campaign once the site's sending identity moves, because a\n * domain verified in November does not change what left in March.\n */\n ...sentAsStamp({\n from: sendingIdentity.from,\n fromName: options.fromName || branding.fromName,\n replyTo: options.replyTo,\n }),\n ...(options.preheader ? { preheader: options.preheader } : {}),\n ...(options.displayName ? { displayName: options.displayName } : {}),\n ...(options.emailCampaignId\n ? { emailCampaignId: options.emailCampaignId }\n : {}),\n ...(experiment ? { experimentId: experiment.$id } : {}),\n stats: {\n recipients: additive(sendable.length),\n sent: additive(sent),\n /*\n * The audience this send was TAKEN FROM, beside what it reached.\n *\n * `audienceSize` above `recipients` is a campaign that did not go to\n * everybody, and the History row is where a merchant answers \"did\n * this reach my list\" months later. Recording only the reached figure\n * makes a truncated send indistinguishable from a complete one, which\n * is the same fault the deferred count below was added to close.\n *\n * `audienceSizeTruncated` marks the figure as a floor: the resolution\n * stopped at its read ceiling, so the audience is at least this and\n * the shortfall is at least the difference.\n */\n ...(measuresTheAudience\n ? {\n audienceSize: additive(resolved.length),\n ...(audienceTruncated ? { audienceSizeTruncated: true } : {}),\n }\n : {}),\n /*\n * WHAT THIS EMAIL ENDED UP NOT DELIVERING.\n *\n * Written only once the email has STOPPED, and as an absolute figure\n * rather than an increment, because a shortfall is a state and not a\n * sum. A batch that was cut short by the hourly ceiling has not\n * fallen short of anything — the people it did not reach are in\n * `resume.remaining` and the next run addresses them — and recording\n * a shortfall there would leave the report carrying \"held back by the\n * hourly limit\" about recipients who got the mail twenty minutes\n * later.\n */\n ...(!plan.resuming && plan.remaining > 0\n ? { deferred: plan.remaining }\n : {}),\n ...(Object.keys(variantSends).length\n ? {\n variantSends: Object.fromEntries(\n Object.entries(variantSends).map(([variantId, count]) => [\n variantId,\n additive(count),\n ]),\n ),\n }\n : {}),\n /*==========================================\n * THE POPULATIONS THIS SEND ALREADY MEASURED.\n *\n * Every one of these was computed above, returned from the DRY RUN\n * so the composer could show it before the send, and then discarded\n * the moment the send was real — so the campaign report could only\n * ever say how many were mailed, never how many were not and why.\n *\n * They are RECORDED here rather than recomputed at read time, and the\n * difference is not an optimisation. Consent records change, addresses\n * get suppressed, and a list grows: recomputing \"how many were\n * withheld\" next month answers a question about the list as it is\n * now, under a heading that says it describes a send that happened in\n * March. The recorded number is the only one that is true of the\n * campaign.\n *\n * Measured over two different wholes, which is why they are stored\n * separately rather than netted: the consent split runs over the whole\n * resolved audience, and `suppressed` over the capped recipient list,\n * because that is where each check actually runs.\n *=========================================*/\n ...(measuresTheAudience\n ? {\n consented: additive(consentSplit.consented),\n consentedByOperator: additive(consentSplit.consentedByOperator),\n grandfathered: additive(consentSplit.grandfathered),\n consentWithheld: additive(consentSplit.withheld),\n }\n : {}),\n suppressed: additive(recipients.length - onTopic.length),\n cadenceHeld: additive(onTopic.length - sendable.length),\n /*\n * That this send's links were trackable at all.\n *\n * Click tracking rewrites links in the HTML part, so a send that\n * carried none reports zero clicks whatever recipients did — a\n * structural zero that is indistinguishable on screen from a campaign\n * nobody clicked. `sendEmail` now synthesises an HTML part for a\n * text-only send, so every send from here on carries one; recording\n * the fact is what lets the report withhold a click RATE for the\n * campaigns that predate it instead of publishing a meaningless one.\n */\n clickTracked: true,\n },\n /*\n * `scheduled` WHILE AN EMAIL IS STILL GOING OUT, and it is a real\n * state rather than a convenience.\n *\n * The scheduled-campaign processor claims anything `scheduled` and due,\n * and that claim is the resume beat — including for a send a merchant\n * pressed by hand, which has no beat of its own. Reusing the state also\n * means the existing collection-group index serves it and that Cancel\n * already works: a merchant who decides mid-campaign that the copy is\n * wrong can stop the rest of it, which is a thing they could not do\n * before because there was no rest.\n *\n * ⚠️ IT READS AS \"NOT SENT YET\" UNLESS A SURFACE SAYS OTHERWISE. The\n * `resume` map below is what makes the row honest — \"reached 500 of\n * 3,000, sending\" rather than a scheduled email that has in fact\n * already delivered five hundred messages. `campaignSendProgress` in\n * the recipient-email renderer derives that sentence from these fields\n * and is the one place it is composed.\n */\n status: plan.resuming ? 'scheduled' : 'sent',\n ...(plan.resuming ? { sendAtMs: nextAtMs } : {}),\n /*\n * HOW FAR THIS EMAIL HAS GOT, written absolutely on every batch.\n *\n * Not part of `stats`, because `stats` is what the email DID and this\n * is where it currently is. A finished email keeps the record — batches\n * of 6, remaining 0 — so \"this went out over six runs\" is answerable\n * afterwards rather than only while it is happening.\n */\n resume: {\n remaining: plan.remaining,\n batch: plan.batch,\n nextAtMs,\n ...(plan.stop ? { stop: plan.stop } : {}),\n },\n /*\n * `sentAt` is WHEN THIS EMAIL WENT OUT, and a follow-up does not change\n * that. The emails list orders on it and the campaign rollup takes its\n * `lastSentAtMs` from it, so moving it would restate an email that went\n * out in March as one that went out today — the same rewriting-history\n * fault the recorded populations above exist to avoid.\n *\n * When the LAST send happened is a different fact and gets a field of\n * its own, beside a count of how many sends this email has had. Both\n * are what the detail page needs to say \"sent twice, most recently on\n * the 14th\" instead of presenting one date for two mailings.\n */\n ...(addsToExistingSend\n ? {\n lastSentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n lastSentBy: options.senderUid,\n }\n : {\n sentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n sentBy: options.senderUid,\n }),\n /*\n * How many times a PERSON sent this email, which is not how many\n * batches it took. A merchant who pressed Send once and watched it go\n * out over six runs sent it once; counting the batches would put \"sent\n * 6 times\" on the detail page of a campaign nobody re-sent.\n */\n ...(options.continuation ? {} : { sendCount: additive(1) }),\n },\n { merge: true },\n )\n return {\n campaignId,\n recipients: sendable.length,\n audienceSize: resolved.length,\n ...(audienceTruncated ? { audienceTruncated: true } : {}),\n ...(options.followUp ? { followUp: true, alreadyReached } : {}),\n sent,\n ...(deferred ? { deferred } : {}),\n remaining: plan.remaining,\n resuming: plan.resuming,\n batch: plan.batch,\n ...(plan.resuming ? { nextAtMs } : {}),\n }\n}\n\n/**\n * The stored configuration of a send, as the options that would mail it.\n *\n * Every field comes off the RECORD and none of it off the request — the\n * caller names a site and an email id and nothing else. That is not tidiness:\n * `campaignId` addresses an existing document, so a caller who could also\n * supply the body and the audience could put arbitrary copy on somebody\n * else's send id, keep its `cid` and its report, and mail it.\n *\n * Both callers rest on that. A follow-up mails the email that is already\n * there; `sendNow` mails a draft or a scheduled email ahead of its time, and\n * the copy it delivers has to be the copy that was composed and previewed\n * rather than whatever a request body happens to carry.\n *\n * Refuses a send carrying neither a template nor a body, which has no message\n * whichever caller asked.\n */\n/*==========================================\n Who a proof may reach, and whose data fills it\n==========================================*/\n\n/** One address a test send is allowed to be delivered to. */\nexport interface ProofRecipient {\n email: string\n /** A person's name where the membership record carries one. */\n label: string\n /** True for the caller's own account address. */\n self: boolean\n}\n\n/**\n * THE ADDRESSES A TEST SEND MAY BE DELIVERED TO.\n *\n * The caller's own account address, plus every account holder on the org that\n * owns this site. Exported because the composer has to OFFER this set — a\n * free-text box beside a rule enforced on the server is a box whose every\n * wrong answer is a refusal the person could not have predicted.\n *\n * ## Why membership, and not \"any address the merchant types\"\n *\n * A test send is exempt from the marketing-consent rule (see the proof\n * carve-out in `performCampaignSend`). An exemption that could be pointed at\n * any address would not be a test-send feature, it would be a way to mail\n * anybody without consent and call it a test. Membership is the narrowest\n * boundary that still answers what the button is for: proofing a draft to the\n * people who work on it.\n *\n * ## What is NOT here\n *\n * `siteMembers`, `leads` and contacts. They are the tenant's audience, and\n * the audience is precisely the population the consent rule protects. A\n * contact can be chosen as the PERSONA a proof renders as — see\n * {@link resolveProofPersona} — which reaches nobody.\n *\n * ## What this does not relax\n *\n * Everything else. The send still runs both suppression lists, so an address\n * that bounced or complained is refused however senior its owner; and a\n * stored `declined` on the address still refuses, because a refusal is the\n * one thing no policy may mail.\n */\nexport async function proofRecipientsForHost(options: {\n hostId: string\n callerEmail: string\n}): Promise<ProofRecipient[]> {\n const callerEmail = String(options?.callerEmail ?? '')\n .trim()\n .toLowerCase()\n const found = new Map<string, ProofRecipient>()\n if (callerEmail) {\n found.set(callerEmail, { email: callerEmail, label: 'You', self: true })\n }\n\n const orgForHost = await getOrgForHost(options?.hostId).catch(() => null)\n const orgId = String(orgForHost?.orgId ?? '')\n if (!orgId) return [...found.values()]\n\n const members = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n /*\n * A ceiling rather than the whole roster, because this feeds a picker and\n * a picker of two thousand names is not a picker. It is ordered by\n * document id so the page is stable across calls — a `limit` with no\n * order is a random sample, and a person who saw a colleague in the list\n * yesterday must not find them missing today.\n */\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(200)\n .get()\n .catch(() => null)\n\n for (const doc of members?.docs ?? []) {\n const email = String(doc.get('email') ?? '')\n .trim()\n .toLowerCase()\n if (!email || found.has(email)) continue\n found.set(email, {\n email,\n label: String(doc.get('displayName') ?? '').trim() || email,\n self: false,\n })\n }\n return [...found.values()]\n}\n\n/**\n * Whether one address may receive a proof of this site's mail.\n *\n * Asked as a membership question rather than by scanning\n * {@link proofRecipientsForHost}'s ceilinged page: the picker is allowed to\n * show the first two hundred colleagues, and the gate is not allowed to\n * refuse the two hundred and first.\n */\nasync function eligibleProofAddress(options: {\n hostId: string\n callerEmail: string\n address: string\n}): Promise<boolean> {\n const address = String(options?.address ?? '')\n .trim()\n .toLowerCase()\n if (!address) return false\n if (address === String(options?.callerEmail ?? '').trim().toLowerCase()) {\n return true\n }\n\n const orgForHost = await getOrgForHost(options?.hostId).catch(() => null)\n const orgId = String(orgForHost?.orgId ?? '')\n if (!orgId) return false\n\n const match = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .where('email', '==', address)\n .limit(1)\n .get()\n .catch(() => null)\n return Boolean(match && !match.empty)\n}\n\n/** One person a proof can be rendered as. Reaches nobody. */\nexport interface ProofPersona {\n email: string\n name: string\n /** Which audience the record came from, so the drawer can say. */\n source: 'lead' | 'member' | 'contact'\n}\n\n/** How many of each source the persona picker offers. */\nconst PROOF_PERSONA_SAMPLE = 20\n\n/**\n * A SAMPLE of the people this site's mail is addressed to, for the picker.\n *\n * A sample and not a search: the question the drawer asks is \"show me this\n * email as somebody real\", and twenty names from each source answers it for\n * the cost of three small reads. A contact picker with a query behind it is a\n * different feature, and it would put a text input in front of the org's\n * whole contact list on a surface whose job is to prove one email.\n *\n * Ordered by document id in every source, because a `limit` with no `orderBy`\n * is a random sample in doc-id order that a client `sort` then makes LOOK\n * newest-first. Naming the order keeps the picker stable between openings —\n * and on the org path it is also the order the automatic index for the scope\n * filter can actually serve.\n */\nexport async function proofPersonasForHost(\n hostId: string,\n): Promise<ProofPersona[]> {\n const byId = firebaseAdmin.firestore.FieldPath.documentId()\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const found = new Map<string, ProofPersona>()\n\n const collect = (\n docs: FirebaseFirestore.QueryDocumentSnapshot[] | undefined,\n source: ProofPersona['source'],\n nameFields: readonly string[],\n ) => {\n for (const doc of docs ?? []) {\n const email = String(doc.get('email') ?? '')\n .trim()\n .toLowerCase()\n if (!email || found.has(email)) continue\n const name = nameFields\n .map((field) => String(doc.get(field) ?? '').trim())\n .find(Boolean)\n found.set(email, { email, name: name ?? '', source })\n }\n }\n\n const [leads, members, contacts] = await Promise.all([\n hostRef\n .collection('leads')\n .orderBy(byId)\n .limit(PROOF_PERSONA_SAMPLE)\n .get()\n .catch(() => null),\n hostRef\n .collection('siteMembers')\n .orderBy(byId)\n .limit(PROOF_PERSONA_SAMPLE)\n .get()\n .catch(() => null),\n orgDataQueryForHost(hostId, 'contacts')\n .then(({ query }) => query.orderBy(byId).limit(PROOF_PERSONA_SAMPLE).get())\n .catch(() => null),\n ])\n\n collect(leads?.docs, 'lead', ['name'])\n // `displayName` first, and `name` only as a fallback: `siteMembers` has\n // never had a `name` field, so reading it alone renders every member\n // persona nameless — which is the exact defect that made merge tags resolve\n // to empty strings for whole audiences.\n collect(members?.docs, 'member', ['displayName', 'name'])\n collect(contacts?.docs, 'contact', ['name', 'firstName'])\n return [...found.values()]\n}\n\n/**\n * THE DOCUMENT ONE ADDRESS HAS ON THIS SITE, wherever it lives, or null.\n *\n * ONE lookup for both of the questions a proof asks about a person — what is\n * their name, and did they opt out — because the two must not disagree about\n * WHO the address is. A persona resolved from the contact record beside a\n * consent basis resolved from a lead record would be two different people\n * wearing one address.\n *\n * The three sources are the three an audience is built from, tried in the\n * order a small site grows them. Nothing here is taken from the request.\n *\n * The org `contacts` lookup goes through the org's address index narrowed\n * to this site (AGL-2633): the index is consulted for the address, the\n * contact it names is checked against `visibleTo` in memory, and only\n * then does the `email ==` query run. So a person whose two records were\n * merged is found under the address that became an alternate, and the\n * scope check is not skipped — it is applied to the one document an\n * address names, which is the same shape the segment branch above uses.\n */\nasync function findAudienceDocument(\n hostId: string,\n email: string,\n): Promise<{ data: Record<string, unknown>; nameFields: readonly string[] } | null> {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n for (const [collection, nameFields] of [\n ['leads', ['name']],\n // `displayName` first: `siteMembers` has never had a `name` field, and\n // reading only that is how merge tags came to render empty for a whole\n // audience.\n ['siteMembers', ['displayName', 'name']],\n ] as const) {\n const snapshot = await hostRef\n .collection(collection)\n .where('email', '==', email)\n .limit(1)\n .get()\n .catch(() => null)\n const doc = snapshot?.docs?.[0]\n if (doc) {\n return { data: (doc.data() ?? {}) as Record<string, unknown>, nameFields }\n }\n }\n\n const contacts = await orgDataCollectionForHost(hostId, 'contacts').catch(\n () => null,\n )\n const contact = contacts\n ? await findContactByEmail(contacts, email, { hostId }).catch(() => null)\n : null\n return contact\n ? {\n data: (contact.data() ?? {}) as Record<string, unknown>,\n nameFields: ['name', 'firstName'],\n }\n : null\n}\n\n/**\n * The stored marketing-consent fields for one address, or null when this site\n * holds no document for it at all.\n *\n * Null and an opted-out record are different answers and the caller treats\n * them differently: an address we have never seen keeps whatever basis the\n * policy assigns an unrecorded person, and one that said no is refused.\n */\nasync function readStoredConsent(\n hostId: string,\n email: string,\n): Promise<Record<string, unknown> | null> {\n if (!email || !EMAIL_PATTERN.test(email)) return null\n const found = await findAudienceDocument(hostId, email).catch(() => null)\n return found?.data ?? null\n}\n\n/**\n * The person whose stored data a proof renders as, or null.\n *\n * Read from the audience documents rather than taken from the request, so a\n * proof demonstrates what the MERGE will do to real data.\n */\nasync function resolveProofPersona(\n hostId: string,\n rawEmail: string,\n): Promise<{ email: string; name?: string } | null> {\n const email = String(rawEmail ?? '')\n .trim()\n .toLowerCase()\n if (!email || !EMAIL_PATTERN.test(email)) return null\n\n const found = await findAudienceDocument(hostId, email).catch(() => null)\n const name = (found?.nameFields ?? [])\n .map((field) => String(found?.data?.[field] ?? '').trim())\n .find(Boolean)\n\n /*\n * An address that matches nobody still renders, as itself with no name.\n *\n * Refusing would be worse: the merchant asked to see what the email looks\n * like addressed to this person, and \"we could not find them\" is an answer\n * about our storage rather than about their email. The drawer says which\n * record a persona came from, so a proof that fell through to this reads as\n * the address it is.\n */\n return name ? { email, name } : { email }\n}\n\nfunction storedSendOptionsFrom(\n snapshot: FirebaseFirestore.DocumentSnapshot,\n hostId: string,\n senderUid: string,\n followUp: boolean,\n): CampaignSendOptions {\n const audience = String(snapshot.get('audience') ?? '')\n /*\n * A `manual` audience is stored on a scheduled or drafted email and is not\n * stored on one that has already gone out, so the same audience kind is\n * repeatable for one caller and not the other.\n *\n * `emails` is written by the branches that store a send for later. An\n * immediate send takes its addresses from the request and keeps none of\n * them, which is what leaves a follow-up with nothing to re-resolve.\n */\n const emails = Array.isArray(snapshot.get('emails'))\n ? (snapshot.get('emails') as unknown[]).map(String)\n : undefined\n if (audience === 'manual' && (followUp || !emails?.length)) {\n throw new CampaignSendError(\n followUp\n ? 'This email went to addresses typed into the composer, which are ' +\n 'not kept, so there is no audience to add anybody from. Compose a ' +\n 'new email to the people you want to reach.'\n : 'This email is addressed to typed-in recipients but records none, ' +\n 'so there is nobody to send it to.',\n 400,\n )\n }\n const templateScreenId = String(snapshot.get('templateScreenId') ?? '')\n const body = String(snapshot.get('body') ?? '')\n if (!templateScreenId && !body) {\n throw new CampaignSendError('This email has no message to send', 400)\n }\n const optional = (field: string) => {\n const value = String(snapshot.get(field) ?? '')\n return value ? { [field]: value } : {}\n }\n return {\n hostId,\n campaignId: snapshot.id,\n ...(followUp ? { followUp: true } : {}),\n senderUid,\n subject: String(snapshot.get('subject') ?? ''),\n body,\n audience,\n ...(emails?.length ? { emails } : {}),\n ...(templateScreenId ? { templateScreenId } : {}),\n /*\n * The plain-text part somebody wrote, read back with everything else the\n * record holds. A follow-up or a `sendNow` that re-generated it from the\n * design instead would mail a different text part to the second half of\n * an audience than the first half received.\n */\n ...optional('plainText'),\n ...optional('segmentId'),\n ...optional('listId'),\n ...optional('topicId'),\n ...optional('fromName'),\n ...optional('replyTo'),\n /*\n * The sender the email ALREADY went out as, so a follow-up reaches the\n * rest of its audience from the same address. Taking the site's current\n * default instead would split one mailing across two `From:` lines, which\n * is the same drift the stored name and reply address are read back for.\n */\n ...optional('senderId'),\n ...optional('preheader'),\n ...optional('displayName'),\n ...optional('emailCampaignId'),\n /*\n * The experiment is deliberately NOT carried over on a follow-up.\n *\n * `performCampaignSend` refuses an experiment that is neither running nor\n * decided, so a finished one would fail the whole follow-up — and a\n * running one would take the new recipients as fresh exposures on an\n * experiment whose result the first send has already influenced. So the\n * follow-up mails the subject and body the record holds, which is the\n * campaign's own copy rather than any variant's override.\n *\n * A first send is the opposite case: the email has not run anywhere yet,\n * so the experiment it was composed under is the one it is supposed to\n * go out under, and dropping it here would silently mail the control.\n */\n ...(followUp ? {} : optional('experimentId')),\n }\n}\n\n/**\n * Campaign API (AGL-161/272): `action` picks the operation.\n *\n * Every one of them requires a site admin or editor.\n *\n * ## The ones that mail something\n *\n * `send` (the default) delivers copy carried in the request. `sendNow` mails\n * a draft or a scheduled email ahead of its time, and `followUp` mails an\n * already-sent one to the people it has not reached; both of those take every\n * field off the RECORD rather than the request. `test` delivers to the caller\n * alone and records nothing.\n *\n * ## The ones that only write\n *\n * `draft` stores an email that has not been sent, `schedule` stores one with\n * a `sendAtMs` for the processor to deliver, `update` corrects the merchant's\n * own name for an email at any point in its life, and `cancel` withdraws a\n * scheduled one. None of them reserves allowance or moves a meter.\n *\n * ## The ones that answer a question\n *\n * `preview` resolves the audience and reports the counts, `renderPreview`\n * renders the composed message. Neither writes.\n */\nexport const campaignSendHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n\n const hostId = String(req.body?.hostId ?? '')\n const action = String(req.body?.action ?? 'send')\n const subject = String(req.body?.subject ?? '')\n .trim()\n .slice(0, 150)\n const body = String(req.body?.body ?? '')\n .trim()\n .slice(0, 20000)\n const audience = String(req.body?.audience ?? 'leads')\n const templateScreenId = String(req.body?.templateScreenId ?? '')\n /*\n * THE AUTHOR'S OWN PLAIN-TEXT PART, for a designed email.\n *\n * Capped like the body and NOT flattened like the header fields beside it:\n * this is a message part rather than a header value, so its line breaks are\n * the formatting a text-only reader gets rather than an injection shape.\n */\n const plainText = String(req.body?.plainText ?? '')\n .trim()\n .slice(0, 20000)\n /*\n * Which design version that part was written against, so a composer can say\n * when it has gone stale. Validated as a document id because it is stored\n * and compared against one.\n */\n const plainTextVersionId = String(req.body?.plainTextVersionId ?? '')\n /*\n * The composer's sender fields, and the one rule they all obey: a value a\n * merchant typed reaches a MIME header, so it is flattened to a single line\n * before it goes anywhere. `applyFromName` quotes the display name and\n * strips quotes from it, but nothing downstream removes a CR or an LF, and\n * a header value carrying one is the injection shape.\n */\n const headerSafe = (value: unknown, max: number): string =>\n String(value ?? '')\n /*\n * The control characters are the POINT of this class rather than an\n * accident in it: CR and LF inside a header value ARE the injection\n * shape, and `no-control-regex` cannot tell a pattern that matches\n * them in order to remove them from one that matches them by mistake.\n */\n // eslint-disable-next-line no-control-regex\n .replace(/[\\s\\u0000-\\u001f\\u007f]+/g, ' ')\n .trim()\n .slice(0, max)\n // 78 characters is the line length a display name has to live inside.\n const fromName = headerSafe(req.body?.fromName, 78)\n const replyTo = headerSafe(req.body?.replyTo, 254).toLowerCase()\n const preheader = headerSafe(req.body?.preheader, 200)\n /*\n * The email's own name, which is console-only. Flattened to a single line\n * with the header fields beside it even though it reaches no header: it is\n * rendered into a table and a page title, and a value carrying control\n * characters is worth normalizing wherever it is going.\n */\n const displayName = headerSafe(req.body?.displayName, 60)\n /*\n * `req.body.sendingIdentity` is READ BY NOTHING.\n *\n * A body naming `acme.com`, or `platform`, is not an error and is not\n * honored: the DOMAIN is resolved from the host document, so there is no\n * value this field could carry that would reach it. Left undocumented it\n * would look like an oversight; said here, it is the closure.\n *\n * `senderId` below does not reopen it, and is worth reading against it. What\n * was closed is a request naming an ADDRESS — a domain, or a free local part\n * — because a mailbox has to be one somebody serves. What this names is a\n * row in `hosts/{hostId}/senders`, written under the `org.settings` gate and\n * validated there, so the addresses it can reach are the ones this site was\n * already configured to send as. An id this site does not hold is refused\n * rather than defaulted, which is the property that keeps the two apart:\n * nothing a request says can produce an address that was not configured.\n */\n const senderId = String(req.body?.senderId ?? '')\n if (senderId && !isDocumentId(senderId)) {\n return res.status(400).json({ error: 'Invalid sender' })\n }\n if (plainTextVersionId && !isDocumentId(plainTextVersionId)) {\n return res.status(400).json({ error: 'Invalid design version' })\n }\n // The campaign this send joins. Validated as a document id here because it\n // is stored and later queried as one.\n const emailCampaignId = String(req.body?.emailCampaignId ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n if (emailCampaignId && !isDocumentId(emailCampaignId)) {\n return res.status(400).json({ error: 'Invalid campaign' })\n }\n if (replyTo && !EMAIL_PATTERN.test(replyTo)) {\n return res.status(400).json({ error: 'Reply-to must be an email address' })\n }\n /*\n * Designed emails carry their content in the template; plain sends still\n * need subject + body.\n *\n * THE ACTIONS THAT MAIL NOTHING ARE EXEMPT, and the composer is the reason.\n * It asks for the recipient count as soon as it mounts — before any copy\n * exists, which is the whole point of asking — so requiring copy of\n * `preview` refused every count a plain-text campaign ever asked for, and\n * the readout under the Subject field showed this message instead of the\n * audience size and the consent split. The preview branch below substitutes\n * placeholder copy precisely because it needs none: the count is a fact\n * about the audience, and no part of resolving it reads the subject or the\n * body. `renderPreview` is exempt for the same reason in the other\n * direction — it renders whatever has been typed so far, including nothing.\n */\n /*\n * `followUp` and `sendNow` join the exempt actions, and for a stricter\n * reason than the other three: they do not merely need no copy, they must\n * be given none. The message they mail is the one already on the record —\n * see `storedSendOptionsFrom` — so a subject and body in the request would\n * be fields the route silently discards, and a required field that is\n * discarded is the shape that teaches a caller it was used.\n *\n * `draft` is exempt for the opposite reason. A draft is an email that has\n * not been written yet: requiring a subject and a body of it would mean\n * there is no way to create one, which is the whole state.\n *\n * `update` is exempt because it edits neither — it carries a name and\n * nothing else.\n */\n /*\n * `proofOptions` joins them for the plainest of the reasons: it answers who\n * a test may be sent to and whose data could fill it, which is a question\n * about the workspace and not about the message. The composer asks it when\n * the test drawer opens, which is routinely before a subject exists.\n */\n const mails =\n action !== 'cancel' &&\n action !== 'preview' &&\n action !== 'proofOptions' &&\n action !== 'renderPreview' &&\n action !== 'followUp' &&\n action !== 'sendNow' &&\n action !== 'draft' &&\n action !== 'update'\n if (mails && !templateScreenId && (!subject || !body)) {\n return res.status(400).json({ error: 'Missing subject or body' })\n }\n /*==========================================\n * ONE MESSAGE, ONE SOURCE.\n *\n * `body` is the MESSAGE of a plain-text email — the thing recipients read,\n * from which the HTML part is synthesized. `plainText` is the text HALF of a\n * designed email, whose message lives in the nodes. Two strings of plain\n * text, two different jobs, and each belongs to exactly one mode.\n *\n * A `body` arriving beside a template used to be accepted, computed for\n * merge tags and then dropped: `renderRecipientEmail` read it only when no\n * template was given, and both gates — this one and the composer's — passed\n * on EITHER input, so a merchant who picked a design and also wrote a\n * message lost the message with nothing said.\n *\n * It is refused rather than quietly reinterpreted as the text part. Copy\n * written as \"the message\" is not copy reviewed as \"what a text-only reader\n * gets\", and promoting it would start mailing unreviewed text out of records\n * that already exist. What the author wants is offered explicitly instead —\n * `plainText`, which they can see, edit and preview — and the composer\n * shows a stranded body rather than adopting it.\n *\n * `plainText` without a template is refused for the mirror reason: a\n * plain-text email's text part IS its body, so a second string claiming to\n * be the text part is the same two-sources problem under a new name.\n *\n * Only the actions that carry COMPOSED copy are checked. `preview`\n * substitutes placeholder copy and reads neither; `followUp`, `sendNow`,\n * `update` and `cancel` mail what the record already holds, and a record\n * written before this rule may legitimately still carry an inert body\n * beside its template — refusing those would strand an existing draft over\n * a field its send has never read.\n *=========================================*/\n const carriesComposedCopy =\n action === 'send' ||\n action === 'schedule' ||\n action === 'draft' ||\n action === 'test' ||\n action === 'renderPreview'\n if (carriesComposedCopy && templateScreenId && body) {\n return res.status(400).json({\n error:\n 'This email is built from a design, which carries its own message. ' +\n 'Write the plain-text version instead, or switch this email to ' +\n 'plain text.',\n })\n }\n if (carriesComposedCopy && !templateScreenId && plainText) {\n return res.status(400).json({\n error:\n 'A plain-text email has no separate text version — what you type is ' +\n 'the message.',\n })\n }\n if (!['leads', 'members', 'manual', 'segment', 'list'].includes(audience)) {\n return res.status(400).json({ error: 'Unknown audience' })\n }\n /*\n * The composer's topic, refused here as well as inside `performCampaignSend`.\n *\n * Both, because the SCHEDULE branch below writes the campaign document\n * without going through the send — the same asymmetry AGL-1771 found for\n * `campaignId` — so a topic that only the send validated would be stored\n * unchecked and then signed into a link a fortnight later.\n *\n * An empty value is not an error: it means \"the composer did not say\", which\n * `performCampaignSend` resolves to the default topic.\n */\n const topicId = String(req.body?.topicId ?? '')\n if (topicId && !isEmailTopicId(topicId)) {\n return res.status(400).json({ error: 'Unknown topic' })\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 /*\n * A VERIFIED ADDRESS IS A PRECONDITION OF SENDING MAIL (AGL-479/2589),\n * and this is the door every send arrives at — broadcast, scheduled and\n * test alike, since all three branches sit below this check.\n *\n * Stated here rather than inferred, which is the whole change. The role\n * lookup on the next lines already refused an unverified account, but\n * only as a side effect of provisioning: nothing can enter a host's\n * `memberRoles` map without having verified, so the role resolved to\n * undefined and the 403 came from the wrong sentence. That is a fact\n * about how accounts are created, not a rule about who may send, and it\n * has already moved once — signup provisions an ORG for an account that\n * has not verified yet (AGL-2585). A comparable grace on host creation\n * would have opened this surface with nothing else in the way, on a\n * `p=reject` sending domain, and no test would have gone red.\n *\n * `campaign-send-verification.spec.ts` holds the property directly, so a\n * future grace fails a test instead of leaking reputation.\n *\n * Impersonation is exempt on the AGL-480 reasoning the other ~135 gates\n * use: staff have authenticated separately, the act is audited, and the\n * account most likely to need support is the newest one.\n */\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n // The shape of `emailUnverifiedResponse()` (AGL-479), written out\n // because that helper returns a fetch `Response` and this is a\n // (req, res) plugin handler.\n return res.status(403).json({\n error: 'Verify your email to continue',\n reason: 'email-unverified',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\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\n if (action === 'proofOptions') {\n /*\n * THE TWO LISTS THE TEST DRAWER IS BUILT FROM, and they are different\n * kinds of thing.\n *\n * `recipients` is a RULE made visible: these are the only addresses a\n * test may be delivered to, so the drawer offers them instead of a text\n * box whose every other answer is a refusal nobody could have predicted.\n *\n * `personas` is a CONVENIENCE: whose stored data a proof renders as.\n * Choosing one mails that person nothing, which the drawer says in as\n * many words — the two controls sit next to each other and the whole\n * risk of the feature is somebody reading the second as the first.\n */\n const [recipients, personas] = await Promise.all([\n proofRecipientsForHost({ hostId, callerEmail: String(decoded.email ?? '') }),\n proofPersonasForHost(hostId),\n ])\n return res.status(200).json({ recipients, personas })\n }\n\n if (action === 'test') {\n /*\n * PROOF ONE EMAIL: as somebody, to somebody, from an identity — and\n * still no campaign record and no counter.\n *\n * The three choices are independent and only one of them decides who\n * receives mail. `personaEmail` changes what the merge tags RESOLVE to\n * and reaches nobody; `to` is the only address anything is delivered\n * to. Which identity it leaves on is not among them: that is resolved\n * from the host document, and no request field reaches it.\n */\n const ownEmail = String(decoded.email ?? '')\n .trim()\n .toLowerCase()\n const requestedTo = String(req.body?.to ?? '')\n .trim()\n .toLowerCase()\n const testEmail = requestedTo || ownEmail\n if (!testEmail) {\n return res\n .status(400)\n .json({ error: 'Your account has no email address for tests' })\n }\n if (!EMAIL_PATTERN.test(testEmail)) {\n return res\n .status(400)\n .json({ error: 'Enter a valid address to send the test to' })\n }\n /*\n * WHO A TEST SEND MAY REACH, decided here because it is the only layer\n * that can ask whose address this is.\n *\n * A proof may go to the caller's own account address or to another\n * ACCOUNT HOLDER on the owning workspace, and to nobody else. The two\n * populations a merchant could otherwise reach through this button are\n * exactly the two that must not be reachable: a contact or lead, who is\n * subject to the consent rule this send is exempt from, and a stranger,\n * who has no relationship with the workspace at all.\n *\n * `siteMembers` are deliberately NOT eligible. They are the tenant's\n * customers — the audience — and an audience member reached by a\n * \"test\" is an audience member who has been mailed.\n */\n const eligible = await eligibleProofAddress({\n hostId,\n callerEmail: ownEmail,\n address: testEmail,\n })\n if (!eligible) {\n return res.status(403).json({\n error:\n `A test can only be sent to you or to someone with an account on ` +\n `this workspace, and ${testEmail} is neither. Add them to the ` +\n `workspace first, or send the test to yourself and forward it.`,\n })\n }\n /*\n * The person whose data fills the merge tags. Looked up rather than\n * taken from the request, so what a proof shows is what the audience\n * document actually holds — a persona assembled from a request would\n * demonstrate the composer's own typing rather than the merge.\n */\n const persona = await resolveProofPersona(\n hostId,\n String(req.body?.personaEmail ?? ''),\n )\n const result = await performCampaignSend({\n hostId,\n subject,\n body: body || 'Test send',\n audience: 'manual',\n emails: [testEmail],\n templateScreenId: templateScreenId || undefined,\n // A proof mails the message as composed, the text half included:\n // checking the plain-text version in a real inbox is one of the three\n // things a proof is asked.\n ...(plainText ? { plainText } : {}),\n fromName,\n replyTo,\n // The proof leaves as the sender the composer has chosen, so what a\n // merchant reads in their own inbox is the `From:` their audience\n // will see rather than the site's default.\n ...(senderId ? { senderId } : {}),\n preheader,\n ...(persona ? { proofPersona: persona } : {}),\n recordCampaign: false,\n senderUid: decoded.uid,\n // Not marketing: the recipient holds an account on this workspace.\n proofFor: testEmail,\n })\n return res.status(200).json({\n ...result,\n test: true,\n to: testEmail,\n ...(persona ? { personaEmail: persona.email } : {}),\n })\n }\n\n if (action === 'renderPreview') {\n /*\n * THE MESSAGE, RENDERED, AND NOT ONE ADDRESS RESOLVED.\n *\n * Separate from `preview` because the two answer different questions at\n * different costs. `preview` sweeps the audience — up to 5,000 documents\n * — to count people, and its answer changes only when the audience does.\n * This one answers \"what does my email look like\", which changes on\n * every keystroke, and reads at most the template and its products.\n * Folding the render into `preview` would page the merchant's whole\n * contact list once per debounce tick, for a number that had not moved.\n *\n * Rendered through `renderRecipientEmail`, which is what the per-recipient\n * send loop calls, so this is the HTML that will be mailed and not a\n * likeness of it.\n */\n const template = templateScreenId\n ? await loadEmailTemplate(hostId, templateScreenId)\n : null\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n const orgForHost = await getOrgForHost(hostId)\n const branding = resolveBrandingProfile(orgForHost?.org as never)\n /*\n * Personalized for the REQUESTER, because a preview showing raw\n * `{{firstName|there}}` tells a merchant nothing about what a recipient\n * will read, and inventing a fictional contact would make a merge tag\n * that resolves to nothing look like one that works.\n */\n const rendered = renderRecipientEmail({\n subject,\n preheader,\n content: template\n ? {\n mode: 'design',\n template,\n ...(plainText ? { plainText } : {}),\n }\n : { mode: 'text', body },\n recipient: {\n email: String(decoded.email ?? ''),\n name: String((decoded as Record<string, unknown>)['name'] ?? ''),\n },\n siteBase,\n hostId,\n // Unsigned, and it is not a working opt-out: minting a real signature\n // here would put a live preference link for the requester's own\n // address into a page they are only reading. The footer's presence,\n // and its wording, is what the preview is showing.\n unsubscribeUrl: `${siteBase}/api/email/preferences`,\n })\n return res.status(200).json({\n ...rendered,\n preheader: preheader || template?.preheader || '',\n fromName: fromName || branding.fromName,\n ...(replyTo ? { replyTo } : {}),\n })\n }\n\n if (action === 'preview') {\n // Read-only, and it needs the same admin/editor role as a send: the\n // audience size of someone else's site is not public information.\n const result = await performCampaignSend({\n hostId,\n subject: subject || 'preview',\n body: body || 'preview',\n audience,\n segmentId: String(req.body?.segmentId ?? ''),\n listId: String(req.body?.listId ?? ''),\n topicId: topicId || undefined,\n emails: Array.isArray(req.body?.emails)\n ? req.body.emails.map(String)\n : undefined,\n templateScreenId: templateScreenId || undefined,\n /*\n * The composer's identity choice rides the preview, which is what\n * makes the refusal arrive BEFORE the Send button rather than from\n * it. The dry run resolves the identity on the same terms a send\n * does and throws the same 409, so picking a domain whose DNS is\n * unfinished says so the moment it is picked — and a sender this site\n * no longer holds is refused at the picker rather than at Send.\n */\n ...(senderId ? { senderId } : {}),\n senderUid: decoded.uid,\n dryRun: true,\n })\n return res.status(200).json(result)\n }\n\n if (action === 'followUp') {\n /*\n * SEND AN EMAIL THAT HAS ALREADY GONE OUT TO THE PEOPLE IT HAS NOT\n * REACHED.\n *\n * The request names a site and an email and carries nothing else that\n * is read. Everything the send needs comes back off the record, and\n * `performCampaignSend` re-checks the admission rules this branch does\n * not check at all — it is the same authorization, the same consent\n * split, the same two suppression lists, the same monthly reservation\n * and the same hourly ceilings, because it is the same function.\n *\n * `dryRun` rides through so the console can ask how many people are\n * left before offering the button, and the answer comes from the code\n * that would do the sending rather than from a count of its own.\n */\n const followUpId = String(req.body?.campaignId ?? '')\n if (!isDocumentId(followUpId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const sendSnapshot = await hostRef\n .collection('campaigns')\n .doc(followUpId)\n .get()\n if (!sendSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n const result = await performCampaignSend({\n ...storedSendOptionsFrom(sendSnapshot, hostId, decoded.uid, true),\n ...(req.body?.dryRun ? { dryRun: true } : {}),\n })\n return res.status(200).json(result)\n }\n\n /*==========================================\n * AN EMAIL THAT EXISTS BEFORE IT IS SENT.\n *\n * `draft` is the same write the `schedule` branch below makes, minus the\n * send time — a record in the same collection, under the same id it will\n * keep forever, carrying the copy composed so far and `status: 'draft'`.\n *\n * ## Why a state on the record rather than a collection of its own\n *\n * The id is the reason. `performCampaignSend` adopts a `campaignId` it is\n * given, so a draft becomes the sent email AT ITS OWN ID — which is what\n * makes `/marketing/campaigns/{sendId}` resolve from the moment the email is\n * created, and what keeps the `cid=` inside every delivered unsubscribe\n * HMAC pointing at the record it was minted for. A draft in a second\n * collection would have to be copied to a new id at send time, and the\n * URL a merchant had open would stop being the email's URL.\n *\n * ## What a draft costs\n *\n * Nothing. This branch reserves no monthly allowance, claims no hourly\n * budget and moves no meter — it writes one document, exactly as\n * `schedule` always has. The scheduled processor queries\n * `status == 'scheduled'`, so a draft is never picked up and cannot\n * escape on its own; `performCampaignSend` is the only thing that mails\n * it, and only when somebody asks.\n *=========================================*/\n if (action === 'draft' || action === 'schedule' || action === 'update') {\n const scheduling = action === 'schedule'\n const sendAtMs = Number(req.body?.sendAtMs ?? 0)\n if (scheduling && (!Number.isFinite(sendAtMs) || sendAtMs <= Date.now())) {\n return res.status(400).json({ error: 'Pick a future send time' })\n }\n const campaignId =\n String(req.body?.campaignId ?? '') || createResourceUid()\n // AGL-1771: this branch WRITES, and it is the only campaign write that\n // does not go through `performCampaignSend`'s guard. A `campaignId` of\n // `a/b/c` scheduled the campaign at `campaigns/a/b/c` — which the\n // scheduled-campaign processor would then pick up by `collectionGroup`\n // and send, from a document the merchant can neither see in their\n // campaigns list nor cancel.\n if (!isDocumentId(campaignId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const targetRef = hostRef.collection('campaigns').doc(campaignId)\n const targetSnapshot = await targetRef.get()\n const targetState = targetSnapshot.exists\n ? String(targetSnapshot.get('status') ?? '')\n : ''\n\n /*==========================================\n * WHAT IS ALREADY IN AN INBOX IS NOT EDITABLE.\n *\n * These branches address an EXISTING document by id, so without this\n * check `schedule` would happily merge a new subject and body onto an\n * email that went out last March and set its status back to\n * `scheduled` — rewriting the record of what was delivered, and handing\n * the processor a message to mail a second time under a `cid` whose\n * unsubscribe links are already in inboxes.\n *\n * So copy may only be written while the email is still unsent. `update`\n * is the deliberate exception and is why it is in this branch at all:\n * it writes the merchant's own NAME for the email and nothing else —\n * see the write below — which is console-only text that reached no\n * recipient and therefore contradicts no delivered mail.\n *=========================================*/\n const rewritable = !targetSnapshot.exists ||\n targetState === 'draft' ||\n targetState === 'scheduled'\n if (action !== 'update' && !rewritable) {\n return res.status(409).json({\n error:\n targetState === 'sent'\n ? 'This email has already been sent, so its message and ' +\n 'audience can no longer be changed. Compose a new email.'\n : 'This email was canceled, so it can no longer be scheduled. ' +\n 'Compose a new email.',\n })\n }\n if (action === 'update') {\n /*\n * The one field a sent email still owns. It is the friendly name the\n * create drawer captures — never the subject, which describes mail\n * that is already in inboxes — so it can be corrected at any point in\n * an email's life without making the record disagree with what was\n * delivered.\n */\n if (!targetSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n await targetRef.set({ displayName }, { merge: true })\n return res.status(200).json({ campaignId, displayName })\n }\n await targetRef.set(\n {\n subject,\n body,\n audience,\n /*\n * The one date every message carries — see `email-record.ts`.\n * Stamped only where this write CREATES the record: a draft saved\n * over and over is the same email, and re-stamping it would walk\n * its creation date forward on every keystroke's save. The\n * existence read above is the one this branch already does.\n */\n ...(targetSnapshot.exists ? {} : { createdAtMs: Date.now() }),\n ...(req.body?.segmentId\n ? { segmentId: String(req.body.segmentId) }\n : {}),\n ...(req.body?.listId ? { listId: String(req.body.listId) } : {}),\n ...(topicId ? { topicId } : {}),\n ...(Array.isArray(req.body?.emails)\n ? { emails: req.body.emails.map(String).slice(0, 500) }\n : {}),\n ...(req.body?.experimentId\n ? { experimentId: String(req.body.experimentId) }\n : {}),\n /*\n * THE TEMPLATE IS CLEARED WHEN THERE IS NONE, not merely omitted.\n *\n * `templateScreenId` is the field that decides which of the two ways\n * this email is written — see `emailMessageMode` — so leaving it\n * standing under `merge: true` is not a stale pointer, it is the\n * wrong mode. A draft moved from a design to a typed message would\n * be stored carrying BOTH, reopen as designed, and mail the design\n * while the message the merchant just wrote sat unread on the\n * record. The same discard this pair of fields already produced\n * once, arriving by the save path instead of the send path.\n */\n templateScreenId:\n templateScreenId || firebaseAdmin.firestore.FieldValue.delete(),\n /*\n * The authored plain-text part, and the design version it was\n * written against. Cleared together and for the same reason the\n * template is: an override left standing under `merge: true` after\n * somebody switched this email to plain text would be a text part\n * for a design the record no longer names, and the staleness the\n * version id exists to expose would be measured against nothing.\n */\n plainText: plainText || firebaseAdmin.firestore.FieldValue.delete(),\n plainTextVersionId:\n (plainText && plainTextVersionId) ||\n firebaseAdmin.firestore.FieldValue.delete(),\n // The composer's sender fields ride on the stored campaign so the\n // scheduled processor mails the message that was composed rather\n // than one that reverts to the org's branding defaults.\n ...(fromName ? { fromName } : {}),\n ...(replyTo ? { replyTo } : {}),\n ...(senderId ? { senderId } : {}),\n ...(preheader ? { preheader } : {}),\n ...(displayName ? { displayName } : {}),\n ...(emailCampaignId ? { emailCampaignId } : {}),\n ...(scheduling\n ? {\n status: 'scheduled',\n sendAtMs,\n scheduledAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n scheduledBy: decoded.uid,\n }\n : {\n status: 'draft',\n /*\n * The send time is CLEARED rather than left standing.\n *\n * Saving a scheduled email back to a draft is how a merchant\n * takes it off the clock, and `merge: true` leaves any field\n * this write does not name — so a `sendAtMs` left behind\n * would sit on a draft as a due date nothing acts on, and the\n * emails list orders on exactly that field.\n */\n sendAtMs: firebaseAdmin.firestore.FieldValue.delete(),\n draftedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n draftedBy: decoded.uid,\n }),\n },\n { merge: true },\n )\n return res\n .status(200)\n .json({ campaignId, status: scheduling ? 'scheduled' : 'draft' })\n }\n\n if (action === 'cancel') {\n const campaignId = String(req.body?.campaignId ?? '')\n // AGL-1771: the ref used to be built one line ABOVE the `campaignId ?`\n // check below, which defeated that check — `.doc('')` throws on an empty\n // path segment, so the 400 this branch intends became a 500. Guarding\n // first is what lets the ref be built at all.\n if (!isDocumentId(campaignId)) {\n return res.status(400).json({ error: 'Not a scheduled campaign' })\n }\n const campaignRef = hostRef.collection('campaigns').doc(campaignId)\n const campaignSnapshot = await campaignRef.get()\n if (\n !campaignSnapshot?.exists ||\n campaignSnapshot.get('status') !== 'scheduled'\n ) {\n return res.status(400).json({ error: 'Not a scheduled campaign' })\n }\n await campaignRef.set(\n {\n status: 'canceled',\n canceledAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n canceledBy: decoded.uid,\n },\n { merge: true },\n )\n return res.status(200).json({ campaignId, status: 'canceled' })\n }\n\n if (action === 'sendNow') {\n /*==========================================\n * MAIL A DRAFTED OR SCHEDULED EMAIL, NOW.\n *\n * The request names a site and an email and carries nothing else that\n * is read. Everything comes off the record through\n * `storedSendOptionsFrom`, for the reason documented there: this branch\n * addresses an existing document by id, and a caller who could also\n * supply the copy could put arbitrary text on somebody else's send id\n * and mail it under that id's `cid`.\n *\n * It is not a second send path. `performCampaignSend` runs whole — the\n * same authorization, the same consent split, the same two suppression\n * lists, the same topic filtering, the same monthly reservation and the\n * same hourly governor — because it is the same function the scheduled\n * processor and the composer both call.\n *=========================================*/\n const sendNowId = String(req.body?.campaignId ?? '')\n if (!isDocumentId(sendNowId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const sendNowRef = hostRef.collection('campaigns').doc(sendNowId)\n const sendNowSnapshot = await sendNowRef.get()\n if (!sendNowSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n const sendNowState = String(sendNowSnapshot.get('status') ?? '')\n /*\n * Only an email that has not gone out yet. A sent one would be mailed a\n * second time to the whole audience under the same id — which is what\n * `followUp` exists to do safely, minus everyone already reached — and\n * a canceled one was withdrawn on purpose.\n */\n if (sendNowState !== 'draft' && sendNowState !== 'scheduled') {\n return res.status(400).json({\n error:\n sendNowState === 'sent'\n ? 'This email has already been sent. Use \"Send to more ' +\n 'recipients\" to reach the people it has not.'\n : 'Only a draft or a scheduled email can be sent now',\n })\n }\n /*==========================================\n * A CAMPAIGN BETWEEN BATCHES IS `scheduled`, AND MUST NOT RESTART.\n *\n * An audience larger than one send goes out over several runs, and\n * between them the email is stored as `scheduled` — the state the\n * processor claims to continue it. So the check above admits it, and\n * this branch would then call `performCampaignSend` with no\n * `continuation`: the whole audience resolved again, nobody subtracted,\n * and a second copy delivered to everybody the earlier batches already\n * reached, under the same `cid` whose unsubscribe links are in their\n * inboxes.\n *\n * Refused rather than quietly turned into a continuation. The rest of\n * this campaign is already due — the processor picks it up on its next\n * beat — so there is nothing for a merchant to ask for here, and\n * \"Send now\" on a send already in flight is a request made under a\n * misunderstanding the answer should correct.\n *=========================================*/\n const resumeRemaining = Math.floor(\n Number(sendNowSnapshot.get('resume')?.remaining ?? 0),\n )\n if (Number.isFinite(resumeRemaining) && resumeRemaining > 0) {\n return res.status(409).json({\n error:\n 'This email is already being sent, and the rest of it goes out ' +\n 'on its own. Sending it now would mail a second copy to ' +\n 'everyone it has already reached.',\n })\n }\n /*\n * CLAIMED BEFORE IT IS MAILED, exactly as the scheduled processor\n * claims one.\n *\n * A scheduled email whose time arrives mid-send would otherwise be\n * picked up by the processor's `status == 'scheduled'` query while this\n * request is still resolving its audience, and mailed twice. Moving it\n * to `sending` first is the same claim under the same transaction the\n * processor uses, so whichever gets there first is the only one that\n * sends.\n */\n const claimed = await firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(sendNowRef)\n if (String(fresh.get('status') ?? '') !== sendNowState) return false\n transaction.update(sendNowRef, { status: 'sending' })\n return true\n })\n if (!claimed) {\n return res\n .status(409)\n .json({ error: 'This email is already being sent' })\n }\n try {\n const result = await performCampaignSend({\n ...storedSendOptionsFrom(sendNowSnapshot, hostId, decoded.uid, false),\n ...(req.body?.dryRun ? { dryRun: true } : {}),\n })\n /*\n * A dry run writes nothing, so the claim above is the only change it\n * made and it has to be put back — otherwise asking how many people\n * an email would reach would leave it stuck in `sending`.\n */\n if (req.body?.dryRun) {\n await sendNowRef.set({ status: sendNowState }, { merge: true })\n }\n return res.status(200).json(result)\n } catch (error) {\n // The claim is released on every failure. `performCampaignSend`\n // writes `status: 'sent'` itself on the way out, so nothing here\n // needs to set it — but a refusal that left the email in `sending`\n // would be an email the merchant can neither send nor cancel.\n await sendNowRef.set({ status: sendNowState }, { merge: true })\n throw error\n }\n }\n\n /*==========================================\n * AN IMMEDIATE SEND MAY NAME AN EMAIL, BUT ONLY AN UNSENT ONE.\n *\n * This branch takes its copy from the REQUEST, which is what the composer\n * needs — it is sending the message being typed. But it also accepts a\n * `campaignId`, and `performCampaignSend` adopts it, so without this check\n * a request naming a send that already went out would write new copy over\n * the record of what was delivered, replace its counters with this send's\n * own, and mail the whole audience a second copy under a `cid` whose\n * unsubscribe links are already in inboxes.\n *\n * Reaching the people an existing email has NOT reached is `followUp`,\n * which takes no copy from the request at all and subtracts everyone the\n * earlier sends recorded.\n *=========================================*/\n const sendId = String(req.body?.campaignId ?? '')\n if (sendId) {\n if (!isDocumentId(sendId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const existing = await hostRef.collection('campaigns').doc(sendId).get()\n const existingState = existing.exists\n ? String(existing.get('status') ?? '')\n : ''\n if (existing.exists && existingState !== 'draft' && existingState !== 'scheduled') {\n return res.status(409).json({\n error:\n existingState === 'sent'\n ? 'This email has already been sent. Use \"Send to more ' +\n 'recipients\" to reach the people it has not.'\n : 'This email was canceled, so it cannot be sent. Compose a ' +\n 'new email.',\n })\n }\n }\n\n const result = await performCampaignSend({\n hostId,\n subject,\n body,\n audience,\n segmentId: String(req.body?.segmentId ?? ''),\n listId: String(req.body?.listId ?? ''),\n topicId: topicId || undefined,\n emails: Array.isArray(req.body?.emails) ? req.body.emails : undefined,\n campaignId: sendId,\n experimentId: String(req.body?.experimentId ?? ''),\n templateScreenId: templateScreenId || undefined,\n ...(plainText ? { plainText } : {}),\n fromName,\n replyTo,\n ...(senderId ? { senderId } : {}),\n preheader,\n displayName,\n emailCampaignId,\n senderUid: decoded.uid,\n })\n return res.status(200).json(result)\n } catch (error) {\n if (error instanceof CampaignSendError) {\n return res.status(error.status).json({ error: error.message })\n }\n console.error(error)\n return res.status(500).json({ error: 'Campaign send failed' })\n }\n}\n"],"names":["checkQuota","contactMatchesSegment","isEmailTopicId","DEFAULT_CAMPAIGN_TOPIC_ID","readMarketingBasis","resolveMarketingConsentPolicy","splitByMarketingConsent","createResourceUid","decodeStoredNodes","emailStarterSendBlock","resolveBrandingProfile","visibleToHost","renderRecipientEmail","assignExperimentVariant","readPluginRecordCard","hostPublicOrigin","consentGroupForSite","orgDataCollectionForHost","orgDataQueryForHost","filterSendableForHost","filterTopicSendable","firebaseAdmin","getOrgForHost","meterHostEmail","claimOrgEmailSendBudget","orgCampaignEmailSendsForMonth","readEmailSendRateConfig","readEmailSendRateWindow","reconcileCampaignSendReservation","reserveCampaignEmailSends","resolveHostSendingIdentity","findContactByEmail","isDocumentId","isImpersonationSession","buildUnsubscribeUrl","unsubscribeSignature","sharedUnsubscribeSignature","filterCadenceSendable","recordMarketingSends","CAMPAIGN_REACH_CEILING","campaignReachCovers","campaignSettledSize","partitionByCampaignReach","readCampaignReach","readCampaignSettled","recordCampaignReach","recordCampaignSkipped","claimOrgEmailSendDay","orgAgeDays","readSenderReputation","reconcileOrgEmailSendDay","recordCampaignAccepted","resolveOrgEmailRamp","createHash","EMAIL_MAX_AUDIENCE_PER_SEND","EMAIL_MAX_RECIPIENTS_PER_SEND","campaignBatchPlan","createProviderRequestPacer","effectiveReputationPolicy","HOST_SENDERS_COLLECTION","isEmailConfigured","rateLimitedRetryAtMs","readHostSender","sendEmail","sendingIdentityRefusal","sentAsStamp","MAX_RECIPIENTS_PER_SEND","EMAIL_PATTERN","AUDIENCE_PAGE_SIZE","AUDIENCE_SCAN_CEILING","sweepAudience","base","ordered","orderBy","firestore","FieldPath","documentId","docs","cursor","page","startAfter","limit","get","push","length","truncated","probe","suppressionId","email","update","toLowerCase","digest","hostId","secret","campaignId","topicId","CampaignSendError","Error","message","status","CampaignSendDeferredError","retryAtMs","loadEmailTemplate","screenId","screenSnapshot","app","screenRef","collection","doc","exists","versionId","versionSnapshot","String","nodes","Object","keys","installedFrom","listingId","revocation","data","block","reason","productIds","Set","values","filter","node","componentId","map","props","productId","slice","products","Promise","all","card","id","name","title","priceLabel","caption","imageUrl","url","path","subject","preheader","performCampaignSend","options","orgForHost","consent","unsubscribeSecret","process","env","EMAIL_UNSUBSCRIBE_SECRET","CRON_SECRET","body","audience","value","experimentId","templateScreenId","segmentId","listId","addsToExistingSend","Boolean","followUp","continuation","hostRef","hostSnapshot","reachedKeys","batchesSoFar","sendSnapshot","settled","sentSoFar","Number","sent","reached","toLocaleString","skipped","Math","max","floor","batch","finishContinuation","sendId","set","resume","remaining","nextAtMs","lastSentAt","FieldValue","serverTimestamp","merge","recipients","audienceSize","resuming","size","audienceTruncated","listName","names","Map","consentGroup","collectConsent","cleaned","trim","collectName","leads","members","segmentSnapshot","segment","tags","sources","contacts","query","listRef","parent","Array","isArray","emails","addressable","test","partitioned","unreached","alreadyReached","resolved","catch","orgId","senderId","senderSnapshot","chosenSender","sendingIdentity","selectedDomain","selectedLocalPart","localPart","poolMember","purpose","identityRefusal","missing","join","reputationPolicy","source","org","consentPolicy","proofFor","proofAddress","includes","has","stored","readStoredConsent","basis","consentSplit","one","mailable","unshift","platformRate","proofOnly","recordCampaign","batchCap","ramp","reputation","policy","reinstatedUntilMs","blocked","ageDays","deliveredLifetime","window","accepted","platformPerHour","perHour","graduated","enabled","dayRemaining","perDay","claimedToday","detail","Date","now","min","notSuppressed","onTopic","sendable","monthKey","toISOString","campaignSendLimit","overCapError","used","dryRun","suppressed","cadenceHeld","consented","consentedByOperator","grandfathered","consentWithheld","withheld","identity","summary","identitySource","siteBase","cname","subdomain","branding","mintsRecord","template","designedContent","mode","plainText","experiment","experimentSnapshot","target","winnerVariantId","$id","config","resetMs","hourly","count","allowed","ceiling","dayClaim","claim","month","ok","reservation","dayReservation","variantSends","sendableSet","settledOut","deferred","paceProviderRequest","index","variant","link","oneClickUrl","surface","unsubscribeUrl","content","recipient","proofPersona","result","to","html","text","headers","fromName","replyTo","context","variantId","entries","exposures","increment","updatedAt","undefined","plan","addressed","retryable","additive","measuresTheAudience","createdAtMs","from","displayName","emailCampaignId","stats","audienceSizeTruncated","fromEntries","clickTracked","sendAtMs","stop","lastSentBy","senderUid","sentAt","sentBy","sendCount","proofRecipientsForHost","callerEmail","found","label","self","eligibleProofAddress","address","match","where","empty","PROOF_PERSONA_SAMPLE","proofPersonasForHost","byId","collect","nameFields","field","find","then","findAudienceDocument","contact","snapshot","resolveProofPersona","rawEmail","storedSendOptionsFrom","optional","campaignSendHandler","req","res","method","json","error","action","plainTextVersionId","headerSafe","replace","mails","carriesComposedCopy","authorization","idToken","startsWith","decoded","auth","verifyIdToken","email_verified","memberRole","uid","personas","ownEmail","requestedTo","testEmail","eligible","persona","personaEmail","rendered","followUpId","targetSnapshot","scheduling","isFinite","targetRef","targetState","rewritable","delete","scheduledAt","scheduledBy","draftedAt","draftedBy","campaignRef","campaignSnapshot","canceledAt","canceledBy","sendNowSnapshot","sendNowId","sendNowRef","sendNowState","resumeRemaining","claimed","runTransaction","transaction","fresh","existing","existingState","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-send.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkQuota,\n contactMatchesSegment,\n isEmailTopicId,\n DEFAULT_CAMPAIGN_TOPIC_ID,\n readMarketingBasis,\n resolveMarketingConsentPolicy,\n splitByMarketingConsent,\n type MarketingConsentRecord,\n createResourceUid,\n decodeStoredNodes,\n emailStarterSendBlock,\n resolveBrandingProfile,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport type { PluginRevocation } from '@aglyn/aglyn/server'\nimport { renderRecipientEmail } from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport type { EmailRenderProduct } from '@aglyn/shared-util-email'\nimport { assignExperimentVariant, type HostExperiment } from '../model'\nimport { readPluginRecordCard } from '@aglyn/aglyn/plugin-manager/plugin-record-cards'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n orgDataCollectionForHost,\n orgDataQueryForHost,\n filterSendableForHost,\n filterTopicSendable,\n firebaseAdmin,\n getOrgForHost,\n meterHostEmail,\n claimOrgEmailSendBudget,\n orgCampaignEmailSendsForMonth,\n readEmailSendRateConfig,\n readEmailSendRateWindow,\n reconcileCampaignSendReservation,\n reserveCampaignEmailSends,\n type CampaignSendReservation,\n resolveHostSendingIdentity,\n} from '@aglyn/tenant-data-admin'\n// The leaf, not the barrel: this plugin's specs substitute the barrel\n// wholesale, and the lookup must reach the real index logic under them.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\n/*\n * The LEAF module for the impersonation exemption too, and for a sharper\n * version of the reason the notes below give: every spec that reaches this\n * file mocks the `@aglyn/tenant-data-admin` barrel, and a factory that\n * omitted this name would make the verification gate THROW rather than\n * refuse. A control that fails by exception is one nobody can reason about.\n * Resolved from the module itself, the real predicate runs under every\n * harness.\n */\nimport { isImpersonationSession } from '@aglyn/tenant-data-admin/server/firebase-admin'\n/*\n * The LEAF module, not the barrel, for the reason `document-id` is imported\n * the same way: a barrel import resolves to whatever a spec's `jest.mock` of\n * `@aglyn/tenant-data-admin` happens to contain, and nearly every spec that\n * reaches this file mocks it.\n */\nimport {\n buildUnsubscribeUrl,\n unsubscribeSignature as sharedUnsubscribeSignature,\n} from '@aglyn/tenant-data-admin/server/email-unsubscribe-link'\nimport {\n filterCadenceSendable,\n recordMarketingSends,\n} from '@aglyn/tenant-data-admin/server/email-marketing-gate'\n/*\n * The LEAF module again, and for the third reason listed above `document-id`:\n * the specs that reach this file mock the `@aglyn/tenant-data-admin` barrel,\n * and a reach helper resolved through it would be whatever their factory\n * happens to contain. The one function here that may not be wrong — the read\n * that decides who has already had this email — is the one that must come\n * from the real module.\n */\nimport {\n CAMPAIGN_REACH_CEILING,\n campaignReachCovers,\n campaignSettledSize,\n partitionByCampaignReach,\n readCampaignReach,\n readCampaignSettled,\n recordCampaignReach,\n recordCampaignSkipped,\n} from './email-campaign-reach'\n/*\n * The LEAF module for the reputation controls too, and for the same reason\n * as the three above it: every spec that reaches this file mocks the\n * `@aglyn/tenant-data-admin` barrel, so a breaker resolved through it would\n * be whatever a factory happened to contain — which for a control that\n * REFUSES a send means a test could pass against a breaker that is not there.\n * Resolved from the module itself, the real control runs, and it fails open\n * against a harness that cannot serve it.\n */\nimport {\n claimOrgEmailSendDay,\n orgAgeDays,\n readSenderReputation,\n reconcileOrgEmailSendDay,\n recordCampaignAccepted,\n resolveOrgEmailRamp,\n type OrgEmailSendDayReservation,\n type SenderReputationRead,\n} from '@aglyn/tenant-data-admin/server/email-sender-reputation'\nimport { createHash, createHmac } from 'crypto'\nimport {\n EMAIL_MAX_AUDIENCE_PER_SEND,\n EMAIL_MAX_RECIPIENTS_PER_SEND,\n campaignBatchPlan,\n createProviderRequestPacer,\n effectiveReputationPolicy,\n HOST_SENDERS_COLLECTION,\n isEmailConfigured,\n rateLimitedRetryAtMs,\n readHostSender,\n sendEmail,\n sendingIdentityRefusal,\n sentAsStamp,\n type SendingIdentitySource,\n type EmailRampVerdict,\n} from '@aglyn/shared-util-email'\n\n/**\n * Recipients one send may address.\n *\n * The number lives in `send-ceilings.ts` with the other two email ceilings\n * rather than here, because it only means anything in relation to them: it has\n * to fit inside a workspace's share of the platform hour, which in turn has to\n * fit inside the platform hour. Held privately here it was a third number\n * nobody could check against the other two.\n */\nconst MAX_RECIPIENTS_PER_SEND = EMAIL_MAX_RECIPIENTS_PER_SEND\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** How many audience documents one Firestore round trip fetches. */\nconst AUDIENCE_PAGE_SIZE = 500\n\n/**\n * The read budget for resolving one audience — a ceiling on the SCAN, not a\n * silent cap on the answer.\n *\n * It is deliberately the largest window this file already spent (`contacts`\n * and list members read 5,000), so no audience costs more to resolve than it\n * did before; `leads` and `siteMembers` read 1,000 and now share the same\n * number, which is the point — a site with 3,000 leads was told its audience\n * was 1,000.\n *\n * Reaching it does not truncate anything silently. The resolution reports\n * {@link CampaignSendResult.audienceTruncated}, `audienceSize` becomes a\n * floor rather than a total, and the composer and the History row both say so.\n * An audience larger than one send may carry is delivered across several\n * batches; an audience larger than THIS is one the sender cannot resolve at\n * all, and batching does not change that. It is the same number as\n * `CAMPAIGN_REACH_CEILING`, taken from the one place both are stated.\n */\nconst AUDIENCE_SCAN_CEILING = EMAIL_MAX_AUDIENCE_PER_SEND\n\n/**\n * Page a query to exhaustion in document-name order.\n *\n * ## Why the read has to be ordered at all\n *\n * Firestore answers a `limit()` with no `orderBy` in document-id order, and\n * these ids are generated — so the old bare `limit(1000)` / `limit(5000)`\n * picked an arbitrary slice of any audience larger than the window, and the\n * merchant was told that slice was the whole audience. A cursor needs an\n * ordering regardless; this makes the selection explicable (\"the first N by\n * document name\") and stable across sends instead of merely bounded.\n *\n * ## Why the document NAME and not a date\n *\n * `orderBy(field)` drops every document that lacks that field, so ordering an\n * audience newest-first would silently un-invite people. There is no field\n * every writer of these four collections sets: list members carry `addedAt`\n * only when `enrollListMember` CREATED the row, and the newsletter handler\n * that wrote the collection before it stored `{ email, name, source }` and no\n * date at all — so `orderBy('addedAt')` would drop every newsletter\n * subscriber from every list campaign. `__name__` is the one key every\n * document has, and an unfiltered collection ordered by it needs no index.\n *\n * `startAfter` takes the SNAPSHOT rather than its id, so the cursor keeps\n * working if a filter is ever added ahead of the ordering.\n *\n * @returns the documents, and whether {@link AUDIENCE_SCAN_CEILING} — rather\n * than the end of the collection — is what stopped the sweep. The\n * flag is settled by one extra single-document read, so \"more than\n * 5,000\" is never claimed of a collection holding exactly 5,000.\n */\nasync function sweepAudience(base: FirebaseFirestore.Query): Promise<{\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n truncated: boolean\n}> {\n const ordered = base.orderBy(\n firebaseAdmin.firestore.FieldPath.documentId(),\n )\n const docs: FirebaseFirestore.QueryDocumentSnapshot[] = []\n let cursor: FirebaseFirestore.QueryDocumentSnapshot | undefined\n for (;;) {\n const page = await (cursor ? ordered.startAfter(cursor) : ordered)\n .limit(AUDIENCE_PAGE_SIZE)\n .get()\n docs.push(...page.docs)\n cursor = page.docs[page.docs.length - 1]\n if (!cursor || page.docs.length < AUDIENCE_PAGE_SIZE) {\n return { docs, truncated: false }\n }\n if (docs.length >= AUDIENCE_SCAN_CEILING) {\n const probe = await ordered.startAfter(cursor).limit(1).get()\n return { docs, truncated: probe.docs.length > 0 }\n }\n }\n}\n\n/**\n * Stable doc id for a suppression entry (emails are PII — hash them).\n *\n * A WRITER's derivation now — `email-events.ts` files bounces and complaints\n * under it. The send path reads through `emailSuppressionKey`, which hashes\n * the same trimmed, lowercased form and additionally refuses to guess an id\n * for a value that is not an address.\n */\nexport function suppressionId(email: string): string {\n return createHash('sha256').update(email.toLowerCase()).digest('hex')\n}\n\n/** HMAC for unsubscribe links; env-gated on the shared secret. */\nexport function unsubscribeSignature(\n hostId: string,\n email: string,\n secret: string,\n campaignId?: string,\n topicId?: string,\n): string {\n /*\n * Delegated to the module that owns the signed subject.\n *\n * The campaign sender mints links, the unsubscribe handler verifies them,\n * and the marketing gate mints them for every other audience path — three\n * parties to one HMAC subject, which stays correct only while there is one\n * implementation of it. The name stays exported because it is this\n * module's published surface.\n */\n return sharedUnsubscribeSignature(hostId, email, secret, campaignId, topicId)\n}\n\n/** Send failures carry the HTTP status the API route should answer. */\nexport class CampaignSendError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n ) {\n super(message)\n }\n}\n\n/**\n * The platform send rate had no room for this campaign THIS HOUR (AGL-2409).\n *\n * A subclass rather than a status code the caller has to recognise, because\n * the difference it carries is not \"which HTTP number\" — it is **retry, do not\n * fail**. A scheduled campaign that trips the hourly ceiling must go back to\n * `scheduled` and be picked up by the next 15-minute run; marking it `failed`,\n * which is what every other `CampaignSendError` correctly does, would turn a\n * ramp into a lost campaign that a merchant has to notice and re-create.\n *\n * Thrown ONLY before anything has been sent, so a caller that retries cannot\n * double-send.\n */\nexport class CampaignSendDeferredError extends CampaignSendError {\n constructor(\n message: string,\n /** When the hourly window rolls. */\n public readonly retryAtMs: number,\n ) {\n super(message, 429)\n }\n}\n\nexport interface CampaignSendOptions {\n hostId: string\n subject: string\n /**\n * The MESSAGE of a plain-text email — what recipients read, and what the\n * HTML part is synthesized from.\n *\n * Not read on a designed send, where the nodes are the message. The route\n * refuses a request carrying both rather than accepting one and dropping it,\n * which is what it used to do.\n */\n body: string\n audience: string\n segmentId?: string\n listId?: string\n /**\n * The stream this campaign belongs to, chosen in the composer.\n *\n * Resolved to {@link DEFAULT_CAMPAIGN_TOPIC_ID} when absent, so every send\n * belongs to some topic: a campaign with none would mint an unsubscribe link\n * the preference page can render but not place, offering the recipient a\n * catalog without saying which entry the message in front of them was.\n */\n topicId?: string\n /**\n * Resolve the audience and return the count WITHOUT sending anything\n * (AGL-2178). Returns before the first write, so it mints no campaign\n * id and touches no counter.\n */\n dryRun?: boolean\n emails?: string[]\n campaignId?: string\n experimentId?: string\n /**\n * Designed email template (AGL-349): screen id of a besigner email\n * document, which is what makes this a DESIGNED send.\n *\n * The nodes produce both parts an inbox receives — the HTML and a plain-text\n * rendering of the same design — so `body` has no job here and is not read.\n * `plainText` below is the one thing an author may substitute.\n */\n templateScreenId?: string\n /**\n * The author's own PLAIN-TEXT PART for a designed email.\n *\n * Empty means the design generates it, which is the default and what every\n * designed campaign did before this existed. Read only on the designed\n * path: a plain-text campaign's text part IS its body, and a second string\n * claiming to be the text part beside it would be the two-sources problem\n * again under a new name.\n */\n plainText?: string\n /**\n * The sender's DISPLAY NAME for this campaign, overriding the org's\n * branding default.\n *\n * A display name and nothing more: `applyFromName` keeps the verified\n * address it is applied to, so this cannot move the mail onto a domain the\n * org has not proved. The route strips control characters before it gets\n * here, because the value is merchant-typed and lands in a header.\n */\n fromName?: string\n /** Where replies go, when it is not the sending address. */\n replyTo?: string\n /**\n * WHICH OF THE SITE'S SENDERS THIS EMAIL LEAVES AS —\n * `hosts/{hostId}/senders/{senderId}`.\n *\n * The mailbox, and only the mailbox. A sender also carries a display name\n * and a reply address, and those stay per-send fields resolved from\n * {@link fromName} and {@link replyTo}: the composer seeds them from the\n * chosen sender so a merchant sees what will go out, and the send records\n * what was submitted, which is what keeps the stored report and the message\n * one answer rather than two.\n *\n * ## An id, which is not the input path that was closed\n *\n * `req.body.sendingIdentity` is read by nothing, and a per-send local part\n * would reopen it: a mailbox is where a bounce returns and where a client\n * ignoring `Reply-To:` answers, so one that exists in a single campaign's\n * headers is an address nobody serves. A sender id names a mailbox that was\n * configured once, validated once and stored on this site under an\n * `org.settings` gate. The set of addresses this field can reach is the set\n * an org admin already approved.\n *\n * Absent means the site's DEFAULT sender, which is the projection the host\n * document has always carried. Naming a sender this site does not hold is\n * REFUSED — see the resolution below — rather than falling back to it.\n */\n senderId?: string\n /**\n * The CAMPAIGN this send belongs to — `hosts/{hostId}/emailCampaigns/{id}`.\n *\n * Not the send's own id, which is what `campaignId` means here and what\n * every delivered unsubscribe link carries as `cid`. Absent on a send\n * composed outside a campaign, and on every send that predates containers;\n * the campaigns list adopts those as a campaign of one at read time rather\n * than rewriting them.\n */\n emailCampaignId?: string\n /**\n * The preview line inboxes show after the subject. Overrides a designed\n * template's own, and gives a plain-text campaign one at all.\n */\n preheader?: string\n /**\n * What the merchant called this email, for the record rather than for the\n * recipient.\n *\n * Never leaves the console — it is not the subject and reaches no header,\n * so it is stored as typed apart from a length cap. Absent on every send\n * composed from a campaign, where the campaign is what carries the name.\n */\n displayName?: string\n /** Test sends (AGL-349) skip the campaign record and stats. */\n recordCampaign?: boolean\n /**\n * SEND THIS EMAIL AGAIN, TO PEOPLE IT HAS NOT REACHED.\n *\n * Set with a `campaignId` naming a send that has already gone out. The send\n * is not copied and no second document is made: this send addresses the\n * same audience under the same id, minus everyone the earlier sends\n * reached, and ADDS to the counters already on the record.\n *\n * ## Why the same document, rather than a new send\n *\n * Two reasons, and both are properties a copy would break.\n *\n * The unsubscribe link is the first. Every message this email has already\n * delivered carries `cid={campaignId}` inside its own HMAC, and those\n * messages sit in inboxes forever. A follow-up under a new id would be a\n * second `cid` for one email — two opt-out scopes for one mailing, and an\n * unsubscribe rate split across two records neither of which is the answer.\n *\n * The report is the second. Opens and clicks are attributed by the\n * `campaignId` tag on the delivered message, so a copy would collect the\n * follow-up's engagement on a document whose `sent` counts only the\n * follow-up. Keeping one document keeps every rate over a denominator that\n * covers the same mail the numerator does — see the additive write at the\n * bottom of this function for the arithmetic that holds it.\n *\n * ## What it does NOT relax\n *\n * Nothing. The follow-up runs the whole of this function: the consent\n * split, both suppression lists, the topic filter, the monthly pre-check\n * and reservation, the platform and per-workspace hourly ceilings, and the\n * per-message governor. Its only additions are the subtraction below and\n * the shape of the write at the end.\n */\n followUp?: boolean\n /**\n * THE NEXT BATCH OF AN EMAIL THAT IS STILL GOING OUT.\n *\n * Set by the scheduled-campaign processor, never by a request. An audience\n * larger than {@link EMAIL_MAX_RECIPIENTS_PER_SEND} is delivered across\n * several invocations; each one addresses the people the earlier ones did\n * not, and the campaign is written back as `scheduled` until nothing is\n * left. See the batch plan at the bottom of this function.\n *\n * ## Why it is not {@link followUp}\n *\n * They share the subtraction and the additive write, and they differ on the\n * two things that matter. A follow-up is a MERCHANT's act on an email that\n * has finished — it subtracts who was reached, and somebody suppressed at\n * the time who has since been released is entitled to get it. A batch is\n * one email still in flight — it subtracts everyone the email has SETTLED,\n * reached or refused, because a refused address sitting at the head of a\n * stable order would consume a slot in every remaining batch and, in\n * enough numbers, stop the campaign dead having mailed nobody.\n *\n * They also record differently. A follow-up measures a NEW population, so\n * the audience size and the consent split add. A batch measures a slice of\n * the population the first batch already measured, so those figures are\n * left exactly as the first batch wrote them.\n */\n continuation?: boolean\n /**\n * ONE address the caller has been proved to be entitled to proof to, for\n * the composer's test send. Exempts exactly this address from the\n * marketing-consent rule.\n *\n * The route decides entitlement and this file decides what the exemption\n * can do. See {@link eligibleProofAddress} for the first half and the\n * carve-out at the consent join for the second — including the two\n * properties that keep the exemption one address wide however the caller\n * arrived at it.\n */\n proofFor?: string\n /**\n * Test send only: render every message as if it were addressed to this\n * person, while DELIVERING to the recipient resolved in the ordinary way.\n *\n * What a merge tag resolves to is the whole question a proof answers. A\n * test rendered against the tester shows `{{firstName|there}}` falling back\n * for an audience whose contacts all have names, which reports the merge\n * tags as broken when they work — and, more expensively, hides the reverse.\n *\n * It changes the RENDER and nothing else. The unsubscribe link, the\n * `List-Unsubscribe` header and the suppression key are all minted from the\n * delivery address, deliberately: a proof carrying the persona's opt-out\n * link would let the person testing the email unsubscribe a real contact by\n * clicking a link in their own inbox.\n */\n proofPersona?: { email: string; name?: string }\n /*\n * THERE IS NO `sendingIdentity` OPTION.\n *\n * There was one, with exactly two values: empty for the site's standing\n * selection, and `platform` for the shared Aglyn domain. The second value\n * is gone, and with it the whole option, because a campaign is a site\n * talking to its own audience and `aglyn.com` is where Aglyn's billing and\n * account mail leaves from. A merchant choosing to put their campaign there\n * is choosing to charge their list's complaint rate against every other\n * customer's password reset — which is not a decision that is theirs to\n * make, however deliberately they make it.\n *\n * Deleting it rather than validating it against a plan is what makes that a\n * property instead of a rule. The address a campaign leaves on is now\n * resolved from the host document alone, in one place, and there is no\n * field a request can carry that changes it.\n */\n /** Recorded as `sentBy`; the scheduler passes the scheduling user. */\n senderUid: string\n}\n\n/**\n * Loads a designed email template's nodes + referenced products for the\n * render pipeline. Throws 400 when the screen isn't an email document.\n */\nasync function loadEmailTemplate(hostId: string, screenId: string) {\n const firestore = firebaseAdmin.app().firestore()\n const screenRef = firestore\n .collection('hosts')\n .doc(hostId)\n .collection('screens')\n .doc(screenId)\n const screenSnapshot = await screenRef.get()\n if (!screenSnapshot.exists) {\n throw new CampaignSendError('Unknown email template', 400)\n }\n const versionId = screenSnapshot.get('versionId')\n const versionSnapshot = versionId\n ? await screenRef.collection('versions').doc(String(versionId)).get()\n : null\n /**\n * Decoded, because a `kind: 'email'` screen is a SCREEN document and its\n * versions are compressed msgpack `Bytes` from the first designer save\n * onward (AGL-1394). `createEmailScreen` writes it under\n * `hosts/{h}/screens/{id}` and the Emails list opens it in the SCREEN\n * besigner, which saves through `use-screen-version`'s converter —\n * `Bytes.fromUint8Array(compress(nodes))`. Only the very first version,\n * seeded from a JSON body through `/api/hosts/versions`, is a plain map.\n *\n * `publish-email-template.ts` reading `nodes` raw is not evidence that this\n * one may: that reads `emailTemplates`, a different collection whose\n * besigner saves with a bare `setDoc` and no converter.\n *\n * The guard below is why this was silent rather than loud. `Object.keys` of\n * a Buffer yields the byte INDICES, so a compressed version read as a\n * populated template, `Object.values` found no `emailProduct` node, and a\n * designed campaign went out to real customer inboxes with its product\n * blocks missing — discovered by the recipients. Decoding first is also what\n * makes the guard mean something: `decodeStoredNodes` returns null for an\n * undecodable payload, so the send is refused instead of mailed empty.\n */\n const nodes = (decodeStoredNodes(versionSnapshot?.get('nodes')) ??\n {}) as Record<string, any>\n if (!Object.keys(nodes).length) {\n throw new CampaignSendError('The email template is empty', 400)\n }\n /**\n * The marketplace kill switch, reaching an email somebody already installed\n * (AGL-657's copy-on-install is what makes this necessary).\n *\n * An installed starter is a copy in this site's own screens, so every other\n * marketplace lever — unpublish, takedown, a rejected version — stops at the\n * storefront and is felt by nobody who already has the design. The kill is\n * the one that has to reach a tenant who installed last week and is sending\n * today, and this is the only chokepoint every campaign passes through.\n *\n * It refuses the SEND and leaves the document alone. The tenant keeps the\n * design, keeps editing it, keeps previewing it; what they cannot do is put\n * it on the sending domain every other tenant shares. Reaching into somebody\n * else's content to enforce a decision about a third party's artifact would\n * take the wrong thing away.\n *\n * Read off the VERSION first, then the screen: the version is the document\n * these bytes came out of, and a screen whose design was later replaced\n * wholesale should be judged on what it is now. Costs one document read on a\n * design that carries no marketplace provenance at all, which is every email\n * a site wrote itself — the `listingId` guard inside `emailStarterSendBlock`\n * is what keeps that read from happening.\n */\n const installedFrom = (versionSnapshot?.get('installedFrom') ??\n screenSnapshot.get('installedFrom')) as\n | { listingId?: string | null; version?: string | null }\n | undefined\n if (installedFrom?.listingId) {\n const revocation = (\n await firestore.collection('revocations').doc(installedFrom.listingId).get()\n ).data() as PluginRevocation | undefined\n const block = emailStarterSendBlock({ installedFrom, revocation })\n if (block) throw new CampaignSendError(block.reason, 409)\n }\n // Resolve emailProduct references (by id — rename-safe, AGL-343).\n const productIds = [\n ...new Set(\n Object.values(nodes)\n .filter((node: any) => node?.componentId === 'emailProduct')\n .map((node: any) => String(node?.props?.productId ?? ''))\n // AGL-1771: a besigner node prop is merchant-authored and reaches\n // `.doc()` below, where a slash-bearing value throws and turns the\n // whole send into a 500. Dropped rather than refused: the block simply\n // resolves to no product, exactly as it does for a deleted one, and a\n // designed campaign is not worth blocking over one bad reference.\n .filter(isDocumentId),\n ),\n ].slice(0, 20)\n const products: Record<string, EmailRenderProduct> = {}\n await Promise.all(\n productIds.map(async (productId) => {\n // Asked of whichever plugin keeps products: its name, its \"from\" price\n // as that plugin words it, its image and where it is read. A site with\n // no such plugin, and a product since deleted, both resolve to no card —\n // the block draws nothing, as it always has for a missing product.\n const card = await readPluginRecordCard('product', { hostId, id: productId })\n if (!card) return\n products[productId] = {\n name: card.title,\n priceLabel: card.caption,\n imageUrl: card.imageUrl,\n url: card.path,\n }\n }),\n )\n return {\n nodes,\n products,\n subject: String(screenSnapshot.get('emailSubject') ?? ''),\n preheader: String(screenSnapshot.get('emailPreheader') ?? ''),\n }\n}\n\n/**\n * Campaign delivery core (AGL-161, extracted for AGL-272): resolves the\n * audience server-side, drops suppressed addresses, enforces the plan's\n * monthly send cap, personalizes merge tags per recipient, and delivers\n * through Resend with a signed unsubscribe link. Shared by the\n * authenticated send route and the scheduled-campaign processor. The\n * caller owns authorization.\n */\nexport interface CampaignSendResult {\n campaignId: string\n /** Addresses this send ADDRESSED — the audience after the per-send cap. */\n recipients: number\n sent: number\n /**\n * The whole audience, deduplicated and validated, BEFORE the per-send cap.\n *\n * Reported separately from `recipients` because the two differ whenever an\n * audience is larger than one send may carry, and a merchant who is only\n * shown the smaller number has no way to learn that the rest were never\n * mailed. `recipients` of 500 against an `audienceSize` of 3,000 is the\n * whole point of the field.\n *\n * Named for the SIZE because `audience` on the options and on the stored\n * campaign is the audience KIND — `'leads'`, `'list'` — and one word\n * meaning both a name and a count on the same send path is how the two get\n * read into each other.\n */\n audienceSize: number\n /**\n * `audienceSize` is a FLOOR, not a total: the resolution stopped at its\n * read ceiling with documents still unread. Absent means it is exact.\n */\n audienceTruncated?: boolean\n /**\n * Dry run only (AGL-2178): recipients that will actually be mailed —\n * after the consent join, the per-send cap and both suppression lists.\n */\n sendable?: number\n /** Dry run only: of `audienceSize`, how many carry a recorded consent basis. */\n consented?: number\n /**\n * Dry run only: of `consented`, how many hold a basis an OPERATOR asserted\n * on their behalf rather than one they gave — a backfill over seed data,\n * say. A subset of `consented` and not a fourth population.\n */\n consentedByOperator?: number\n /**\n * Dry run only: of `audienceSize`, how many are reachable only because\n * consent enforcement is not retroactive. This is the population a strict\n * policy would remove.\n */\n grandfathered?: number\n /** Dry run only: how many of `audienceSize` the consent rule refused. */\n consentWithheld?: number\n /**\n * Dry run only: how many of `recipients` are suppressed or off-topic.\n *\n * Not netted with {@link cadenceHeld}: leaving this site and asking it for\n * less mail are different statements, and only one of them is a list a\n * merchant has to rebuild.\n */\n suppressed?: number\n /**\n * Dry run only: how many of `recipients` asked this site for mail less\n * often than this send would arrive.\n *\n * They stay on every audience they were on and are mailed by the next\n * campaign that falls outside their interval — the filter refuses a SEND,\n * never a person.\n */\n cadenceHeld?: number\n /** Dry run only: which sending identity this campaign would leave on. */\n identity?: string\n /**\n * Dry run only: which KIND of identity this campaign would leave on.\n *\n * The union from the resolver rather than a local copy of it, so a new arm\n * cannot be added there and silently narrowed here. `'shared'` never reaches\n * a real campaign — the resolution above declares `purpose: 'marketing'`, and\n * a pooled identity refuses before this is assembled — but the type says what\n * the resolver can return, not what this path expects to see.\n */\n identitySource?: SendingIdentitySource | null\n dryRun?: boolean\n /** Recipients the hourly governor refused mid-batch (AGL-2409). */\n deferred?: number\n /**\n * Follow-up only: people in the audience this email had already reached, and\n * which this send therefore did not address.\n *\n * Reported rather than netted away, because it is the number that explains\n * the other ones. A follow-up over a 3,000-person list that addresses 40\n * people has not failed — 2,960 of them already have the email — and\n * without this figure the merchant cannot tell that from a broken audience.\n */\n alreadyReached?: number\n /** Whether this send added to an existing email rather than starting one. */\n followUp?: boolean\n /**\n * People this email has resolved and not yet addressed.\n *\n * Non-zero on a send that is still going out, and the figure that makes\n * \"reached 500 of 3,000\" a sentence rather than a truncation. Zero when the\n * email is finished.\n */\n remaining?: number\n /** True when another batch of this email will run on its own. */\n resuming?: boolean\n /** When the next batch may go, ms. Present only while {@link resuming}. */\n nextAtMs?: number\n /** Batches this email has run, including this one. */\n batch?: number\n}\n\nexport async function performCampaignSend(\n options: CampaignSendOptions,\n): Promise<CampaignSendResult> {\n const unsubscribeSecret =\n process.env.EMAIL_UNSUBSCRIBE_SECRET || process.env.CRON_SECRET\n if (!isEmailConfigured() || !unsubscribeSecret) {\n throw new CampaignSendError(\n 'Campaigns are not configured (RESEND_API_KEY, USAGE_EMAIL_FROM, ' +\n 'EMAIL_UNSUBSCRIBE_SECRET).',\n 501,\n )\n }\n const { hostId, subject, body, audience } = options\n\n // AGL-1771: every optional id on `options` becomes a `.doc()` argument\n // below, and `.doc()` appends a SLASH-SEPARATED path rather than taking one\n // opaque id — so an unvalidated one names the nesting as well as the\n // document. `campaignId` is the one that matters most: it is WRITTEN at the\n // bottom of this function, so `a/b/c` filed the campaign at\n // `campaigns/a/b/c`, beneath a document that does not exist and therefore\n // invisible to the merchant's own campaigns list — and it is the same value\n // that comes back on every Resend tag days later (AGL-1768), which is why\n // tracing where an id was MINTED matters more than where it was last\n // handled. The rest are read-only, where the cost is a 500 dressed up as an\n // outage rather than a stray document; refused here so the caller is told\n // which id was wrong.\n //\n // `hostId` is deliberately NOT guarded here, and that is measured rather\n // than overlooked: both callers prove it first — the handler resolves the\n // host document and checks the caller's role on it before calling in, and\n // the scheduled processor passes `hostRef.id` off a document it just read. A\n // guard here could not fail today. A third caller would need to earn that.\n for (const [name, value] of [\n ['campaignId', options.campaignId],\n ['experimentId', options.experimentId],\n ['templateScreenId', options.templateScreenId],\n ['segmentId', options.segmentId],\n ['listId', options.listId],\n ] as const) {\n if (value && !isDocumentId(value)) {\n throw new CampaignSendError(`Invalid ${name}`, 400)\n }\n }\n /*\n * `topicId` is checked against its OWN predicate, not `isDocumentId`.\n *\n * It is a path component like the others, but it is also a colon-joined\n * component of the unsubscribe link's signed subject, and `isDocumentId`\n * permits a colon. Signing one would let a single subject string be read as\n * two different parameter tuples — see `signedSubject` in the email plugin's\n * `unsubscribe-link.ts`. Refused at the point the topic ENTERS the send, so\n * the link that leaves it is unambiguous by construction.\n */\n if (options.topicId && !isEmailTopicId(options.topicId)) {\n throw new CampaignSendError('Invalid topicId', 400)\n }\n const topicId = options.topicId || DEFAULT_CAMPAIGN_TOPIC_ID\n /**\n * Whether this send ADDS to an email that already exists, rather than\n * starting one.\n *\n * True for a merchant's follow-up and for an automatic batch. The two\n * differ in what they subtract and in what they record — see\n * `CampaignSendOptions.continuation` — but they agree on the one thing this\n * flag decides: every counter on the campaign is an increment rather than a\n * replacement, so no rate can come out over a denominator that counts less\n * mail than its numerator.\n */\n const addsToExistingSend = Boolean(options.followUp || options.continuation)\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n throw new CampaignSendError('Unknown site', 404)\n }\n\n /*==========================================\n * THE FOLLOW-UP'S ADMISSION CHECKS.\n *\n * Here rather than in the route, and re-run even though the route has\n * already loaded the same document to build these options: the properties\n * below are the ones that decide whether somebody gets a second copy of an\n * email, and a check a caller performs is a check the next caller forgets.\n * Two reads of one small document on a deliberate button press is what that\n * costs.\n *=========================================*/\n let reachedKeys: ReadonlySet<string> | null = null\n /**\n * Batches this email has already run, so the plan below can tell the second\n * from the twentieth. Zero on every first send.\n */\n let batchesSoFar = 0\n /*==========================================\n * THE NEXT BATCH'S ADMISSION CHECKS.\n *\n * The same two properties the follow-up checks below, read off the same\n * record and refused for the same reasons — with `skipped` folded into the\n * subtraction, which is the whole difference between the two. See\n * `CampaignSendOptions.continuation`.\n *=========================================*/\n if (options.continuation) {\n if (!options.campaignId) {\n throw new CampaignSendError(\n 'A batch has to name the email it is continuing',\n 400,\n )\n }\n const sendSnapshot = await hostRef\n .collection('campaigns')\n .doc(options.campaignId)\n .get()\n if (!sendSnapshot.exists) {\n throw new CampaignSendError('Unknown email', 404)\n }\n /*\n * A batch continues an email the processor has CLAIMED. `sending` is what\n * that claim writes; `scheduled` is accepted beside it so a campaign that\n * was written back by a deferral and is picked up again is not refused by\n * its own retry.\n */\n const status = String(sendSnapshot.get('status') ?? '')\n if (status !== 'sending' && status !== 'scheduled') {\n throw new CampaignSendError(\n 'This email is not in the middle of being sent',\n 400,\n )\n }\n /*\n * FAILS CLOSED, exactly as the follow-up does. The record of who this\n * email has already mailed is the only thing standing between a resumed\n * send and a second copy in somebody's inbox, and a batch that cannot\n * read it must not run.\n */\n const settled = await readCampaignSettled(\n hostId,\n options.campaignId,\n firestore,\n )\n const sentSoFar = Number(sendSnapshot.get('stats')?.sent ?? 0)\n if (!campaignReachCovers(settled.reached, sentSoFar)) {\n throw new CampaignSendError(\n 'This email does not have a complete record of who it reached, so ' +\n 'the rest of it cannot be sent without risking a second copy for ' +\n 'somebody who already has it.',\n 409,\n )\n }\n if (campaignSettledSize(settled) >= CAMPAIGN_REACH_CEILING) {\n throw new CampaignSendError(\n `This email has already addressed ${CAMPAIGN_REACH_CEILING.toLocaleString()} ` +\n 'people, which is the most one email may reach. Compose a new ' +\n 'email for the rest of the audience.',\n 409,\n )\n }\n reachedKeys = new Set([...settled.reached, ...settled.skipped])\n batchesSoFar = Math.max(\n 0,\n Math.floor(Number(sendSnapshot.get('resume')?.batch ?? 0)) || 0,\n )\n }\n /**\n * A batch that found nothing left to do: the email is FINISHED, not broken.\n *\n * Every \"there is nobody to send to\" refusal below is a 400 that tells a\n * merchant their audience is empty — which is the right answer to a send\n * they just pressed, and the wrong one to a batch of an email that has\n * already delivered two thousand messages. The processor would mark it\n * `failed`, and a campaign that reached most of its list would be filed\n * under the same word as one that never left.\n *\n * So a continuation closes the email out instead, with the counters the\n * earlier batches wrote left exactly as they are.\n */\n const finishContinuation = async (): Promise<CampaignSendResult> => {\n const sendId = String(options.campaignId ?? '')\n await hostRef\n .collection('campaigns')\n .doc(sendId)\n .set(\n {\n status: 'sent',\n resume: { remaining: 0, batch: batchesSoFar + 1, nextAtMs: 0 },\n lastSentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n return {\n campaignId: sendId,\n recipients: 0,\n audienceSize: 0,\n sent: 0,\n remaining: 0,\n resuming: false,\n batch: batchesSoFar + 1,\n }\n }\n\n if (options.followUp) {\n if (!options.campaignId) {\n throw new CampaignSendError(\n 'A follow-up has to name the email it is adding to',\n 400,\n )\n }\n const sendSnapshot = await hostRef\n .collection('campaigns')\n .doc(options.campaignId)\n .get()\n if (!sendSnapshot.exists) {\n throw new CampaignSendError('Unknown email', 404)\n }\n /*\n * Only a SENT email has anybody to add to. A scheduled one has not gone\n * out — sending it now is what the scheduler is for, and doing it here\n * would deliver it twice; a canceled one was withdrawn on purpose.\n */\n if (sendSnapshot.get('status') !== 'sent') {\n throw new CampaignSendError(\n 'Only an email that has already been sent can go to more people',\n 400,\n )\n }\n /*\n * FAILS CLOSED. `readCampaignReach` throws rather than answering\n * \"nobody\", and that throw is deliberately not caught: the whole feature\n * rests on being able to name who already has this email, and a send that\n * cannot is a send that must not happen.\n */\n const keys = await readCampaignReach(hostId, options.campaignId, firestore)\n const sentSoFar = Number(sendSnapshot.get('stats')?.sent ?? 0)\n if (!campaignReachCovers(keys, sentSoFar)) {\n throw new CampaignSendError(\n 'This email does not have a complete record of who it reached, so ' +\n 'it cannot be sent to more people without risking a second copy ' +\n 'for somebody who already has it. Compose a new email to the ' +\n 'people you want to add.',\n 409,\n )\n }\n if (keys.size >= CAMPAIGN_REACH_CEILING) {\n throw new CampaignSendError(\n `This email has already reached ${keys.size.toLocaleString()} ` +\n `people, which is the most one email may reach ` +\n `(${CAMPAIGN_REACH_CEILING.toLocaleString()}). Compose a new email ` +\n 'for the rest of the audience.',\n 409,\n )\n }\n reachedKeys = keys\n }\n\n /*\n Audience resolution. Names ride along for merge tags (AGL-272).\n\n ⚠️ THE FIELD NAME IS PER COLLECTION AND THEY DO NOT AGREE (AGL-2303).\n `contacts` and `leads` store `name`; `siteMembers` stores `displayName`.\n A merge tag whose source field does not exist does not error — it\n substitutes an empty string into mail that has already been sent. Whenever\n an audience is added here, check what its collection actually writes.\n */\n let recipients: string[]\n /**\n * Set when {@link AUDIENCE_SCAN_CEILING} stopped the resolution, so\n * `audience` below is a floor and every number derived from it says so.\n * A `manual` audience arrives whole in the request and can never be one.\n */\n let audienceTruncated = false\n /**\n * The list's name as it stands at the moment of the send.\n *\n * Recorded on the campaign rather than looked up when a report is read, for\n * the reason every other send-time figure is recorded: a list can be\n * renamed or deleted, and resolving the name later either rewrites the\n * history of a campaign that went out months ago or loses it entirely. The\n * campaign went to the list under this name, and that stays true.\n */\n let listName = ''\n const names = new Map<string, string>()\n /*\n * The consent facts ride out of the audience sweep with the names\n * (`docs/specs/email-overhaul.md` §3f), because paging the audience is the\n * only point in the send where the person's DOCUMENT is in hand.\n *\n * That placement is the correction §3f carries: consent is a property of\n * the PERSON, knowable while the sweep is already reading them, where\n * suppression is a per-address keyed lookup deliberately deferred until\n * after the cap. Reading consent back later would be a second pass over\n * every silo, per campaign, to recover data this loop already had.\n *\n * An audience whose members carry no consent field records an `unrecorded`\n * basis — a THIRD state, handled by the policy, and never a quiet `true`.\n */\n /*\n * THE CONTROLLER THIS SEND IS MADE BY — the declared group of sites that\n * are one sender, or this site alone.\n *\n * Resolved before the sweep because every consent read below is about a\n * controller and not about a site: three sites a business declared as one\n * sender share a basis, and twelve unrelated client brands in an agency's\n * account share nothing. The org read is deduped per request, so the policy\n * lookup further down pays nothing for this.\n */\n const consentGroup = await consentGroupForSite(hostId)\n const consent = new Map<string, MarketingConsentRecord>()\n const collectConsent = (email: string, data: unknown) => {\n const cleaned = email.trim().toLowerCase()\n if (!cleaned) return\n consent.set(\n cleaned,\n readMarketingBasis(\n data as Record<string, unknown> | null | undefined,\n consentGroup,\n ),\n )\n }\n const collectName = (email: string, name: unknown) => {\n const cleaned = email.trim().toLowerCase()\n if (cleaned && typeof name === 'string' && name.trim()) {\n names.set(cleaned, name.trim())\n }\n }\n if (audience === 'leads') {\n // Through the scoped org query (AGL-3275), as the contacts audience below\n // already is: the collection is org-wide, so an unnarrowed sweep would\n // MAIL a sibling brand's leads under this site's campaign.\n const leads = await sweepAudience(\n (await orgDataQueryForHost(hostId, 'leads')).query,\n )\n audienceTruncated = leads.truncated\n recipients = leads.docs.map((doc) => {\n const email = String(doc.get('email') ?? '')\n collectName(email, doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else if (audience === 'members') {\n const members = await sweepAudience(hostRef.collection('siteMembers'))\n audienceTruncated = members.truncated\n recipients = members.docs.map((doc) => {\n const email = String(doc.get('email') ?? '')\n /*==========================================\n * `displayName`, NOT `name` (AGL-2303).\n *\n * `siteMembers` has never had a `name` field — sign-up, the account\n * page and the admin password route all write `displayName`. So this\n * read matched nothing on every member campaign ever sent, `names` was\n * empty for the whole audience, and `{{contact.name}}` and\n * `{{contact.firstName}}` rendered as EMPTY STRINGS in mail that went\n * out to real people. `resolveMergeTags` substitutes rather than\n * failing, so nothing errored and nothing looked wrong here.\n *\n * `name` is kept as a fallback and read second: a lead promoted to a\n * member, or a future writer, may carry either.\n *=========================================*/\n collectName(email, doc.get('displayName') ?? doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else if (audience === 'segment') {\n // Contact segments (AGL-199): resolve the saved filter against the\n // contacts collection server-side.\n const segmentId = String(options.segmentId ?? '')\n const segmentSnapshot = segmentId\n ? await (await orgDataCollectionForHost(hostId, 'contactSegments')).doc(segmentId).get()\n : null\n // A doc get cannot carry the scope filter, so check after the read\n // (AGL-1039). Reported as \"unknown\" rather than \"forbidden\": whether a\n // segment exists in another site's scope is not this caller's business.\n if (\n !segmentSnapshot?.exists ||\n !visibleToHost(segmentSnapshot.get('visibleTo'), hostId)\n ) {\n throw new CampaignSendError('Unknown segment', 400)\n }\n const segment = {\n tags: segmentSnapshot.get('tags') ?? [],\n sources: segmentSnapshot.get('sources') ?? [],\n }\n // Scoped (AGL-1039): a campaign sent from one site must not reach\n // another site's audience — the agency case is a client's campaign\n // blasting the whole org's contact list.\n /*\n * The only audience whose sweep carries a FILTER, and the reason the\n * ordering is `__name__` rather than a field: Firestore's automatic\n * single-field index for an array member is keyed on that value and the\n * document name, so `array-contains-any` plus `orderBy(__name__)` is\n * served by it. Ordering on any other field would need a composite index\n * per audience — `cloud/firebase-firestore.indexes.json` carries exactly\n * that shape for `media`, and a missing one fails the whole send.\n */\n const contacts = await sweepAudience(\n (await orgDataQueryForHost(hostId, 'contacts')).query,\n )\n /*\n * A segment's membership is decided HERE rather than by the query — the\n * tag and source rules are evaluated in `contactMatchesSegment` against\n * documents the scan already fetched — so the ceiling bounds CONTACTS\n * READ, not contacts matched. A narrow segment over a large org therefore\n * reports a small `audienceSize` with `audienceTruncated` set, reading as\n * \"at least this many, we stopped counting\" and not as a total.\n */\n audienceTruncated = contacts.truncated\n recipients = contacts.docs\n .filter((doc) =>\n contactMatchesSegment(\n { tags: doc.get('tags') ?? [], sources: doc.get('sources') ?? {} },\n segment,\n ),\n )\n .map((doc) => {\n const email = String(doc.get('email') ?? '')\n collectName(email, doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else if (audience === 'list') {\n // Org lists (AGL-254): static audiences enrolled manually or by the\n // enrollList automation step.\n const listId = String(options.listId ?? '')\n const listRef = listId\n ? (await orgDataCollectionForHost(hostId, 'contacts')).parent\n ?.collection('lists')\n .doc(listId)\n : null\n if (!listRef) throw new CampaignSendError('Unknown list', 400)\n // One document beside a sweep that reads up to the audience ceiling, and\n // the only place the name is knowable without a second round trip later.\n listName = String((await listRef.get()).get('name') ?? '')\n const members = await sweepAudience(listRef.collection('members'))\n audienceTruncated = members.truncated\n recipients = members.docs.map((doc) => {\n const email = String(doc.get('email') ?? '')\n collectName(email, doc.get('name'))\n collectConsent(email, doc.data())\n return email\n })\n } else {\n recipients = Array.isArray(options.emails)\n ? options.emails.map((value: unknown) => String(value))\n : []\n }\n /*\n * The AUDIENCE — deduplicated and validated, and deliberately measured\n * BEFORE the per-send cap.\n *\n * The cap and the audience are two different numbers and the composer has\n * always shown only the smaller one, which is how a site with 3,000 leads\n * was told its audience was 500 and never found out the other 2,500 were\n * not being mailed. Every result from here down carries both, so a send can\n * report \"reached N of M\" rather than reporting N as if it were M.\n */\n const addressable = [\n ...new Set(\n recipients\n .map((email) => email.trim().toLowerCase())\n .filter((email) => EMAIL_PATTERN.test(email)),\n ),\n ]\n if (!addressable.length) {\n if (options.continuation) return finishContinuation()\n throw new CampaignSendError('The audience is empty', 400)\n }\n\n /*==========================================\n * THE SUBTRACTION: NOBODY GETS THIS EMAIL TWICE.\n *\n * ## Why it is HERE, above the per-send cap\n *\n * Not a preference — the only position that works. The cap a few lines\n * below takes \"the FIRST N of a stable order\", and that stability is what\n * makes it defensible: two sends of an unchanged audience address the same\n * people. Which means a follow-up that subtracted AFTER the cap would be\n * handed the same first 500 addresses the original send took, discover that\n * all 500 have had the email, and mail nobody — every time, forever, for\n * any audience larger than one send.\n *\n * So the subtraction runs on the whole resolved audience and the cap then\n * takes the first N of what is LEFT, which is the same rule pointed at the\n * remainder.\n *\n * ## What it costs, and why that is the right trade\n *\n * Nothing per address: the reach record is one document, read once above,\n * and the test is a hash and a set lookup. This is the one filter in the\n * send that can afford to run over the whole audience rather than the\n * capped list, which is why suppression stays where it is.\n *\n * ## What `resolved` means from here down\n *\n * The people this send may address — so on a follow-up it is the NEW part\n * of the audience, and every figure derived from it (the consent split,\n * `audienceSize`) describes that part. The write at the bottom adds those\n * figures to the ones already recorded, so the totals on the email cover\n * both sends over two populations that cannot overlap.\n *=========================================*/\n const partitioned = reachedKeys\n ? partitionByCampaignReach(addressable, reachedKeys)\n : { unreached: addressable, alreadyReached: 0 }\n const alreadyReached = partitioned.alreadyReached\n const resolved = partitioned.unreached\n if (!resolved.length) {\n if (options.continuation) return finishContinuation()\n throw new CampaignSendError(\n 'Everyone in this audience has already had this email, so nothing has ' +\n 'been sent.',\n 400,\n )\n }\n\n /*\n * THE CONSENT JOIN (`docs/specs/email-overhaul.md` §3f).\n *\n * `marketingConsent` had seven writers and no reader on any send path, so a\n * recorded opt-OUT reached the same inbox as a recorded opt-in.\n *\n * ## Why HERE, above the cap, and not beside suppression\n *\n * §3f used to say \"after suppression and before the cap\", which is not an\n * order this file has ever had — the cap runs first and suppression after\n * it. Consent belongs at the SWEEP: it is a property of the person, already\n * read into `consent` by the loop above at no extra cost, so filtering on\n * it before the cap means the 500 slots go to people who may actually be\n * mailed. Suppression stays where it is, because it is a keyed lookup per\n * address and moving it up would cost the whole audience in reads on every\n * debounced preview.\n *\n * It is also, necessarily, before the meter claim: a recipient the rule\n * withholds is never counted against the org's monthly allowance. Being\n * charged for mail that policy forbids sending would make the consent rule\n * cost the merchant money as well as reach.\n *\n * ## What it does to an audience that exists today\n *\n * NOT retroactive by default. `resolveMarketingConsentPolicy` answers\n * `mode: 'forward'` for an org that has configured nothing, which keeps\n * every address captured before the cutoff reachable and reports it as\n * grandfathered rather than mailing it silently. The one thing enforced\n * unconditionally is a STORED refusal, which no policy may mail.\n *\n * The retroactive mode can shrink an audience sharply, so it is a stored\n * per-org setting and never a default. The split below is what makes that\n * decision informed — it rides the same readout as `audienceSize`, so a\n * merchant sees which population is which before sending.\n *\n * The org is resolved here rather than at the quota block below because the\n * policy lives on it and this is the first thing that needs it; the quota\n * lines further down reuse the same read rather than taking a second one.\n */\n const orgForHost = await getOrgForHost(hostId).catch(() => null)\n const orgId = String(orgForHost?.orgId ?? '')\n\n /*\n * THE SENDING IDENTITY, and the refusal when it is not usable.\n *\n * Resolved ABOVE the dry run on purpose. `preview` is where a merchant finds\n * out what a send will do before writing copy, so it must answer the same\n * question a real send would — both which identity the mail leaves on, and\n * whether it may leave at all. Resolving after this point would let\n * `preview` report a healthy dry run for a campaign that Send then refuses.\n *\n * The address comes from the org document by way of the host's selection,\n * never from `options`. A `From:` assembled from request input is the\n * spoofing path the verified-identity rule exists to close.\n *\n * A refusal is a 409 rather than a silent no-op because that is the whole\n * point: `USAGE_EMAIL_FROM` was empty in production for weeks and no surface\n * ever said so, since every sender treats mail as best-effort. A tenant\n * whose DNS is unfinished has to be told, by name, at the composer.\n */\n /*\n * The DOMAIN is the site's standing selection, and nothing a request says\n * moves it.\n *\n * No option reaches it. It is read from the host document, so a request\n * cannot name a domain to send as — which is the spoofing path — and cannot\n * drop the selection to reach the shared platform domain either, which is\n * the reputation path. Both used to be one field.\n *\n * The MAILBOX in front of it is chosen per send, and by id: `senderId` names\n * a row in `hosts/{hostId}/senders` that an org admin configured, so it can\n * only reach an address this site was already set up to send as. That is the\n * whole difference between it and the free `sendingIdentity` the route reads\n * from nobody.\n */\n /*\n * WHICH SENDER, and the refusal when the campaign names one this site does\n * not hold.\n *\n * The one line that decides the mailbox a campaign leaves on. A site's\n * senders are `hosts/{hostId}/senders/{senderId}`, and the host's\n * `sendingLocalPart` is the DEFAULT sender's projection — so a send that\n * names nobody resolves exactly as it did before the collection existed,\n * including on a site that has never written to it.\n *\n * An unknown id is REFUSED rather than defaulted, and that is the whole\n * reason this is a read and not a `??`. Quietly sending as the default is\n * the same class of failure as the mailbox validation that used to answer\n * `hello` to a name it could not parse: a merchant is told their campaign\n * went out as the sender they picked, and it did not.\n *\n * Refused HERE, above the dry run, so `preview` answers it too — the\n * composer finds out at the picker rather than from the Send button.\n */\n const senderId = String(options.senderId ?? '').trim()\n const senderSnapshot = senderId\n ? await hostRef.collection(HOST_SENDERS_COLLECTION).doc(senderId).get()\n : null\n if (senderId && !senderSnapshot?.exists) {\n throw new CampaignSendError(\n 'The sender this email is set to go out as is no longer one this site ' +\n 'holds. Pick a sender in the composer, or add it back under ' +\n 'Emails → Sending.',\n 404,\n )\n }\n const chosenSender = senderSnapshot?.exists\n ? readHostSender({\n id: senderId,\n data: senderSnapshot.data() as Record<string, unknown>,\n })\n : null\n\n /*\n * `purpose: 'marketing'` is what makes the SUMMARY name the arrangement. A\n * campaign is the one send site that knows for certain what it is carrying,\n * so it says so, and a site on the pool is told in the composer that its\n * reputation is shared and graded more tightly for it.\n */\n const sendingIdentity = await resolveHostSendingIdentity({\n orgId,\n hostId,\n selectedDomain: hostSnapshot.get('sendingDomain'),\n selectedLocalPart:\n chosenSender?.localPart || hostSnapshot.get('sendingLocalPart'),\n poolMember: hostSnapshot.get('sendingPoolMember'),\n purpose: 'marketing',\n })\n const identityRefusal = sendingIdentityRefusal(sendingIdentity)\n if (identityRefusal) {\n const missing = identityRefusal.missing?.length\n ? ` Missing: ${identityRefusal.missing.join(', ')}.`\n : ''\n throw new CampaignSendError(`${identityRefusal.message}${missing}`, 409)\n }\n /**\n * WHOSE REPUTATION THIS CAMPAIGN SPENDS.\n *\n * A pooled sender shares one domain with every other site that has no domain\n * of its own, so a complaint it earns is charged to their receipts as much as\n * to its own. That asymmetry is the whole reason the pool used to refuse\n * marketing outright, and it is answered by grading rather than by\n * prohibition: on the pool a campaign is held to `strict`, which stops it on\n * the WATCH thresholds — Google's \"keep under\" 0.10% complaint rate and a 5%\n * bounce rate — instead of waiting for the trip levels three and two times\n * higher.\n *\n * On a domain the merchant owns, the org's own setting stands. There the\n * reputation being spent is theirs alone, and how fast they spend it is\n * theirs to decide.\n *\n * It overrides `none` as well, which is the point rather than an oversight:\n * a workspace that has switched its own breaker off must not thereby switch\n * off the one protecting the other sites on its pool member. Same posture as\n * the platform frequency ceiling, which is the same number on every plan for\n * the same reason.\n */\n const reputationPolicy = effectiveReputationPolicy(\n sendingIdentity.source,\n (orgForHost?.org as Record<string, unknown> | undefined)?.[\n 'emailReputationPolicy'\n ],\n )\n const consentPolicy = resolveMarketingConsentPolicy(\n (orgForHost?.org as Record<string, unknown> | undefined)?.[\n 'marketingConsentPolicy'\n ],\n )\n /*\n * THE PROOF CARVE-OUT.\n *\n * A proof delivered to an account holder on this workspace is not a\n * marketing send. The recipient is the person who pressed the button, or a\n * colleague of theirs who already receives this workspace's console mail,\n * and the consent rule exists to protect somebody from mail they did not\n * ask for — which is not what proofing a draft to your own team is.\n *\n * Without this the composer's test send is dead under `strict`: it delivers\n * through the `manual` audience, a hand-typed address is backed by no\n * document, and `unrecorded` is withheld before reaching the clause that\n * grandfathers a record carrying no capture date. Proofing your own email\n * would be refused on consent grounds.\n *\n * ⚠️ TWO PROPERTIES KEEP THE EXEMPTION ONE ADDRESS WIDE, and both are here\n * rather than at the call site, because a caller that could widen it is\n * exactly what this must not be:\n *\n * 1. The address must ALREADY be in the resolved audience. The option can\n * therefore only exempt a recipient, never introduce one — passing an\n * address that is not being sent to does nothing at all.\n * 2. A stored `declined` is still refused, below. A refusal is the one\n * thing no policy may mail, and a proof is not the first exception to\n * it: somebody who declined marketing on this site un-declines rather\n * than being quietly overridden.\n *\n * Neither of them is what stops a merchant proofing to a stranger. That is\n * {@link eligibleProofAddress}, at the route, because it is a question\n * about WHO an address belongs to and this function has no way to ask it.\n * The two here are what hold when that check is wrong.\n */\n const proofFor = String(options.proofFor ?? '')\n .trim()\n .toLowerCase()\n const proofAddress = proofFor && resolved.includes(proofFor) ? proofFor : ''\n /*\n * THE ONE READ THAT MAKES THE `declined` GUARANTEE TRUE.\n *\n * A proof is delivered through the `manual` audience, and a manual audience\n * is a list of ADDRESSES — no documents are swept, so `consent` is empty\n * and every proof address arrives as `unrecorded`. The refusal below was\n * therefore unreachable on the only path that can reach it: the carve-out\n * promised that a stored opt-out still refuses, and nothing ever looked one\n * up.\n *\n * Keyed on the single address and only when a proof is in flight, so the\n * cost is one small lookup on an explicit click rather than anything on the\n * campaign path.\n */\n if (proofAddress && !consent.has(proofAddress)) {\n const stored = await readStoredConsent(hostId, proofAddress)\n if (stored) {\n consent.set(proofAddress, readMarketingBasis(stored, consentGroup))\n }\n }\n if (proofAddress && consent.get(proofAddress)?.basis === 'declined') {\n throw new CampaignSendError(\n `${proofAddress} has a recorded marketing opt-out on this site, so the ` +\n 'test send was not delivered. Proof to an address that has not opted ' +\n 'out, or opt that one back in.',\n 400,\n )\n }\n const consentSplit = splitByMarketingConsent(\n proofAddress ? resolved.filter((one) => one !== proofAddress) : resolved,\n consent,\n consentPolicy,\n consentGroup,\n )\n if (proofAddress) consentSplit.mailable.unshift(proofAddress)\n if (!consentSplit.mailable.length) {\n if (options.continuation) return finishContinuation()\n throw new CampaignSendError(\n 'No recipient in this audience has a marketing consent record, so ' +\n 'nothing has been sent. Add an opt-in checkbox to the form or sign-up ' +\n 'this audience comes from, or send to an audience that has one.',\n 400,\n )\n }\n\n /*==========================================\n * THE TWO PLATFORM CONTROLS THAT SIZE THIS BATCH.\n *\n * Both read the same seven-day window, so they are resolved together and\n * the window is read once. Both are campaign-only by construction — this\n * function is the only caller, and transactional mail cannot reach it.\n *\n * A TEST SEND is exempt from both. It delivers one message to the address\n * of the person who pressed the button, it writes no campaign record, and\n * refusing it would leave a merchant whose list has a problem unable to\n * even look at the email they are trying to fix.\n *=========================================*/\n const platformRate = await readEmailSendRateConfig()\n const proofOnly = options.recordCampaign === false\n /**\n * How many people this batch may address.\n *\n * The per-send cap unless the new-sender ramp is lower, in which case the\n * ramp is what the batch takes and the rest of the audience goes out on the\n * following days. Shrinking rather than deferring is the only shape that\n * works: a workspace on a 200-a-day step would defer a 500-recipient batch\n * every single day and never send anything at all.\n */\n let batchCap = MAX_RECIPIENTS_PER_SEND\n /** Today's ramp, resolved once and claimed against below. */\n let ramp: EmailRampVerdict | null = null\n if (!proofOnly) {\n /** This workspace's seven-day grade, and the window both controls read. */\n const reputation: SenderReputationRead = await readSenderReputation({\n orgId,\n policy: reputationPolicy,\n reinstatedUntilMs: (\n orgForHost?.org as Record<string, unknown> | undefined\n )?.['emailReputationReinstatedUntilMs'],\n })\n /*\n * THE CIRCUIT BREAKER.\n *\n * A 409 rather than a deferral, and that difference is the point. A\n * deferral says \"not this hour\" and retries itself; this says \"not until\n * something changes\", and the thing that has to change is the list. A\n * campaign that rescheduled itself against a tripped breaker would mail\n * the same bad addresses on a timer.\n *\n * NOTHING IS REMOVED. No contact is deleted, no audience is trimmed,\n * nobody is unsubscribed and no list membership moves — the refusal is on\n * the SEND, which is a flow, and refusing a flow strands nobody's data.\n * That is the enforce-at-the-reduction rule (`over-limit.ts`) applied to\n * the one control in this file that could be tempted to break it.\n *\n * The message carries the numbers and what to do about them, because a\n * merchant who cannot send and cannot find out why will open a ticket\n * that says the product is broken.\n */\n if (reputation.blocked) {\n throw new CampaignSendError(reputation.reason, 409)\n }\n /*\n * THE NEW-SENDER RAMP.\n *\n * A workspace created today may not put its whole first import onto the\n * domain every other tenant's receipts leave on. The step it is on is\n * earned by clean volume as well as reached by age, and a workspace past\n * its first week — which is every existing customer, and every org whose\n * record predates the creation timestamp — is not ramped at all.\n */\n ramp = resolveOrgEmailRamp({\n ageDays: orgAgeDays(\n (orgForHost?.org as Record<string, unknown> | undefined)?.['createdAt'],\n ),\n deliveredLifetime: reputation.window.accepted,\n platformPerHour: platformRate.perHour,\n })\n if (!ramp.graduated && platformRate.enabled) {\n const dayRemaining = Math.max(\n 0,\n ramp.perDay - reputation.window.claimedToday,\n )\n if (dayRemaining <= 0) {\n throw new CampaignSendDeferredError(\n `${ramp.detail} It has already sent ` +\n `${reputation.window.claimedToday.toLocaleString()} today, so ` +\n 'this campaign has not been sent and nothing has been counted — ' +\n 'it goes out automatically tomorrow. Transactional mail — ' +\n 'receipts, booking reminders, password resets — keeps sending.',\n Math.floor(Date.now() / 86_400_000) * 86_400_000 + 86_400_000,\n )\n }\n batchCap = Math.min(batchCap, dayRemaining)\n }\n }\n\n /*\n * The cap takes the FIRST N of a stable order, which is what makes taking\n * some of the audience defensible at all: two sends of the same unchanged\n * audience now address the same people, and which people is answerable\n * (\"the first N by document name\"). It was previously whichever slice\n * Firestore happened to return.\n *\n * What is left over is not lost. The plan at the bottom of this function\n * writes the email back as `scheduled` with a record of how far it got, and\n * the next run addresses the first N of the REMAINDER — the same rule\n * pointed at what is left, which is the same move the follow-up's\n * subtraction makes one block above.\n */\n recipients = consentSplit.mailable.slice(0, batchCap)\n\n /*\n * BOTH suppression lists, on one derivation (D6 of\n * `docs/specs/email-overhaul.md`).\n *\n * This read used to be the site's own list alone, so an address that hard\n * bounced or reported spam on any OTHER send — another site in the org, or\n * transactional mail carrying no site tag, which is where most of the\n * platform list comes from — was mailed anyway. Every tenant's campaigns\n * leave by one sending domain under `p=reject`, so that is not one\n * merchant's deliverability, it is everyone's.\n *\n * Checked on the capped list rather than the whole audience on purpose: it\n * is a keyed lookup per address, so its cost is the size of what is being\n * mailed, and asking about people this send will not reach would buy a\n * larger read for a number nobody acts on.\n */\n const notSuppressed = await filterSendableForHost(\n hostId,\n recipients,\n firestore,\n )\n /*\n * The THIRD list, and the narrowest: who has left THIS stream.\n *\n * After the two suppression lists rather than before them, because it is the\n * weaker fact and the weaker fact should never be the one that decides. A\n * person who unticked \"Promotions and offers\" is still a subscriber; a\n * person on either suppression list is not, and asking about their topic\n * preferences would be a read taken on a question already answered.\n */\n const onTopic = await filterTopicSendable(\n hostId,\n topicId,\n notSuppressed,\n firestore,\n )\n /*\n * THE FOURTH FILTER: how often the recipient asked to hear from this site.\n *\n * The one pace control a campaign is bound by, and the reason it is bound\n * is the reason the other two do not bind it. A ceiling and a sunset are\n * conclusions the platform draws about a person; this is a request the\n * person made, on a page this product built for them to make it. A\n * campaign that overrode it would make the preference center a form that\n * records a choice nobody honors — the same failure as ignoring an\n * unsubscribe, one notch quieter.\n *\n * HERE rather than at the per-message gate, and that placement is the whole\n * point. `sendEmail`'s marketing gate is not on this path: a campaign\n * carries no `marketing` context, because it mints its own unsubscribe URL\n * upstream and needs it as a merge value long before the message exists.\n * Enforcing a pace one message at a time would also refuse people AFTER the\n * merchant read a number that counted them, which is the thing every filter\n * on this path exists not to do. Subtracted where the other three are\n * subtracted, the count on screen is true before Send is pressed.\n *\n * Fails OPEN on an unreadable counter — see `filterCadenceSendable`. A\n * pace is not a stop, and the two suppression lists above have already\n * removed everybody who asked us to stop entirely.\n */\n const sendable = await filterCadenceSendable(hostId, onTopic, { firestore })\n /*\n * NOBODY IN THIS BATCH, BUT SOMEBODY AFTER IT.\n *\n * A send whose whole audience is suppressed is a 400 a merchant needs to\n * see. A BATCH whose five hundred are all suppressed is not — there are\n * two and a half thousand people behind them, and refusing here would end\n * the campaign at the first block of bad addresses in the list.\n *\n * The batch falls through instead: it addresses nobody, records the\n * addresses it refused so the next batch does not spend its slots on them\n * again, and the plan at the bottom schedules the remainder.\n */\n if (!sendable.length && consentSplit.mailable.length <= recipients.length) {\n if (options.continuation) return finishContinuation()\n /*\n * Which of the four filters emptied the batch, because the answer changes\n * what the merchant should do. An audience that unsubscribed is one they\n * have to rebuild; an audience holding for its own cadence is one that\n * becomes mailable on its own, and telling them it \"unsubscribed\" would\n * send them looking for a problem that is not there.\n */\n throw new CampaignSendError(\n onTopic.length\n ? 'Every recipient has asked this site for mail less often than this'\n : 'Every recipient has unsubscribed or been suppressed',\n 400,\n )\n }\n\n // Monthly cap by the owning org's plan (dark-launch rule, AGL-238).\n //\n // A campaign is the ONLY send a quota may refuse (AGL-1438). It is\n // discretionary — the customer sees a clear message, and upgrades or waits —\n // where refusing a receipt or a password reset would convert a billing event\n // into an outage on their business. So this is measured against\n // `campaignEmailSends` and NOT against `emailSends`, which since AGL-1438\n // also carries every order confirmation, booking reminder and workflow\n // notification the site sent. Enforcing the campaign cap against that total\n // would refuse a campaign because the store had a busy week of orders.\n //\n // SINCE AGL-2267 THE COUNTER IS PER ORG AND THE CLAIM IS ATOMIC. The cap was\n // enforced against `hosts/{hostId}/counters/campaignEmailSends` — per SITE —\n // while `emailSendsPerMonth` is an ORG entitlement, so an org with N sites\n // got N × the cap it bought. And it was read here and incremented after\n // delivery, so two concurrent campaigns both passed the same reading. See\n // `email-metering.ts` for the counter, the transition, and why the existing\n // per-site counters were NOT folded in.\n const monthKey = new Date().toISOString().slice(0, 7)\n // Plan-less orgs resolve as free (AGL-247) — the cap always runs. The org\n // document is read ONCE, at the consent join above, and reused here and for\n // branding below; both used to re-fetch it.\n // The limit itself, read through the one shared resolver. `checkQuota` with\n // a usage of 0 is how a plain limit is read; the ALLOW/REFUSE decision is\n // not made here — it is made by the atomic reservation below.\n const campaignSendLimit = checkQuota(\n orgForHost?.org as any,\n 'emailSendsPerMonth',\n 0,\n ).limit\n const overCapError = () =>\n new CampaignSendError(\n `Monthly campaign email limit reached (${campaignSendLimit}) — upgrade ` +\n 'in Billing or shrink the audience. Transactional mail — receipts, ' +\n 'booking reminders, password resets — keeps sending.',\n 403,\n )\n {\n // A cheap read-only pre-check, so an over-cap campaign is refused before\n // the template load, the experiment read and the campaign id — and so the\n // DRY RUN has an answer without writing anything (AGL-2178: \"nothing has\n // been written above this line\"). It is not the enforcement; it cannot be,\n // because a read is not a claim.\n const used = await orgCampaignEmailSendsForMonth(orgId, monthKey)\n if (used + sendable.length > campaignSendLimit) throw overCapError()\n }\n\n /*\n * Recipient PREVIEW (AGL-2178). The campaign composer mockup shows\n * `Recipients 1,240` beside the audience picker, and the console had no\n * count before a send at all — the number appeared afterwards, in a\n * snackbar.\n *\n * It returns from HERE rather than from a counting function of its own,\n * and that is the whole point: the figure has already been through\n * audience resolution, normalisation, de-duplication, the\n * `MAX_RECIPIENTS_PER_SEND` cap, both suppression lists and the monthly\n * quota. A second implementation would be a second set of rules to\n * drift, and the one number a merchant checks before pressing Send is\n * the worst possible place for an estimate that disagrees with what\n * happens.\n *\n * `audience` rides along so the composer can show the SHORTFALL rather\n * than only the send size. The preview is the surface a merchant reads\n * before deciding, so it is the surface on which \"your audience is 3,000\n * and this send reaches 500\" has to appear.\n *\n * Nothing has been written above this line — every step so far is a\n * read — so an early return here leaves no campaign document, no\n * counter and no id behind.\n */\n if (options.dryRun) {\n return {\n campaignId: '',\n recipients: recipients.length,\n audienceSize: resolved.length,\n ...(audienceTruncated ? { audienceTruncated: true } : {}),\n ...(options.followUp ? { followUp: true, alreadyReached } : {}),\n sendable: sendable.length,\n /*\n * Measured to `onTopic` rather than to `sendable`, so the cadence\n * filter below does not silently enlarge a number labelled\n * \"unsubscribed or suppressed\". Somebody who asked for monthly mail did\n * not unsubscribe, and reporting them under a heading that says they\n * did is the netting this readout has refused everywhere else.\n */\n suppressed: recipients.length - onTopic.length,\n cadenceHeld: onTopic.length - sendable.length,\n /*\n * What this send will NOT reach on its first pass, so the composer can\n * say \"3,000 people, 500 in the first batch, the rest over the next few\n * runs\" instead of showing 500 beside an audience of 3,000 and leaving\n * a merchant to guess which number is the promise.\n */\n remaining: Math.max(0, consentSplit.mailable.length - recipients.length),\n /*\n * The consent split, measured over the WHOLE audience and named rather\n * than netted (§3f).\n *\n * Over the whole audience, not over the capped 500, because it rides\n * the same readout as `audienceSize` and answers a question about the\n * audience: of the 3,200 people this list holds, how many asked for\n * this mail? Reporting it over the capped set would make the figures\n * move whenever the cap bit, for reasons that have nothing to do with\n * consent.\n *\n * Three numbers because one would hide the thing a merchant has to\n * decide about. `consented` is who has a basis; `grandfathered` is who\n * is reachable only because enforcement is not retroactive, and is\n * therefore exactly the population that disappears the day the org\n * turns the strict mode on; `consentWithheld` is who the rule already\n * refuses.\n *\n * `consentedByOperator` splits the first of those, because \"who has a\n * basis\" and \"who asked\" stopped being the same question once a basis\n * could be asserted on somebody's behalf. Reporting only the total\n * would present an operator backfill as that many opt-ins, which is\n * the one thing the provenance field exists to prevent.\n */\n consented: consentSplit.consented,\n consentedByOperator: consentSplit.consentedByOperator,\n grandfathered: consentSplit.grandfathered,\n consentWithheld: consentSplit.withheld,\n // Which identity this campaign would leave on, so the composer can say\n // so rather than leaving a merchant to assume.\n identity: sendingIdentity.summary,\n identitySource: sendingIdentity.source,\n sent: 0,\n dryRun: true,\n }\n }\n\n // `hostPublicOrigin`, not a hand-rolled apex (AGL-2195). Campaign links are\n // mailed out and clicked days later; a wrong apex sends the operator's whole\n // audience to a domain the operator does not control.\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n\n // White-label sender identity (White-Label Phase 3): a campaign sent from a\n // white-label store reads as that store's brand. Resolved once for the whole\n // batch from the owning org doc through the one shared resolver.\n const branding = resolveBrandingProfile(orgForHost?.org as never)\n\n const campaignId = options.campaignId || createResourceUid()\n /*\n * Whether this send MINTS the record or writes onto one that already\n * exists.\n *\n * A caller naming a `campaignId` is addressing a record somebody else\n * created — a draft being sent now, a sent email taking a follow-up — and\n * that record already carries its own creation stamp. Re-stamping it would\n * move an email's creation date forward every time it was sent again, and\n * the emails list orders drafts on exactly that field.\n */\n const mintsRecord = !options.campaignId\n\n // Designed email template (AGL-349): loaded once; rendered per\n // recipient with their merge values.\n const template = options.templateScreenId\n ? await loadEmailTemplate(hostId, options.templateScreenId)\n : null\n /**\n * The message's SOURCE, settled once for the whole send.\n *\n * A designed email supplies both parts an inbox receives from the same\n * nodes, so `options.body` has nowhere to go on this branch and the union\n * gives it nowhere to be put. Deciding it here rather than inside the\n * per-recipient loop is also what stops the two ever disagreeing across a\n * batch.\n */\n const designedContent = template\n ? ({\n mode: 'design',\n template,\n ...(options.plainText ? { plainText: options.plainText } : {}),\n } as const)\n : null\n\n // Email A/B (AGL-255): each recipient deterministically lands in a\n // variant whose subject/body overrides apply; sends count as that\n // variant's exposures. A finished experiment sends the winner copy.\n const experimentId = String(options.experimentId ?? '')\n let experiment: (HostExperiment & { $id: string }) | null = null\n if (experimentId) {\n const experimentSnapshot = await hostRef\n .collection('experiments')\n .doc(experimentId)\n .get()\n const data = experimentSnapshot.data() as HostExperiment | undefined\n if (\n !experimentSnapshot.exists ||\n !data ||\n data.target !== 'email' ||\n (data.status !== 'running' && !data.winnerVariantId)\n ) {\n throw new CampaignSendError('Pick a running email experiment', 400)\n }\n experiment = { $id: experimentSnapshot.id, ...data }\n }\n /*\n * PLATFORM SEND-RATE ADMISSION CONTROL (AGL-2409).\n *\n * `sendEmail` governs every message individually and is the hard ceiling.\n * This is the admission check in front of it, and it exists for one reason:\n * without it, a campaign that does not fit in the current hour would deliver\n * to the first N addresses and stop, and a scheduled campaign in that state\n * cannot be retried without double-sending the N that already went.\n *\n * Asking for room for the WHOLE batch up front turns the ordinary case into\n * \"all of it, or none of it and try again next run\". A read, not a claim —\n * two campaigns can still both pass this and then contend at the per-message\n * governor, which is why the loop below also handles a mid-batch refusal\n * rather than assuming this settled it.\n */\n {\n // The configuration was read above, where it sized this batch against the\n // new-sender ramp. One read, two controls: a second one here could answer\n // differently inside one send, which would let a campaign be sized\n // against one ceiling and admitted against another.\n const config = platformRate\n const window = await readEmailSendRateWindow()\n if (config.enabled && window.used + sendable.length > config.perHour) {\n throw new CampaignSendDeferredError(\n `The platform is sending at its hourly limit (${config.perHour}/hour). ` +\n 'This campaign has not been sent and nothing has been counted — it ' +\n 'will go out automatically on the next run, or you can send it again ' +\n 'after the hour rolls.',\n window.resetMs,\n )\n }\n\n /*\n * THIS WORKSPACE'S SHARE OF THAT HOUR.\n *\n * The check above bounds the platform; it does not bound how much of the\n * platform one tenant may take. Without this, an org with a large audience\n * occupies the whole hour and every other customer's campaigns are refused\n * by a ceiling they did nothing to reach.\n *\n * The ceiling is DERIVED from the live platform ceiling\n * (`orgHourlyCampaignCeiling`), so a staff ramp moves both together and\n * the two can never contradict each other. See `send-ceilings.ts` for the\n * arithmetic tying this to the per-send cap and the plan allowance.\n *\n * A deferral, not a refusal: the campaign stays a draft, the audience is\n * untouched, no list membership changes and no suppression or delivery\n * record is affected. A send is a flow rather than a holding, which is the\n * one place the enforce-at-the-reduction rule does not reach — refusing a\n * flow strands nobody's data.\n *\n * Taken BEFORE the monthly claim so that a workspace deferred for the hour\n * has not spent a month's allowance on a campaign that did not go.\n */\n const hourly = await claimOrgEmailSendBudget({\n orgId,\n count: sendable.length,\n platformPerHour: config.perHour,\n enabled: config.enabled,\n })\n if (!hourly.allowed) {\n throw new CampaignSendDeferredError(\n `This workspace may send ${hourly.ceiling.toLocaleString()} campaign ` +\n `emails an hour and has sent ${hourly.used.toLocaleString()} this ` +\n `hour, so there is room for ${hourly.remaining.toLocaleString()} ` +\n `and this campaign needs ${sendable.length.toLocaleString()}. ` +\n 'Nothing has been sent and nothing has been counted — the campaign ' +\n 'is unchanged and will go out automatically on the next run, or you ' +\n 'can send it again after the hour rolls. Transactional mail — ' +\n 'receipts, booking reminders, password resets — keeps sending.',\n hourly.retryAtMs,\n )\n }\n }\n\n /*\n * THE DAY'S RAMP, CLAIMED.\n *\n * Between the hourly claim and the monthly reservation, and the ordering is\n * the same argument both of its neighbours make. AFTER the hourly one, so a\n * workspace deferred for the hour has not spent a day's budget. BEFORE the\n * monthly one, because an unreconciled claim costs whatever its window is\n * and a day is cheaper to leak than a month.\n *\n * A graduated workspace claims nothing and pays no read; see\n * `claimOrgEmailSendDay`.\n */\n const dayClaim = await claimOrgEmailSendDay({\n orgId,\n count: sendable.length,\n ramp:\n ramp ??\n resolveOrgEmailRamp({\n ageDays: null,\n deliveredLifetime: 0,\n platformPerHour: platformRate.perHour,\n }),\n enabled: platformRate.enabled,\n })\n if (!dayClaim.allowed) {\n throw new CampaignSendDeferredError(\n `This workspace may send ${dayClaim.ceiling.toLocaleString()} campaign ` +\n `emails a day while it establishes a sending history, and has sent ` +\n `${dayClaim.used.toLocaleString()} today. Nothing has been sent and ` +\n 'nothing has been counted — the rest goes out automatically ' +\n 'tomorrow. Transactional mail — receipts, booking reminders, ' +\n 'password resets — keeps sending.',\n dayClaim.retryAtMs,\n )\n }\n\n /*\n * THE MONTHLY CLAIM (AGL-2267), taken here and not at the pre-check above.\n *\n * As late as possible on purpose: everything between the pre-check and this\n * line can throw (an unknown template, a stopped experiment), and a claim\n * taken before them would leak the org's allowance for the rest of the month\n * on a campaign that never existed. From here to the `finally` below there\n * is nothing that can throw before the reconcile runs.\n */\n const claim = await reserveCampaignEmailSends({\n orgId,\n month: monthKey,\n count: sendable.length,\n limit: campaignSendLimit,\n })\n if (!claim.ok) {\n // The day's claim was taken a few lines above and this campaign is not\n // going out, so it is given back before the throw. The `finally` below\n // has not been entered yet, which is exactly why this cannot be left to\n // it.\n await reconcileOrgEmailSendDay(dayClaim.reservation, 0)\n throw overCapError()\n }\n const reservation: CampaignSendReservation = claim.reservation\n const dayReservation: OrgEmailSendDayReservation | null = dayClaim.reservation\n\n const variantSends: Record<string, number> = {}\n /**\n * Who this campaign actually reached, for the marketing frequency window.\n *\n * A campaign is exempt from the frequency REFUSAL — it is a merchant's\n * reviewed, one-shot act with a recipient count on screen before they press\n * Send, and a cap that silently removed people from it would make that\n * number a lie — but it is most of the mail a person receives from a site,\n * so a ceiling that did not count it would describe nothing. Collected here\n * and written once below rather than per recipient, because this loop is\n * already one awaited HTTP POST per person.\n *\n * ## The ENGAGEMENT SUNSET is exempt too, and for a stronger reason\n *\n * `marketingSunsetVerdict` refuses a site that has been mailing somebody\n * past the sunset window with nothing in it to say they are still\n * listening. Nothing on this path consults it, deliberately, and the\n * reasoning is the frequency argument plus two things that only apply here.\n *\n * The obvious objection is that a ceiling guards a merchant's volume while\n * a sunset guards a shared sending domain's reputation, so the platform's\n * interest should outrank a merchant's reviewed act. That distinction does\n * not survive contact with the two controls: `marketingFrequencyCap` is\n * itself \"a deliverability control on a shared sending domain\" that \"cannot\n * be something one plan buys its way past\". Both serve the platform. The\n * exemption was never about whose interest a control serves — it is about\n * whether an invisible subtraction may change a number a person already\n * read and approved.\n *\n * What separates the two is what a refusal is made of, and it separates\n * them in the direction of exempting the sunset MORE readily:\n *\n * 1. **A sunset refuses on an inference, not on a stated fact.**\n * Suppression carries \"stop\", the cadence filter above carries \"less\n * often\" — both said by the recipient. A ceiling counts messages that\n * demonstrably arrived. A sunset concludes from silence that somebody\n * has gone, which is the one refusal on this path that can be wrong\n * about a person who is still reading.\n * 2. **Its refusal is terminal where a ceiling's is retryable.** An\n * automated sweep defers a capped message and sends it later; a\n * sunsetted address is refused every time until they engage with mail\n * they are no longer being sent. Applied to campaigns that is the\n * largest silent subtraction in the system, made on the weakest\n * evidence.\n *\n * And it would break the one campaign written for exactly these people. A\n * win-back is a reviewed marketing act whose audience is, by definition,\n * everybody a sunset would refuse — so binding campaigns to it would make\n * the message that exists to end a disengagement the message that cannot be\n * sent. No automated path has that case: nobody writes a cart reminder\n * aimed at people who stopped reading.\n *\n * The reputation this leaves unguarded on the campaign path is guarded by\n * the four filters that DO run before this loop — both suppression lists,\n * the topic opt-outs and the recipient's own cadence — plus the sender\n * ramp and the hourly governor. The sunset governs the automated paths,\n * which fire with no human present. `MarketingSendContext.capped` carries\n * the same decision for the paths that reach the gate.\n */\n const reached: string[] = []\n /**\n * WHO THIS BATCH CONSIDERED AND WILL NOT MAIL, so the next one does not\n * consider them again.\n *\n * Two populations, and the reason they belong together is what the next\n * batch does with them. The suppression and topic filters above removed\n * people from `sendable`; the loop below removes any address the provider\n * would not take. Both stay at the head of a stable order, so a batch that\n * did not record them would re-select them, spend a slot on each, and — at\n * enough of them — address five hundred people it cannot mail and make no\n * progress at all.\n *\n * A message that failed for a transient reason is settled out with the\n * rest. That is not a new loss: before batching, a failed recipient was\n * never retried either, because there was no second pass. What it buys is\n * that one unreachable address cannot stall the two thousand behind it.\n *\n * The one failure that is NOT settled here is a refusal about the rate —\n * the hourly governor's, or the provider's 429. Those are answers about\n * when, not about whom, and settling a recipient on one would drop somebody\n * the provider never even looked at while the campaign closed as complete.\n * They break the loop into `deferred` instead; see the branch below.\n */\n const sendableSet = new Set(sendable)\n const settledOut: string[] = recipients.filter(\n (email) => !sendableSet.has(email),\n )\n let sent = 0\n /** Recipients a rate refusal left untouched mid-batch, if any. */\n let deferred = 0\n /**\n * THE PACE INSIDE ONE BATCH.\n *\n * The hourly governor above decides HOW MANY messages this workspace may\n * put on the domain in an hour; it says nothing about how closely together\n * they arrive, and the provider only counts the second. A batch of five\n * hundred is the one place in this codebase that issues provider requests\n * in a tight loop, so it is the one place that has to hold the request\n * rate — everything else here sends one message and returns.\n *\n * Spreading rather than concurrency: a sequential `await` already caps this\n * loop at one request in flight, and the defect is that its rate is\n * whatever the round trip happens to be. See\n * {@link createProviderRequestPacer} for why waiting out the REMAINDER of\n * the interval costs a real send nothing.\n */\n const paceProviderRequest = createProviderRequestPacer()\n try {\n for (let index = 0; index < sendable.length; index += 1) {\n const email = sendable[index]\n // `cid` is what lets an unsubscribe be attributed to the campaign that\n // caused it. Without it the suppression list records that somebody left\n // and nothing about which mailing they left over, which is the one\n // question an unsubscribe rate exists to answer.\n const link = {\n siteBase,\n hostId,\n email,\n campaignId,\n topicId,\n secret: unsubscribeSecret,\n }\n /*\n * TWO URLS OVER ONE SIGNATURE, and which one goes where is the whole\n * RFC 8058 story.\n *\n * `oneClickUrl` is what the `List-Unsubscribe` header names. A mailbox\n * provider POSTs it with no human present and expects the act to have\n * happened when it reads the 200 — so it points at the route whose POST\n * writes immediately, and it must never point at a page of checkboxes\n * that has to be submitted by somebody.\n *\n * `unsubscribeUrl` is the link a PERSON clicks in the footer, and it\n * points at the preference center, where the topic this message\n * belonged to is one of the things they can stop instead of all of it.\n * The merge token keeps its name because designed templates in the wild\n * reference `{{unsubscribeUrl}}`, and because the page it opens is still\n * where you go to unsubscribe — with \"Unsubscribe from everything\" on\n * it, one button away.\n */\n const oneClickUrl = buildUnsubscribeUrl({ ...link, surface: 'one-click' })\n const unsubscribeUrl = buildUnsubscribeUrl({\n ...link,\n surface: 'preferences',\n })\n // Variant assignment keys on the recipient address (AGL-255) so a\n // re-send reaches the same variant.\n const variant = experiment\n ? assignExperimentVariant(experiment, experiment.$id, email)\n : null\n /*\n * THIS RECIPIENT'S MESSAGE, through the renderer the composer previews\n * with (`@aglyn/aglyn/app-utils/recipient-email-render`).\n *\n * Merge tags resolve after the variant override so variant copy can use\n * tags too, a designed template renders per recipient, and a plain-text\n * body gets the HTML part `sendEmail` would otherwise synthesize for it.\n * Shared rather than inlined because a preview rendered by a second\n * implementation is a preview of something else — the two defects this\n * send path has already shipped, product blocks silently dropped and\n * merge tags resolving to empty strings for a whole audience, are both\n * invisible to a preview that does not run this exact code.\n */\n const message = renderRecipientEmail({\n subject: variant?.subject?.trim() || subject,\n preheader: options.preheader,\n /*\n * ONE SOURCE, chosen once for the whole send.\n *\n * `designedContent` is resolved above the loop because the mode is a\n * property of the EMAIL, not of the recipient. A designed message\n * carries no body at all — the union has no field for one — so the\n * variant's body override is only reachable on the text branch, which\n * is the only branch where a body is the message.\n */\n content: designedContent ?? {\n mode: 'text',\n body: variant?.body?.trim() || body,\n },\n /*\n * The persona where a proof named one, and the actual recipient\n * everywhere else.\n *\n * Only the RENDER moves. `email` above still decides who the message\n * is delivered to, which unsubscribe link is signed, and which\n * address a suppression would be recorded against — so a proof shows\n * a real contact's merge values without putting that contact's\n * opt-out link in somebody else's inbox.\n */\n recipient: options.proofPersona ?? { email, name: names.get(email) },\n siteBase,\n hostId,\n unsubscribeUrl,\n })\n await paceProviderRequest()\n const result = await sendEmail({\n to: email,\n subject: message.subject,\n ...(message.html ? { html: message.html } : {}),\n // The plain-text footer names what the link actually opens. \"Choose\n // which emails you get\" in front of \"or unsubscribe\" is the only place\n // a text-only reader learns that leaving one stream is an option at\n // all, and the word \"unsubscribe\" stays in the line because that is\n // what a recipient scans the footer for. It is written by\n // `renderRecipientEmail`, so the composer's preview shows the footer\n // that is actually mailed.\n text: message.text,\n // RFC 8058 one-click (AGL-2408). `List-Unsubscribe` alone does NOT\n // satisfy Gmail's and Yahoo's bulk-sender rules — the pair does, and\n // Gmail is where most of a merchant's list lives. A client honoring\n // the pair POSTs `List-Unsubscribe=One-Click` to the URL, which is\n // why the handler had to accept POST first: advertising one-click\n // against a GET-only handler would promise a verb nothing served.\n //\n // `oneClickUrl`, NOT the preference center. Topics narrow what a\n // person can choose on a page; they change nothing about what a\n // machine POSTing this header is promised, which is that the\n // recipient stops hearing from this site.\n headers: {\n 'List-Unsubscribe': `<${oneClickUrl}>`,\n 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',\n },\n // The campaign's own display name where the composer set one, and the\n // org's branding default otherwise. Either way the ADDRESS is the\n // resolved identity's — `applyFromName` replaces the display name in\n // front of it and nothing else.\n fromName: options.fromName || branding.fromName,\n ...(options.replyTo ? { replyTo: options.replyTo } : {}),\n // The server's answer to which verified address this leaves on. The\n // send path re-checks it, so a refusal holds even here where the\n // route has already passed one.\n sendingIdentity,\n // A campaign is a site talking to its own audience, so the shared\n // platform address is not reachable from it even if the resolution\n // above somehow yielded nothing.\n audience: 'tenant',\n // Event attribution (AGL-268): the opens/clicks webhook maps\n // deliveries back to the campaign (and experiment) via tags.\n tags: [\n { name: 'hostId', value: hostId },\n { name: 'campaignId', value: campaignId },\n ...(experiment\n ? [{ name: 'experimentId', value: experiment.$id }]\n : []),\n ],\n context: 'campaign',\n })\n if (result.sent) {\n sent += 1\n reached.push(email)\n if (variant) {\n variantSends[variant.id] = (variantSends[variant.id] ?? 0) + 1\n }\n continue\n }\n /*\n * A REFUSAL ABOUT THE RATE STOPS THE BATCH, whichever control produced\n * it.\n *\n * The hourly governor refuses because a counter that only goes up has\n * reached its ceiling, so every message after this one in the window\n * gets the same answer. The provider refuses with a 429 because\n * requests arrived too close together, and the next one is closer\n * still. Neither says anything about the recipient in hand.\n *\n * Not a throw: some of this batch has already been delivered, and a throw\n * here would lose the delivered count, skip the meters and (on the\n * scheduled path) re-queue a campaign that would double-send. The\n * remainder is reported instead, the reservation is reconciled to what\n * actually went, and the merchant sees a number that is short.\n *\n * Any OTHER failure — a rejection, a network error — is per-recipient and\n * the loop continues.\n */\n if (rateLimitedRetryAtMs(result) !== null) {\n /*\n * Everything from HERE ON is untouched and retryable, which is what\n * makes it the remainder rather than a loss: the campaign schedules\n * itself for the next window and addresses these people then.\n * Counted from the index rather than from `sent`, so a rejection\n * earlier in the batch is not counted twice — once as settled and\n * again as deferred.\n */\n deferred = sendable.length - index\n break\n }\n /*\n * The provider would not take this address. Settled rather than left\n * for the next batch — see `settledOut` above for why an address that\n * keeps failing must not keep consuming a slot.\n */\n settledOut.push(email)\n }\n } finally {\n /*\n * Give back what did not go out (AGL-2267).\n *\n * In a `finally` so it runs on the throw paths too. The claim was taken\n * for the whole batch — that is what makes it a cap — and a campaign that\n * delivered 300 of 500 must not spend 500 of the org's allowance.\n * `reconcileCampaignSendReservation` never throws.\n */\n await reconcileCampaignSendReservation(reservation, sent)\n /*\n * And the day's, for the same reason one line up. The hourly claim is\n * deliberately NOT reconciled — its window is an hour — but a day is long\n * enough that a failed batch would cost a new workspace the rest of it.\n */\n await reconcileOrgEmailSendDay(dayReservation, sent)\n }\n // Both meters, from one call, on the DELIVERED count (AGL-1438). Ahead of\n // the `recordCampaign` early return below, because a test send is a real\n // email with a real cost even though it writes no campaign record — and\n // ahead of nothing else that writes `emailSends`, so a campaign reaches the\n // cost meter exactly once. This sender used to increment that counter\n // itself, which is how a counter named for all email came to hold campaign\n // sends alone.\n // The frequency window, for the messages that left. After the reservation\n // reconcile and never in front of it: this is a deliverability counter and\n // the reconcile is a merchant's allowance, so the allowance is settled\n // first. `recordMarketingSends` never throws.\n await recordMarketingSends(hostId, reached)\n await meterHostEmail(hostId, sent, 'campaign')\n /*\n * The DENOMINATOR every per-tenant rate divides by.\n *\n * Recorded on the DELIVERED count and not on what was attempted, so a\n * bounce rate is bounces over messages that actually left. It is also the\n * volume the new-sender ramp reads to decide which step a workspace has\n * earned, which is why it is written from the send rather than inferred\n * from the delivery webhook: a ramp that only moved when a provider\n * reported back would stall a new tenant on the day the webhook was slow.\n *\n * Never throws, like both meters above it.\n */\n await recordCampaignAccepted(orgId, sent)\n\n /*\n * WHO THIS EMAIL HAS NOW REACHED, so a later send can subtract them.\n *\n * Written for every real send and not only for follow-ups, because the\n * record has to exist BEFORE anybody asks for one — an email whose first\n * send kept no account of itself can never be sent to more people, which is\n * exactly what `campaignReachCovers` refuses above.\n *\n * A test send is excluded with the campaign record it also skips: it\n * delivers to the requester's own address under an id nothing will ever\n * follow up, and counting it would file a real person's address against a\n * mailing that does not exist.\n *\n * Ahead of the campaign write below rather than after it, so a failure\n * between the two leaves an email whose reach record is AHEAD of its\n * recorded `sent`. `campaignReachCovers` reads that as covered, which is\n * the safe direction: a follow-up subtracts more people than it strictly\n * has to. The other order would leave the record short and refuse the\n * follow-up, which is also safe but loses the feature over a transient.\n */\n if (options.recordCampaign !== false) {\n await recordCampaignReach(hostId, campaignId, reached, firestore)\n /*\n * And who it decided NOT to mail, under a field of its own.\n *\n * Written for every real send rather than only for a batched one, for the\n * reason the reach record itself is: the record has to exist before\n * anybody asks for one, and the batch that asks is the NEXT invocation of\n * this function, which will have no way to know that these five hundred\n * addresses were already considered.\n */\n await recordCampaignSkipped(hostId, campaignId, settledOut, firestore)\n }\n\n // Sends are the email variant's exposures (AGL-255).\n if (experiment && experiment.status === 'running') {\n for (const [variantId, count] of Object.entries(variantSends)) {\n // AGL-1771: `variant.id` is MERCHANT-AUTHORED — `validateExperiment`\n // checks the ids are unique and nothing about their shape — and it is a\n // path component here. The same third instance `d51e23df4` found on the\n // conversion write in `email-events.ts`, on the exposure write that\n // pairs with it. This one stays a merge-set and stays a create: the\n // first send for a variant has no stats document, the experiment was\n // just read, and the emails really went out.\n if (!isDocumentId(variantId)) continue\n await hostRef\n .collection('experiments')\n .doc(experiment.$id)\n .collection('stats')\n .doc(variantId)\n .set(\n {\n exposures: firebaseAdmin.firestore.FieldValue.increment(count),\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n\n /*==========================================\n * WHAT HAPPENS TO THE REST OF THE AUDIENCE.\n *\n * The cap took the first N of the people this send may mail. Everyone past\n * it, plus anybody an hourly cut left untouched, is the REMAINDER — and\n * until now the remainder was simply not mailed, and a merchant with three\n * thousand contacts pressed Send six times to reach them.\n *\n * The plan is pure and lives in `send-ceilings.ts` with the numbers it\n * reasons about. It answers two things: how many are left, and whether\n * another batch runs. The second is the one that matters, because a job\n * that reschedules itself has exactly one interesting failure — doing it\n * forever — and there are three ways this one stops: nothing left, the\n * batch guard, and a batch that settled nobody.\n *=========================================*/\n const plan = campaignBatchPlan({\n mailable: consentSplit.mailable.length,\n addressed: recipients.length,\n retryable: deferred,\n settled: sent + settledOut.length,\n batchesSoFar,\n })\n /**\n * When the next batch may go.\n *\n * Now, in the ordinary case: the scheduled-campaign processor claims\n * anything `scheduled` and due, and its next run is what continues this\n * email. A window the send was paced by moves it out — but a send paced by\n * the hour or the day THREW rather than reaching here, so the only reason\n * this is not immediate is a batch that ran to the end of its own cap.\n */\n const nextAtMs = plan.resuming ? Date.now() : 0\n\n if (options.recordCampaign === false) {\n return {\n campaignId,\n recipients: sendable.length,\n audienceSize: resolved.length,\n ...(audienceTruncated ? { audienceTruncated: true } : {}),\n sent,\n ...(deferred ? { deferred } : {}),\n }\n }\n /*==========================================\n * ADDING TO A COUNTER, AS AGAINST REPLACING IT.\n *\n * `set(..., {merge: true})` merges a nested map FIELD BY FIELD, so a\n * follow-up writing a plain `stats.recipients` would overwrite the original\n * send's figure with its own — and every rate on the report divides by\n * `sent` or by `delivered`. `delivered`, `opens`, `bounced` and the rest are\n * incremented by the delivery webhook keyed on this same `campaignId`, so\n * they already cover BOTH sends. Replacing `sent` with the follow-up's\n * smaller number would leave a numerator counting two sends over a\n * denominator counting one, and a delivery rate of 300%.\n *\n * So every send-recorded counter goes through here: a plain number the\n * first time, an increment on a follow-up. The two populations a follow-up\n * measures are disjoint by construction — it addressed nobody the earlier\n * sends reached — so the sums are totals rather than double counts.\n *=========================================*/\n const additive = (value: number) =>\n addsToExistingSend\n ? (firebaseAdmin.firestore.FieldValue.increment(value) as never)\n : value\n /**\n * The figures a BATCH must not add to, because it did not measure a second\n * population — it measured a slice of the one the first batch already\n * counted.\n *\n * `audienceSize` is the arithmetic that makes this concrete. A first batch\n * over three thousand people records 3,000; a second batch sees the 2,500\n * that are left and, adding, would record 5,500 — an audience that does not\n * exist, under every rate on the report. The same holds for the consent\n * split, which is measured over the whole remaining audience rather than\n * over the capped slice.\n *\n * So a continuation omits them entirely and the first batch's figures\n * stand, which is the same \"recorded, not recomputed\" rule the populations\n * below are stored under: they are true of the send that happened.\n */\n const measuresTheAudience = !options.continuation\n await hostRef.collection('campaigns').doc(campaignId).set(\n {\n subject,\n body,\n audience,\n /*\n * The one date every message carries — see `email-record.ts`. Written\n * only where this send mints the document, because a record addressed\n * by id was created by whoever minted it and keeps that date.\n */\n ...(mintsRecord ? { createdAtMs: Date.now() } : {}),\n /*\n * WHICH audience, not only which KIND.\n *\n * `audience` is `'list'` or `'segment'` — a kind — and on its own it\n * cannot answer \"which lists has this design been sent to\", because\n * every list send looks identical to every other. The scheduled branch\n * of the handler has always recorded these; the immediate send dropped\n * them, so a campaign's own document could not say where it went.\n */\n ...(options.listId ? { listId: options.listId } : {}),\n ...(listName ? { listName } : {}),\n ...(options.segmentId ? { segmentId: options.segmentId } : {}),\n // The RESOLVED topic, not `options.topicId`. Recording the default\n // explicitly is what lets the campaign report and the preference page\n // agree about which stream this send belonged to, without either of them\n // re-deriving a default that could drift from the other's.\n topicId,\n ...(options.templateScreenId\n ? { templateScreenId: options.templateScreenId }\n : {}),\n /*\n * The plain-text part somebody wrote, recorded beside the design it\n * belongs to. `storedSendOptionsFrom` reads it back, so a follow-up\n * mails the same text half the first batch did rather than regenerating\n * one from a design that may have moved since.\n */\n ...(options.plainText ? { plainText: options.plainText } : {}),\n // What this send actually left as, recorded beside the copy: the report\n // is read months later, by which time the org's branding default may be\n // a different name than the one this campaign went out under.\n ...(options.fromName ? { fromName: options.fromName } : {}),\n ...(options.replyTo ? { replyTo: options.replyTo } : {}),\n /*\n * WHICH SENDER was chosen, beside the address it resolved to.\n *\n * Not a duplicate of `sentAs.from`: that records what left, and this\n * records what was picked. A follow-up re-sends under the same sender —\n * `storedSendOptionsFrom` reads this field — so an email that went out\n * as `jamie@` reaches the rest of its audience as `jamie@` even after\n * the site's default has moved to somebody else.\n */\n ...(senderId ? { senderId } : {}),\n /*\n * THE ADDRESS, beside the name and for the same reason.\n *\n * The two fields above are what the COMPOSER submitted; this is what\n * the recipient saw. They part company on a campaign that named no\n * display name, which goes out under the org's branding default — and\n * on every campaign once the site's sending identity moves, because a\n * domain verified in November does not change what left in March.\n */\n ...sentAsStamp({\n from: sendingIdentity.from,\n fromName: options.fromName || branding.fromName,\n replyTo: options.replyTo,\n }),\n ...(options.preheader ? { preheader: options.preheader } : {}),\n ...(options.displayName ? { displayName: options.displayName } : {}),\n ...(options.emailCampaignId\n ? { emailCampaignId: options.emailCampaignId }\n : {}),\n ...(experiment ? { experimentId: experiment.$id } : {}),\n stats: {\n recipients: additive(sendable.length),\n sent: additive(sent),\n /*\n * The audience this send was TAKEN FROM, beside what it reached.\n *\n * `audienceSize` above `recipients` is a campaign that did not go to\n * everybody, and the History row is where a merchant answers \"did\n * this reach my list\" months later. Recording only the reached figure\n * makes a truncated send indistinguishable from a complete one, which\n * is the same fault the deferred count below was added to close.\n *\n * `audienceSizeTruncated` marks the figure as a floor: the resolution\n * stopped at its read ceiling, so the audience is at least this and\n * the shortfall is at least the difference.\n */\n ...(measuresTheAudience\n ? {\n audienceSize: additive(resolved.length),\n ...(audienceTruncated ? { audienceSizeTruncated: true } : {}),\n }\n : {}),\n /*\n * WHAT THIS EMAIL ENDED UP NOT DELIVERING.\n *\n * Written only once the email has STOPPED, and as an absolute figure\n * rather than an increment, because a shortfall is a state and not a\n * sum. A batch that was cut short by the hourly ceiling has not\n * fallen short of anything — the people it did not reach are in\n * `resume.remaining` and the next run addresses them — and recording\n * a shortfall there would leave the report carrying \"held back by the\n * hourly limit\" about recipients who got the mail twenty minutes\n * later.\n */\n ...(!plan.resuming && plan.remaining > 0\n ? { deferred: plan.remaining }\n : {}),\n ...(Object.keys(variantSends).length\n ? {\n variantSends: Object.fromEntries(\n Object.entries(variantSends).map(([variantId, count]) => [\n variantId,\n additive(count),\n ]),\n ),\n }\n : {}),\n /*==========================================\n * THE POPULATIONS THIS SEND ALREADY MEASURED.\n *\n * Every one of these was computed above, returned from the DRY RUN\n * so the composer could show it before the send, and then discarded\n * the moment the send was real — so the campaign report could only\n * ever say how many were mailed, never how many were not and why.\n *\n * They are RECORDED here rather than recomputed at read time, and the\n * difference is not an optimisation. Consent records change, addresses\n * get suppressed, and a list grows: recomputing \"how many were\n * withheld\" next month answers a question about the list as it is\n * now, under a heading that says it describes a send that happened in\n * March. The recorded number is the only one that is true of the\n * campaign.\n *\n * Measured over two different wholes, which is why they are stored\n * separately rather than netted: the consent split runs over the whole\n * resolved audience, and `suppressed` over the capped recipient list,\n * because that is where each check actually runs.\n *=========================================*/\n ...(measuresTheAudience\n ? {\n consented: additive(consentSplit.consented),\n consentedByOperator: additive(consentSplit.consentedByOperator),\n grandfathered: additive(consentSplit.grandfathered),\n consentWithheld: additive(consentSplit.withheld),\n }\n : {}),\n suppressed: additive(recipients.length - onTopic.length),\n cadenceHeld: additive(onTopic.length - sendable.length),\n /*\n * That this send's links were trackable at all.\n *\n * Click tracking rewrites links in the HTML part, so a send that\n * carried none reports zero clicks whatever recipients did — a\n * structural zero that is indistinguishable on screen from a campaign\n * nobody clicked. `sendEmail` now synthesises an HTML part for a\n * text-only send, so every send from here on carries one; recording\n * the fact is what lets the report withhold a click RATE for the\n * campaigns that predate it instead of publishing a meaningless one.\n */\n clickTracked: true,\n },\n /*\n * `scheduled` WHILE AN EMAIL IS STILL GOING OUT, and it is a real\n * state rather than a convenience.\n *\n * The scheduled-campaign processor claims anything `scheduled` and due,\n * and that claim is the resume beat — including for a send a merchant\n * pressed by hand, which has no beat of its own. Reusing the state also\n * means the existing collection-group index serves it and that Cancel\n * already works: a merchant who decides mid-campaign that the copy is\n * wrong can stop the rest of it, which is a thing they could not do\n * before because there was no rest.\n *\n * ⚠️ IT READS AS \"NOT SENT YET\" UNLESS A SURFACE SAYS OTHERWISE. The\n * `resume` map below is what makes the row honest — \"reached 500 of\n * 3,000, sending\" rather than a scheduled email that has in fact\n * already delivered five hundred messages. `campaignSendProgress` in\n * the recipient-email renderer derives that sentence from these fields\n * and is the one place it is composed.\n */\n status: plan.resuming ? 'scheduled' : 'sent',\n ...(plan.resuming ? { sendAtMs: nextAtMs } : {}),\n /*\n * HOW FAR THIS EMAIL HAS GOT, written absolutely on every batch.\n *\n * Not part of `stats`, because `stats` is what the email DID and this\n * is where it currently is. A finished email keeps the record — batches\n * of 6, remaining 0 — so \"this went out over six runs\" is answerable\n * afterwards rather than only while it is happening.\n */\n resume: {\n remaining: plan.remaining,\n batch: plan.batch,\n nextAtMs,\n ...(plan.stop ? { stop: plan.stop } : {}),\n },\n /*\n * `sentAt` is WHEN THIS EMAIL WENT OUT, and a follow-up does not change\n * that. The emails list orders on it and the campaign rollup takes its\n * `lastSentAtMs` from it, so moving it would restate an email that went\n * out in March as one that went out today — the same rewriting-history\n * fault the recorded populations above exist to avoid.\n *\n * When the LAST send happened is a different fact and gets a field of\n * its own, beside a count of how many sends this email has had. Both\n * are what the detail page needs to say \"sent twice, most recently on\n * the 14th\" instead of presenting one date for two mailings.\n */\n ...(addsToExistingSend\n ? {\n lastSentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n lastSentBy: options.senderUid,\n }\n : {\n sentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n sentBy: options.senderUid,\n }),\n /*\n * How many times a PERSON sent this email, which is not how many\n * batches it took. A merchant who pressed Send once and watched it go\n * out over six runs sent it once; counting the batches would put \"sent\n * 6 times\" on the detail page of a campaign nobody re-sent.\n */\n ...(options.continuation ? {} : { sendCount: additive(1) }),\n },\n { merge: true },\n )\n return {\n campaignId,\n recipients: sendable.length,\n audienceSize: resolved.length,\n ...(audienceTruncated ? { audienceTruncated: true } : {}),\n ...(options.followUp ? { followUp: true, alreadyReached } : {}),\n sent,\n ...(deferred ? { deferred } : {}),\n remaining: plan.remaining,\n resuming: plan.resuming,\n batch: plan.batch,\n ...(plan.resuming ? { nextAtMs } : {}),\n }\n}\n\n/**\n * The stored configuration of a send, as the options that would mail it.\n *\n * Every field comes off the RECORD and none of it off the request — the\n * caller names a site and an email id and nothing else. That is not tidiness:\n * `campaignId` addresses an existing document, so a caller who could also\n * supply the body and the audience could put arbitrary copy on somebody\n * else's send id, keep its `cid` and its report, and mail it.\n *\n * Both callers rest on that. A follow-up mails the email that is already\n * there; `sendNow` mails a draft or a scheduled email ahead of its time, and\n * the copy it delivers has to be the copy that was composed and previewed\n * rather than whatever a request body happens to carry.\n *\n * Refuses a send carrying neither a template nor a body, which has no message\n * whichever caller asked.\n */\n/*==========================================\n Who a proof may reach, and whose data fills it\n==========================================*/\n\n/** One address a test send is allowed to be delivered to. */\nexport interface ProofRecipient {\n email: string\n /** A person's name where the membership record carries one. */\n label: string\n /** True for the caller's own account address. */\n self: boolean\n}\n\n/**\n * THE ADDRESSES A TEST SEND MAY BE DELIVERED TO.\n *\n * The caller's own account address, plus every account holder on the org that\n * owns this site. Exported because the composer has to OFFER this set — a\n * free-text box beside a rule enforced on the server is a box whose every\n * wrong answer is a refusal the person could not have predicted.\n *\n * ## Why membership, and not \"any address the merchant types\"\n *\n * A test send is exempt from the marketing-consent rule (see the proof\n * carve-out in `performCampaignSend`). An exemption that could be pointed at\n * any address would not be a test-send feature, it would be a way to mail\n * anybody without consent and call it a test. Membership is the narrowest\n * boundary that still answers what the button is for: proofing a draft to the\n * people who work on it.\n *\n * ## What is NOT here\n *\n * `siteMembers`, `leads` and contacts. They are the tenant's audience, and\n * the audience is precisely the population the consent rule protects. A\n * contact can be chosen as the PERSONA a proof renders as — see\n * {@link resolveProofPersona} — which reaches nobody.\n *\n * ## What this does not relax\n *\n * Everything else. The send still runs both suppression lists, so an address\n * that bounced or complained is refused however senior its owner; and a\n * stored `declined` on the address still refuses, because a refusal is the\n * one thing no policy may mail.\n */\nexport async function proofRecipientsForHost(options: {\n hostId: string\n callerEmail: string\n}): Promise<ProofRecipient[]> {\n const callerEmail = String(options?.callerEmail ?? '')\n .trim()\n .toLowerCase()\n const found = new Map<string, ProofRecipient>()\n if (callerEmail) {\n found.set(callerEmail, { email: callerEmail, label: 'You', self: true })\n }\n\n const orgForHost = await getOrgForHost(options?.hostId).catch(() => null)\n const orgId = String(orgForHost?.orgId ?? '')\n if (!orgId) return [...found.values()]\n\n const members = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n /*\n * A ceiling rather than the whole roster, because this feeds a picker and\n * a picker of two thousand names is not a picker. It is ordered by\n * document id so the page is stable across calls — a `limit` with no\n * order is a random sample, and a person who saw a colleague in the list\n * yesterday must not find them missing today.\n */\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(200)\n .get()\n .catch(() => null)\n\n for (const doc of members?.docs ?? []) {\n const email = String(doc.get('email') ?? '')\n .trim()\n .toLowerCase()\n if (!email || found.has(email)) continue\n found.set(email, {\n email,\n label: String(doc.get('displayName') ?? '').trim() || email,\n self: false,\n })\n }\n return [...found.values()]\n}\n\n/**\n * Whether one address may receive a proof of this site's mail.\n *\n * Asked as a membership question rather than by scanning\n * {@link proofRecipientsForHost}'s ceilinged page: the picker is allowed to\n * show the first two hundred colleagues, and the gate is not allowed to\n * refuse the two hundred and first.\n */\nasync function eligibleProofAddress(options: {\n hostId: string\n callerEmail: string\n address: string\n}): Promise<boolean> {\n const address = String(options?.address ?? '')\n .trim()\n .toLowerCase()\n if (!address) return false\n if (address === String(options?.callerEmail ?? '').trim().toLowerCase()) {\n return true\n }\n\n const orgForHost = await getOrgForHost(options?.hostId).catch(() => null)\n const orgId = String(orgForHost?.orgId ?? '')\n if (!orgId) return false\n\n const match = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .where('email', '==', address)\n .limit(1)\n .get()\n .catch(() => null)\n return Boolean(match && !match.empty)\n}\n\n/** One person a proof can be rendered as. Reaches nobody. */\nexport interface ProofPersona {\n email: string\n name: string\n /** Which audience the record came from, so the drawer can say. */\n source: 'lead' | 'member' | 'contact'\n}\n\n/** How many of each source the persona picker offers. */\nconst PROOF_PERSONA_SAMPLE = 20\n\n/**\n * A SAMPLE of the people this site's mail is addressed to, for the picker.\n *\n * A sample and not a search: the question the drawer asks is \"show me this\n * email as somebody real\", and twenty names from each source answers it for\n * the cost of three small reads. A contact picker with a query behind it is a\n * different feature, and it would put a text input in front of the org's\n * whole contact list on a surface whose job is to prove one email.\n *\n * Ordered by document id in every source, because a `limit` with no `orderBy`\n * is a random sample in doc-id order that a client `sort` then makes LOOK\n * newest-first. Naming the order keeps the picker stable between openings —\n * and on the org path it is also the order the automatic index for the scope\n * filter can actually serve.\n */\nexport async function proofPersonasForHost(\n hostId: string,\n): Promise<ProofPersona[]> {\n const byId = firebaseAdmin.firestore.FieldPath.documentId()\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const found = new Map<string, ProofPersona>()\n\n const collect = (\n docs: FirebaseFirestore.QueryDocumentSnapshot[] | undefined,\n source: ProofPersona['source'],\n nameFields: readonly string[],\n ) => {\n for (const doc of docs ?? []) {\n const email = String(doc.get('email') ?? '')\n .trim()\n .toLowerCase()\n if (!email || found.has(email)) continue\n const name = nameFields\n .map((field) => String(doc.get(field) ?? '').trim())\n .find(Boolean)\n found.set(email, { email, name: name ?? '', source })\n }\n }\n\n const [leads, members, contacts] = await Promise.all([\n orgDataQueryForHost(hostId, 'leads')\n .then(({ query }) => query.orderBy(byId).limit(PROOF_PERSONA_SAMPLE).get())\n .catch(() => null),\n hostRef\n .collection('siteMembers')\n .orderBy(byId)\n .limit(PROOF_PERSONA_SAMPLE)\n .get()\n .catch(() => null),\n orgDataQueryForHost(hostId, 'contacts')\n .then(({ query }) => query.orderBy(byId).limit(PROOF_PERSONA_SAMPLE).get())\n .catch(() => null),\n ])\n\n collect(leads?.docs, 'lead', ['name'])\n // `displayName` first, and `name` only as a fallback: `siteMembers` has\n // never had a `name` field, so reading it alone renders every member\n // persona nameless — which is the exact defect that made merge tags resolve\n // to empty strings for whole audiences.\n collect(members?.docs, 'member', ['displayName', 'name'])\n collect(contacts?.docs, 'contact', ['name', 'firstName'])\n return [...found.values()]\n}\n\n/**\n * THE DOCUMENT ONE ADDRESS HAS ON THIS SITE, wherever it lives, or null.\n *\n * ONE lookup for both of the questions a proof asks about a person — what is\n * their name, and did they opt out — because the two must not disagree about\n * WHO the address is. A persona resolved from the contact record beside a\n * consent basis resolved from a lead record would be two different people\n * wearing one address.\n *\n * The three sources are the three an audience is built from, tried in the\n * order a small site grows them. Nothing here is taken from the request.\n *\n * The org `contacts` lookup goes through the org's address index narrowed\n * to this site (AGL-2633): the index is consulted for the address, the\n * contact it names is checked against `visibleTo` in memory, and only\n * then does the `email ==` query run. So a person whose two records were\n * merged is found under the address that became an alternate, and the\n * scope check is not skipped — it is applied to the one document an\n * address names, which is the same shape the segment branch above uses.\n */\nasync function findAudienceDocument(\n hostId: string,\n email: string,\n): Promise<{ data: Record<string, unknown>; nameFields: readonly string[] } | null> {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n for (const [collection, nameFields] of [\n ['leads', ['name']],\n // `displayName` first: `siteMembers` has never had a `name` field, and\n // reading only that is how merge tags came to render empty for a whole\n // audience.\n ['siteMembers', ['displayName', 'name']],\n ] as const) {\n const snapshot = await hostRef\n .collection(collection)\n .where('email', '==', email)\n .limit(1)\n .get()\n .catch(() => null)\n const doc = snapshot?.docs?.[0]\n if (doc) {\n return { data: (doc.data() ?? {}) as Record<string, unknown>, nameFields }\n }\n }\n\n const contacts = await orgDataCollectionForHost(hostId, 'contacts').catch(\n () => null,\n )\n const contact = contacts\n ? await findContactByEmail(contacts, email, { hostId }).catch(() => null)\n : null\n return contact\n ? {\n data: (contact.data() ?? {}) as Record<string, unknown>,\n nameFields: ['name', 'firstName'],\n }\n : null\n}\n\n/**\n * The stored marketing-consent fields for one address, or null when this site\n * holds no document for it at all.\n *\n * Null and an opted-out record are different answers and the caller treats\n * them differently: an address we have never seen keeps whatever basis the\n * policy assigns an unrecorded person, and one that said no is refused.\n */\nasync function readStoredConsent(\n hostId: string,\n email: string,\n): Promise<Record<string, unknown> | null> {\n if (!email || !EMAIL_PATTERN.test(email)) return null\n const found = await findAudienceDocument(hostId, email).catch(() => null)\n return found?.data ?? null\n}\n\n/**\n * The person whose stored data a proof renders as, or null.\n *\n * Read from the audience documents rather than taken from the request, so a\n * proof demonstrates what the MERGE will do to real data.\n */\nasync function resolveProofPersona(\n hostId: string,\n rawEmail: string,\n): Promise<{ email: string; name?: string } | null> {\n const email = String(rawEmail ?? '')\n .trim()\n .toLowerCase()\n if (!email || !EMAIL_PATTERN.test(email)) return null\n\n const found = await findAudienceDocument(hostId, email).catch(() => null)\n const name = (found?.nameFields ?? [])\n .map((field) => String(found?.data?.[field] ?? '').trim())\n .find(Boolean)\n\n /*\n * An address that matches nobody still renders, as itself with no name.\n *\n * Refusing would be worse: the merchant asked to see what the email looks\n * like addressed to this person, and \"we could not find them\" is an answer\n * about our storage rather than about their email. The drawer says which\n * record a persona came from, so a proof that fell through to this reads as\n * the address it is.\n */\n return name ? { email, name } : { email }\n}\n\nfunction storedSendOptionsFrom(\n snapshot: FirebaseFirestore.DocumentSnapshot,\n hostId: string,\n senderUid: string,\n followUp: boolean,\n): CampaignSendOptions {\n const audience = String(snapshot.get('audience') ?? '')\n /*\n * A `manual` audience is stored on a scheduled or drafted email and is not\n * stored on one that has already gone out, so the same audience kind is\n * repeatable for one caller and not the other.\n *\n * `emails` is written by the branches that store a send for later. An\n * immediate send takes its addresses from the request and keeps none of\n * them, which is what leaves a follow-up with nothing to re-resolve.\n */\n const emails = Array.isArray(snapshot.get('emails'))\n ? (snapshot.get('emails') as unknown[]).map(String)\n : undefined\n if (audience === 'manual' && (followUp || !emails?.length)) {\n throw new CampaignSendError(\n followUp\n ? 'This email went to addresses typed into the composer, which are ' +\n 'not kept, so there is no audience to add anybody from. Compose a ' +\n 'new email to the people you want to reach.'\n : 'This email is addressed to typed-in recipients but records none, ' +\n 'so there is nobody to send it to.',\n 400,\n )\n }\n const templateScreenId = String(snapshot.get('templateScreenId') ?? '')\n const body = String(snapshot.get('body') ?? '')\n if (!templateScreenId && !body) {\n throw new CampaignSendError('This email has no message to send', 400)\n }\n const optional = (field: string) => {\n const value = String(snapshot.get(field) ?? '')\n return value ? { [field]: value } : {}\n }\n return {\n hostId,\n campaignId: snapshot.id,\n ...(followUp ? { followUp: true } : {}),\n senderUid,\n subject: String(snapshot.get('subject') ?? ''),\n body,\n audience,\n ...(emails?.length ? { emails } : {}),\n ...(templateScreenId ? { templateScreenId } : {}),\n /*\n * The plain-text part somebody wrote, read back with everything else the\n * record holds. A follow-up or a `sendNow` that re-generated it from the\n * design instead would mail a different text part to the second half of\n * an audience than the first half received.\n */\n ...optional('plainText'),\n ...optional('segmentId'),\n ...optional('listId'),\n ...optional('topicId'),\n ...optional('fromName'),\n ...optional('replyTo'),\n /*\n * The sender the email ALREADY went out as, so a follow-up reaches the\n * rest of its audience from the same address. Taking the site's current\n * default instead would split one mailing across two `From:` lines, which\n * is the same drift the stored name and reply address are read back for.\n */\n ...optional('senderId'),\n ...optional('preheader'),\n ...optional('displayName'),\n ...optional('emailCampaignId'),\n /*\n * The experiment is deliberately NOT carried over on a follow-up.\n *\n * `performCampaignSend` refuses an experiment that is neither running nor\n * decided, so a finished one would fail the whole follow-up — and a\n * running one would take the new recipients as fresh exposures on an\n * experiment whose result the first send has already influenced. So the\n * follow-up mails the subject and body the record holds, which is the\n * campaign's own copy rather than any variant's override.\n *\n * A first send is the opposite case: the email has not run anywhere yet,\n * so the experiment it was composed under is the one it is supposed to\n * go out under, and dropping it here would silently mail the control.\n */\n ...(followUp ? {} : optional('experimentId')),\n }\n}\n\n/**\n * Campaign API (AGL-161/272): `action` picks the operation.\n *\n * Every one of them requires a site admin or editor.\n *\n * ## The ones that mail something\n *\n * `send` (the default) delivers copy carried in the request. `sendNow` mails\n * a draft or a scheduled email ahead of its time, and `followUp` mails an\n * already-sent one to the people it has not reached; both of those take every\n * field off the RECORD rather than the request. `test` delivers to the caller\n * alone and records nothing.\n *\n * ## The ones that only write\n *\n * `draft` stores an email that has not been sent, `schedule` stores one with\n * a `sendAtMs` for the processor to deliver, `update` corrects the merchant's\n * own name for an email at any point in its life, and `cancel` withdraws a\n * scheduled one. None of them reserves allowance or moves a meter.\n *\n * ## The ones that answer a question\n *\n * `preview` resolves the audience and reports the counts, `renderPreview`\n * renders the composed message. Neither writes.\n */\nexport const campaignSendHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n\n const hostId = String(req.body?.hostId ?? '')\n const action = String(req.body?.action ?? 'send')\n const subject = String(req.body?.subject ?? '')\n .trim()\n .slice(0, 150)\n const body = String(req.body?.body ?? '')\n .trim()\n .slice(0, 20000)\n const audience = String(req.body?.audience ?? 'leads')\n const templateScreenId = String(req.body?.templateScreenId ?? '')\n /*\n * THE AUTHOR'S OWN PLAIN-TEXT PART, for a designed email.\n *\n * Capped like the body and NOT flattened like the header fields beside it:\n * this is a message part rather than a header value, so its line breaks are\n * the formatting a text-only reader gets rather than an injection shape.\n */\n const plainText = String(req.body?.plainText ?? '')\n .trim()\n .slice(0, 20000)\n /*\n * Which design version that part was written against, so a composer can say\n * when it has gone stale. Validated as a document id because it is stored\n * and compared against one.\n */\n const plainTextVersionId = String(req.body?.plainTextVersionId ?? '')\n /*\n * The composer's sender fields, and the one rule they all obey: a value a\n * merchant typed reaches a MIME header, so it is flattened to a single line\n * before it goes anywhere. `applyFromName` quotes the display name and\n * strips quotes from it, but nothing downstream removes a CR or an LF, and\n * a header value carrying one is the injection shape.\n */\n const headerSafe = (value: unknown, max: number): string =>\n String(value ?? '')\n /*\n * The control characters are the POINT of this class rather than an\n * accident in it: CR and LF inside a header value ARE the injection\n * shape, and `no-control-regex` cannot tell a pattern that matches\n * them in order to remove them from one that matches them by mistake.\n */\n // eslint-disable-next-line no-control-regex\n .replace(/[\\s\\u0000-\\u001f\\u007f]+/g, ' ')\n .trim()\n .slice(0, max)\n // 78 characters is the line length a display name has to live inside.\n const fromName = headerSafe(req.body?.fromName, 78)\n const replyTo = headerSafe(req.body?.replyTo, 254).toLowerCase()\n const preheader = headerSafe(req.body?.preheader, 200)\n /*\n * The email's own name, which is console-only. Flattened to a single line\n * with the header fields beside it even though it reaches no header: it is\n * rendered into a table and a page title, and a value carrying control\n * characters is worth normalizing wherever it is going.\n */\n const displayName = headerSafe(req.body?.displayName, 60)\n /*\n * `req.body.sendingIdentity` is READ BY NOTHING.\n *\n * A body naming `acme.com`, or `platform`, is not an error and is not\n * honored: the DOMAIN is resolved from the host document, so there is no\n * value this field could carry that would reach it. Left undocumented it\n * would look like an oversight; said here, it is the closure.\n *\n * `senderId` below does not reopen it, and is worth reading against it. What\n * was closed is a request naming an ADDRESS — a domain, or a free local part\n * — because a mailbox has to be one somebody serves. What this names is a\n * row in `hosts/{hostId}/senders`, written under the `org.settings` gate and\n * validated there, so the addresses it can reach are the ones this site was\n * already configured to send as. An id this site does not hold is refused\n * rather than defaulted, which is the property that keeps the two apart:\n * nothing a request says can produce an address that was not configured.\n */\n const senderId = String(req.body?.senderId ?? '')\n if (senderId && !isDocumentId(senderId)) {\n return res.status(400).json({ error: 'Invalid sender' })\n }\n if (plainTextVersionId && !isDocumentId(plainTextVersionId)) {\n return res.status(400).json({ error: 'Invalid design version' })\n }\n // The campaign this send joins. Validated as a document id here because it\n // is stored and later queried as one.\n const emailCampaignId = String(req.body?.emailCampaignId ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n if (emailCampaignId && !isDocumentId(emailCampaignId)) {\n return res.status(400).json({ error: 'Invalid campaign' })\n }\n if (replyTo && !EMAIL_PATTERN.test(replyTo)) {\n return res.status(400).json({ error: 'Reply-to must be an email address' })\n }\n /*\n * Designed emails carry their content in the template; plain sends still\n * need subject + body.\n *\n * THE ACTIONS THAT MAIL NOTHING ARE EXEMPT, and the composer is the reason.\n * It asks for the recipient count as soon as it mounts — before any copy\n * exists, which is the whole point of asking — so requiring copy of\n * `preview` refused every count a plain-text campaign ever asked for, and\n * the readout under the Subject field showed this message instead of the\n * audience size and the consent split. The preview branch below substitutes\n * placeholder copy precisely because it needs none: the count is a fact\n * about the audience, and no part of resolving it reads the subject or the\n * body. `renderPreview` is exempt for the same reason in the other\n * direction — it renders whatever has been typed so far, including nothing.\n */\n /*\n * `followUp` and `sendNow` join the exempt actions, and for a stricter\n * reason than the other three: they do not merely need no copy, they must\n * be given none. The message they mail is the one already on the record —\n * see `storedSendOptionsFrom` — so a subject and body in the request would\n * be fields the route silently discards, and a required field that is\n * discarded is the shape that teaches a caller it was used.\n *\n * `draft` is exempt for the opposite reason. A draft is an email that has\n * not been written yet: requiring a subject and a body of it would mean\n * there is no way to create one, which is the whole state.\n *\n * `update` is exempt because it edits neither — it carries a name and\n * nothing else.\n */\n /*\n * `proofOptions` joins them for the plainest of the reasons: it answers who\n * a test may be sent to and whose data could fill it, which is a question\n * about the workspace and not about the message. The composer asks it when\n * the test drawer opens, which is routinely before a subject exists.\n */\n const mails =\n action !== 'cancel' &&\n action !== 'preview' &&\n action !== 'proofOptions' &&\n action !== 'renderPreview' &&\n action !== 'followUp' &&\n action !== 'sendNow' &&\n action !== 'draft' &&\n action !== 'update'\n if (mails && !templateScreenId && (!subject || !body)) {\n return res.status(400).json({ error: 'Missing subject or body' })\n }\n /*==========================================\n * ONE MESSAGE, ONE SOURCE.\n *\n * `body` is the MESSAGE of a plain-text email — the thing recipients read,\n * from which the HTML part is synthesized. `plainText` is the text HALF of a\n * designed email, whose message lives in the nodes. Two strings of plain\n * text, two different jobs, and each belongs to exactly one mode.\n *\n * A `body` arriving beside a template used to be accepted, computed for\n * merge tags and then dropped: `renderRecipientEmail` read it only when no\n * template was given, and both gates — this one and the composer's — passed\n * on EITHER input, so a merchant who picked a design and also wrote a\n * message lost the message with nothing said.\n *\n * It is refused rather than quietly reinterpreted as the text part. Copy\n * written as \"the message\" is not copy reviewed as \"what a text-only reader\n * gets\", and promoting it would start mailing unreviewed text out of records\n * that already exist. What the author wants is offered explicitly instead —\n * `plainText`, which they can see, edit and preview — and the composer\n * shows a stranded body rather than adopting it.\n *\n * `plainText` without a template is refused for the mirror reason: a\n * plain-text email's text part IS its body, so a second string claiming to\n * be the text part is the same two-sources problem under a new name.\n *\n * Only the actions that carry COMPOSED copy are checked. `preview`\n * substitutes placeholder copy and reads neither; `followUp`, `sendNow`,\n * `update` and `cancel` mail what the record already holds, and a record\n * written before this rule may legitimately still carry an inert body\n * beside its template — refusing those would strand an existing draft over\n * a field its send has never read.\n *=========================================*/\n const carriesComposedCopy =\n action === 'send' ||\n action === 'schedule' ||\n action === 'draft' ||\n action === 'test' ||\n action === 'renderPreview'\n if (carriesComposedCopy && templateScreenId && body) {\n return res.status(400).json({\n error:\n 'This email is built from a design, which carries its own message. ' +\n 'Write the plain-text version instead, or switch this email to ' +\n 'plain text.',\n })\n }\n if (carriesComposedCopy && !templateScreenId && plainText) {\n return res.status(400).json({\n error:\n 'A plain-text email has no separate text version — what you type is ' +\n 'the message.',\n })\n }\n if (!['leads', 'members', 'manual', 'segment', 'list'].includes(audience)) {\n return res.status(400).json({ error: 'Unknown audience' })\n }\n /*\n * The composer's topic, refused here as well as inside `performCampaignSend`.\n *\n * Both, because the SCHEDULE branch below writes the campaign document\n * without going through the send — the same asymmetry AGL-1771 found for\n * `campaignId` — so a topic that only the send validated would be stored\n * unchecked and then signed into a link a fortnight later.\n *\n * An empty value is not an error: it means \"the composer did not say\", which\n * `performCampaignSend` resolves to the default topic.\n */\n const topicId = String(req.body?.topicId ?? '')\n if (topicId && !isEmailTopicId(topicId)) {\n return res.status(400).json({ error: 'Unknown topic' })\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 /*\n * A VERIFIED ADDRESS IS A PRECONDITION OF SENDING MAIL (AGL-479/2589),\n * and this is the door every send arrives at — broadcast, scheduled and\n * test alike, since all three branches sit below this check.\n *\n * Stated here rather than inferred, which is the whole change. The role\n * lookup on the next lines already refused an unverified account, but\n * only as a side effect of provisioning: nothing can enter a host's\n * `memberRoles` map without having verified, so the role resolved to\n * undefined and the 403 came from the wrong sentence. That is a fact\n * about how accounts are created, not a rule about who may send, and it\n * has already moved once — signup provisions an ORG for an account that\n * has not verified yet (AGL-2585). A comparable grace on host creation\n * would have opened this surface with nothing else in the way, on a\n * `p=reject` sending domain, and no test would have gone red.\n *\n * `campaign-send-verification.spec.ts` holds the property directly, so a\n * future grace fails a test instead of leaking reputation.\n *\n * Impersonation is exempt on the AGL-480 reasoning the other ~135 gates\n * use: staff have authenticated separately, the act is audited, and the\n * account most likely to need support is the newest one.\n */\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n // The shape of `emailUnverifiedResponse()` (AGL-479), written out\n // because that helper returns a fetch `Response` and this is a\n // (req, res) plugin handler.\n return res.status(403).json({\n error: 'Verify your email to continue',\n reason: 'email-unverified',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\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\n if (action === 'proofOptions') {\n /*\n * THE TWO LISTS THE TEST DRAWER IS BUILT FROM, and they are different\n * kinds of thing.\n *\n * `recipients` is a RULE made visible: these are the only addresses a\n * test may be delivered to, so the drawer offers them instead of a text\n * box whose every other answer is a refusal nobody could have predicted.\n *\n * `personas` is a CONVENIENCE: whose stored data a proof renders as.\n * Choosing one mails that person nothing, which the drawer says in as\n * many words — the two controls sit next to each other and the whole\n * risk of the feature is somebody reading the second as the first.\n */\n const [recipients, personas] = await Promise.all([\n proofRecipientsForHost({ hostId, callerEmail: String(decoded.email ?? '') }),\n proofPersonasForHost(hostId),\n ])\n return res.status(200).json({ recipients, personas })\n }\n\n if (action === 'test') {\n /*\n * PROOF ONE EMAIL: as somebody, to somebody, from an identity — and\n * still no campaign record and no counter.\n *\n * The three choices are independent and only one of them decides who\n * receives mail. `personaEmail` changes what the merge tags RESOLVE to\n * and reaches nobody; `to` is the only address anything is delivered\n * to. Which identity it leaves on is not among them: that is resolved\n * from the host document, and no request field reaches it.\n */\n const ownEmail = String(decoded.email ?? '')\n .trim()\n .toLowerCase()\n const requestedTo = String(req.body?.to ?? '')\n .trim()\n .toLowerCase()\n const testEmail = requestedTo || ownEmail\n if (!testEmail) {\n return res\n .status(400)\n .json({ error: 'Your account has no email address for tests' })\n }\n if (!EMAIL_PATTERN.test(testEmail)) {\n return res\n .status(400)\n .json({ error: 'Enter a valid address to send the test to' })\n }\n /*\n * WHO A TEST SEND MAY REACH, decided here because it is the only layer\n * that can ask whose address this is.\n *\n * A proof may go to the caller's own account address or to another\n * ACCOUNT HOLDER on the owning workspace, and to nobody else. The two\n * populations a merchant could otherwise reach through this button are\n * exactly the two that must not be reachable: a contact or lead, who is\n * subject to the consent rule this send is exempt from, and a stranger,\n * who has no relationship with the workspace at all.\n *\n * `siteMembers` are deliberately NOT eligible. They are the tenant's\n * customers — the audience — and an audience member reached by a\n * \"test\" is an audience member who has been mailed.\n */\n const eligible = await eligibleProofAddress({\n hostId,\n callerEmail: ownEmail,\n address: testEmail,\n })\n if (!eligible) {\n return res.status(403).json({\n error:\n `A test can only be sent to you or to someone with an account on ` +\n `this workspace, and ${testEmail} is neither. Add them to the ` +\n `workspace first, or send the test to yourself and forward it.`,\n })\n }\n /*\n * The person whose data fills the merge tags. Looked up rather than\n * taken from the request, so what a proof shows is what the audience\n * document actually holds — a persona assembled from a request would\n * demonstrate the composer's own typing rather than the merge.\n */\n const persona = await resolveProofPersona(\n hostId,\n String(req.body?.personaEmail ?? ''),\n )\n const result = await performCampaignSend({\n hostId,\n subject,\n body: body || 'Test send',\n audience: 'manual',\n emails: [testEmail],\n templateScreenId: templateScreenId || undefined,\n // A proof mails the message as composed, the text half included:\n // checking the plain-text version in a real inbox is one of the three\n // things a proof is asked.\n ...(plainText ? { plainText } : {}),\n fromName,\n replyTo,\n // The proof leaves as the sender the composer has chosen, so what a\n // merchant reads in their own inbox is the `From:` their audience\n // will see rather than the site's default.\n ...(senderId ? { senderId } : {}),\n preheader,\n ...(persona ? { proofPersona: persona } : {}),\n recordCampaign: false,\n senderUid: decoded.uid,\n // Not marketing: the recipient holds an account on this workspace.\n proofFor: testEmail,\n })\n return res.status(200).json({\n ...result,\n test: true,\n to: testEmail,\n ...(persona ? { personaEmail: persona.email } : {}),\n })\n }\n\n if (action === 'renderPreview') {\n /*\n * THE MESSAGE, RENDERED, AND NOT ONE ADDRESS RESOLVED.\n *\n * Separate from `preview` because the two answer different questions at\n * different costs. `preview` sweeps the audience — up to 5,000 documents\n * — to count people, and its answer changes only when the audience does.\n * This one answers \"what does my email look like\", which changes on\n * every keystroke, and reads at most the template and its products.\n * Folding the render into `preview` would page the merchant's whole\n * contact list once per debounce tick, for a number that had not moved.\n *\n * Rendered through `renderRecipientEmail`, which is what the per-recipient\n * send loop calls, so this is the HTML that will be mailed and not a\n * likeness of it.\n */\n const template = templateScreenId\n ? await loadEmailTemplate(hostId, templateScreenId)\n : null\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n const orgForHost = await getOrgForHost(hostId)\n const branding = resolveBrandingProfile(orgForHost?.org as never)\n /*\n * Personalized for the REQUESTER, because a preview showing raw\n * `{{firstName|there}}` tells a merchant nothing about what a recipient\n * will read, and inventing a fictional contact would make a merge tag\n * that resolves to nothing look like one that works.\n */\n const rendered = renderRecipientEmail({\n subject,\n preheader,\n content: template\n ? {\n mode: 'design',\n template,\n ...(plainText ? { plainText } : {}),\n }\n : { mode: 'text', body },\n recipient: {\n email: String(decoded.email ?? ''),\n name: String((decoded as Record<string, unknown>)['name'] ?? ''),\n },\n siteBase,\n hostId,\n // Unsigned, and it is not a working opt-out: minting a real signature\n // here would put a live preference link for the requester's own\n // address into a page they are only reading. The footer's presence,\n // and its wording, is what the preview is showing.\n unsubscribeUrl: `${siteBase}/api/email/preferences`,\n })\n return res.status(200).json({\n ...rendered,\n preheader: preheader || template?.preheader || '',\n fromName: fromName || branding.fromName,\n ...(replyTo ? { replyTo } : {}),\n })\n }\n\n if (action === 'preview') {\n // Read-only, and it needs the same admin/editor role as a send: the\n // audience size of someone else's site is not public information.\n const result = await performCampaignSend({\n hostId,\n subject: subject || 'preview',\n body: body || 'preview',\n audience,\n segmentId: String(req.body?.segmentId ?? ''),\n listId: String(req.body?.listId ?? ''),\n topicId: topicId || undefined,\n emails: Array.isArray(req.body?.emails)\n ? req.body.emails.map(String)\n : undefined,\n templateScreenId: templateScreenId || undefined,\n /*\n * The composer's identity choice rides the preview, which is what\n * makes the refusal arrive BEFORE the Send button rather than from\n * it. The dry run resolves the identity on the same terms a send\n * does and throws the same 409, so picking a domain whose DNS is\n * unfinished says so the moment it is picked — and a sender this site\n * no longer holds is refused at the picker rather than at Send.\n */\n ...(senderId ? { senderId } : {}),\n senderUid: decoded.uid,\n dryRun: true,\n })\n return res.status(200).json(result)\n }\n\n if (action === 'followUp') {\n /*\n * SEND AN EMAIL THAT HAS ALREADY GONE OUT TO THE PEOPLE IT HAS NOT\n * REACHED.\n *\n * The request names a site and an email and carries nothing else that\n * is read. Everything the send needs comes back off the record, and\n * `performCampaignSend` re-checks the admission rules this branch does\n * not check at all — it is the same authorization, the same consent\n * split, the same two suppression lists, the same monthly reservation\n * and the same hourly ceilings, because it is the same function.\n *\n * `dryRun` rides through so the console can ask how many people are\n * left before offering the button, and the answer comes from the code\n * that would do the sending rather than from a count of its own.\n */\n const followUpId = String(req.body?.campaignId ?? '')\n if (!isDocumentId(followUpId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const sendSnapshot = await hostRef\n .collection('campaigns')\n .doc(followUpId)\n .get()\n if (!sendSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n const result = await performCampaignSend({\n ...storedSendOptionsFrom(sendSnapshot, hostId, decoded.uid, true),\n ...(req.body?.dryRun ? { dryRun: true } : {}),\n })\n return res.status(200).json(result)\n }\n\n /*==========================================\n * AN EMAIL THAT EXISTS BEFORE IT IS SENT.\n *\n * `draft` is the same write the `schedule` branch below makes, minus the\n * send time — a record in the same collection, under the same id it will\n * keep forever, carrying the copy composed so far and `status: 'draft'`.\n *\n * ## Why a state on the record rather than a collection of its own\n *\n * The id is the reason. `performCampaignSend` adopts a `campaignId` it is\n * given, so a draft becomes the sent email AT ITS OWN ID — which is what\n * makes `/marketing/campaigns/{sendId}` resolve from the moment the email is\n * created, and what keeps the `cid=` inside every delivered unsubscribe\n * HMAC pointing at the record it was minted for. A draft in a second\n * collection would have to be copied to a new id at send time, and the\n * URL a merchant had open would stop being the email's URL.\n *\n * ## What a draft costs\n *\n * Nothing. This branch reserves no monthly allowance, claims no hourly\n * budget and moves no meter — it writes one document, exactly as\n * `schedule` always has. The scheduled processor queries\n * `status == 'scheduled'`, so a draft is never picked up and cannot\n * escape on its own; `performCampaignSend` is the only thing that mails\n * it, and only when somebody asks.\n *=========================================*/\n if (action === 'draft' || action === 'schedule' || action === 'update') {\n const scheduling = action === 'schedule'\n const sendAtMs = Number(req.body?.sendAtMs ?? 0)\n if (scheduling && (!Number.isFinite(sendAtMs) || sendAtMs <= Date.now())) {\n return res.status(400).json({ error: 'Pick a future send time' })\n }\n const campaignId =\n String(req.body?.campaignId ?? '') || createResourceUid()\n // AGL-1771: this branch WRITES, and it is the only campaign write that\n // does not go through `performCampaignSend`'s guard. A `campaignId` of\n // `a/b/c` scheduled the campaign at `campaigns/a/b/c` — which the\n // scheduled-campaign processor would then pick up by `collectionGroup`\n // and send, from a document the merchant can neither see in their\n // campaigns list nor cancel.\n if (!isDocumentId(campaignId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const targetRef = hostRef.collection('campaigns').doc(campaignId)\n const targetSnapshot = await targetRef.get()\n const targetState = targetSnapshot.exists\n ? String(targetSnapshot.get('status') ?? '')\n : ''\n\n /*==========================================\n * WHAT IS ALREADY IN AN INBOX IS NOT EDITABLE.\n *\n * These branches address an EXISTING document by id, so without this\n * check `schedule` would happily merge a new subject and body onto an\n * email that went out last March and set its status back to\n * `scheduled` — rewriting the record of what was delivered, and handing\n * the processor a message to mail a second time under a `cid` whose\n * unsubscribe links are already in inboxes.\n *\n * So copy may only be written while the email is still unsent. `update`\n * is the deliberate exception and is why it is in this branch at all:\n * it writes the merchant's own NAME for the email and nothing else —\n * see the write below — which is console-only text that reached no\n * recipient and therefore contradicts no delivered mail.\n *=========================================*/\n const rewritable = !targetSnapshot.exists ||\n targetState === 'draft' ||\n targetState === 'scheduled'\n if (action !== 'update' && !rewritable) {\n return res.status(409).json({\n error:\n targetState === 'sent'\n ? 'This email has already been sent, so its message and ' +\n 'audience can no longer be changed. Compose a new email.'\n : 'This email was canceled, so it can no longer be scheduled. ' +\n 'Compose a new email.',\n })\n }\n if (action === 'update') {\n /*\n * The one field a sent email still owns. It is the friendly name the\n * create drawer captures — never the subject, which describes mail\n * that is already in inboxes — so it can be corrected at any point in\n * an email's life without making the record disagree with what was\n * delivered.\n */\n if (!targetSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n await targetRef.set({ displayName }, { merge: true })\n return res.status(200).json({ campaignId, displayName })\n }\n await targetRef.set(\n {\n subject,\n body,\n audience,\n /*\n * The one date every message carries — see `email-record.ts`.\n * Stamped only where this write CREATES the record: a draft saved\n * over and over is the same email, and re-stamping it would walk\n * its creation date forward on every keystroke's save. The\n * existence read above is the one this branch already does.\n */\n ...(targetSnapshot.exists ? {} : { createdAtMs: Date.now() }),\n ...(req.body?.segmentId\n ? { segmentId: String(req.body.segmentId) }\n : {}),\n ...(req.body?.listId ? { listId: String(req.body.listId) } : {}),\n ...(topicId ? { topicId } : {}),\n ...(Array.isArray(req.body?.emails)\n ? { emails: req.body.emails.map(String).slice(0, 500) }\n : {}),\n ...(req.body?.experimentId\n ? { experimentId: String(req.body.experimentId) }\n : {}),\n /*\n * THE TEMPLATE IS CLEARED WHEN THERE IS NONE, not merely omitted.\n *\n * `templateScreenId` is the field that decides which of the two ways\n * this email is written — see `emailMessageMode` — so leaving it\n * standing under `merge: true` is not a stale pointer, it is the\n * wrong mode. A draft moved from a design to a typed message would\n * be stored carrying BOTH, reopen as designed, and mail the design\n * while the message the merchant just wrote sat unread on the\n * record. The same discard this pair of fields already produced\n * once, arriving by the save path instead of the send path.\n */\n templateScreenId:\n templateScreenId || firebaseAdmin.firestore.FieldValue.delete(),\n /*\n * The authored plain-text part, and the design version it was\n * written against. Cleared together and for the same reason the\n * template is: an override left standing under `merge: true` after\n * somebody switched this email to plain text would be a text part\n * for a design the record no longer names, and the staleness the\n * version id exists to expose would be measured against nothing.\n */\n plainText: plainText || firebaseAdmin.firestore.FieldValue.delete(),\n plainTextVersionId:\n (plainText && plainTextVersionId) ||\n firebaseAdmin.firestore.FieldValue.delete(),\n // The composer's sender fields ride on the stored campaign so the\n // scheduled processor mails the message that was composed rather\n // than one that reverts to the org's branding defaults.\n ...(fromName ? { fromName } : {}),\n ...(replyTo ? { replyTo } : {}),\n ...(senderId ? { senderId } : {}),\n ...(preheader ? { preheader } : {}),\n ...(displayName ? { displayName } : {}),\n ...(emailCampaignId ? { emailCampaignId } : {}),\n ...(scheduling\n ? {\n status: 'scheduled',\n sendAtMs,\n scheduledAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n scheduledBy: decoded.uid,\n }\n : {\n status: 'draft',\n /*\n * The send time is CLEARED rather than left standing.\n *\n * Saving a scheduled email back to a draft is how a merchant\n * takes it off the clock, and `merge: true` leaves any field\n * this write does not name — so a `sendAtMs` left behind\n * would sit on a draft as a due date nothing acts on, and the\n * emails list orders on exactly that field.\n */\n sendAtMs: firebaseAdmin.firestore.FieldValue.delete(),\n draftedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n draftedBy: decoded.uid,\n }),\n },\n { merge: true },\n )\n return res\n .status(200)\n .json({ campaignId, status: scheduling ? 'scheduled' : 'draft' })\n }\n\n if (action === 'cancel') {\n const campaignId = String(req.body?.campaignId ?? '')\n // AGL-1771: the ref used to be built one line ABOVE the `campaignId ?`\n // check below, which defeated that check — `.doc('')` throws on an empty\n // path segment, so the 400 this branch intends became a 500. Guarding\n // first is what lets the ref be built at all.\n if (!isDocumentId(campaignId)) {\n return res.status(400).json({ error: 'Not a scheduled campaign' })\n }\n const campaignRef = hostRef.collection('campaigns').doc(campaignId)\n const campaignSnapshot = await campaignRef.get()\n if (\n !campaignSnapshot?.exists ||\n campaignSnapshot.get('status') !== 'scheduled'\n ) {\n return res.status(400).json({ error: 'Not a scheduled campaign' })\n }\n await campaignRef.set(\n {\n status: 'canceled',\n canceledAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n canceledBy: decoded.uid,\n },\n { merge: true },\n )\n return res.status(200).json({ campaignId, status: 'canceled' })\n }\n\n if (action === 'sendNow') {\n /*==========================================\n * MAIL A DRAFTED OR SCHEDULED EMAIL, NOW.\n *\n * The request names a site and an email and carries nothing else that\n * is read. Everything comes off the record through\n * `storedSendOptionsFrom`, for the reason documented there: this branch\n * addresses an existing document by id, and a caller who could also\n * supply the copy could put arbitrary text on somebody else's send id\n * and mail it under that id's `cid`.\n *\n * It is not a second send path. `performCampaignSend` runs whole — the\n * same authorization, the same consent split, the same two suppression\n * lists, the same topic filtering, the same monthly reservation and the\n * same hourly governor — because it is the same function the scheduled\n * processor and the composer both call.\n *=========================================*/\n const sendNowId = String(req.body?.campaignId ?? '')\n if (!isDocumentId(sendNowId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const sendNowRef = hostRef.collection('campaigns').doc(sendNowId)\n const sendNowSnapshot = await sendNowRef.get()\n if (!sendNowSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n const sendNowState = String(sendNowSnapshot.get('status') ?? '')\n /*\n * Only an email that has not gone out yet. A sent one would be mailed a\n * second time to the whole audience under the same id — which is what\n * `followUp` exists to do safely, minus everyone already reached — and\n * a canceled one was withdrawn on purpose.\n */\n if (sendNowState !== 'draft' && sendNowState !== 'scheduled') {\n return res.status(400).json({\n error:\n sendNowState === 'sent'\n ? 'This email has already been sent. Use \"Send to more ' +\n 'recipients\" to reach the people it has not.'\n : 'Only a draft or a scheduled email can be sent now',\n })\n }\n /*==========================================\n * A CAMPAIGN BETWEEN BATCHES IS `scheduled`, AND MUST NOT RESTART.\n *\n * An audience larger than one send goes out over several runs, and\n * between them the email is stored as `scheduled` — the state the\n * processor claims to continue it. So the check above admits it, and\n * this branch would then call `performCampaignSend` with no\n * `continuation`: the whole audience resolved again, nobody subtracted,\n * and a second copy delivered to everybody the earlier batches already\n * reached, under the same `cid` whose unsubscribe links are in their\n * inboxes.\n *\n * Refused rather than quietly turned into a continuation. The rest of\n * this campaign is already due — the processor picks it up on its next\n * beat — so there is nothing for a merchant to ask for here, and\n * \"Send now\" on a send already in flight is a request made under a\n * misunderstanding the answer should correct.\n *=========================================*/\n const resumeRemaining = Math.floor(\n Number(sendNowSnapshot.get('resume')?.remaining ?? 0),\n )\n if (Number.isFinite(resumeRemaining) && resumeRemaining > 0) {\n return res.status(409).json({\n error:\n 'This email is already being sent, and the rest of it goes out ' +\n 'on its own. Sending it now would mail a second copy to ' +\n 'everyone it has already reached.',\n })\n }\n /*\n * CLAIMED BEFORE IT IS MAILED, exactly as the scheduled processor\n * claims one.\n *\n * A scheduled email whose time arrives mid-send would otherwise be\n * picked up by the processor's `status == 'scheduled'` query while this\n * request is still resolving its audience, and mailed twice. Moving it\n * to `sending` first is the same claim under the same transaction the\n * processor uses, so whichever gets there first is the only one that\n * sends.\n */\n const claimed = await firestore.runTransaction(async (transaction) => {\n const fresh = await transaction.get(sendNowRef)\n if (String(fresh.get('status') ?? '') !== sendNowState) return false\n transaction.update(sendNowRef, { status: 'sending' })\n return true\n })\n if (!claimed) {\n return res\n .status(409)\n .json({ error: 'This email is already being sent' })\n }\n try {\n const result = await performCampaignSend({\n ...storedSendOptionsFrom(sendNowSnapshot, hostId, decoded.uid, false),\n ...(req.body?.dryRun ? { dryRun: true } : {}),\n })\n /*\n * A dry run writes nothing, so the claim above is the only change it\n * made and it has to be put back — otherwise asking how many people\n * an email would reach would leave it stuck in `sending`.\n */\n if (req.body?.dryRun) {\n await sendNowRef.set({ status: sendNowState }, { merge: true })\n }\n return res.status(200).json(result)\n } catch (error) {\n // The claim is released on every failure. `performCampaignSend`\n // writes `status: 'sent'` itself on the way out, so nothing here\n // needs to set it — but a refusal that left the email in `sending`\n // would be an email the merchant can neither send nor cancel.\n await sendNowRef.set({ status: sendNowState }, { merge: true })\n throw error\n }\n }\n\n /*==========================================\n * AN IMMEDIATE SEND MAY NAME AN EMAIL, BUT ONLY AN UNSENT ONE.\n *\n * This branch takes its copy from the REQUEST, which is what the composer\n * needs — it is sending the message being typed. But it also accepts a\n * `campaignId`, and `performCampaignSend` adopts it, so without this check\n * a request naming a send that already went out would write new copy over\n * the record of what was delivered, replace its counters with this send's\n * own, and mail the whole audience a second copy under a `cid` whose\n * unsubscribe links are already in inboxes.\n *\n * Reaching the people an existing email has NOT reached is `followUp`,\n * which takes no copy from the request at all and subtracts everyone the\n * earlier sends recorded.\n *=========================================*/\n const sendId = String(req.body?.campaignId ?? '')\n if (sendId) {\n if (!isDocumentId(sendId)) {\n return res.status(400).json({ error: 'Invalid campaignId' })\n }\n const existing = await hostRef.collection('campaigns').doc(sendId).get()\n const existingState = existing.exists\n ? String(existing.get('status') ?? '')\n : ''\n if (existing.exists && existingState !== 'draft' && existingState !== 'scheduled') {\n return res.status(409).json({\n error:\n existingState === 'sent'\n ? 'This email has already been sent. Use \"Send to more ' +\n 'recipients\" to reach the people it has not.'\n : 'This email was canceled, so it cannot be sent. Compose a ' +\n 'new email.',\n })\n }\n }\n\n const result = await performCampaignSend({\n hostId,\n subject,\n body,\n audience,\n segmentId: String(req.body?.segmentId ?? ''),\n listId: String(req.body?.listId ?? ''),\n topicId: topicId || undefined,\n emails: Array.isArray(req.body?.emails) ? req.body.emails : undefined,\n campaignId: sendId,\n experimentId: String(req.body?.experimentId ?? ''),\n templateScreenId: templateScreenId || undefined,\n ...(plainText ? { plainText } : {}),\n fromName,\n replyTo,\n ...(senderId ? { senderId } : {}),\n preheader,\n displayName,\n emailCampaignId,\n senderUid: decoded.uid,\n })\n return res.status(200).json(result)\n } catch (error) {\n if (error instanceof CampaignSendError) {\n return res.status(error.status).json({ error: error.message })\n }\n console.error(error)\n return res.status(500).json({ error: 'Campaign send failed' })\n }\n}\n"],"names":["checkQuota","contactMatchesSegment","isEmailTopicId","DEFAULT_CAMPAIGN_TOPIC_ID","readMarketingBasis","resolveMarketingConsentPolicy","splitByMarketingConsent","createResourceUid","decodeStoredNodes","emailStarterSendBlock","resolveBrandingProfile","visibleToHost","renderRecipientEmail","assignExperimentVariant","readPluginRecordCard","hostPublicOrigin","consentGroupForSite","orgDataCollectionForHost","orgDataQueryForHost","filterSendableForHost","filterTopicSendable","firebaseAdmin","getOrgForHost","meterHostEmail","claimOrgEmailSendBudget","orgCampaignEmailSendsForMonth","readEmailSendRateConfig","readEmailSendRateWindow","reconcileCampaignSendReservation","reserveCampaignEmailSends","resolveHostSendingIdentity","findContactByEmail","isDocumentId","isImpersonationSession","buildUnsubscribeUrl","unsubscribeSignature","sharedUnsubscribeSignature","filterCadenceSendable","recordMarketingSends","CAMPAIGN_REACH_CEILING","campaignReachCovers","campaignSettledSize","partitionByCampaignReach","readCampaignReach","readCampaignSettled","recordCampaignReach","recordCampaignSkipped","claimOrgEmailSendDay","orgAgeDays","readSenderReputation","reconcileOrgEmailSendDay","recordCampaignAccepted","resolveOrgEmailRamp","createHash","EMAIL_MAX_AUDIENCE_PER_SEND","EMAIL_MAX_RECIPIENTS_PER_SEND","campaignBatchPlan","createProviderRequestPacer","effectiveReputationPolicy","HOST_SENDERS_COLLECTION","isEmailConfigured","rateLimitedRetryAtMs","readHostSender","sendEmail","sendingIdentityRefusal","sentAsStamp","MAX_RECIPIENTS_PER_SEND","EMAIL_PATTERN","AUDIENCE_PAGE_SIZE","AUDIENCE_SCAN_CEILING","sweepAudience","base","ordered","orderBy","firestore","FieldPath","documentId","docs","cursor","page","startAfter","limit","get","push","length","truncated","probe","suppressionId","email","update","toLowerCase","digest","hostId","secret","campaignId","topicId","CampaignSendError","Error","message","status","CampaignSendDeferredError","retryAtMs","loadEmailTemplate","screenId","screenSnapshot","app","screenRef","collection","doc","exists","versionId","versionSnapshot","String","nodes","Object","keys","installedFrom","listingId","revocation","data","block","reason","productIds","Set","values","filter","node","componentId","map","props","productId","slice","products","Promise","all","card","id","name","title","priceLabel","caption","imageUrl","url","path","subject","preheader","performCampaignSend","options","orgForHost","consent","unsubscribeSecret","process","env","EMAIL_UNSUBSCRIBE_SECRET","CRON_SECRET","body","audience","value","experimentId","templateScreenId","segmentId","listId","addsToExistingSend","Boolean","followUp","continuation","hostRef","hostSnapshot","reachedKeys","batchesSoFar","sendSnapshot","settled","sentSoFar","Number","sent","reached","toLocaleString","skipped","Math","max","floor","batch","finishContinuation","sendId","set","resume","remaining","nextAtMs","lastSentAt","FieldValue","serverTimestamp","merge","recipients","audienceSize","resuming","size","audienceTruncated","listName","names","Map","consentGroup","collectConsent","cleaned","trim","collectName","leads","query","members","segmentSnapshot","segment","tags","sources","contacts","listRef","parent","Array","isArray","emails","addressable","test","partitioned","unreached","alreadyReached","resolved","catch","orgId","senderId","senderSnapshot","chosenSender","sendingIdentity","selectedDomain","selectedLocalPart","localPart","poolMember","purpose","identityRefusal","missing","join","reputationPolicy","source","org","consentPolicy","proofFor","proofAddress","includes","has","stored","readStoredConsent","basis","consentSplit","one","mailable","unshift","platformRate","proofOnly","recordCampaign","batchCap","ramp","reputation","policy","reinstatedUntilMs","blocked","ageDays","deliveredLifetime","window","accepted","platformPerHour","perHour","graduated","enabled","dayRemaining","perDay","claimedToday","detail","Date","now","min","notSuppressed","onTopic","sendable","monthKey","toISOString","campaignSendLimit","overCapError","used","dryRun","suppressed","cadenceHeld","consented","consentedByOperator","grandfathered","consentWithheld","withheld","identity","summary","identitySource","siteBase","cname","subdomain","branding","mintsRecord","template","designedContent","mode","plainText","experiment","experimentSnapshot","target","winnerVariantId","$id","config","resetMs","hourly","count","allowed","ceiling","dayClaim","claim","month","ok","reservation","dayReservation","variantSends","sendableSet","settledOut","deferred","paceProviderRequest","index","variant","link","oneClickUrl","surface","unsubscribeUrl","content","recipient","proofPersona","result","to","html","text","headers","fromName","replyTo","context","variantId","entries","exposures","increment","updatedAt","undefined","plan","addressed","retryable","additive","measuresTheAudience","createdAtMs","from","displayName","emailCampaignId","stats","audienceSizeTruncated","fromEntries","clickTracked","sendAtMs","stop","lastSentBy","senderUid","sentAt","sentBy","sendCount","proofRecipientsForHost","callerEmail","found","label","self","eligibleProofAddress","address","match","where","empty","PROOF_PERSONA_SAMPLE","proofPersonasForHost","byId","collect","nameFields","field","find","then","findAudienceDocument","contact","snapshot","resolveProofPersona","rawEmail","storedSendOptionsFrom","optional","campaignSendHandler","req","res","method","json","error","action","plainTextVersionId","headerSafe","replace","mails","carriesComposedCopy","authorization","idToken","startsWith","decoded","auth","verifyIdToken","email_verified","memberRole","uid","personas","ownEmail","requestedTo","testEmail","eligible","persona","personaEmail","rendered","followUpId","targetSnapshot","scheduling","isFinite","targetRef","targetState","rewritable","delete","scheduledAt","scheduledBy","draftedAt","draftedBy","campaignRef","campaignSnapshot","canceledAt","canceledBy","sendNowSnapshot","sendNowId","sendNowRef","sendNowState","resumeRemaining","claimed","runTransaction","transaction","fresh","existing","existingState","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,UAAU,EACVC,qBAAqB,EACrBC,cAAc,EACdC,yBAAyB,EACzBC,kBAAkB,EAClBC,6BAA6B,EAC7BC,uBAAuB,EAEvBC,iBAAiB,EACjBC,iBAAiB,EACjBC,qBAAqB,EACrBC,sBAAsB,EACtBC,aAAa,QACR,sBAAqB;AAE5B,SAASC,oBAAoB,QAAQ,gDAA+C;AAEpF,SAASC,uBAAuB,QAA6B,oBAAU;AACvE,SAASC,oBAAoB,QAAQ,kDAAiD;AAEtF,SAASC,gBAAgB,QAAQ,sBAAqB;AACtD,SACEC,mBAAmB,EACnBC,wBAAwB,EACxBC,mBAAmB,EACnBC,qBAAqB,EACrBC,mBAAmB,EACnBC,aAAa,EACbC,aAAa,EACbC,cAAc,EACdC,uBAAuB,EACvBC,6BAA6B,EAC7BC,uBAAuB,EACvBC,uBAAuB,EACvBC,gCAAgC,EAChCC,yBAAyB,EAEzBC,0BAA0B,QACrB,2BAA0B;AACjC,sEAAsE;AACtE,wEAAwE;AACxE,SAASC,kBAAkB,QAAQ,sDAAqD;AACxF,SAASC,YAAY,QAAQ,8CAA6C;AAC1E;;;;;;;;CAQC,GACD,SAASC,sBAAsB,QAAQ,iDAAgD;AACvF;;;;;CAKC,GACD,SACEC,mBAAmB,EACnBC,wBAAwBC,0BAA0B,QAC7C,yDAAwD;AAC/D,SACEC,qBAAqB,EACrBC,oBAAoB,QACf,uDAAsD;AAC7D;;;;;;;CAOC,GACD,SACEC,sBAAsB,EACtBC,mBAAmB,EACnBC,mBAAmB,EACnBC,wBAAwB,EACxBC,iBAAiB,EACjBC,mBAAmB,EACnBC,mBAAmB,EACnBC,qBAAqB,QAChB,4BAAwB;AAC/B;;;;;;;;CAQC,GACD,SACEC,oBAAoB,EACpBC,UAAU,EACVC,oBAAoB,EACpBC,wBAAwB,EACxBC,sBAAsB,EACtBC,mBAAmB,QAGd,0DAAyD;AAChE,SAASC,UAAU,QAAoB,SAAQ;AAC/C,SACEC,2BAA2B,EAC3BC,6BAA6B,EAC7BC,iBAAiB,EACjBC,0BAA0B,EAC1BC,yBAAyB,EACzBC,uBAAuB,EACvBC,iBAAiB,EACjBC,oBAAoB,EACpBC,cAAc,EACdC,SAAS,EACTC,sBAAsB,EACtBC,WAAW,QAGN,2BAA0B;AAEjC;;;;;;;;CAQC,GACD,MAAMC,0BAA0BX;AAChC,MAAMY,gBAAgB;AAEtB,kEAAkE,GAClE,MAAMC,qBAAqB;AAE3B;;;;;;;;;;;;;;;;;CAiBC,GACD,MAAMC,wBAAwBf;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,eAAegB,cAAcC,IAA6B;IAIxD,MAAMC,UAAUD,KAAKE,OAAO,CAC1BpD,cAAcqD,SAAS,CAACC,SAAS,CAACC,UAAU;IAE9C,MAAMC,OAAkD,EAAE;IAC1D,IAAIC;IACJ,OAAS;QACP,MAAMC,OAAO,MAAM,AAACD,CAAAA,SAASN,QAAQQ,UAAU,CAACF,UAAUN,OAAM,EAC7DS,KAAK,CAACb,oBACNc,GAAG;QACNL,KAAKM,IAAI,IAAIJ,KAAKF,IAAI;QACtBC,SAASC,KAAKF,IAAI,CAACE,KAAKF,IAAI,CAACO,MAAM,GAAG,EAAE;QACxC,IAAI,CAACN,UAAUC,KAAKF,IAAI,CAACO,MAAM,GAAGhB,oBAAoB;YACpD,OAAO;gBAAES;gBAAMQ,WAAW;YAAM;QAClC;QACA,IAAIR,KAAKO,MAAM,IAAIf,uBAAuB;YACxC,MAAMiB,QAAQ,MAAMd,QAAQQ,UAAU,CAACF,QAAQG,KAAK,CAAC,GAAGC,GAAG;YAC3D,OAAO;gBAAEL;gBAAMQ,WAAWC,MAAMT,IAAI,CAACO,MAAM,GAAG;YAAE;QAClD;IACF;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,SAASG,cAAcC,KAAa;IACzC,OAAOnC,WAAW,UAAUoC,MAAM,CAACD,MAAME,WAAW,IAAIC,MAAM,CAAC;AACjE;AAEA,gEAAgE,GAChE,OAAO,SAASxD,qBACdyD,MAAc,EACdJ,KAAa,EACbK,MAAc,EACdC,UAAmB,EACnBC,OAAgB;IAEhB;;;;;;;;GAQC,GACD,OAAO3D,2BAA2BwD,QAAQJ,OAAOK,QAAQC,YAAYC;AACvE;AAEA,qEAAqE,GACrE,OAAO,MAAMC,0BAA0BC;IACrC,YACEC,OAAe,EACf,AAAgBC,MAAc,CAC9B;QACA,KAAK,CAACD,eAFUC,SAAAA;IAGlB;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,kCAAkCJ;IAC7C,YACEE,OAAe,EACf,kCAAkC,GAClC,AAAgBG,SAAiB,CACjC;QACA,KAAK,CAACH,SAAS,WAFCG,YAAAA;IAGlB;AACF;AAmOA;;;CAGC,GACD,eAAeC,kBAAkBV,MAAc,EAAEW,QAAgB;QAoChD/F,0BAyEGgG,qBACEA;IA7GpB,MAAM9B,YAAYrD,cAAcoF,GAAG,GAAG/B,SAAS;IAC/C,MAAMgC,YAAYhC,UACfiC,UAAU,CAAC,SACXC,GAAG,CAAChB,QACJe,UAAU,CAAC,WACXC,GAAG,CAACL;IACP,MAAMC,iBAAiB,MAAME,UAAUxB,GAAG;IAC1C,IAAI,CAACsB,eAAeK,MAAM,EAAE;QAC1B,MAAM,IAAIb,kBAAkB,0BAA0B;IACxD;IACA,MAAMc,YAAYN,eAAetB,GAAG,CAAC;IACrC,MAAM6B,kBAAkBD,YACpB,MAAMJ,UAAUC,UAAU,CAAC,YAAYC,GAAG,CAACI,OAAOF,YAAY5B,GAAG,KACjE;IACJ;;;;;;;;;;;;;;;;;;;;GAoBC,GACD,MAAM+B,SAASzG,qBAAAA,kBAAkBuG,mCAAAA,gBAAiB7B,GAAG,CAAC,qBAAvC1E,qBACb,CAAC;IACH,IAAI,CAAC0G,OAAOC,IAAI,CAACF,OAAO7B,MAAM,EAAE;QAC9B,MAAM,IAAIY,kBAAkB,+BAA+B;IAC7D;IACA;;;;;;;;;;;;;;;;;;;;;;GAsBC,GACD,MAAMoB,wBAAiBL,mCAAAA,gBAAiB7B,GAAG,CAAC,mCAC1CsB,eAAetB,GAAG,CAAC;IAGrB,IAAIkC,iCAAAA,cAAeC,SAAS,EAAE;QAC5B,MAAMC,aAAa,AACjB,CAAA,MAAM5C,UAAUiC,UAAU,CAAC,eAAeC,GAAG,CAACQ,cAAcC,SAAS,EAAEnC,GAAG,EAAC,EAC3EqC,IAAI;QACN,MAAMC,QAAQ/G,sBAAsB;YAAE2G;YAAeE;QAAW;QAChE,IAAIE,OAAO,MAAM,IAAIxB,kBAAkBwB,MAAMC,MAAM,EAAE;IACvD;IACA,kEAAkE;IAClE,MAAMC,aAAa;WACd,IAAIC,IACLT,OAAOU,MAAM,CAACX,OACXY,MAAM,CAAC,CAACC,OAAcA,CAAAA,wBAAAA,KAAMC,WAAW,MAAK,gBAC5CC,GAAG,CAAC,CAACF;;gBAAqBA;mBAAPd,eAAOc,yBAAAA,cAAAA,KAAMG,KAAK,qBAAXH,YAAaI,SAAS,mBAAI;UACrD,kEAAkE;QAClE,mEAAmE;QACnE,uEAAuE;QACvE,sEAAsE;QACtE,kEAAkE;SACjEL,MAAM,CAAC7F;KAEb,CAACmG,KAAK,CAAC,GAAG;IACX,MAAMC,WAA+C,CAAC;IACtD,MAAMC,QAAQC,GAAG,CACfZ,WAAWM,GAAG,CAAC,OAAOE;QACpB,uEAAuE;QACvE,uEAAuE;QACvE,yEAAyE;QACzE,mEAAmE;QACnE,MAAMK,OAAO,MAAMzH,qBAAqB,WAAW;YAAE8E;YAAQ4C,IAAIN;QAAU;QAC3E,IAAI,CAACK,MAAM;QACXH,QAAQ,CAACF,UAAU,GAAG;YACpBO,MAAMF,KAAKG,KAAK;YAChBC,YAAYJ,KAAKK,OAAO;YACxBC,UAAUN,KAAKM,QAAQ;YACvBC,KAAKP,KAAKQ,IAAI;QAChB;IACF;IAEF,OAAO;QACL9B;QACAmB;QACAY,SAAShC,QAAOR,sBAAAA,eAAetB,GAAG,CAAC,2BAAnBsB,sBAAsC;QACtDyC,WAAWjC,QAAOR,uBAAAA,eAAetB,GAAG,CAAC,6BAAnBsB,uBAAwC;IAC5D;AACF;AAoHA,OAAO,eAAe0C,oBACpBC,OAA4B;cA4lBJA,mBA0GAA,mBA8XtBpI,mBAgD0BoI;QAvdzBC,iBAKAA,kBA4DiBC;IA5tBpB,MAAMC,oBACJC,QAAQC,GAAG,CAACC,wBAAwB,IAAIF,QAAQC,GAAG,CAACE,WAAW;IACjE,IAAI,CAAC9F,uBAAuB,CAAC0F,mBAAmB;QAC9C,MAAM,IAAItD,kBACR,qEACE,8BACF;IAEJ;IACA,MAAM,EAAEJ,MAAM,EAAEoD,OAAO,EAAEW,IAAI,EAAEC,QAAQ,EAAE,GAAGT;IAE5C,uEAAuE;IACvE,4EAA4E;IAC5E,qEAAqE;IACrE,4EAA4E;IAC5E,4DAA4D;IAC5D,0EAA0E;IAC1E,4EAA4E;IAC5E,0EAA0E;IAC1E,qEAAqE;IACrE,4EAA4E;IAC5E,0EAA0E;IAC1E,sBAAsB;IACtB,EAAE;IACF,yEAAyE;IACzE,0EAA0E;IAC1E,0EAA0E;IAC1E,6EAA6E;IAC7E,2EAA2E;IAC3E,KAAK,MAAM,CAACV,MAAMoB,MAAM,IAAI;QAC1B;YAAC;YAAcV,QAAQrD,UAAU;SAAC;QAClC;YAAC;YAAgBqD,QAAQW,YAAY;SAAC;QACtC;YAAC;YAAoBX,QAAQY,gBAAgB;SAAC;QAC9C;YAAC;YAAaZ,QAAQa,SAAS;SAAC;QAChC;YAAC;YAAUb,QAAQc,MAAM;SAAC;KAC3B,CAAW;QACV,IAAIJ,SAAS,CAAC7H,aAAa6H,QAAQ;YACjC,MAAM,IAAI7D,kBAAkB,CAAC,QAAQ,EAAEyC,MAAM,EAAE;QACjD;IACF;IACA;;;;;;;;;GASC,GACD,IAAIU,QAAQpD,OAAO,IAAI,CAAC7F,eAAeiJ,QAAQpD,OAAO,GAAG;QACvD,MAAM,IAAIC,kBAAkB,mBAAmB;IACjD;IACA,MAAMD,UAAUoD,QAAQpD,OAAO,IAAI5F;IACnC;;;;;;;;;;GAUC,GACD,MAAM+J,qBAAqBC,QAAQhB,QAAQiB,QAAQ,IAAIjB,QAAQkB,YAAY;IAE3E,MAAM3F,YAAYrD,cAAcoF,GAAG,GAAG/B,SAAS;IAC/C,MAAM4F,UAAU5F,UAAUiC,UAAU,CAAC,SAASC,GAAG,CAAChB;IAClD,MAAM2E,eAAe,MAAMD,QAAQpF,GAAG;IACtC,IAAI,CAACqF,aAAa1D,MAAM,EAAE;QACxB,MAAM,IAAIb,kBAAkB,gBAAgB;IAC9C;IAEA;;;;;;;;;6CAS2C,GAC3C,IAAIwE,cAA0C;IAC9C;;;GAGC,GACD,IAAIC,eAAe;IACnB;;;;;;;6CAO2C,GAC3C,IAAItB,QAAQkB,YAAY,EAAE;YAoBFK;YAkBGA,oBAoBLA;QAzDpB,IAAI,CAACvB,QAAQrD,UAAU,EAAE;YACvB,MAAM,IAAIE,kBACR,kDACA;QAEJ;QACA,MAAM0E,eAAe,MAAMJ,QACxB3D,UAAU,CAAC,aACXC,GAAG,CAACuC,QAAQrD,UAAU,EACtBZ,GAAG;QACN,IAAI,CAACwF,aAAa7D,MAAM,EAAE;YACxB,MAAM,IAAIb,kBAAkB,iBAAiB;QAC/C;QACA;;;;;KAKC,GACD,MAAMG,SAASa,QAAO0D,oBAAAA,aAAaxF,GAAG,CAAC,qBAAjBwF,oBAA8B;QACpD,IAAIvE,WAAW,aAAaA,WAAW,aAAa;YAClD,MAAM,IAAIH,kBACR,iDACA;QAEJ;QACA;;;;;KAKC,GACD,MAAM2E,UAAU,MAAM/H,oBACpBgD,QACAuD,QAAQrD,UAAU,EAClBpB;QAEF,MAAMkG,YAAYC,iBAAOH,qBAAAA,aAAaxF,GAAG,CAAC,6BAAjBwF,mBAA2BI,IAAI,oBAAI;QAC5D,IAAI,CAACtI,oBAAoBmI,QAAQI,OAAO,EAAEH,YAAY;YACpD,MAAM,IAAI5E,kBACR,sEACE,qEACA,gCACF;QAEJ;QACA,IAAIvD,oBAAoBkI,YAAYpI,wBAAwB;YAC1D,MAAM,IAAIyD,kBACR,CAAC,iCAAiC,EAAEzD,uBAAuByI,cAAc,GAAG,CAAC,CAAC,GAC5E,kEACA,uCACF;QAEJ;QACAR,cAAc,IAAI7C,IAAI;eAAIgD,QAAQI,OAAO;eAAKJ,QAAQM,OAAO;SAAC;QAC9DR,eAAeS,KAAKC,GAAG,CACrB,GACAD,KAAKE,KAAK,CAACP,iBAAOH,qBAAAA,aAAaxF,GAAG,CAAC,8BAAjBwF,mBAA4BW,KAAK,oBAAI,OAAO;IAElE;IACA;;;;;;;;;;;;GAYC,GACD,MAAMC,qBAAqB;YACHnC;QAAtB,MAAMoC,SAASvE,QAAOmC,sBAAAA,QAAQrD,UAAU,YAAlBqD,sBAAsB;QAC5C,MAAMmB,QACH3D,UAAU,CAAC,aACXC,GAAG,CAAC2E,QACJC,GAAG,CACF;YACErF,QAAQ;YACRsF,QAAQ;gBAAEC,WAAW;gBAAGL,OAAOZ,eAAe;gBAAGkB,UAAU;YAAE;YAC7DC,YAAYvK,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;QAChE,GACA;YAAEC,OAAO;QAAK;QAElB,OAAO;YACLjG,YAAYyF;YACZS,YAAY;YACZC,cAAc;YACdnB,MAAM;YACNY,WAAW;YACXQ,UAAU;YACVb,OAAOZ,eAAe;QACxB;IACF;IAEA,IAAItB,QAAQiB,QAAQ,EAAE;;YAgCKM;QA/BzB,IAAI,CAACvB,QAAQrD,UAAU,EAAE;YACvB,MAAM,IAAIE,kBACR,qDACA;QAEJ;QACA,MAAM0E,eAAe,MAAMJ,QACxB3D,UAAU,CAAC,aACXC,GAAG,CAACuC,QAAQrD,UAAU,EACtBZ,GAAG;QACN,IAAI,CAACwF,aAAa7D,MAAM,EAAE;YACxB,MAAM,IAAIb,kBAAkB,iBAAiB;QAC/C;QACA;;;;KAIC,GACD,IAAI0E,aAAaxF,GAAG,CAAC,cAAc,QAAQ;YACzC,MAAM,IAAIc,kBACR,kEACA;QAEJ;QACA;;;;;KAKC,GACD,MAAMmB,OAAO,MAAMxE,kBAAkBiD,QAAQuD,QAAQrD,UAAU,EAAEpB;QACjE,MAAMkG,YAAYC,iBAAOH,qBAAAA,aAAaxF,GAAG,CAAC,6BAAjBwF,mBAA2BI,IAAI,oBAAI;QAC5D,IAAI,CAACtI,oBAAoB2E,MAAMyD,YAAY;YACzC,MAAM,IAAI5E,kBACR,sEACE,oEACA,iEACA,2BACF;QAEJ;QACA,IAAImB,KAAKgF,IAAI,IAAI5J,wBAAwB;YACvC,MAAM,IAAIyD,kBACR,CAAC,+BAA+B,EAAEmB,KAAKgF,IAAI,CAACnB,cAAc,GAAG,CAAC,CAAC,GAC7D,CAAC,8CAA8C,CAAC,GAChD,CAAC,CAAC,EAAEzI,uBAAuByI,cAAc,GAAG,uBAAuB,CAAC,GACpE,iCACF;QAEJ;QACAR,cAAcrD;IAChB;IAEA;;;;;;;;EAQA,GACA,IAAI6E;IACJ;;;;GAIC,GACD,IAAII,oBAAoB;IACxB;;;;;;;;GAQC,GACD,IAAIC,WAAW;IACf,MAAMC,QAAQ,IAAIC;IAClB;;;;;;;;;;;;;GAaC,GACD;;;;;;;;;GASC,GACD,MAAMC,eAAe,MAAMxL,oBAAoB4E;IAC/C,MAAMyD,UAAU,IAAIkD;IACpB,MAAME,iBAAiB,CAACjH,OAAe+B;QACrC,MAAMmF,UAAUlH,MAAMmH,IAAI,GAAGjH,WAAW;QACxC,IAAI,CAACgH,SAAS;QACdrD,QAAQmC,GAAG,CACTkB,SACAtM,mBACEmH,MACAiF;IAGN;IACA,MAAMI,cAAc,CAACpH,OAAeiD;QAClC,MAAMiE,UAAUlH,MAAMmH,IAAI,GAAGjH,WAAW;QACxC,IAAIgH,WAAW,OAAOjE,SAAS,YAAYA,KAAKkE,IAAI,IAAI;YACtDL,MAAMd,GAAG,CAACkB,SAASjE,KAAKkE,IAAI;QAC9B;IACF;IACA,IAAI/C,aAAa,SAAS;QACxB,0EAA0E;QAC1E,uEAAuE;QACvE,2DAA2D;QAC3D,MAAMiD,QAAQ,MAAMvI,cAClB,AAAC,CAAA,MAAMpD,oBAAoB0E,QAAQ,QAAO,EAAGkH,KAAK;QAEpDV,oBAAoBS,MAAMxH,SAAS;QACnC2G,aAAaa,MAAMhI,IAAI,CAACmD,GAAG,CAAC,CAACpB;gBACNA;YAArB,MAAMpB,QAAQwB,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC,oBAAR0B,WAAoB;YACzCgG,YAAYpH,OAAOoB,IAAI1B,GAAG,CAAC;YAC3BuH,eAAejH,OAAOoB,IAAIW,IAAI;YAC9B,OAAO/B;QACT;IACF,OAAO,IAAIoE,aAAa,WAAW;QACjC,MAAMmD,UAAU,MAAMzI,cAAcgG,QAAQ3D,UAAU,CAAC;QACvDyF,oBAAoBW,QAAQ1H,SAAS;QACrC2G,aAAae,QAAQlI,IAAI,CAACmD,GAAG,CAAC,CAACpB;gBACRA,UAeFA;YAfnB,MAAMpB,QAAQwB,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC,oBAAR0B,WAAoB;YACzC;;;;;;;;;;;;;iDAa2C,GAC3CgG,YAAYpH,QAAOoB,YAAAA,IAAI1B,GAAG,CAAC,0BAAR0B,YAA0BA,IAAI1B,GAAG,CAAC;YACrDuH,eAAejH,OAAOoB,IAAIW,IAAI;YAC9B,OAAO/B;QACT;IACF,OAAO,IAAIoE,aAAa,WAAW;YAGRT,oBAcjB6D,sBACGA;QAjBX,mEAAmE;QACnE,mCAAmC;QACnC,MAAMhD,YAAYhD,QAAOmC,qBAAAA,QAAQa,SAAS,YAAjBb,qBAAqB;QAC9C,MAAM6D,kBAAkBhD,YACpB,MAAM,AAAC,CAAA,MAAM/I,yBAAyB2E,QAAQ,kBAAiB,EAAGgB,GAAG,CAACoD,WAAW9E,GAAG,KACpF;QACJ,mEAAmE;QACnE,uEAAuE;QACvE,wEAAwE;QACxE,IACE,EAAC8H,mCAAAA,gBAAiBnG,MAAM,KACxB,CAAClG,cAAcqM,gBAAgB9H,GAAG,CAAC,cAAcU,SACjD;YACA,MAAM,IAAII,kBAAkB,mBAAmB;QACjD;QACA,MAAMiH,UAAU;YACdC,IAAI,GAAEF,uBAAAA,gBAAgB9H,GAAG,CAAC,mBAApB8H,uBAA+B,EAAE;YACvCG,OAAO,GAAEH,wBAAAA,gBAAgB9H,GAAG,CAAC,sBAApB8H,wBAAkC,EAAE;QAC/C;QACA,kEAAkE;QAClE,mEAAmE;QACnE,yCAAyC;QACzC;;;;;;;;KAQC,GACD,MAAMI,WAAW,MAAM9I,cACrB,AAAC,CAAA,MAAMpD,oBAAoB0E,QAAQ,WAAU,EAAGkH,KAAK;QAEvD;;;;;;;KAOC,GACDV,oBAAoBgB,SAAS/H,SAAS;QACtC2G,aAAaoB,SAASvI,IAAI,CACvBgD,MAAM,CAAC,CAACjB;gBAEGA,UAAgCA;mBAD1C3G,sBACE;gBAAEiN,IAAI,GAAEtG,WAAAA,IAAI1B,GAAG,CAAC,mBAAR0B,WAAmB,EAAE;gBAAEuG,OAAO,GAAEvG,YAAAA,IAAI1B,GAAG,CAAC,sBAAR0B,YAAsB,CAAC;YAAE,GACjEqG;WAGHjF,GAAG,CAAC,CAACpB;gBACiBA;YAArB,MAAMpB,QAAQwB,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC,oBAAR0B,WAAoB;YACzCgG,YAAYpH,OAAOoB,IAAI1B,GAAG,CAAC;YAC3BuH,eAAejH,OAAOoB,IAAIW,IAAI;YAC9B,OAAO/B;QACT;IACJ,OAAO,IAAIoE,aAAa,QAAQ;YAGRT,iBASJ;YAPd;QAJJ,oEAAoE;QACpE,8BAA8B;QAC9B,MAAMc,SAASjD,QAAOmC,kBAAAA,QAAQc,MAAM,YAAdd,kBAAkB;QACxC,MAAMkE,UAAUpD,UACZ,UAAA,AAAC,CAAA,MAAMhJ,yBAAyB2E,QAAQ,WAAU,EAAG0H,MAAM,qBAA3D,QACI3G,UAAU,CAAC,SACZC,GAAG,CAACqD,UACP;QACJ,IAAI,CAACoD,SAAS,MAAM,IAAIrH,kBAAkB,gBAAgB;QAC1D,yEAAyE;QACzE,yEAAyE;QACzEqG,WAAWrF,QAAO,OAAA,AAAC,CAAA,MAAMqG,QAAQnI,GAAG,EAAC,EAAGA,GAAG,CAAC,mBAA1B,OAAqC;QACvD,MAAM6H,UAAU,MAAMzI,cAAc+I,QAAQ1G,UAAU,CAAC;QACvDyF,oBAAoBW,QAAQ1H,SAAS;QACrC2G,aAAae,QAAQlI,IAAI,CAACmD,GAAG,CAAC,CAACpB;gBACRA;YAArB,MAAMpB,QAAQwB,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC,oBAAR0B,WAAoB;YACzCgG,YAAYpH,OAAOoB,IAAI1B,GAAG,CAAC;YAC3BuH,eAAejH,OAAOoB,IAAIW,IAAI;YAC9B,OAAO/B;QACT;IACF,OAAO;QACLwG,aAAauB,MAAMC,OAAO,CAACrE,QAAQsE,MAAM,IACrCtE,QAAQsE,MAAM,CAACzF,GAAG,CAAC,CAAC6B,QAAmB7C,OAAO6C,UAC9C,EAAE;IACR;IACA;;;;;;;;;GASC,GACD,MAAM6D,cAAc;WACf,IAAI/F,IACLqE,WACGhE,GAAG,CAAC,CAACxC,QAAUA,MAAMmH,IAAI,GAAGjH,WAAW,IACvCmC,MAAM,CAAC,CAACrC,QAAUrB,cAAcwJ,IAAI,CAACnI;KAE3C;IACD,IAAI,CAACkI,YAAYtI,MAAM,EAAE;QACvB,IAAI+D,QAAQkB,YAAY,EAAE,OAAOiB;QACjC,MAAM,IAAItF,kBAAkB,yBAAyB;IACvD;IAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CA+B2C,GAC3C,MAAM4H,cAAcpD,cAChB9H,yBAAyBgL,aAAalD,eACtC;QAAEqD,WAAWH;QAAaI,gBAAgB;IAAE;IAChD,MAAMA,iBAAiBF,YAAYE,cAAc;IACjD,MAAMC,WAAWH,YAAYC,SAAS;IACtC,IAAI,CAACE,SAAS3I,MAAM,EAAE;QACpB,IAAI+D,QAAQkB,YAAY,EAAE,OAAOiB;QACjC,MAAM,IAAItF,kBACR,0EACE,cACF;IAEJ;IAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCC,GACD,MAAMoD,aAAa,MAAM9H,cAAcsE,QAAQoI,KAAK,CAAC,IAAM;IAC3D,MAAMC,QAAQjH,eAAOoC,8BAAAA,WAAY6E,KAAK,mBAAI;IAE1C;;;;;;;;;;;;;;;;;GAiBC,GACD;;;;;;;;;;;;;;GAcC,GACD;;;;;;;;;;;;;;;;;;GAkBC,GACD,MAAMC,WAAWlH,QAAOmC,oBAAAA,QAAQ+E,QAAQ,YAAhB/E,oBAAoB,IAAIwD,IAAI;IACpD,MAAMwB,iBAAiBD,WACnB,MAAM5D,QAAQ3D,UAAU,CAAChD,yBAAyBiD,GAAG,CAACsH,UAAUhJ,GAAG,KACnE;IACJ,IAAIgJ,YAAY,EAACC,kCAAAA,eAAgBtH,MAAM,GAAE;QACvC,MAAM,IAAIb,kBACR,0EACE,gEACA,qBACF;IAEJ;IACA,MAAMoI,eAAeD,CAAAA,kCAAAA,eAAgBtH,MAAM,IACvC/C,eAAe;QACb0E,IAAI0F;QACJ3G,MAAM4G,eAAe5G,IAAI;IAC3B,KACA;IAEJ;;;;;GAKC,GACD,MAAM8G,kBAAkB,MAAMvM,2BAA2B;QACvDmM;QACArI;QACA0I,gBAAgB/D,aAAarF,GAAG,CAAC;QACjCqJ,mBACEH,CAAAA,gCAAAA,aAAcI,SAAS,KAAIjE,aAAarF,GAAG,CAAC;QAC9CuJ,YAAYlE,aAAarF,GAAG,CAAC;QAC7BwJ,SAAS;IACX;IACA,MAAMC,kBAAkB3K,uBAAuBqK;IAC/C,IAAIM,iBAAiB;YACHA;QAAhB,MAAMC,UAAUD,EAAAA,2BAAAA,gBAAgBC,OAAO,qBAAvBD,yBAAyBvJ,MAAM,IAC3C,CAAC,UAAU,EAAEuJ,gBAAgBC,OAAO,CAACC,IAAI,CAAC,MAAM,CAAC,CAAC,GAClD;QACJ,MAAM,IAAI7I,kBAAkB,GAAG2I,gBAAgBzI,OAAO,GAAG0I,SAAS,EAAE;IACtE;IACA;;;;;;;;;;;;;;;;;;;;;GAqBC,GACD,MAAME,mBAAmBpL,0BACvB2K,gBAAgBU,MAAM,EACrB3F,+BAAAA,kBAAAA,WAAY4F,GAAG,qBAAhB,AAAC5F,eAAyD,CACxD,wBACD;IAEH,MAAM6F,gBAAgB5O,8BACnB+I,+BAAAA,mBAAAA,WAAY4F,GAAG,qBAAhB,AAAC5F,gBAAyD,CACxD,yBACD;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BC,GACD,MAAM8F,WAAWlI,QAAOmC,oBAAAA,QAAQ+F,QAAQ,YAAhB/F,oBAAoB,IACzCwD,IAAI,GACJjH,WAAW;IACd,MAAMyJ,eAAeD,YAAYnB,SAASqB,QAAQ,CAACF,YAAYA,WAAW;IAC1E;;;;;;;;;;;;;GAaC,GACD,IAAIC,gBAAgB,CAAC9F,QAAQgG,GAAG,CAACF,eAAe;QAC9C,MAAMG,SAAS,MAAMC,kBAAkB3J,QAAQuJ;QAC/C,IAAIG,QAAQ;YACVjG,QAAQmC,GAAG,CAAC2D,cAAc/O,mBAAmBkP,QAAQ9C;QACvD;IACF;IACA,IAAI2C,gBAAgB9F,EAAAA,eAAAA,QAAQnE,GAAG,CAACiK,kCAAZ9F,aAA2BmG,KAAK,MAAK,YAAY;QACnE,MAAM,IAAIxJ,kBACR,GAAGmJ,aAAa,uDAAuD,CAAC,GACtE,yEACA,iCACF;IAEJ;IACA,MAAMM,eAAenP,wBACnB6O,eAAepB,SAASlG,MAAM,CAAC,CAAC6H,MAAQA,QAAQP,gBAAgBpB,UAChE1E,SACA4F,eACAzC;IAEF,IAAI2C,cAAcM,aAAaE,QAAQ,CAACC,OAAO,CAACT;IAChD,IAAI,CAACM,aAAaE,QAAQ,CAACvK,MAAM,EAAE;QACjC,IAAI+D,QAAQkB,YAAY,EAAE,OAAOiB;QACjC,MAAM,IAAItF,kBACR,sEACE,0EACA,kEACF;IAEJ;IAEA;;;;;;;;;;;6CAW2C,GAC3C,MAAM6J,eAAe,MAAMnO;IAC3B,MAAMoO,YAAY3G,QAAQ4G,cAAc,KAAK;IAC7C;;;;;;;;GAQC,GACD,IAAIC,WAAW9L;IACf,2DAA2D,GAC3D,IAAI+L,OAAgC;IACpC,IAAI,CAACH,WAAW;YAMV1G,kBAoCCA;QAzCL,yEAAyE,GACzE,MAAM8G,aAAmC,MAAMjN,qBAAqB;YAClEgL;YACAkC,QAAQrB;YACRsB,iBAAiB,EACfhH,+BAAAA,mBAAAA,WAAY4F,GAAG,qBADE,AACjB5F,gBACC,CAAC,mCAAmC;QACzC;QACA;;;;;;;;;;;;;;;;;;KAkBC,GACD,IAAI8G,WAAWG,OAAO,EAAE;YACtB,MAAM,IAAIrK,kBAAkBkK,WAAWzI,MAAM,EAAE;QACjD;QACA;;;;;;;;KAQC,GACDwI,OAAO7M,oBAAoB;YACzBkN,SAAStN,WACNoG,+BAAAA,mBAAAA,WAAY4F,GAAG,qBAAhB,AAAC5F,gBAAyD,CAAC,YAAY;YAEzEmH,mBAAmBL,WAAWM,MAAM,CAACC,QAAQ;YAC7CC,iBAAiBb,aAAac,OAAO;QACvC;QACA,IAAI,CAACV,KAAKW,SAAS,IAAIf,aAAagB,OAAO,EAAE;YAC3C,MAAMC,eAAe5F,KAAKC,GAAG,CAC3B,GACA8E,KAAKc,MAAM,GAAGb,WAAWM,MAAM,CAACQ,YAAY;YAE9C,IAAIF,gBAAgB,GAAG;gBACrB,MAAM,IAAI1K,0BACR,GAAG6J,KAAKgB,MAAM,CAAC,qBAAqB,CAAC,GACnC,GAAGf,WAAWM,MAAM,CAACQ,YAAY,CAAChG,cAAc,GAAG,WAAW,CAAC,GAC/D,oEACA,8DACA,iEACFE,KAAKE,KAAK,CAAC8F,KAAKC,GAAG,KAAK,YAAc,WAAa;YAEvD;YACAnB,WAAW9E,KAAKkG,GAAG,CAACpB,UAAUc;QAChC;IACF;IAEA;;;;;;;;;;;;GAYC,GACD9E,aAAayD,aAAaE,QAAQ,CAACxH,KAAK,CAAC,GAAG6H;IAE5C;;;;;;;;;;;;;;;GAeC,GACD,MAAMqB,gBAAgB,MAAMlQ,sBAC1ByE,QACAoG,YACAtH;IAEF;;;;;;;;GAQC,GACD,MAAM4M,UAAU,MAAMlQ,oBACpBwE,QACAG,SACAsL,eACA3M;IAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBC,GACD,MAAM6M,WAAW,MAAMlP,sBAAsBuD,QAAQ0L,SAAS;QAAE5M;IAAU;IAC1E;;;;;;;;;;;GAWC,GACD,IAAI,CAAC6M,SAASnM,MAAM,IAAIqK,aAAaE,QAAQ,CAACvK,MAAM,IAAI4G,WAAW5G,MAAM,EAAE;QACzE,IAAI+D,QAAQkB,YAAY,EAAE,OAAOiB;QACjC;;;;;;KAMC,GACD,MAAM,IAAItF,kBACRsL,QAAQlM,MAAM,GACV,sEACA,uDACJ;IAEJ;IAEA,oEAAoE;IACpE,EAAE;IACF,mEAAmE;IACnE,6EAA6E;IAC7E,6EAA6E;IAC7E,gEAAgE;IAChE,0EAA0E;IAC1E,uEAAuE;IACvE,4EAA4E;IAC5E,uEAAuE;IACvE,EAAE;IACF,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,wEAAwE;IACxE,0EAA0E;IAC1E,4EAA4E;IAC5E,wCAAwC;IACxC,MAAMoM,WAAW,IAAIN,OAAOO,WAAW,GAAGtJ,KAAK,CAAC,GAAG;IACnD,0EAA0E;IAC1E,4EAA4E;IAC5E,4CAA4C;IAC5C,4EAA4E;IAC5E,0EAA0E;IAC1E,8DAA8D;IAC9D,MAAMuJ,oBAAoB1R,WACxBoJ,8BAAAA,WAAY4F,GAAG,EACf,sBACA,GACA/J,KAAK;IACP,MAAM0M,eAAe,IACnB,IAAI3L,kBACF,CAAC,sCAAsC,EAAE0L,kBAAkB,YAAY,CAAC,GACtE,uEACA,uDACF;IAEJ;QACE,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,2EAA2E;QAC3E,iCAAiC;QACjC,MAAME,OAAO,MAAMnQ,8BAA8BwM,OAAOuD;QACxD,IAAII,OAAOL,SAASnM,MAAM,GAAGsM,mBAAmB,MAAMC;IACxD;IAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBC,GACD,IAAIxI,QAAQ0I,MAAM,EAAE;QAClB,OAAO;YACL/L,YAAY;YACZkG,YAAYA,WAAW5G,MAAM;YAC7B6G,cAAc8B,SAAS3I,MAAM;WACzBgH,oBAAoB;YAAEA,mBAAmB;QAAK,IAAI,CAAC,GACnDjD,QAAQiB,QAAQ,GAAG;YAAEA,UAAU;YAAM0D;QAAe,IAAI,CAAC;YAC7DyD,UAAUA,SAASnM,MAAM;YACzB;;;;;;OAMC,GACD0M,YAAY9F,WAAW5G,MAAM,GAAGkM,QAAQlM,MAAM;YAC9C2M,aAAaT,QAAQlM,MAAM,GAAGmM,SAASnM,MAAM;YAC7C;;;;;OAKC,GACDsG,WAAWR,KAAKC,GAAG,CAAC,GAAGsE,aAAaE,QAAQ,CAACvK,MAAM,GAAG4G,WAAW5G,MAAM;YACvE;;;;;;;;;;;;;;;;;;;;;;;OAuBC,GACD4M,WAAWvC,aAAauC,SAAS;YACjCC,qBAAqBxC,aAAawC,mBAAmB;YACrDC,eAAezC,aAAayC,aAAa;YACzCC,iBAAiB1C,aAAa2C,QAAQ;YACtC,uEAAuE;YACvE,+CAA+C;YAC/CC,UAAUhE,gBAAgBiE,OAAO;YACjCC,gBAAgBlE,gBAAgBU,MAAM;YACtCjE,MAAM;YACN+G,QAAQ;;IAEZ;IAEA,4EAA4E;IAC5E,6EAA6E;IAC7E,sDAAsD;IACtD,MAAMW,YACJzR,oBAAAA,iBAAiB;QACf0R,OAAOlI,aAAarF,GAAG,CAAC;QACxBwN,WAAWnI,aAAarF,GAAG,CAAC;IAC9B,cAHAnE,oBAGM;IAER,4EAA4E;IAC5E,6EAA6E;IAC7E,iEAAiE;IACjE,MAAM4R,WAAWjS,uBAAuB0I,8BAAAA,WAAY4F,GAAG;IAEvD,MAAMlJ,aAAaqD,QAAQrD,UAAU,IAAIvF;IACzC;;;;;;;;;GASC,GACD,MAAMqS,cAAc,CAACzJ,QAAQrD,UAAU;IAEvC,+DAA+D;IAC/D,qCAAqC;IACrC,MAAM+M,WAAW1J,QAAQY,gBAAgB,GACrC,MAAMzD,kBAAkBV,QAAQuD,QAAQY,gBAAgB,IACxD;IACJ;;;;;;;;GAQC,GACD,MAAM+I,kBAAkBD,WACnB;QACCE,MAAM;QACNF;OACI1J,QAAQ6J,SAAS,GAAG;QAAEA,WAAW7J,QAAQ6J,SAAS;IAAC,IAAI,CAAC,KAE9D;IAEJ,mEAAmE;IACnE,kEAAkE;IAClE,oEAAoE;IACpE,MAAMlJ,eAAe9C,QAAOmC,wBAAAA,QAAQW,YAAY,YAApBX,wBAAwB;IACpD,IAAI8J,aAAwD;IAC5D,IAAInJ,cAAc;QAChB,MAAMoJ,qBAAqB,MAAM5I,QAC9B3D,UAAU,CAAC,eACXC,GAAG,CAACkD,cACJ5E,GAAG;QACN,MAAMqC,OAAO2L,mBAAmB3L,IAAI;QACpC,IACE,CAAC2L,mBAAmBrM,MAAM,IAC1B,CAACU,QACDA,KAAK4L,MAAM,KAAK,WACf5L,KAAKpB,MAAM,KAAK,aAAa,CAACoB,KAAK6L,eAAe,EACnD;YACA,MAAM,IAAIpN,kBAAkB,mCAAmC;QACjE;QACAiN,aAAa;YAAEI,KAAKH,mBAAmB1K,EAAE;WAAKjB;IAChD;IACA;;;;;;;;;;;;;;GAcC,GACD;QACE,0EAA0E;QAC1E,0EAA0E;QAC1E,mEAAmE;QACnE,oDAAoD;QACpD,MAAM+L,SAASzD;QACf,MAAMW,SAAS,MAAM7O;QACrB,IAAI2R,OAAOzC,OAAO,IAAIL,OAAOoB,IAAI,GAAGL,SAASnM,MAAM,GAAGkO,OAAO3C,OAAO,EAAE;YACpE,MAAM,IAAIvK,0BACR,CAAC,6CAA6C,EAAEkN,OAAO3C,OAAO,CAAC,QAAQ,CAAC,GACtE,uEACA,yEACA,yBACFH,OAAO+C,OAAO;QAElB;QAEA;;;;;;;;;;;;;;;;;;;;;KAqBC,GACD,MAAMC,SAAS,MAAMhS,wBAAwB;YAC3CyM;YACAwF,OAAOlC,SAASnM,MAAM;YACtBsL,iBAAiB4C,OAAO3C,OAAO;YAC/BE,SAASyC,OAAOzC,OAAO;QACzB;QACA,IAAI,CAAC2C,OAAOE,OAAO,EAAE;YACnB,MAAM,IAAItN,0BACR,CAAC,wBAAwB,EAAEoN,OAAOG,OAAO,CAAC3I,cAAc,GAAG,UAAU,CAAC,GACpE,CAAC,4BAA4B,EAAEwI,OAAO5B,IAAI,CAAC5G,cAAc,GAAG,MAAM,CAAC,GACnE,CAAC,2BAA2B,EAAEwI,OAAO9H,SAAS,CAACV,cAAc,GAAG,CAAC,CAAC,GAClE,CAAC,wBAAwB,EAAEuG,SAASnM,MAAM,CAAC4F,cAAc,GAAG,EAAE,CAAC,GAC/D,uEACA,wEACA,kEACA,iEACFwI,OAAOnN,SAAS;QAEpB;IACF;IAEA;;;;;;;;;;;GAWC,GACD,MAAMuN,WAAW,MAAM7Q,qBAAqB;QAC1CkL;QACAwF,OAAOlC,SAASnM,MAAM;QACtB6K,IAAI,EACFA,eAAAA,OACA7M,oBAAoB;YAClBkN,SAAS;YACTC,mBAAmB;YACnBG,iBAAiBb,aAAac,OAAO;QACvC;QACFE,SAAShB,aAAagB,OAAO;IAC/B;IACA,IAAI,CAAC+C,SAASF,OAAO,EAAE;QACrB,MAAM,IAAItN,0BACR,CAAC,wBAAwB,EAAEwN,SAASD,OAAO,CAAC3I,cAAc,GAAG,UAAU,CAAC,GACtE,CAAC,kEAAkE,CAAC,GACpE,GAAG4I,SAAShC,IAAI,CAAC5G,cAAc,GAAG,kCAAkC,CAAC,GACrE,gEACA,iEACA,oCACF4I,SAASvN,SAAS;IAEtB;IAEA;;;;;;;;GAQC,GACD,MAAMwN,QAAQ,MAAMhS,0BAA0B;QAC5CoM;QACA6F,OAAOtC;QACPiC,OAAOlC,SAASnM,MAAM;QACtBH,OAAOyM;IACT;IACA,IAAI,CAACmC,MAAME,EAAE,EAAE;QACb,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,MAAM;QACN,MAAM7Q,yBAAyB0Q,SAASI,WAAW,EAAE;QACrD,MAAMrC;IACR;IACA,MAAMqC,cAAuCH,MAAMG,WAAW;IAC9D,MAAMC,iBAAoDL,SAASI,WAAW;IAE9E,MAAME,eAAuC,CAAC;IAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDC,GACD,MAAMnJ,UAAoB,EAAE;IAC5B;;;;;;;;;;;;;;;;;;;;;;GAsBC,GACD,MAAMoJ,cAAc,IAAIxM,IAAI4J;IAC5B,MAAM6C,aAAuBpI,WAAWnE,MAAM,CAC5C,CAACrC,QAAU,CAAC2O,YAAY9E,GAAG,CAAC7J;IAE9B,IAAIsF,OAAO;IACX,gEAAgE,GAChE,IAAIuJ,WAAW;IACf;;;;;;;;;;;;;;;GAeC,GACD,MAAMC,sBAAsB7Q;IAC5B,IAAI;QACF,IAAK,IAAI8Q,QAAQ,GAAGA,QAAQhD,SAASnM,MAAM,EAAEmP,SAAS,EAAG;gBAiF1CpL;gBAzBFqL,kBAaDA;YApEV,MAAMhP,QAAQ+L,QAAQ,CAACgD,MAAM;YAC7B,uEAAuE;YACvE,wEAAwE;YACxE,mEAAmE;YACnE,iDAAiD;YACjD,MAAME,OAAO;gBACXjC;gBACA5M;gBACAJ;gBACAM;gBACAC;gBACAF,QAAQyD;YACV;YACA;;;;;;;;;;;;;;;;;OAiBC,GACD,MAAMoL,cAAcxS,oBAAoB,aAAKuS;gBAAME,SAAS;;YAC5D,MAAMC,iBAAiB1S,oBAAoB,aACtCuS;gBACHE,SAAS;;YAEX,kEAAkE;YAClE,oCAAoC;YACpC,MAAMH,UAAUvB,aACZpS,wBAAwBoS,YAAYA,WAAWI,GAAG,EAAE7N,SACpD;YACJ;;;;;;;;;;;;OAYC,GACD,MAAMU,UAAUtF,qBAAqB;gBACnCoI,SAASwL,CAAAA,4BAAAA,mBAAAA,QAASxL,OAAO,qBAAhBwL,iBAAkB7H,IAAI,OAAM3D;gBACrCC,WAAWE,QAAQF,SAAS;gBAC5B;;;;;;;;SAQC,GACD4L,OAAO,EAAE/B,0BAAAA,kBAAmB;oBAC1BC,MAAM;oBACNpJ,MAAM6K,CAAAA,4BAAAA,gBAAAA,QAAS7K,IAAI,qBAAb6K,cAAe7H,IAAI,OAAMhD;gBACjC;gBACA;;;;;;;;;SASC,GACDmL,SAAS,GAAE3L,wBAAAA,QAAQ4L,YAAY,YAApB5L,wBAAwB;oBAAE3D;oBAAOiD,MAAM6D,MAAMpH,GAAG,CAACM;gBAAO;gBACnEgN;gBACA5M;gBACAgP;YACF;YACA,MAAMN;YACN,MAAMU,SAAS,MAAMjR,UAAU;gBAC7BkR,IAAIzP;gBACJwD,SAAS9C,QAAQ8C,OAAO;eACpB9C,QAAQgP,IAAI,GAAG;gBAAEA,MAAMhP,QAAQgP,IAAI;YAAC,IAAI,CAAC;gBAC7C,oEAAoE;gBACpE,uEAAuE;gBACvE,oEAAoE;gBACpE,oEAAoE;gBACpE,0DAA0D;gBAC1D,qEAAqE;gBACrE,2BAA2B;gBAC3BC,MAAMjP,QAAQiP,IAAI;gBAClB,mEAAmE;gBACnE,qEAAqE;gBACrE,oEAAoE;gBACpE,mEAAmE;gBACnE,kEAAkE;gBAClE,kEAAkE;gBAClE,EAAE;gBACF,iEAAiE;gBACjE,gEAAgE;gBAChE,6DAA6D;gBAC7D,0CAA0C;gBAC1CC,SAAS;oBACP,oBAAoB,CAAC,CAAC,EAAEV,YAAY,CAAC,CAAC;oBACtC,yBAAyB;gBAC3B;gBACA,sEAAsE;gBACtE,kEAAkE;gBAClE,qEAAqE;gBACrE,gCAAgC;gBAChCW,UAAUlM,QAAQkM,QAAQ,IAAI1C,SAAS0C,QAAQ;eAC3ClM,QAAQmM,OAAO,GAAG;gBAAEA,SAASnM,QAAQmM,OAAO;YAAC,IAAI,CAAC;gBACtD,oEAAoE;gBACpE,iEAAiE;gBACjE,gCAAgC;gBAChCjH;gBACA,kEAAkE;gBAClE,mEAAmE;gBACnE,iCAAiC;gBACjCzE,UAAU;gBACV,6DAA6D;gBAC7D,6DAA6D;gBAC7DsD,MAAM;oBACJ;wBAAEzE,MAAM;wBAAUoB,OAAOjE;oBAAO;oBAChC;wBAAE6C,MAAM;wBAAcoB,OAAO/D;oBAAW;uBACpCmN,aACA;wBAAC;4BAAExK,MAAM;4BAAgBoB,OAAOoJ,WAAWI,GAAG;wBAAC;qBAAE,GACjD,EAAE;iBACP;gBACDkC,SAAS;;YAEX,IAAIP,OAAOlK,IAAI,EAAE;gBACfA,QAAQ;gBACRC,QAAQ5F,IAAI,CAACK;gBACb,IAAIgP,SAAS;wBACiBN;oBAA5BA,YAAY,CAACM,QAAQhM,EAAE,CAAC,GAAG,EAAC0L,2BAAAA,YAAY,CAACM,QAAQhM,EAAE,CAAC,YAAxB0L,2BAA4B,KAAK;gBAC/D;gBACA;YACF;YACA;;;;;;;;;;;;;;;;;;OAkBC,GACD,IAAIrQ,qBAAqBmR,YAAY,MAAM;gBACzC;;;;;;;SAOC,GACDX,WAAW9C,SAASnM,MAAM,GAAGmP;gBAC7B;YACF;YACA;;;;OAIC,GACDH,WAAWjP,IAAI,CAACK;QAClB;IACF,SAAU;QACR;;;;;;;KAOC,GACD,MAAM5D,iCAAiCoS,aAAalJ;QACpD;;;;KAIC,GACD,MAAM5H,yBAAyB+Q,gBAAgBnJ;IACjD;IACA,0EAA0E;IAC1E,yEAAyE;IACzE,wEAAwE;IACxE,4EAA4E;IAC5E,sEAAsE;IACtE,2EAA2E;IAC3E,eAAe;IACf,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,8CAA8C;IAC9C,MAAMxI,qBAAqBsD,QAAQmF;IACnC,MAAMxJ,eAAeqE,QAAQkF,MAAM;IACnC;;;;;;;;;;;GAWC,GACD,MAAM3H,uBAAuB8K,OAAOnD;IAEpC;;;;;;;;;;;;;;;;;;;GAmBC,GACD,IAAI3B,QAAQ4G,cAAc,KAAK,OAAO;QACpC,MAAMlN,oBAAoB+C,QAAQE,YAAYiF,SAASrG;QACvD;;;;;;;;KAQC,GACD,MAAM5B,sBAAsB8C,QAAQE,YAAYsO,YAAY1P;IAC9D;IAEA,qDAAqD;IACrD,IAAIuO,cAAcA,WAAW9M,MAAM,KAAK,WAAW;QACjD,KAAK,MAAM,CAACqP,WAAW/B,MAAM,IAAIvM,OAAOuO,OAAO,CAACvB,cAAe;YAC7D,qEAAqE;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,oEAAoE;YACpE,oEAAoE;YACpE,qEAAqE;YACrE,6CAA6C;YAC7C,IAAI,CAAClS,aAAawT,YAAY;YAC9B,MAAMlL,QACH3D,UAAU,CAAC,eACXC,GAAG,CAACqM,WAAWI,GAAG,EAClB1M,UAAU,CAAC,SACXC,GAAG,CAAC4O,WACJhK,GAAG,CACF;gBACEkK,WAAWrU,cAAcqD,SAAS,CAACmH,UAAU,CAAC8J,SAAS,CAAClC;gBACxDmC,WAAWvU,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;YAC/D,GACA;gBAAEC,OAAO;YAAK,GAEfiC,KAAK,CAAC,IAAM6H;QACjB;IACF;IAEA;;;;;;;;;;;;;;6CAc2C,GAC3C,MAAMC,OAAOtS,kBAAkB;QAC7BmM,UAAUF,aAAaE,QAAQ,CAACvK,MAAM;QACtC2Q,WAAW/J,WAAW5G,MAAM;QAC5B4Q,WAAW3B;QACX1J,SAASG,OAAOsJ,WAAWhP,MAAM;QACjCqF;IACF;IACA;;;;;;;;GAQC,GACD,MAAMkB,WAAWmK,KAAK5J,QAAQ,GAAGgF,KAAKC,GAAG,KAAK;IAE9C,IAAIhI,QAAQ4G,cAAc,KAAK,OAAO;QACpC,OAAO;YACLjK;YACAkG,YAAYuF,SAASnM,MAAM;YAC3B6G,cAAc8B,SAAS3I,MAAM;WACzBgH,oBAAoB;YAAEA,mBAAmB;QAAK,IAAI,CAAC;YACvDtB;WACIuJ,WAAW;YAAEA;QAAS,IAAI,CAAC;IAEnC;IACA;;;;;;;;;;;;;;;;6CAgB2C,GAC3C,MAAM4B,WAAW,CAACpM,QAChBK,qBACK7I,cAAcqD,SAAS,CAACmH,UAAU,CAAC8J,SAAS,CAAC9L,SAC9CA;IACN;;;;;;;;;;;;;;;GAeC,GACD,MAAMqM,sBAAsB,CAAC/M,QAAQkB,YAAY;IACjD,MAAMC,QAAQ3D,UAAU,CAAC,aAAaC,GAAG,CAACd,YAAY0F,GAAG,CACvD;QACExC;QACAW;QACAC;OAMIgJ,cAAc;QAAEuD,aAAajF,KAAKC,GAAG;IAAG,IAAI,CAAC,GAU7ChI,QAAQc,MAAM,GAAG;QAAEA,QAAQd,QAAQc,MAAM;IAAC,IAAI,CAAC,GAC/CoC,WAAW;QAAEA;IAAS,IAAI,CAAC,GAC3BlD,QAAQa,SAAS,GAAG;QAAEA,WAAWb,QAAQa,SAAS;IAAC,IAAI,CAAC;QAC5D,mEAAmE;QACnE,sEAAsE;QACtE,yEAAyE;QACzE,2DAA2D;QAC3DjE;OACIoD,QAAQY,gBAAgB,GACxB;QAAEA,kBAAkBZ,QAAQY,gBAAgB;IAAC,IAC7C,CAAC,GAODZ,QAAQ6J,SAAS,GAAG;QAAEA,WAAW7J,QAAQ6J,SAAS;IAAC,IAAI,CAAC,GAIxD7J,QAAQkM,QAAQ,GAAG;QAAEA,UAAUlM,QAAQkM,QAAQ;IAAC,IAAI,CAAC,GACrDlM,QAAQmM,OAAO,GAAG;QAAEA,SAASnM,QAAQmM,OAAO;IAAC,IAAI,CAAC,GAUlDpH,WAAW;QAAEA;IAAS,IAAI,CAAC,GAU5BjK,YAAY;QACbmS,MAAM/H,gBAAgB+H,IAAI;QAC1Bf,UAAUlM,QAAQkM,QAAQ,IAAI1C,SAAS0C,QAAQ;QAC/CC,SAASnM,QAAQmM,OAAO;IAC1B,IACInM,QAAQF,SAAS,GAAG;QAAEA,WAAWE,QAAQF,SAAS;IAAC,IAAI,CAAC,GACxDE,QAAQkN,WAAW,GAAG;QAAEA,aAAalN,QAAQkN,WAAW;IAAC,IAAI,CAAC,GAC9DlN,QAAQmN,eAAe,GACvB;QAAEA,iBAAiBnN,QAAQmN,eAAe;IAAC,IAC3C,CAAC,GACDrD,aAAa;QAAEnJ,cAAcmJ,WAAWI,GAAG;IAAC,IAAI,CAAC;QACrDkD,OAAO;YACLvK,YAAYiK,SAAS1E,SAASnM,MAAM;YACpC0F,MAAMmL,SAASnL;WAcXoL,sBACA;YACEjK,cAAcgK,SAASlI,SAAS3I,MAAM;WAClCgH,oBAAoB;YAAEoK,uBAAuB;QAAK,IAAI,CAAC,KAE7D,CAAC,GAaD,CAACV,KAAK5J,QAAQ,IAAI4J,KAAKpK,SAAS,GAAG,IACnC;YAAE2I,UAAUyB,KAAKpK,SAAS;QAAC,IAC3B,CAAC,GACDxE,OAAOC,IAAI,CAAC+M,cAAc9O,MAAM,GAChC;YACE8O,cAAchN,OAAOuP,WAAW,CAC9BvP,OAAOuO,OAAO,CAACvB,cAAclM,GAAG,CAAC,CAAC,CAACwN,WAAW/B,MAAM,GAAK;oBACvD+B;oBACAS,SAASxC;iBACV;QAEL,IACA,CAAC,GAsBDyC,sBACA;YACElE,WAAWiE,SAASxG,aAAauC,SAAS;YAC1CC,qBAAqBgE,SAASxG,aAAawC,mBAAmB;YAC9DC,eAAe+D,SAASxG,aAAayC,aAAa;YAClDC,iBAAiB8D,SAASxG,aAAa2C,QAAQ;QACjD,IACA,CAAC;YACLN,YAAYmE,SAASjK,WAAW5G,MAAM,GAAGkM,QAAQlM,MAAM;YACvD2M,aAAakE,SAAS3E,QAAQlM,MAAM,GAAGmM,SAASnM,MAAM;YACtD;;;;;;;;;;SAUC,GACDsR,cAAc;;QAEhB;;;;;;;;;;;;;;;;;;OAkBC,GACDvQ,QAAQ2P,KAAK5J,QAAQ,GAAG,cAAc;OAClC4J,KAAK5J,QAAQ,GAAG;QAAEyK,UAAUhL;IAAS,IAAI,CAAC;QAC9C;;;;;;;OAOC,GACDF,QAAQ;YACNC,WAAWoK,KAAKpK,SAAS;YACzBL,OAAOyK,KAAKzK,KAAK;YACjBM;WACImK,KAAKc,IAAI,GAAG;YAAEA,MAAMd,KAAKc,IAAI;QAAC,IAAI,CAAC;OAcrC1M,qBACA;QACE0B,YAAYvK,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;QAC9D+K,YAAY1N,QAAQ2N,SAAS;IAC/B,IACA;QACEC,QAAQ1V,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;QAC1DkL,QAAQ7N,QAAQ2N,SAAS;IAC3B,GAOA3N,QAAQkB,YAAY,GAAG,CAAC,IAAI;QAAE4M,WAAWhB,SAAS;IAAG,IAE3D;QAAElK,OAAO;IAAK;IAEhB,OAAO;QACLjG;QACAkG,YAAYuF,SAASnM,MAAM;QAC3B6G,cAAc8B,SAAS3I,MAAM;OACzBgH,oBAAoB;QAAEA,mBAAmB;IAAK,IAAI,CAAC,GACnDjD,QAAQiB,QAAQ,GAAG;QAAEA,UAAU;QAAM0D;IAAe,IAAI,CAAC;QAC7DhD;OACIuJ,WAAW;QAAEA;IAAS,IAAI,CAAC;QAC/B3I,WAAWoK,KAAKpK,SAAS;QACzBQ,UAAU4J,KAAK5J,QAAQ;QACvBb,OAAOyK,KAAKzK,KAAK;OACbyK,KAAK5J,QAAQ,GAAG;QAAEP;IAAS,IAAI,CAAC;AAExC;AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,eAAeuL,uBAAuB/N,OAG5C;;IACC,MAAMgO,cAAcnQ,eAAOmC,2BAAAA,QAASgO,WAAW,mBAAI,IAChDxK,IAAI,GACJjH,WAAW;IACd,MAAM0R,QAAQ,IAAI7K;IAClB,IAAI4K,aAAa;QACfC,MAAM5L,GAAG,CAAC2L,aAAa;YAAE3R,OAAO2R;YAAaE,OAAO;YAAOC,MAAM;QAAK;IACxE;IAEA,MAAMlO,aAAa,MAAM9H,cAAc6H,2BAAAA,QAASvD,MAAM,EAAEoI,KAAK,CAAC,IAAM;IACpE,MAAMC,QAAQjH,gBAAOoC,8BAAAA,WAAY6E,KAAK,oBAAI;IAC1C,IAAI,CAACA,OAAO,OAAO;WAAImJ,MAAMxP,MAAM;KAAG;IAEtC,MAAMmF,UAAU,MAAM1L,cACnBoF,GAAG,GACH/B,SAAS,GACTiC,UAAU,CAAC,QACXC,GAAG,CAACqH,OACJtH,UAAU,CAAC,UACZ;;;;;;KAMC,IACAlC,OAAO,CAACpD,cAAcqD,SAAS,CAACC,SAAS,CAACC,UAAU,IACpDK,KAAK,CAAC,KACNC,GAAG,GACH8I,KAAK,CAAC,IAAM;IAEf,KAAK,MAAMpH,gBAAOmG,2BAAAA,QAASlI,IAAI,oBAAI,EAAE,CAAE;YAChB+B,UAMLA;QANhB,MAAMpB,QAAQwB,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC,oBAAR0B,WAAoB,IACtC+F,IAAI,GACJjH,WAAW;QACd,IAAI,CAACF,SAAS4R,MAAM/H,GAAG,CAAC7J,QAAQ;QAChC4R,MAAM5L,GAAG,CAAChG,OAAO;YACfA;YACA6R,OAAOrQ,QAAOJ,YAAAA,IAAI1B,GAAG,CAAC,0BAAR0B,YAA0B,IAAI+F,IAAI,MAAMnH;YACtD8R,MAAM;QACR;IACF;IACA,OAAO;WAAIF,MAAMxP,MAAM;KAAG;AAC5B;AAEA;;;;;;;CAOC,GACD,eAAe2P,qBAAqBpO,OAInC;;IACC,MAAMqO,UAAUxQ,eAAOmC,2BAAAA,QAASqO,OAAO,mBAAI,IACxC7K,IAAI,GACJjH,WAAW;IACd,IAAI,CAAC8R,SAAS,OAAO;IACrB,IAAIA,YAAYxQ,gBAAOmC,2BAAAA,QAASgO,WAAW,oBAAI,IAAIxK,IAAI,GAAGjH,WAAW,IAAI;QACvE,OAAO;IACT;IAEA,MAAM0D,aAAa,MAAM9H,cAAc6H,2BAAAA,QAASvD,MAAM,EAAEoI,KAAK,CAAC,IAAM;IACpE,MAAMC,QAAQjH,gBAAOoC,8BAAAA,WAAY6E,KAAK,oBAAI;IAC1C,IAAI,CAACA,OAAO,OAAO;IAEnB,MAAMwJ,QAAQ,MAAMpW,cACjBoF,GAAG,GACH/B,SAAS,GACTiC,UAAU,CAAC,QACXC,GAAG,CAACqH,OACJtH,UAAU,CAAC,WACX+Q,KAAK,CAAC,SAAS,MAAMF,SACrBvS,KAAK,CAAC,GACNC,GAAG,GACH8I,KAAK,CAAC,IAAM;IACf,OAAO7D,QAAQsN,SAAS,CAACA,MAAME,KAAK;AACtC;AAUA,uDAAuD,GACvD,MAAMC,uBAAuB;AAE7B;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,qBACpBjS,MAAc;IAEd,MAAMkS,OAAOzW,cAAcqD,SAAS,CAACC,SAAS,CAACC,UAAU;IACzD,MAAM0F,UAAUjJ,cAAcoF,GAAG,GAAG/B,SAAS,GAAGiC,UAAU,CAAC,SAASC,GAAG,CAAChB;IACxE,MAAMwR,QAAQ,IAAI7K;IAElB,MAAMwL,UAAU,CACdlT,MACAkK,QACAiJ;QAEA,KAAK,MAAMpR,OAAO/B,eAAAA,OAAQ,EAAE,CAAE;gBACP+B;YAArB,MAAMpB,QAAQwB,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC,oBAAR0B,WAAoB,IACtC+F,IAAI,GACJjH,WAAW;YACd,IAAI,CAACF,SAAS4R,MAAM/H,GAAG,CAAC7J,QAAQ;YAChC,MAAMiD,OAAOuP,WACVhQ,GAAG,CAAC,CAACiQ;oBAAiBrR;uBAAPI,QAAOJ,WAAAA,IAAI1B,GAAG,CAAC+S,kBAARrR,WAAkB,IAAI+F,IAAI;eAChDuL,IAAI,CAAC/N;YACRiN,MAAM5L,GAAG,CAAChG,OAAO;gBAAEA;gBAAOiD,IAAI,EAAEA,eAAAA,OAAQ;gBAAIsG;YAAO;QACrD;IACF;IAEA,MAAM,CAAClC,OAAOE,SAASK,SAAS,GAAG,MAAM/E,QAAQC,GAAG,CAAC;QACnDpH,oBAAoB0E,QAAQ,SACzBuS,IAAI,CAAC,CAAC,EAAErL,KAAK,EAAE,GAAKA,MAAMrI,OAAO,CAACqT,MAAM7S,KAAK,CAAC2S,sBAAsB1S,GAAG,IACvE8I,KAAK,CAAC,IAAM;QACf1D,QACG3D,UAAU,CAAC,eACXlC,OAAO,CAACqT,MACR7S,KAAK,CAAC2S,sBACN1S,GAAG,GACH8I,KAAK,CAAC,IAAM;QACf9M,oBAAoB0E,QAAQ,YACzBuS,IAAI,CAAC,CAAC,EAAErL,KAAK,EAAE,GAAKA,MAAMrI,OAAO,CAACqT,MAAM7S,KAAK,CAAC2S,sBAAsB1S,GAAG,IACvE8I,KAAK,CAAC,IAAM;KAChB;IAED+J,QAAQlL,yBAAAA,MAAOhI,IAAI,EAAE,QAAQ;QAAC;KAAO;IACrC,wEAAwE;IACxE,qEAAqE;IACrE,4EAA4E;IAC5E,wCAAwC;IACxCkT,QAAQhL,2BAAAA,QAASlI,IAAI,EAAE,UAAU;QAAC;QAAe;KAAO;IACxDkT,QAAQ3K,4BAAAA,SAAUvI,IAAI,EAAE,WAAW;QAAC;QAAQ;KAAY;IACxD,OAAO;WAAIuS,MAAMxP,MAAM;KAAG;AAC5B;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,eAAewQ,qBACbxS,MAAc,EACdJ,KAAa;QA8BA6S;IA5Bb,MAAM/N,UAAUjJ,cAAcoF,GAAG,GAAG/B,SAAS,GAAGiC,UAAU,CAAC,SAASC,GAAG,CAAChB;IACxE,KAAK,MAAM,CAACe,YAAYqR,WAAW,IAAI;QACrC;YAAC;YAAS;gBAAC;aAAO;SAAC;QACnB,uEAAuE;QACvE,uEAAuE;QACvE,YAAY;QACZ;YAAC;YAAe;gBAAC;gBAAe;aAAO;SAAC;KACzC,CAAW;YAOEM;QANZ,MAAMA,WAAW,MAAMhO,QACpB3D,UAAU,CAACA,YACX+Q,KAAK,CAAC,SAAS,MAAMlS,OACrBP,KAAK,CAAC,GACNC,GAAG,GACH8I,KAAK,CAAC,IAAM;QACf,MAAMpH,MAAM0R,6BAAAA,iBAAAA,SAAUzT,IAAI,qBAAdyT,cAAgB,CAAC,EAAE;QAC/B,IAAI1R,KAAK;gBACSA;YAAhB,OAAO;gBAAEW,IAAI,GAAGX,YAAAA,IAAIW,IAAI,cAARX,YAAc,CAAC;gBAA+BoR;YAAW;QAC3E;IACF;IAEA,MAAM5K,WAAW,MAAMnM,yBAAyB2E,QAAQ,YAAYoI,KAAK,CACvE,IAAM;IAER,MAAMqK,UAAUjL,WACZ,MAAMrL,mBAAmBqL,UAAU5H,OAAO;QAAEI;IAAO,GAAGoI,KAAK,CAAC,IAAM,QAClE;IACJ,OAAOqK,UACH;QACE9Q,IAAI,GAAG8Q,gBAAAA,QAAQ9Q,IAAI,cAAZ8Q,gBAAkB,CAAC;QAC1BL,YAAY;YAAC;YAAQ;SAAY;IACnC,IACA;AACN;AAEA;;;;;;;CAOC,GACD,eAAezI,kBACb3J,MAAc,EACdJ,KAAa;;IAEb,IAAI,CAACA,SAAS,CAACrB,cAAcwJ,IAAI,CAACnI,QAAQ,OAAO;IACjD,MAAM4R,QAAQ,MAAMgB,qBAAqBxS,QAAQJ,OAAOwI,KAAK,CAAC,IAAM;IACpE,eAAOoJ,yBAAAA,MAAO7P,IAAI,mBAAI;AACxB;AAEA;;;;;CAKC,GACD,eAAegR,oBACb3S,MAAc,EACd4S,QAAgB;;IAEhB,MAAMhT,QAAQwB,OAAOwR,mBAAAA,WAAY,IAC9B7L,IAAI,GACJjH,WAAW;IACd,IAAI,CAACF,SAAS,CAACrB,cAAcwJ,IAAI,CAACnI,QAAQ,OAAO;IAEjD,MAAM4R,QAAQ,MAAMgB,qBAAqBxS,QAAQJ,OAAOwI,KAAK,CAAC,IAAM;IACpE,MAAMvF,OAAO,SAAC2O,yBAAAA,MAAOY,UAAU,mBAAI,EAAE,EAClChQ,GAAG,CAAC,CAACiQ;;YAAiBb;eAAPpQ,eAAOoQ,0BAAAA,cAAAA,MAAO7P,IAAI,qBAAX6P,WAAa,CAACa,MAAM,mBAAI,IAAItL,IAAI;OACtDuL,IAAI,CAAC/N;IAER;;;;;;;;GAQC,GACD,OAAO1B,OAAO;QAAEjD;QAAOiD;IAAK,IAAI;QAAEjD;IAAM;AAC1C;AAEA,SAASiT,sBACPH,QAA4C,EAC5C1S,MAAc,EACdkR,SAAiB,EACjB1M,QAAiB;QAEOkO,eAwBQA,gBACZA,gBAaFA;IAtClB,MAAM1O,WAAW5C,QAAOsR,gBAAAA,SAASpT,GAAG,CAAC,uBAAboT,gBAA4B;IACpD;;;;;;;;GAQC,GACD,MAAM7K,SAASF,MAAMC,OAAO,CAAC8K,SAASpT,GAAG,CAAC,aACtC,AAACoT,SAASpT,GAAG,CAAC,UAAwB8C,GAAG,CAAChB,UAC1C6O;IACJ,IAAIjM,aAAa,YAAaQ,CAAAA,YAAY,EAACqD,0BAAAA,OAAQrI,MAAM,CAAD,GAAI;QAC1D,MAAM,IAAIY,kBACRoE,WACI,qEACA,sEACA,+CACA,sEACA,qCACJ;IAEJ;IACA,MAAML,mBAAmB/C,QAAOsR,iBAAAA,SAASpT,GAAG,CAAC,+BAAboT,iBAAoC;IACpE,MAAM3O,OAAO3C,QAAOsR,iBAAAA,SAASpT,GAAG,CAAC,mBAAboT,iBAAwB;IAC5C,IAAI,CAACvO,oBAAoB,CAACJ,MAAM;QAC9B,MAAM,IAAI3D,kBAAkB,qCAAqC;IACnE;IACA,MAAM0S,WAAW,CAACT;YACKK;QAArB,MAAMzO,QAAQ7C,QAAOsR,gBAAAA,SAASpT,GAAG,CAAC+S,kBAAbK,gBAAuB;QAC5C,OAAOzO,QAAQ;YAAE,CAACoO,MAAM,EAAEpO;QAAM,IAAI,CAAC;IACvC;IACA,OAAO;QACLjE;QACAE,YAAYwS,SAAS9P,EAAE;OACnB4B,WAAW;QAAEA,UAAU;IAAK,IAAI,CAAC;QACrC0M;QACA9N,SAAShC,QAAOsR,iBAAAA,SAASpT,GAAG,CAAC,sBAAboT,iBAA2B;QAC3C3O;QACAC;OACI6D,CAAAA,0BAAAA,OAAQrI,MAAM,IAAG;QAAEqI;IAAO,IAAI,CAAC,GAC/B1D,mBAAmB;QAAEA;IAAiB,IAAI,CAAC,GAO5C2O,SAAS,cACTA,SAAS,cACTA,SAAS,WACTA,SAAS,YACTA,SAAS,aACTA,SAAS,YAOTA,SAAS,aACTA,SAAS,cACTA,SAAS,gBACTA,SAAS,oBAeRtO,WAAW,CAAC,IAAIsO,SAAS;AAEjC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,MAAMC,sBAAwC,OAAOC,KAAKC;qFAuNlCD;QAlNPA,WACAA,YACCA,YAGHA,YAGIA,YACQA,YAQPA,YAQSA,YAqBNA,YACDA,YACEA,aAOEA,aAkBPA,aASOA,aA2HRA;IAjNvB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IAEA,MAAMpT,SAASoB,gBAAO4R,YAAAA,IAAIjP,IAAI,qBAARiP,UAAUhT,MAAM,mBAAI;IAC1C,MAAMqT,SAASjS,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAUK,MAAM,oBAAI;IAC1C,MAAMjQ,UAAUhC,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAU5P,OAAO,oBAAI,IACzC2D,IAAI,GACJxE,KAAK,CAAC,GAAG;IACZ,MAAMwB,OAAO3C,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAUjP,IAAI,oBAAI,IACnCgD,IAAI,GACJxE,KAAK,CAAC,GAAG;IACZ,MAAMyB,WAAW5C,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAUhP,QAAQ,oBAAI;IAC9C,MAAMG,mBAAmB/C,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAU7O,gBAAgB,oBAAI;IAC9D;;;;;;GAMC,GACD,MAAMiJ,YAAYhM,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAU5F,SAAS,oBAAI,IAC7CrG,IAAI,GACJxE,KAAK,CAAC,GAAG;IACZ;;;;GAIC,GACD,MAAM+Q,qBAAqBlS,iBAAO4R,aAAAA,IAAIjP,IAAI,qBAARiP,WAAUM,kBAAkB,oBAAI;IAClE;;;;;;GAMC,GACD,MAAMC,aAAa,CAACtP,OAAgBsB,MAClCnE,OAAO6C,gBAAAA,QAAS,GACd;;;;;OAKC,GACD,4CAA4C;SAC3CuP,OAAO,CAAC,6BAA6B,KACrCzM,IAAI,GACJxE,KAAK,CAAC,GAAGgD;IACd,sEAAsE;IACtE,MAAMkK,WAAW8D,YAAWP,aAAAA,IAAIjP,IAAI,qBAARiP,WAAUvD,QAAQ,EAAE;IAChD,MAAMC,UAAU6D,YAAWP,aAAAA,IAAIjP,IAAI,qBAARiP,WAAUtD,OAAO,EAAE,KAAK5P,WAAW;IAC9D,MAAMuD,YAAYkQ,YAAWP,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU3P,SAAS,EAAE;IAClD;;;;;GAKC,GACD,MAAMoN,cAAc8C,YAAWP,cAAAA,IAAIjP,IAAI,qBAARiP,YAAUvC,WAAW,EAAE;IACtD;;;;;;;;;;;;;;;;GAgBC,GACD,MAAMnI,WAAWlH,iBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU1K,QAAQ,oBAAI;IAC9C,IAAIA,YAAY,CAAClM,aAAakM,WAAW;QACvC,OAAO2K,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAiB;IACxD;IACA,IAAIE,sBAAsB,CAAClX,aAAakX,qBAAqB;QAC3D,OAAOL,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAyB;IAChE;IACA,2EAA2E;IAC3E,sCAAsC;IACtC,MAAM1C,kBAAkBtP,iBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAUtC,eAAe,oBAAI;IAC5D,IAAI,CAAC1Q,QAAQ,OAAOiT,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;QAAEC,OAAO;IAAiB;IACnE,IAAI1C,mBAAmB,CAACtU,aAAasU,kBAAkB;QACrD,OAAOuC,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAmB;IAC1D;IACA,IAAI1D,WAAW,CAACnR,cAAcwJ,IAAI,CAAC2H,UAAU;QAC3C,OAAOuD,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAoC;IAC3E;IACA;;;;;;;;;;;;;;GAcC,GACD;;;;;;;;;;;;;;GAcC,GACD;;;;;GAKC,GACD,MAAMK,QACJJ,WAAW,YACXA,WAAW,aACXA,WAAW,kBACXA,WAAW,mBACXA,WAAW,cACXA,WAAW,aACXA,WAAW,WACXA,WAAW;IACb,IAAII,SAAS,CAACtP,oBAAqB,CAAA,CAACf,WAAW,CAACW,IAAG,GAAI;QACrD,OAAOkP,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAA0B;IACjE;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CA+B2C,GAC3C,MAAMM,sBACJL,WAAW,UACXA,WAAW,cACXA,WAAW,WACXA,WAAW,UACXA,WAAW;IACb,IAAIK,uBAAuBvP,oBAAoBJ,MAAM;QACnD,OAAOkP,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAC1BC,OACE,uEACA,mEACA;QACJ;IACF;IACA,IAAIM,uBAAuB,CAACvP,oBAAoBiJ,WAAW;QACzD,OAAO6F,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAC1BC,OACE,wEACA;QACJ;IACF;IACA,IAAI,CAAC;QAAC;QAAS;QAAW;QAAU;QAAW;KAAO,CAAC5J,QAAQ,CAACxF,WAAW;QACzE,OAAOiP,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAmB;IAC1D;IACA;;;;;;;;;;GAUC,GACD,MAAMjT,UAAUiB,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU7S,OAAO,qBAAI;IAC5C,IAAIA,WAAW,CAAC7F,eAAe6F,UAAU;QACvC,OAAO8S,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAgB;IACvD;IAEA,MAAMO,gBAAgBvS,QAAO4R,6BAAAA,IAAIxD,OAAO,CAACmE,aAAa,YAAzBX,6BAA6B;IAC1D,MAAMY,UAAUD,cAAcE,UAAU,CAAC,aACrCF,cAAcpR,KAAK,CAAC,UAAU/C,MAAM,IACpCyQ;IACJ,IAAI,CAAC2D,SAAS,OAAOX,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YAwCkBzO;YAglBEqO,aA0BFA,aACHA,aAEOA,aAEDA;QAtpBvB,MAAMc,UAAU,MAAMrY,cAAcoF,GAAG,GAAGkT,IAAI,GAAGC,aAAa,CAACJ;QAC/D;;;;;;;;;;;;;;;;;;;;;;KAsBC,GACD,IAAI,CAACE,QAAQG,cAAc,IAAI,CAAC5X,uBAAuByX,UAAU;YAC/D,kEAAkE;YAClE,+DAA+D;YAC/D,6BAA6B;YAC7B,OAAOb,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;gBAC1BC,OAAO;gBACPvR,QAAQ;YACV;QACF;QACA,MAAM/C,YAAYrD,cAAcoF,GAAG,GAAG/B,SAAS;QAC/C,MAAM4F,UAAU5F,UAAUiC,UAAU,CAAC,SAASC,GAAG,CAAChB;QAClD,MAAM2E,eAAe,MAAMD,QAAQpF,GAAG;QACtC,IAAI,CAACqF,aAAa1D,MAAM,EAAE;YACxB,OAAOgS,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,MAAMc,aAAa,EAACvP,oBAAAA,aAAarF,GAAG,CAAC,0BAAjBqF,oBAAmC,CAAC,EAAE,CAACmP,QAAQK,GAAG,CAAC;QACvE,IAAID,eAAe,WAAWA,eAAe,UAAU;YACrD,OAAOjB,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QAEA,IAAIC,WAAW,gBAAgB;gBAe0BS;YAdvD;;;;;;;;;;;;OAYC,GACD,MAAM,CAAC1N,YAAYgO,SAAS,GAAG,MAAM3R,QAAQC,GAAG,CAAC;gBAC/C4O,uBAAuB;oBAAEtR;oBAAQuR,aAAanQ,QAAO0S,iBAAAA,QAAQlU,KAAK,YAAbkU,iBAAiB;gBAAI;gBAC1E7B,qBAAqBjS;aACtB;YACD,OAAOiT,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;gBAAE/M;gBAAYgO;YAAS;QACrD;QAEA,IAAIf,WAAW,QAAQ;gBAWGS;gBAGGd,aAkDlBA;YA/DT;;;;;;;;;OASC,GACD,MAAMqB,WAAWjT,QAAO0S,kBAAAA,QAAQlU,KAAK,YAAbkU,kBAAiB,IACtC/M,IAAI,GACJjH,WAAW;YACd,MAAMwU,cAAclT,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU3D,EAAE,qBAAI,IACxCtI,IAAI,GACJjH,WAAW;YACd,MAAMyU,YAAYD,eAAeD;YACjC,IAAI,CAACE,WAAW;gBACd,OAAOtB,IACJ1S,MAAM,CAAC,KACP4S,IAAI,CAAC;oBAAEC,OAAO;gBAA8C;YACjE;YACA,IAAI,CAAC7U,cAAcwJ,IAAI,CAACwM,YAAY;gBAClC,OAAOtB,IACJ1S,MAAM,CAAC,KACP4S,IAAI,CAAC;oBAAEC,OAAO;gBAA4C;YAC/D;YACA;;;;;;;;;;;;;;OAcC,GACD,MAAMoB,WAAW,MAAM7C,qBAAqB;gBAC1C3R;gBACAuR,aAAa8C;gBACbzC,SAAS2C;YACX;YACA,IAAI,CAACC,UAAU;gBACb,OAAOvB,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAC1BC,OACE,CAAC,gEAAgE,CAAC,GAClE,CAAC,oBAAoB,EAAEmB,UAAU,6BAA6B,CAAC,GAC/D,CAAC,6DAA6D,CAAC;gBACnE;YACF;YACA;;;;;OAKC,GACD,MAAME,UAAU,MAAM9B,oBACpB3S,QACAoB,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU0B,YAAY,qBAAI;YAEnC,MAAMtF,SAAS,MAAM9L,oBAAoB;gBACvCtD;gBACAoD;gBACAW,MAAMA,QAAQ;gBACdC,UAAU;gBACV6D,QAAQ;oBAAC0M;iBAAU;gBACnBpQ,kBAAkBA,oBAAoB8L;eAIlC7C,YAAY;gBAAEA;YAAU,IAAI,CAAC;gBACjCqC;gBACAC;eAIIpH,WAAW;gBAAEA;YAAS,IAAI,CAAC;gBAC/BjF;eACIoR,UAAU;gBAAEtF,cAAcsF;YAAQ,IAAI,CAAC;gBAC3CtK,gBAAgB;gBAChB+G,WAAW4C,QAAQK,GAAG;gBACtB,mEAAmE;gBACnE7K,UAAUiL;;YAEZ,OAAOtB,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC,aACvB/D;gBACHrH,MAAM;gBACNsH,IAAIkF;eACAE,UAAU;gBAAEC,cAAcD,QAAQ7U,KAAK;YAAC,IAAI,CAAC;QAErD;QAEA,IAAIyT,WAAW,iBAAiB;gBAoB5BlY,mBAuBgB2Y,iBACD;YA3CjB;;;;;;;;;;;;;;OAcC,GACD,MAAM7G,WAAW9I,mBACb,MAAMzD,kBAAkBV,QAAQmE,oBAChC;YACJ,MAAMyI,YACJzR,oBAAAA,iBAAiB;gBACf0R,OAAOlI,aAAarF,GAAG,CAAC;gBACxBwN,WAAWnI,aAAarF,GAAG,CAAC;YAC9B,cAHAnE,oBAGM;YACR,MAAMqI,aAAa,MAAM9H,cAAcsE;YACvC,MAAM+M,WAAWjS,uBAAuB0I,8BAAAA,WAAY4F,GAAG;YACvD;;;;;OAKC,GACD,MAAMuL,WAAW3Z,qBAAqB;gBACpCoI;gBACAC;gBACA4L,SAAShC,WACL;oBACEE,MAAM;oBACNF;mBACIG,YAAY;oBAAEA;gBAAU,IAAI,CAAC,KAEnC;oBAAED,MAAM;oBAAQpJ;gBAAK;gBACzBmL,WAAW;oBACTtP,OAAOwB,QAAO0S,kBAAAA,QAAQlU,KAAK,YAAbkU,kBAAiB;oBAC/BjR,MAAMzB,QAAO,gBAAA,AAAC0S,OAAmC,CAAC,OAAO,YAA5C,gBAAgD;gBAC/D;gBACAlH;gBACA5M;gBACA,sEAAsE;gBACtE,gEAAgE;gBAChE,oEAAoE;gBACpE,mDAAmD;gBACnDgP,gBAAgB,GAAGpC,SAAS,sBAAsB,CAAC;YACrD;YACA,OAAOqG,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC,aACvBwB;gBACHtR,WAAWA,cAAa4J,4BAAAA,SAAU5J,SAAS,KAAI;gBAC/CoM,UAAUA,YAAY1C,SAAS0C,QAAQ;eACnCC,UAAU;gBAAEA;YAAQ,IAAI,CAAC;QAEjC;QAEA,IAAI2D,WAAW,WAAW;;gBAQJL,aACHA,aAEOA;YAVxB,oEAAoE;YACpE,kEAAkE;YAClE,MAAM5D,SAAS,MAAM9L,oBAAoB;gBACvCtD;gBACAoD,SAASA,WAAW;gBACpBW,MAAMA,QAAQ;gBACdC;gBACAI,WAAWhD,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU5O,SAAS,qBAAI;gBACzCC,QAAQjD,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU3O,MAAM,qBAAI;gBACnClE,SAASA,WAAW8P;gBACpBpI,QAAQF,MAAMC,OAAO,EAACoL,cAAAA,IAAIjP,IAAI,qBAARiP,YAAUnL,MAAM,IAClCmL,IAAIjP,IAAI,CAAC8D,MAAM,CAACzF,GAAG,CAAChB,UACpB6O;gBACJ9L,kBAAkBA,oBAAoB8L;eASlC3H,WAAW;gBAAEA;YAAS,IAAI,CAAC;gBAC/B4I,WAAW4C,QAAQK,GAAG;gBACtBlI,QAAQ;;YAEV,OAAOgH,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC/D;QAC9B;QAEA,IAAIiE,WAAW,YAAY;;gBAgBCL,aAapBA;YA5BN;;;;;;;;;;;;;;OAcC,GACD,MAAM4B,aAAaxT,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9S,UAAU,qBAAI;YAClD,IAAI,CAAC9D,aAAawY,aAAa;gBAC7B,OAAO3B,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAAqB;YAC5D;YACA,MAAMtO,eAAe,MAAMJ,QACxB3D,UAAU,CAAC,aACXC,GAAG,CAAC4T,YACJtV,GAAG;YACN,IAAI,CAACwF,aAAa7D,MAAM,EAAE;gBACxB,OAAOgS,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAAgB;YACvD;YACA,MAAMhE,SAAS,MAAM9L,oBAAoB,aACpCuP,sBAAsB/N,cAAc9E,QAAQ8T,QAAQK,GAAG,EAAE,OACxDnB,EAAAA,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU/G,MAAM,IAAG;gBAAEA,QAAQ;YAAK,IAAI,CAAC;YAE7C,OAAOgH,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC/D;QAC9B;QAEA;;;;;;;;;;;;;;;;;;;;;;;;;+CAyB2C,GAC3C,IAAIiE,WAAW,WAAWA,WAAW,cAAcA,WAAW,UAAU;gCAoB3DwB;gBAlBa7B,aAKfA,aAwEDA,aAGAA,aAEcA,aAGdA;YAtFR,MAAM8B,aAAazB,WAAW;YAC9B,MAAMtC,WAAW9L,kBAAO+N,cAAAA,IAAIjP,IAAI,qBAARiP,YAAUjC,QAAQ,qBAAI;YAC9C,IAAI+D,cAAe,CAAA,CAAC7P,OAAO8P,QAAQ,CAAChE,aAAaA,YAAYzF,KAAKC,GAAG,EAAC,GAAI;gBACxE,OAAO0H,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAA0B;YACjE;YACA,MAAMlT,aACJkB,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9S,UAAU,qBAAI,OAAOvF;YACxC,uEAAuE;YACvE,uEAAuE;YACvE,kEAAkE;YAClE,uEAAuE;YACvE,kEAAkE;YAClE,6BAA6B;YAC7B,IAAI,CAACyB,aAAa8D,aAAa;gBAC7B,OAAO+S,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAAqB;YAC5D;YACA,MAAM4B,YAAYtQ,QAAQ3D,UAAU,CAAC,aAAaC,GAAG,CAACd;YACtD,MAAM2U,iBAAiB,MAAMG,UAAU1V,GAAG;YAC1C,MAAM2V,cAAcJ,eAAe5T,MAAM,GACrCG,QAAOyT,sBAAAA,eAAevV,GAAG,CAAC,qBAAnBuV,sBAAgC,MACvC;YAEJ;;;;;;;;;;;;;;;iDAe2C,GAC3C,MAAMK,aAAa,CAACL,eAAe5T,MAAM,IACvCgU,gBAAgB,WAChBA,gBAAgB;YAClB,IAAI5B,WAAW,YAAY,CAAC6B,YAAY;gBACtC,OAAOjC,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAC1BC,OACE6B,gBAAgB,SACZ,0DACA,4DACA,gEACA;gBACR;YACF;YACA,IAAI5B,WAAW,UAAU;gBACvB;;;;;;SAMC,GACD,IAAI,CAACwB,eAAe5T,MAAM,EAAE;oBAC1B,OAAOgS,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;wBAAEC,OAAO;oBAAgB;gBACvD;gBACA,MAAM4B,UAAUpP,GAAG,CAAC;oBAAE6K;gBAAY,GAAG;oBAAEtK,OAAO;gBAAK;gBACnD,OAAO8M,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEjT;oBAAYuQ;gBAAY;YACxD;YACA,MAAMuE,UAAUpP,GAAG,CACjB;gBACExC;gBACAW;gBACAC;eAQI6Q,eAAe5T,MAAM,GAAG,CAAC,IAAI;gBAAEsP,aAAajF,KAAKC,GAAG;YAAG,GACvDyH,EAAAA,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU5O,SAAS,IACnB;gBAAEA,WAAWhD,OAAO4R,IAAIjP,IAAI,CAACK,SAAS;YAAE,IACxC,CAAC,GACD4O,EAAAA,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU3O,MAAM,IAAG;gBAAEA,QAAQjD,OAAO4R,IAAIjP,IAAI,CAACM,MAAM;YAAE,IAAI,CAAC,GAC1DlE,UAAU;gBAAEA;YAAQ,IAAI,CAAC,GACzBwH,MAAMC,OAAO,EAACoL,cAAAA,IAAIjP,IAAI,qBAARiP,YAAUnL,MAAM,IAC9B;gBAAEA,QAAQmL,IAAIjP,IAAI,CAAC8D,MAAM,CAACzF,GAAG,CAAChB,QAAQmB,KAAK,CAAC,GAAG;YAAK,IACpD,CAAC,GACDyQ,EAAAA,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9O,YAAY,IACtB;gBAAEA,cAAc9C,OAAO4R,IAAIjP,IAAI,CAACG,YAAY;YAAE,IAC9C,CAAC;gBACL;;;;;;;;;;;WAWC,GACDC,kBACEA,oBAAoB1I,cAAcqD,SAAS,CAACmH,UAAU,CAACkP,MAAM;gBAC/D;;;;;;;WAOC,GACD/H,WAAWA,aAAa3R,cAAcqD,SAAS,CAACmH,UAAU,CAACkP,MAAM;gBACjE7B,oBACE,AAAClG,aAAakG,sBACd7X,cAAcqD,SAAS,CAACmH,UAAU,CAACkP,MAAM;eAIvC1F,WAAW;gBAAEA;YAAS,IAAI,CAAC,GAC3BC,UAAU;gBAAEA;YAAQ,IAAI,CAAC,GACzBpH,WAAW;gBAAEA;YAAS,IAAI,CAAC,GAC3BjF,YAAY;gBAAEA;YAAU,IAAI,CAAC,GAC7BoN,cAAc;gBAAEA;YAAY,IAAI,CAAC,GACjCC,kBAAkB;gBAAEA;YAAgB,IAAI,CAAC,GACzCoE,aACA;gBACEvU,QAAQ;gBACRwQ;gBACAqE,aACE3Z,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;gBACpDmP,aAAavB,QAAQK,GAAG;YAC1B,IACA;gBACE5T,QAAQ;gBACR;;;;;;;;iBAQC,GACDwQ,UAAUtV,cAAcqD,SAAS,CAACmH,UAAU,CAACkP,MAAM;gBACnDG,WAAW7Z,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;gBAC7DqP,WAAWzB,QAAQK,GAAG;YACxB,IAEN;gBAAEhO,OAAO;YAAK;YAEhB,OAAO8M,IACJ1S,MAAM,CAAC,KACP4S,IAAI,CAAC;gBAAEjT;gBAAYK,QAAQuU,aAAa,cAAc;YAAQ;QACnE;QAEA,IAAIzB,WAAW,UAAU;;gBACGL;YAA1B,MAAM9S,aAAakB,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9S,UAAU,qBAAI;YAClD,uEAAuE;YACvE,yEAAyE;YACzE,sEAAsE;YACtE,8CAA8C;YAC9C,IAAI,CAAC9D,aAAa8D,aAAa;gBAC7B,OAAO+S,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAA2B;YAClE;YACA,MAAMoC,cAAc9Q,QAAQ3D,UAAU,CAAC,aAAaC,GAAG,CAACd;YACxD,MAAMuV,mBAAmB,MAAMD,YAAYlW,GAAG;YAC9C,IACE,EAACmW,oCAAAA,iBAAkBxU,MAAM,KACzBwU,iBAAiBnW,GAAG,CAAC,cAAc,aACnC;gBACA,OAAO2T,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAA2B;YAClE;YACA,MAAMoC,YAAY5P,GAAG,CACnB;gBACErF,QAAQ;gBACRmV,YAAYja,cAAcqD,SAAS,CAACmH,UAAU,CAACC,eAAe;gBAC9DyP,YAAY7B,QAAQK,GAAG;YACzB,GACA;gBAAEhO,OAAO;YAAK;YAEhB,OAAO8M,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;gBAAEjT;gBAAYK,QAAQ;YAAW;QAC/D;QAEA,IAAI8S,WAAW,WAAW;wBA0BIuC;gBATH5C,aA4ChB4C;YA5DT;;;;;;;;;;;;;;;iDAe2C,GAC3C,MAAMC,YAAYzU,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9S,UAAU,qBAAI;YACjD,IAAI,CAAC9D,aAAayZ,YAAY;gBAC5B,OAAO5C,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAAqB;YAC5D;YACA,MAAM0C,aAAapR,QAAQ3D,UAAU,CAAC,aAAaC,GAAG,CAAC6U;YACvD,MAAMD,kBAAkB,MAAME,WAAWxW,GAAG;YAC5C,IAAI,CAACsW,gBAAgB3U,MAAM,EAAE;gBAC3B,OAAOgS,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAAgB;YACvD;YACA,MAAM2C,eAAe3U,QAAOwU,uBAAAA,gBAAgBtW,GAAG,CAAC,qBAApBsW,uBAAiC;YAC7D;;;;;OAKC,GACD,IAAIG,iBAAiB,WAAWA,iBAAiB,aAAa;gBAC5D,OAAO9C,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAC1BC,OACE2C,iBAAiB,SACb,yDACA,gDACA;gBACR;YACF;YACA;;;;;;;;;;;;;;;;;iDAiB2C,GAC3C,MAAMC,kBAAkB1Q,KAAKE,KAAK,CAChCP,kBAAO2Q,wBAAAA,gBAAgBtW,GAAG,CAAC,8BAApBsW,sBAA+B9P,SAAS,qBAAI;YAErD,IAAIb,OAAO8P,QAAQ,CAACiB,oBAAoBA,kBAAkB,GAAG;gBAC3D,OAAO/C,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAC1BC,OACE,mEACA,4DACA;gBACJ;YACF;YACA;;;;;;;;;;OAUC,GACD,MAAM6C,UAAU,MAAMnX,UAAUoX,cAAc,CAAC,OAAOC;oBAEzCC;gBADX,MAAMA,QAAQ,MAAMD,YAAY7W,GAAG,CAACwW;gBACpC,IAAI1U,QAAOgV,aAAAA,MAAM9W,GAAG,CAAC,qBAAV8W,aAAuB,QAAQL,cAAc,OAAO;gBAC/DI,YAAYtW,MAAM,CAACiW,YAAY;oBAAEvV,QAAQ;gBAAU;gBACnD,OAAO;YACT;YACA,IAAI,CAAC0V,SAAS;gBACZ,OAAOhD,IACJ1S,MAAM,CAAC,KACP4S,IAAI,CAAC;oBAAEC,OAAO;gBAAmC;YACtD;YACA,IAAI;oBAGIJ,aAOFA;gBATJ,MAAM5D,SAAS,MAAM9L,oBAAoB,aACpCuP,sBAAsB+C,iBAAiB5V,QAAQ8T,QAAQK,GAAG,EAAE,QAC3DnB,EAAAA,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU/G,MAAM,IAAG;oBAAEA,QAAQ;gBAAK,IAAI,CAAC;gBAE7C;;;;SAIC,GACD,KAAI+G,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU/G,MAAM,EAAE;oBACpB,MAAM6J,WAAWlQ,GAAG,CAAC;wBAAErF,QAAQwV;oBAAa,GAAG;wBAAE5P,OAAO;oBAAK;gBAC/D;gBACA,OAAO8M,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC/D;YAC9B,EAAE,OAAOgE,OAAO;gBACd,gEAAgE;gBAChE,iEAAiE;gBACjE,mEAAmE;gBACnE,8DAA8D;gBAC9D,MAAM0C,WAAWlQ,GAAG,CAAC;oBAAErF,QAAQwV;gBAAa,GAAG;oBAAE5P,OAAO;gBAAK;gBAC7D,MAAMiN;YACR;QACF;QAEA;;;;;;;;;;;;;;+CAc2C,GAC3C,MAAMzN,SAASvE,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9S,UAAU,qBAAI;QAC9C,IAAIyF,QAAQ;gBAMC0Q;YALX,IAAI,CAACja,aAAauJ,SAAS;gBACzB,OAAOsN,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAAEC,OAAO;gBAAqB;YAC5D;YACA,MAAMiD,WAAW,MAAM3R,QAAQ3D,UAAU,CAAC,aAAaC,GAAG,CAAC2E,QAAQrG,GAAG;YACtE,MAAMgX,gBAAgBD,SAASpV,MAAM,GACjCG,QAAOiV,gBAAAA,SAAS/W,GAAG,CAAC,qBAAb+W,gBAA0B,MACjC;YACJ,IAAIA,SAASpV,MAAM,IAAIqV,kBAAkB,WAAWA,kBAAkB,aAAa;gBACjF,OAAOrD,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;oBAC1BC,OACEkD,kBAAkB,SACd,yDACA,gDACA,8DACA;gBACR;YACF;QACF;QAEA,MAAMlH,SAAS,MAAM9L,oBAAoB;YACvCtD;YACAoD;YACAW;YACAC;YACAI,WAAWhD,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU5O,SAAS,qBAAI;YACzCC,QAAQjD,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU3O,MAAM,qBAAI;YACnClE,SAASA,WAAW8P;YACpBpI,QAAQF,MAAMC,OAAO,EAACoL,cAAAA,IAAIjP,IAAI,qBAARiP,YAAUnL,MAAM,IAAImL,IAAIjP,IAAI,CAAC8D,MAAM,GAAGoI;YAC5D/P,YAAYyF;YACZzB,cAAc9C,kBAAO4R,cAAAA,IAAIjP,IAAI,qBAARiP,YAAU9O,YAAY,qBAAI;YAC/CC,kBAAkBA,oBAAoB8L;WAClC7C,YAAY;YAAEA;QAAU,IAAI,CAAC;YACjCqC;YACAC;WACIpH,WAAW;YAAEA;QAAS,IAAI,CAAC;YAC/BjF;YACAoN;YACAC;YACAQ,WAAW4C,QAAQK,GAAG;;QAExB,OAAOlB,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC/D;IAC9B,EAAE,OAAOgE,OAAO;QACd,IAAIA,iBAAiBhT,mBAAmB;YACtC,OAAO6S,IAAI1S,MAAM,CAAC6S,MAAM7S,MAAM,EAAE4S,IAAI,CAAC;gBAAEC,OAAOA,MAAM9S,OAAO;YAAC;QAC9D;QACAiW,QAAQnD,KAAK,CAACA;QACd,OAAOH,IAAI1S,MAAM,CAAC,KAAK4S,IAAI,CAAC;YAAEC,OAAO;QAAuB;IAC9D;AACF,EAAC"}
|