@aglyn/plugins-outreach 1.0.0-beta.224 → 1.0.0-beta.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +10 -4
  2. package/package.json +14 -14
  3. package/src/lib/components/enrollment-detail.d.ts +8 -0
  4. package/src/lib/components/enrollment-detail.js +17 -5
  5. package/src/lib/components/enrollment-detail.js.map +1 -1
  6. package/src/lib/components/mailbox-card.d.ts +3 -3
  7. package/src/lib/components/mailbox-card.js +11 -5
  8. package/src/lib/components/mailbox-card.js.map +1 -1
  9. package/src/lib/components/mailboxes-section.d.ts +6 -5
  10. package/src/lib/components/mailboxes-section.js +61 -26
  11. package/src/lib/components/mailboxes-section.js.map +1 -1
  12. package/src/lib/components/sequence-detail.js +1 -1
  13. package/src/lib/components/sequence-detail.js.map +1 -1
  14. package/src/lib/components/sequence-editor.js +29 -4
  15. package/src/lib/components/sequence-editor.js.map +1 -1
  16. package/src/lib/components/sequence-mailbox-picker.d.ts +21 -0
  17. package/src/lib/components/sequence-mailbox-picker.js +74 -13
  18. package/src/lib/components/sequence-mailbox-picker.js.map +1 -1
  19. package/src/lib/components/use-outreach-mailbox-api.d.ts +10 -2
  20. package/src/lib/components/use-outreach-mailbox-api.js +7 -2
  21. package/src/lib/components/use-outreach-mailbox-api.js.map +1 -1
  22. package/src/lib/engine/enrollment-state.d.ts +6 -0
  23. package/src/lib/engine/enrollment-state.js +1 -1
  24. package/src/lib/engine/enrollment-state.js.map +1 -1
  25. package/src/lib/engine/index.d.ts +1 -0
  26. package/src/lib/engine/index.js +1 -0
  27. package/src/lib/engine/index.js.map +1 -1
  28. package/src/lib/engine/mailbox-notice.d.ts +5 -2
  29. package/src/lib/engine/mailbox-notice.js +6 -2
  30. package/src/lib/engine/mailbox-notice.js.map +1 -1
  31. package/src/lib/engine/mailbox-rotation.d.ts +41 -0
  32. package/src/lib/engine/mailbox-rotation.js +71 -0
  33. package/src/lib/engine/mailbox-rotation.js.map +1 -0
  34. package/src/lib/mailboxes/mailbox-api.d.ts +30 -11
  35. package/src/lib/mailboxes/mailbox-api.js +7 -3
  36. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  37. package/src/lib/mailboxes/mailbox-credentials.d.ts +15 -12
  38. package/src/lib/mailboxes/mailbox-credentials.js +12 -8
  39. package/src/lib/mailboxes/mailbox-credentials.js.map +1 -1
  40. package/src/lib/mailboxes/mailbox-erasure.js +3 -0
  41. package/src/lib/mailboxes/mailbox-erasure.js.map +1 -1
  42. package/src/lib/mailboxes/mailbox-revoke.d.ts +10 -5
  43. package/src/lib/mailboxes/mailbox-revoke.js +1 -0
  44. package/src/lib/mailboxes/mailbox-revoke.js.map +1 -1
  45. package/src/lib/mailboxes/mailbox-routes.d.ts +11 -3
  46. package/src/lib/mailboxes/mailbox-routes.js +218 -35
  47. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  48. package/src/lib/mailboxes/mailbox-settings.d.ts +6 -2
  49. package/src/lib/mailboxes/mailbox-settings.js +5 -1
  50. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  51. package/src/lib/mailboxes/mailbox-transport.d.ts +15 -9
  52. package/src/lib/mailboxes/mailbox-transport.js +47 -17
  53. package/src/lib/mailboxes/mailbox-transport.js.map +1 -1
  54. package/src/lib/mailboxes/oauth-state.d.ts +11 -3
  55. package/src/lib/mailboxes/oauth-state.js +17 -8
  56. package/src/lib/mailboxes/oauth-state.js.map +1 -1
  57. package/src/lib/mailboxes/outreach-config.d.ts +30 -0
  58. package/src/lib/mailboxes/outreach-config.js +53 -10
  59. package/src/lib/mailboxes/outreach-config.js.map +1 -1
  60. package/src/lib/mailboxes/register-mailbox-routes.js +2 -1
  61. package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -1
  62. package/src/lib/model/outreach.types.d.ts +13 -2
  63. package/src/lib/model/outreach.types.js +7 -2
  64. package/src/lib/model/outreach.types.js.map +1 -1
  65. package/src/lib/model/sequence-draft.d.ts +6 -1
  66. package/src/lib/model/sequence-draft.js +16 -4
  67. package/src/lib/model/sequence-draft.js.map +1 -1
  68. package/src/lib/routes/enroll-routes.js +52 -1
  69. package/src/lib/routes/enroll-routes.js.map +1 -1
  70. package/src/lib/routes/sequence-routes.js +11 -3
  71. package/src/lib/routes/sequence-routes.js.map +1 -1
  72. package/src/lib/runtime/fixtures/fake-gmail.d.ts +18 -3
  73. package/src/lib/runtime/fixtures/fake-gmail.js +21 -1
  74. package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -1
  75. package/src/lib/runtime/mailbox-notices.js +2 -2
  76. package/src/lib/runtime/mailbox-notices.js.map +1 -1
  77. package/src/lib/runtime/send-job.js +2 -8
  78. package/src/lib/runtime/send-job.js.map +1 -1
  79. package/src/lib/runtime/sync-job.js +24 -11
  80. package/src/lib/runtime/sync-job.js.map +1 -1
  81. package/src/lib/subprocessors.d.ts +12 -1
  82. package/src/lib/subprocessors.js +25 -2
  83. package/src/lib/subprocessors.js.map +1 -1
  84. package/src/lib/transfer/sequences-package.js +7 -2
  85. package/src/lib/transfer/sequences-package.js.map +1 -1
  86. package/src/lib/transport/gmail-client.d.ts +4 -11
  87. package/src/lib/transport/gmail-client.js +42 -17
  88. package/src/lib/transport/gmail-client.js.map +1 -1
  89. package/src/lib/transport/graph-client.d.ts +87 -0
  90. package/src/lib/transport/graph-client.js +359 -0
  91. package/src/lib/transport/graph-client.js.map +1 -0
  92. package/src/lib/transport/mail-client.d.ts +138 -0
  93. package/src/lib/transport/mail-client.js +34 -0
  94. package/src/lib/transport/mail-client.js.map +1 -0
  95. package/src/lib/transport/microsoft-errors.d.ts +32 -0
  96. package/src/lib/transport/microsoft-errors.js +94 -0
  97. package/src/lib/transport/microsoft-errors.js.map +1 -0
  98. package/src/lib/transport/microsoft-oauth.d.ts +140 -0
  99. package/src/lib/transport/microsoft-oauth.js +225 -0
  100. package/src/lib/transport/microsoft-oauth.js.map +1 -0
  101. package/src/lib/transport/mime-message.d.ts +30 -0
  102. package/src/lib/transport/mime-message.js +235 -0
  103. package/src/lib/transport/mime-message.js.map +1 -0
  104. package/src/lib/transport/send-message.d.ts +15 -8
  105. package/src/lib/transport/send-message.js +4 -3
  106. package/src/lib/transport/send-message.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.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 { stampRecordOrigin } from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n filePluginPersonUnder,\n pluginPeopleInView,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { outreachCuratedEntry, outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport {\n buildOutreachEnrollment,\n planOutreachFirstDue,\n readOutreachStartStepIndex,\n} from '../engine/enrollment-state'\nimport { outreachEnrollmentSearchTokens } from '../enrollment/enrollment-search'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport {\n outreachStepOverridesRefused,\n readOutreachStepOverrideRequests,\n type OutreachStepOverrideRead,\n} from '../enrollment/step-overrides'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence, OutreachStepOverrides } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { type OutreachDomainIntelReadInput, outreachMailboxSendingDomain } from '../storage/domain-intel-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n *\n * Confirm may name `startStepIndex` (AGL-3228) for people who already had\n * the earlier steps by hand: everyone in the request begins at that step,\n * scheduled by its own delay from now, and the steps before it are marked\n * skipped on their enrollments. Absent, it is the first step; a step the\n * sequence does not have refuses the whole request.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n /**\n * The workspace's text generator on the core's text-generation seam\n * (AGL-3324), which drafts one person's copies of the steps; `null` when\n * no plugin generates text, and the curate route says so.\n */\n textGenerator(): PluginTextGenerator | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n gateway: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n /** The sequence site's consent group, whose opt-outs all apply. */\n consentHostIds: readonly string[]\n /** Whether that group's sites wait for each other's confirmation click. */\n consentAwaitsConfirmation: boolean\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n const consentGroup = consentGroupForHost(caller.org, sequence.hostId)\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroup.groupId,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\n/**\n * The domain the sequence's mailbox sends from (AGL-3328), whose gateway\n * ledger the Check-people chips read — or `null` when the mailbox cannot\n * be read, which shows the gateways without a history.\n */\nasync function sequenceSendingDomain(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n): Promise<string | null> {\n if (!sequence.mailboxId) return null\n try {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(sequence.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(sequence.mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n return outreachMailboxSendingDomain(mailbox)\n } catch (error) {\n console.error('[outreach] the sequence mailbox could not be read for its sending domain', error)\n return null\n }\n}\n\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n gateway: OutreachDomainIntelReadInput,\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates({\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n consentHostIds: context.consentHostIds,\n consentAwaitsConfirmation: context.consentAwaitsConfirmation,\n gateway,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n orgId: string,\n sequence: OutreachSequence,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeContainerIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const id = candidate.target === 'lead' ? candidate.leadId : candidate.contactId\n if (id) {\n // Filed by the plugin that keeps people (AGL-3080), as the sequence's\n // site holds the person: on a lead its own field, on a contact the\n // site's facet — the field every other campaign member carries.\n await filePluginPersonUnder({\n hostId: sequence.hostId,\n orgId,\n record: { kind: candidate.target, id },\n containerKind: 'campaign',\n ids: campaignIds,\n })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers of\n * the `campaign` kind. Read once per request, not per person; a container\n * that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeContainerIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => container.name).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n // The record system takes the view for the sequence's site (AGL-3080):\n // a colleague's private view is theirs, so it does not exist for this\n // member; a Leads view takes the site's leads by the view's status, a\n // Contacts view the contacts it selects there.\n const taken = viewId\n ? await pluginPeopleInView({\n orgId: caller.orgId,\n hostId: sequence.hostId,\n viewId,\n viewerUid: caller.uid,\n limit: OUTREACH_ENROLL_BATCH_MAX,\n })\n : null\n if (!taken || (taken.ok === false && taken.reason === 'not-found')) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (taken.ok === false && taken.reason === 'not-people') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n if (taken.ok === false) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${(taken.unsupported ?? []).join(', ') || 'something'}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n return {\n people: taken.people.map((person) => ({ kind: person.kind === 'lead' ? 'lead' : 'contact', id: person.id })),\n total: taken.total,\n truncated: taken.truncated,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n {\n resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs: deps.now(),\n sendingDomain: await sequenceSendingDomain(firestore, caller.orgId, sequence),\n },\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n const nowMs = deps.now()\n const start = readOutreachStartStepIndex(body['startStepIndex'], sequence.steps)\n if (start.refusal !== null) return outreachRefusal(400, 'invalid-start-step', start.refusal)\n const startStepIndex = start.startStepIndex\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[]; overrides: OutreachStepOverrideRead }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n // The steps the member curated for this person (AGL-3324), validated\n // here rather than trusted: a copy that breaks a rule refuses the\n // whole request, so nobody is enrolled with a copy the route dropped.\n const overrides = readOutreachStepOverrideRequests(\n person['stepOverrides'],\n sequence.steps,\n { uid: caller.uid, nowMs },\n startStepIndex,\n )\n if (outreachStepOverridesRefused(overrides)) {\n return outreachRefusal(\n 400,\n 'invalid-override',\n overrides.refusal ?? 'A curated step breaks a rule every sequence email keeps.',\n { issues: overrides.issues },\n )\n }\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n overrides,\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random, startStepIndex }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n {\n resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n },\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n // The member's name for the curated entries (AGL-3324), read once per\n // request and only when a person carries a curated step.\n let memberNameRead: Promise<string | null> | null = null\n const memberName = () => {\n memberNameRead ??= firestore\n .collection('orgs')\n .doc(caller.orgId)\n .collection('members')\n .doc(caller.uid)\n .get()\n .then(\n (member) =>\n (member.exists\n ? String(member.get('displayName') ?? '').trim() || String(member.get('email') ?? '').trim()\n : '') || caller.email,\n )\n .catch(() => caller.email)\n return memberNameRead\n }\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? {\n personalLine: '',\n attestations: [],\n overrides: { overrides: {}, issues: [], refusal: null } satisfies OutreachStepOverrideRead,\n }\n const stepOverrides: OutreachStepOverrides = supplied.overrides.overrides\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n startStepIndex,\n })\n // The member ticked this person past the red gateway chip\n // (AGL-3326): that is their say-so, stamped as the hold's release,\n // so the engine does not hold the send they were just shown.\n if (decision.hold) {\n enrollment.gatewayHold = {\n gateway: decision.hold.gateway,\n heldAtMs: null,\n releasedByUid: caller.uid,\n releasedAtMs: nowMs,\n }\n }\n // The person's own copies of steps (AGL-3324) ride with the\n // enrollment from its first write: nothing sends between.\n if (Object.keys(stepOverrides).length) enrollment.stepOverrides = stepOverrides\n // What the enrollments table searches the person by (AGL-3321),\n // from the name and address this document captures for good.\n enrollment.searchTokens = outreachEnrollmentSearchTokens(enrollment)\n // Nobody has clicked yet (AGL-3332): stored, because the \"Clicked: No\"\n // filter is a query, and a query cannot find a field's absence.\n enrollment.clicked = false\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(caller.orgId, sequence, candidate, nowMs)\n // A person reached first by this sequence came from it (AGL-3519):\n // the record system stamps its Sequence lead source on a record\n // naming none, and leaves any other alone.\n await stampRecordOrigin({\n orgId: caller.orgId,\n hostId: sequence.hostId,\n email: decision.email,\n origin: 'sequence',\n })\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({\n enrollmentId: id,\n sequenceName: sequence.name,\n campaignNames,\n startStepIndex,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n // And one line per step the member curated for them (AGL-3324),\n // after the enrollment's own, so the record reads in order.\n for (const [key, override] of Object.entries(stepOverrides)) {\n const curated = outreachCuratedEntry({\n enrollmentId: id,\n stepIndex: Number(key),\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: curated.dedupeKey,\n body: curated.body,\n atMs: nowMs,\n })\n }\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["stampRecordOrigin","normalizeContainerIds","consentGroupForHost","filePluginPersonUnder","pluginPeopleInView","readOrgContainers","outreachCuratedEntry","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","readOutreachStartStepIndex","outreachEnrollmentSearchTokens","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","outreachStepOverridesRefused","readOutreachStepOverrideRequests","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachMailboxSendingDomain","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","gateway","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","consentGroup","org","settings","allowedCountries","contactGroupId","groupId","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","siteName","String","sequenceSendingDomain","mailboxId","mailbox","error","console","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","target","record","kind","containerKind","ids","creditCampaign","outcome","atMs","sequenceCampaignNames","found","container","name","Boolean","sourcePeople","source","slice","total","truncated","viewId","taken","viewerUid","uid","limit","ok","reason","unsupported","join","person","preview","request","method","body","gate","Response","named","resolveMx","resolveAddress","now","sendingDomain","alreadyInSequence","has","confirm","start","steps","startStepIndex","requested","ref","overrides","issues","set","personalLine","attestations","size","mailboxSnapshot","enrolledAtMs","random","campaignNames","memberNameRead","memberName","then","member","trim","catch","results","supplied","stepOverrides","decision","allowed","blocks","enrollment","contactName","cold","enrolledByUid","hold","gatewayHold","heldAtMs","releasedByUid","releasedAtMs","Object","keys","searchTokens","clicked","create","code","origin","enrollmentId","sequenceName","link","dedupeKey","override","entries","curated","stepIndex","Number","edited","enrolled","result","logOrgActivity","type"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.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 { stampRecordOrigin } from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n filePluginPersonUnder,\n pluginPeopleInView,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { outreachCuratedEntry, outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport {\n buildOutreachEnrollment,\n planOutreachFirstDue,\n readOutreachStartStepIndex,\n} from '../engine/enrollment-state'\nimport { outreachEnrollmentSearchTokens } from '../enrollment/enrollment-search'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n createOutreachMailboxRotation,\n outreachSequenceMailboxIds,\n type OutreachRotationCandidate,\n} from '../engine/mailbox-rotation'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport {\n outreachStepOverridesRefused,\n readOutreachStepOverrideRequests,\n type OutreachStepOverrideRead,\n} from '../enrollment/step-overrides'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachMailbox, OutreachSequence, OutreachStepOverrides } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { type OutreachDomainIntelReadInput, outreachMailboxSendingDomain } from '../storage/domain-intel-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n *\n * Confirm may name `startStepIndex` (AGL-3228) for people who already had\n * the earlier steps by hand: everyone in the request begins at that step,\n * scheduled by its own delay from now, and the steps before it are marked\n * skipped on their enrollments. Absent, it is the first step; a step the\n * sequence does not have refuses the whole request.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n /**\n * The workspace's text generator on the core's text-generation seam\n * (AGL-3324), which drafts one person's copies of the steps; `null` when\n * no plugin generates text, and the curate route says so.\n */\n textGenerator(): PluginTextGenerator | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n gateway: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n /** The sequence site's consent group, whose opt-outs all apply. */\n consentHostIds: readonly string[]\n /** Whether that group's sites wait for each other's confirmation click. */\n consentAwaitsConfirmation: boolean\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n const consentGroup = consentGroupForHost(caller.org, sequence.hostId)\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroup.groupId,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\n/**\n * Who sends to each person this request enrolls (AGL-3489): the sequence's\n * own mailbox, or — when it rotates through more — the next one\n * `createOutreachMailboxRotation` picks, by the active enrollments each\n * holds now. Answers a function the enroll loop calls once per person it\n * enrolls. A sequence with one mailbox costs no extra reads.\n */\nasync function outreachEnrollRotation(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n sequence: OutreachSequence,\n own: OutreachMailbox,\n schedulable: (mailbox: OutreachMailbox) => boolean,\n): Promise<() => OutreachMailbox> {\n const ids = outreachSequenceMailboxIds(sequence)\n if (ids.length < 2) return () => own\n const mailboxes = outreachOrgCollection(firestore, orgId, 'mailboxes')\n const pool = (\n await Promise.all(\n ids.map(async (id) => {\n if (id === own.id) return own\n const snapshot = await mailboxes.doc(id).get()\n return readStoredOutreachMailbox(id, snapshot.exists ? snapshot.data() : undefined)\n }),\n )\n ).filter((mailbox): mailbox is OutreachMailbox => Boolean(mailbox))\n const usable = pool.filter((mailbox) => mailbox.status === 'connected' && schedulable(mailbox))\n if (usable.length < 2) return () => usable[0] ?? own\n const enrollments = outreachOrgCollection(firestore, orgId, 'enrollments')\n const candidates: OutreachRotationCandidate[] = await Promise.all(\n usable.map(async (mailbox) => {\n const active = await enrollments\n .where('mailboxId', '==', mailbox.id)\n .where('status', '==', 'active')\n .count()\n .get()\n return {\n id: mailbox.id,\n status: mailbox.status,\n activeEnrollments: Number(active.data().count) || 0,\n schedulable: true,\n }\n }),\n )\n const next = createOutreachMailboxRotation(candidates)\n const byId = new Map(usable.map((mailbox) => [mailbox.id, mailbox]))\n return () => byId.get(next() ?? '') ?? own\n}\n\n/**\n * The domain the sequence's mailbox sends from (AGL-3328), whose gateway\n * ledger the Check-people chips read — or `null` when the mailbox cannot\n * be read, which shows the gateways without a history.\n */\nasync function sequenceSendingDomain(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n): Promise<string | null> {\n if (!sequence.mailboxId) return null\n try {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(sequence.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(sequence.mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n return outreachMailboxSendingDomain(mailbox)\n } catch (error) {\n console.error('[outreach] the sequence mailbox could not be read for its sending domain', error)\n return null\n }\n}\n\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n gateway: OutreachDomainIntelReadInput,\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates({\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n consentHostIds: context.consentHostIds,\n consentAwaitsConfirmation: context.consentAwaitsConfirmation,\n gateway,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n orgId: string,\n sequence: OutreachSequence,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeContainerIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const id = candidate.target === 'lead' ? candidate.leadId : candidate.contactId\n if (id) {\n // Filed by the plugin that keeps people (AGL-3080), as the sequence's\n // site holds the person: on a lead its own field, on a contact the\n // site's facet — the field every other campaign member carries.\n await filePluginPersonUnder({\n hostId: sequence.hostId,\n orgId,\n record: { kind: candidate.target, id },\n containerKind: 'campaign',\n ids: campaignIds,\n })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers of\n * the `campaign` kind. Read once per request, not per person; a container\n * that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeContainerIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => container.name).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n // The record system takes the view for the sequence's site (AGL-3080):\n // a colleague's private view is theirs, so it does not exist for this\n // member; a Leads view takes the site's leads by the view's status, a\n // Contacts view the contacts it selects there.\n const taken = viewId\n ? await pluginPeopleInView({\n orgId: caller.orgId,\n hostId: sequence.hostId,\n viewId,\n viewerUid: caller.uid,\n limit: OUTREACH_ENROLL_BATCH_MAX,\n })\n : null\n if (!taken || (taken.ok === false && taken.reason === 'not-found')) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (taken.ok === false && taken.reason === 'not-people') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n if (taken.ok === false) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${(taken.unsupported ?? []).join(', ') || 'something'}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n return {\n people: taken.people.map((person) => ({ kind: person.kind === 'lead' ? 'lead' : 'contact', id: person.id })),\n total: taken.total,\n truncated: taken.truncated,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n {\n resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs: deps.now(),\n sendingDomain: await sequenceSendingDomain(firestore, caller.orgId, sequence),\n },\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n const nowMs = deps.now()\n const start = readOutreachStartStepIndex(body['startStepIndex'], sequence.steps)\n if (start.refusal !== null) return outreachRefusal(400, 'invalid-start-step', start.refusal)\n const startStepIndex = start.startStepIndex\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[]; overrides: OutreachStepOverrideRead }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n // The steps the member curated for this person (AGL-3324), validated\n // here rather than trusted: a copy that breaks a rule refuses the\n // whole request, so nobody is enrolled with a copy the route dropped.\n const overrides = readOutreachStepOverrideRequests(\n person['stepOverrides'],\n sequence.steps,\n { uid: caller.uid, nowMs },\n startStepIndex,\n )\n if (outreachStepOverridesRefused(overrides)) {\n return outreachRefusal(\n 400,\n 'invalid-override',\n overrides.refusal ?? 'A curated step breaks a rule every sequence email keeps.',\n { issues: overrides.issues },\n )\n }\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n overrides,\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random, startStepIndex }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const nextMailbox = await outreachEnrollRotation(firestore, caller.orgId, sequence, mailbox, (candidate) =>\n planOutreachFirstDue({ sequence, mailbox: candidate, enrolledAtMs: nowMs, random: deps.random, startStepIndex }) !==\n null,\n )\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n {\n resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n },\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n // The member's name for the curated entries (AGL-3324), read once per\n // request and only when a person carries a curated step.\n let memberNameRead: Promise<string | null> | null = null\n const memberName = () => {\n memberNameRead ??= firestore\n .collection('orgs')\n .doc(caller.orgId)\n .collection('members')\n .doc(caller.uid)\n .get()\n .then(\n (member) =>\n (member.exists\n ? String(member.get('displayName') ?? '').trim() || String(member.get('email') ?? '').trim()\n : '') || caller.email,\n )\n .catch(() => caller.email)\n return memberNameRead\n }\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? {\n personalLine: '',\n attestations: [],\n overrides: { overrides: {}, issues: [], refusal: null } satisfies OutreachStepOverrideRead,\n }\n const stepOverrides: OutreachStepOverrides = supplied.overrides.overrides\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n // Taken before anything awaits, so a batch is shared out in order.\n const assigned = nextMailbox()\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox: assigned,\n mailboxId: assigned.id,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n startStepIndex,\n })\n // The member ticked this person past the red gateway chip\n // (AGL-3326): that is their say-so, stamped as the hold's release,\n // so the engine does not hold the send they were just shown.\n if (decision.hold) {\n enrollment.gatewayHold = {\n gateway: decision.hold.gateway,\n heldAtMs: null,\n releasedByUid: caller.uid,\n releasedAtMs: nowMs,\n }\n }\n // The person's own copies of steps (AGL-3324) ride with the\n // enrollment from its first write: nothing sends between.\n if (Object.keys(stepOverrides).length) enrollment.stepOverrides = stepOverrides\n // What the enrollments table searches the person by (AGL-3321),\n // from the name and address this document captures for good.\n enrollment.searchTokens = outreachEnrollmentSearchTokens(enrollment)\n // Nobody has clicked yet (AGL-3332): stored, because the \"Clicked: No\"\n // filter is a query, and a query cannot find a field's absence.\n enrollment.clicked = false\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(caller.orgId, sequence, candidate, nowMs)\n // A person reached first by this sequence came from it (AGL-3519):\n // the record system stamps its Sequence lead source on a record\n // naming none, and leaves any other alone.\n await stampRecordOrigin({\n orgId: caller.orgId,\n hostId: sequence.hostId,\n email: decision.email,\n origin: 'sequence',\n })\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({\n enrollmentId: id,\n sequenceName: sequence.name,\n campaignNames,\n startStepIndex,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n // And one line per step the member curated for them (AGL-3324),\n // after the enrollment's own, so the record reads in order.\n for (const [key, override] of Object.entries(stepOverrides)) {\n const curated = outreachCuratedEntry({\n enrollmentId: id,\n stepIndex: Number(key),\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: curated.dedupeKey,\n body: curated.body,\n atMs: nowMs,\n })\n }\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["stampRecordOrigin","normalizeContainerIds","consentGroupForHost","filePluginPersonUnder","pluginPeopleInView","readOrgContainers","outreachCuratedEntry","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","readOutreachStartStepIndex","outreachEnrollmentSearchTokens","createOutreachMailboxRotation","outreachSequenceMailboxIds","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","outreachStepOverridesRefused","readOutreachStepOverrideRequests","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachMailboxSendingDomain","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","gateway","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","consentGroup","org","settings","allowedCountries","contactGroupId","groupId","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","siteName","String","outreachEnrollRotation","own","schedulable","ids","length","mailboxes","pool","map","id","filter","mailbox","Boolean","usable","enrollments","candidates","active","where","activeEnrollments","Number","next","byId","Map","sequenceSendingDomain","mailboxId","error","console","readPeople","context","people","existing","getAll","candidate","personId","enrolledHere","Set","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","open","some","entry","add","uniqueIds","values","Array","isArray","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","target","record","kind","containerKind","creditCampaign","outcome","atMs","sequenceCampaignNames","found","container","name","sourcePeople","source","slice","total","truncated","viewId","taken","viewerUid","uid","limit","ok","reason","unsupported","join","person","preview","request","method","body","gate","Response","named","resolveMx","resolveAddress","now","sendingDomain","alreadyInSequence","has","confirm","start","steps","startStepIndex","requested","ref","overrides","issues","set","personalLine","attestations","size","mailboxSnapshot","enrolledAtMs","random","nextMailbox","campaignNames","memberNameRead","memberName","then","member","trim","catch","results","supplied","stepOverrides","decision","allowed","blocks","assigned","enrollment","contactName","cold","enrolledByUid","hold","gatewayHold","heldAtMs","releasedByUid","releasedAtMs","Object","keys","searchTokens","clicked","create","code","origin","enrollmentId","sequenceName","link","dedupeKey","override","entries","curated","stepIndex","edited","enrolled","result","logOrgActivity","type"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,mDAAkD;AACpF,SAASC,qBAAqB,QAAQ,8CAA6C;AACnF,SAASC,mBAAmB,QAAQ,wCAAuC;AAC3E,SACEC,qBAAqB,EACrBC,kBAAkB,QACb,oDAAmD;AAI1D,SAASC,iBAAiB,QAAQ,iDAAgD;AAClF,SAASC,oBAAoB,EAAEC,qBAAqB,QAAQ,mCAA+B;AAC3F,SACEC,uBAAuB,EACvBC,oBAAoB,EACpBC,0BAA0B,QACrB,gCAA4B;AACnC,SAASC,8BAA8B,QAAQ,qCAAiC;AAEhF,SACEC,6BAA6B,EAC7BC,0BAA0B,QAErB,gCAA4B;AACnC,SACEC,wBAAwB,EACxBC,qBAAqB,EACrBC,4BAA4B,QAGvB,iCAA6B;AACpC,SAASC,uBAAuB,QAAQ,gCAA4B;AACpE,SACEC,4BAA4B,EAC5BC,gCAAgC,QAE3B,kCAA8B;AACrC,SAASC,iCAAiC,QAAQ,kCAA8B;AAChF,SACEC,yBAAyB,QAKpB,2BAAuB;AAE9B,SAASC,iCAAiC,QAAQ,0CAAsC;AACxF,SAA4CC,4BAA4B,QAAQ,mCAA+B;AAC/G,SAASC,gBAAgB,QAAQ,yBAAqB;AACtD,SACEC,oBAAoB,EACpBC,sBAAsB,EACtBC,qBAAqB,EACrBC,yBAAyB,EACzBC,0BAA0B,QACrB,iCAA6B;AACpC,SAASC,iCAAiC,QAAgC,kBAAc;AACxF,SAASC,iBAAiB,QAAqE,kBAAc;AAC7G,SACEC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,sBAAsB,EACtBC,oBAAoB,QACf,kBAAc;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,iDAAiD,GACjD,OAAO,MAAMC,wBAAsD;IACjEC,KAAK;IACLC,SAAS;AACX,EAAC;AAuBD,wCAAwC,GACxC,OAAO,SAASC,yBAAyBC,KAAa;IACpD,OAAOA,UAAU,IACb,oCACA,CAAC,SAAS,EAAEA,MAAM,qBAAqB,CAAC;AAC9C;AAEA,mFAAmF,GACnF,MAAMC,YAAiC;IACrCC,oBAAoB;IACpBC,gBAAgB;IAChBC,iBAAiB;IACjBC,cAAc;IACdC,oBAAoB;IACpBC,kBAAkB;IAClBC,iBAAiB;IACjBC,iBAAiB;IACjBC,SAAS;AACX;AAIA,qFAAqF,GACrF,MAAMC,iBAAiB;AAEvB,eAAeC,mBACbC,SAAoB,EACpBC,KAAa,EACbC,aAAsB;IAEtB,MAAMC,aAAatB,uBAAuBqB;IAC1C,IAAI,CAACC,YAAY,OAAOvB,gBAAgB,KAAK,mBAAmB;IAChE,MAAMwB,WAAW,MAAM/B,sBAAsB2B,WAAWC,OAAO,aAAaI,GAAG,CAACF,YAAYG,GAAG;IAC/F,MAAMC,WAAWhC,2BAA2B4B,YAAYC,SAASI,MAAM,GAAGJ,SAASK,IAAI,KAAKC;IAC5F,IAAI,CAACH,UAAU,OAAO3B,gBAAgB,KAAK,sBAAsB;IACjE,IAAI2B,SAASI,MAAM,KAAK,YAAY;QAClC,OAAO/B,gBAAgB,KAAK,qBAAqB;IACnD;IACA,IAAI2B,SAASI,MAAM,KAAK,UAAU;QAChC,OAAO/B,gBAAgB,KAAK,uBAAuB;IACrD;IACA,OAAO2B;AACT;AAaA,eAAeK,cACbZ,SAAoB,EACpBa,MAA2B,EAC3BN,QAA0B;QAmBOO;IAjBjC,MAAM,CAACC,aAAaD,KAAK,GAAG,MAAME,QAAQC,GAAG,CAAC;QAC5CjD,kCAAkCgC,WAAWa,OAAOZ,KAAK;QACzDD,UAAUkB,UAAU,CAAC,SAASb,GAAG,CAACE,SAASY,MAAM,EAAEb,GAAG;KACvD;IACD,MAAMc,eAAexE,oBAAoBiE,OAAOQ,GAAG,EAAEd,SAASY,MAAM;IACpE,OAAO;QACL,kEAAkE;QAClEG,UAAU,aACLf,SAASe,QAAQ;YACpBC,kBAAkBzD,kCAChByC,SAASe,QAAQ,CAACC,gBAAgB,EAClCR,YAAYQ,gBAAgB;;QAGhCC,gBAAgBJ,aAAaK,OAAO;QACpCC,gBAAgBN,aAAaO,OAAO;QACpCC,2BAA2BR,aAAaS,kBAAkB;QAC1DC,UAAUhB,KAAKN,MAAM,GAAGuB,QAAOjB,YAAAA,KAAKR,GAAG,CAAC,mBAATQ,YAAoB,MAAM;IAC3D;AACF;AAEA;;;;;;;;;CASC,GACD;;;;;;CAMC,GACD,eAAekB,uBACbhC,SAAsC,EACtCC,KAAa,EACbM,QAA0B,EAC1B0B,GAAoB,EACpBC,WAAkD;IAElD,MAAMC,MAAM5E,2BAA2BgD;IACvC,IAAI4B,IAAIC,MAAM,GAAG,GAAG,OAAO,IAAMH;IACjC,MAAMI,YAAYhE,sBAAsB2B,WAAWC,OAAO;IAC1D,MAAMqC,OAAO,AACX,CAAA,MAAMtB,QAAQC,GAAG,CACfkB,IAAII,GAAG,CAAC,OAAOC;QACb,IAAIA,OAAOP,IAAIO,EAAE,EAAE,OAAOP;QAC1B,MAAM7B,WAAW,MAAMiC,UAAUhC,GAAG,CAACmC,IAAIlC,GAAG;QAC5C,OAAOhC,0BAA0BkE,IAAIpC,SAASI,MAAM,GAAGJ,SAASK,IAAI,KAAKC;IAC3E,GACF,EACA+B,MAAM,CAAC,CAACC,UAAwCC,QAAQD;IAC1D,MAAME,SAASN,KAAKG,MAAM,CAAC,CAACC,UAAYA,QAAQ/B,MAAM,KAAK,eAAeuB,YAAYQ;IACtF,IAAIE,OAAOR,MAAM,GAAG,GAAG,OAAO;YAAMQ;gBAAAA,WAAAA,MAAM,CAAC,EAAE,YAATA,WAAaX;;IACjD,MAAMY,cAAcxE,sBAAsB2B,WAAWC,OAAO;IAC5D,MAAM6C,aAA0C,MAAM9B,QAAQC,GAAG,CAC/D2B,OAAOL,GAAG,CAAC,OAAOG;QAChB,MAAMK,SAAS,MAAMF,YAClBG,KAAK,CAAC,aAAa,MAAMN,QAAQF,EAAE,EACnCQ,KAAK,CAAC,UAAU,MAAM,UACtB7D,KAAK,GACLmB,GAAG;QACN,OAAO;YACLkC,IAAIE,QAAQF,EAAE;YACd7B,QAAQ+B,QAAQ/B,MAAM;YACtBsC,mBAAmBC,OAAOH,OAAOtC,IAAI,GAAGtB,KAAK,KAAK;YAClD+C,aAAa;QACf;IACF;IAEF,MAAMiB,OAAO7F,8BAA8BwF;IAC3C,MAAMM,OAAO,IAAIC,IAAIT,OAAOL,GAAG,CAAC,CAACG,UAAY;YAACA,QAAQF,EAAE;YAAEE;SAAQ;IAClE,OAAO;YAAMU,WAASD;gBAATC,YAAAA,KAAK9C,GAAG,EAAC6C,QAAAA,kBAAAA,QAAU,eAAnBC,YAA0BnB;;AACzC;AAEA;;;;CAIC,GACD,eAAeqB,sBACbtD,SAAoB,EACpBC,KAAa,EACbM,QAA0B;IAE1B,IAAI,CAACA,SAASgD,SAAS,EAAE,OAAO;IAChC,IAAI;QACF,MAAMnD,WAAW,MAAM/B,sBAAsB2B,WAAWC,OAAO,aAAaI,GAAG,CAACE,SAASgD,SAAS,EAAEjD,GAAG;QACvG,MAAMoC,UAAUpE,0BAA0BiC,SAASgD,SAAS,EAAEnD,CAAAA,4BAAAA,SAAUI,MAAM,IAAGJ,SAASK,IAAI,KAAKC;QACnG,OAAOzC,6BAA6ByE;IACtC,EAAE,OAAOc,OAAO;QACdC,QAAQD,KAAK,CAAC,4EAA4EA;QAC1F,OAAO;IACT;AACF;AAEA,eAAeE,WACb1D,SAAoB,EACpBa,MAA2B,EAC3BN,QAA0B,EAC1BoD,OAAsB,EACtBC,MAAoC,EACpC/D,OAAqC;IAMrC,MAAMiD,aAAa,MAAMpF,6BAA6B;QACpDuC,OAAOY,OAAOZ,KAAK;QACnBkB,QAAQZ,SAASY,MAAM;QACvBK,gBAAgBmC,QAAQnC,cAAc;QACtCoC;IACF;IACA,MAAMf,cAAcxE,sBAAsB2B,WAAWa,OAAOZ,KAAK,EAAE;IACnE,MAAM4D,WAAWf,WAAWV,MAAM,GAC9B,MAAMpC,UAAU8D,MAAM,IACjBhB,WAAWP,GAAG,CAAC,CAACwB,YAAclB,YAAYxC,GAAG,CAAClC,qBAAqBoC,SAASiC,EAAE,EAAEuB,UAAUC,QAAQ,OAEvG,EAAE;IACN,MAAMC,eAAe,IAAIC,IACvBpB,WAAWL,MAAM,CAAC,CAAC0B,YAAYC;YAAUP;gBAAAA,kBAAAA,QAAQ,CAACO,MAAM,qBAAfP,gBAAiBrD,MAAM;OAAE+B,GAAG,CAAC,CAACwB,YAAcA,UAAUC,QAAQ;IAEzG,MAAMK,UAAUvB,WAAWL,MAAM,CAAC,CAACsB,YAAcA,UAAUO,OAAO,IAAIP,UAAUQ,OAAO,IAAIR,UAAUS,KAAK;IAC1G,MAAMC,UAAUJ,QAAQjC,MAAM,GAC1B,MAAMzE,wBAAwBqC,WAAW;QACvCC,OAAOY,OAAOZ,KAAK;QACnBkB,QAAQZ,SAASY,MAAM;QACvBO,gBAAgBiC,QAAQjC,cAAc;QACtCE,2BAA2B+B,QAAQ/B,yBAAyB;QAC5D/B;QACA+D,QAAQS,QAAQ9B,GAAG,CAAC,CAACwB,YAAe,CAAA;gBAClCC,UAAUD,UAAUC,QAAQ;gBAC5BU,WAAWX,UAAUW,SAAS,IAAI;gBAClCC,QAAQZ,UAAUY,MAAM;gBACxBH,OAAOzC,OAAOgC,UAAUS,KAAK;YAC/B,CAAA;IACF,KACA,IAAInB;IACR,KAAK,MAAMU,aAAaM,QAAS;;YAClBI;QAAb,MAAMG,gBAAOH,eAAAA,QAAQnE,GAAG,CAACyD,UAAUC,QAAQ,sBAA9BS,aAAiC9E,eAAe,mBAAI,EAAE;QACnE,IAAIiF,KAAKC,IAAI,CAAC,CAACC,QAAUA,MAAM3E,UAAU,KAAKI,SAASiC,EAAE,GAAGyB,aAAac,GAAG,CAAChB,UAAUC,QAAQ;IACjG;IACA,OAAO;QAAElB;QAAYmB;QAAcQ;IAAQ;AAC7C;AAEA,MAAMO,YAAY,CAACC,SACjBC,MAAMC,OAAO,CAACF,UACV;WAAI,IAAIf,IAAIe,OAAO1C,GAAG,CAAC1D,wBAAwB4D,MAAM,CAAC,CAACD,KAAqBA,OAAO;KAAO,GAC1F,EAAE;AAER,OAAO,SAAS4C,2BAA2BC,IAA6B;IACtE;;;;;;;;;GASC,GACD,eAAeC,sBACbrF,KAAa,EACbM,QAA0B,EAC1BwD,SAAkC,EAClCwB,KAAa;QAEb,MAAMC,cAAc7I,sBAAsB4D,SAASiF,WAAW;QAC9D,IAAI,CAACA,YAAYpD,MAAM,EAAE;QACzB,IAAI;YACF,MAAMI,KAAKuB,UAAU0B,MAAM,KAAK,SAAS1B,UAAUY,MAAM,GAAGZ,UAAUW,SAAS;YAC/E,IAAIlC,IAAI;gBACN,sEAAsE;gBACtE,mEAAmE;gBACnE,gEAAgE;gBAChE,MAAM3F,sBAAsB;oBAC1BsE,QAAQZ,SAASY,MAAM;oBACvBlB;oBACAyF,QAAQ;wBAAEC,MAAM5B,UAAU0B,MAAM;wBAAEjD;oBAAG;oBACrCoD,eAAe;oBACfzD,KAAKqD;gBACP;YACF;QACF,EAAE,OAAOhC,OAAO;YACdC,QAAQD,KAAK,CAAC,0EAA0EA;QAC1F;QACA,IAAI;YACF,MAAM6B,KAAKQ,cAAc,CAAC;gBAAE1E,QAAQZ,SAASY,MAAM;gBAAElB;gBAAOuF;gBAAaM,SAAS;gBAAYC,MAAMR;YAAM;QAC5G,EAAE,OAAO/B,OAAO;YACdC,QAAQD,KAAK,CAAC,+EAA+EA;QAC/F;IACF;IAEA;;;;;GAKC,GACD,eAAewC,sBACbhG,SAAoB,EACpBC,KAAa,EACbM,QAA0B;QAE1B,MAAMiF,cAAc7I,sBAAsB4D,SAASiF,WAAW;QAC9D,IAAI,CAACA,YAAYpD,MAAM,IAAI,CAACnC,OAAO,OAAO,EAAE;QAC5C,IAAI;YACF,MAAMgG,QAAQ,MAAMlJ,kBAAkBiD,WAAW,YAAYC,OAAOuF;YACpE,OAAOS,MAAM1D,GAAG,CAAC,CAAC2D,YAAcA,UAAUC,IAAI,EAAE1D,MAAM,CAACE;QACzD,EAAE,OAAOa,OAAO;YACdC,QAAQD,KAAK,CAAC,yEAAyEA;YACvF,OAAO,EAAE;QACX;IACF;IAEA;;;;;GAKC,GACD,eAAe4C,aACbvF,MAA2B,EAC3BN,QAA0B,EAC1B8F,MAA+B;QAE/B,IAAIA,MAAM,CAAC,OAAO,KAAK,cAAcA,MAAM,CAAC,OAAO,KAAK,SAAS;YAC/D,MAAMV,OAAOU,MAAM,CAAC,OAAO,KAAK,UAAU,SAAS;YACnD,MAAMlE,MAAM6C,UAAUW,SAAS,SAASU,MAAM,CAAC,UAAU,GAAGA,MAAM,CAAC,aAAa;YAChF,IAAI,CAAClE,IAAIC,MAAM,EAAE,OAAOxD,gBAAgB,KAAK,mBAAmB;YAChE,OAAO;gBACLgF,QAAQzB,IAAImE,KAAK,CAAC,GAAGvI,2BAA2BwE,GAAG,CAAC,CAACC,KAAQ,CAAA;wBAAEmD;wBAAMnD;oBAAG,CAAA;gBACxE+D,OAAOpE,IAAIC,MAAM;gBACjBoE,WAAWrE,IAAIC,MAAM,GAAGrE;YAC1B;QACF;QACA,IAAIsI,MAAM,CAAC,OAAO,KAAK,QAAQ;YAC7B,OAAOzH,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAM6H,SAAS5H,uBAAuBwH,MAAM,CAAC,SAAS;QACtD,uEAAuE;QACvE,sEAAsE;QACtE,sEAAsE;QACtE,+CAA+C;QAC/C,MAAMK,QAAQD,SACV,MAAM3J,mBAAmB;YACvBmD,OAAOY,OAAOZ,KAAK;YACnBkB,QAAQZ,SAASY,MAAM;YACvBsF;YACAE,WAAW9F,OAAO+F,GAAG;YACrBC,OAAO9I;QACT,KACA;QACJ,IAAI,CAAC2I,SAAUA,MAAMI,EAAE,KAAK,SAASJ,MAAMK,MAAM,KAAK,aAAc;YAClE,OAAOnI,gBAAgB,KAAK,kBAAkB;QAChD;QACA,IAAI8H,MAAMI,EAAE,KAAK,SAASJ,MAAMK,MAAM,KAAK,cAAc;YACvD,OAAOnI,gBAAgB,KAAK,oBAAoB;QAClD;QACA,IAAI8H,MAAMI,EAAE,KAAK,OAAO;gBAMKJ;YAL3B,qEAAqE;YACrE,qDAAqD;YACrD,OAAO9H,gBACL,KACA,oBACA,CAAC,qBAAqB,EAAE,EAAC8H,qBAAAA,MAAMM,WAAW,YAAjBN,qBAAqB,EAAE,EAAEO,IAAI,CAAC,SAAS,YAAY,4DAA4D,CAAC;QAE7I;QACA,OAAO;YACLrD,QAAQ8C,MAAM9C,MAAM,CAACrB,GAAG,CAAC,CAAC2E,SAAY,CAAA;oBAAEvB,MAAMuB,OAAOvB,IAAI,KAAK,SAAS,SAAS;oBAAWnD,IAAI0E,OAAO1E,EAAE;gBAAC,CAAA;YACzG+D,OAAOG,MAAMH,KAAK;YAClBC,WAAWE,MAAMF,SAAS;QAC5B;IACF;IAEA,MAAMW,UAA+B,OAAOC;QAC1C,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAO3I,yBAAyB;QAC/D,MAAM4I,OAAO,MAAMxI,qBAAqBsI;QACxC,MAAMvG,SAAS,MAAMpC,kBAAkB2I,SAASE,IAAI,CAAC,QAAQ,EAAEjC,KAAKkC,IAAI,EAAE;YAACxI;SAAsB;QACjG,IAAI8B,kBAAkB2G,UAAU,OAAO3G;QACvC,MAAMb,YAAYqF,KAAKrF,SAAS;QAChC,MAAMO,WAAW,MAAMR,mBAAmBC,WAAWa,OAAOZ,KAAK,EAAEqH,IAAI,CAAC,aAAa;QACrF,IAAI/G,oBAAoBiH,UAAU,OAAOjH;QACzC,MAAM8F,SAASiB,IAAI,CAAC,SAAS,IAAI,OAAOA,IAAI,CAAC,SAAS,KAAK,WAAYA,IAAI,CAAC,SAAS,GAA+B,CAAC;QACrH,MAAMG,QAAQ,MAAMrB,aAAavF,QAAQN,UAAU8F;QACnD,IAAIoB,iBAAiBD,UAAU,OAAOC;QACtC,MAAM9D,UAAU,MAAM/C,cAAcZ,WAAWa,QAAQN;QACvD,MAAM,EAAEuC,UAAU,EAAEmB,YAAY,EAAEQ,OAAO,EAAE,GAAG,MAAMf,WAClD1D,WACAa,QACAN,UACAoD,SACA8D,MAAM7D,MAAM,EACZ;YACE8D,WAAWrC,KAAKqC,SAAS;YACzBC,gBAAgBtC,KAAKsC,cAAc;YACnCpC,OAAOF,KAAKuC,GAAG;YACfC,eAAe,MAAMvE,sBAAsBtD,WAAWa,OAAOZ,KAAK,EAAEM;QACtE;QAEF,MAAMqD,SAASd,WAAWP,GAAG,CAAC,CAACwB;gBAIhBU;mBAHbhH,sBACE;gBACEsG;gBACAU,OAAO,GAAEA,eAAAA,QAAQnE,GAAG,CAACyD,UAAUC,QAAQ,aAA9BS,eAAmCrF;gBAC5CkC,UAAUqC,QAAQrC,QAAQ;gBAC1BE,gBAAgBmC,QAAQnC,cAAc;YACxC,GACA;gBAAEM,UAAU6B,QAAQ7B,QAAQ;gBAAEgG,mBAAmB7D,aAAa8D,GAAG,CAAChE,UAAUC,QAAQ;YAAE;;QAG1F,OAAOrF,WAAW;YAChBmI,IAAI;YACJlD;YACA2C,OAAOkB,MAAMlB,KAAK;YAClBC,WAAWiB,MAAMjB,SAAS;QAC5B;IACF;IAEA,MAAMwB,UAA+B,OAAOZ;QAC1C,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAO3I,yBAAyB;QAC/D,MAAM4I,OAAO,MAAMxI,qBAAqBsI;QACxC,MAAMvG,SAAS,MAAMpC,kBAAkB2I,SAASE,IAAI,CAAC,QAAQ,EAAEjC,KAAKkC,IAAI,EAAE;YAACxI;SAAsB;QACjG,IAAI8B,kBAAkB2G,UAAU,OAAO3G;QACvC,MAAMb,YAAYqF,KAAKrF,SAAS;QAChC,MAAMO,WAAW,MAAMR,mBAAmBC,WAAWa,OAAOZ,KAAK,EAAEqH,IAAI,CAAC,aAAa;QACrF,IAAI/G,oBAAoBiH,UAAU,OAAOjH;QAEzC,MAAMgF,QAAQF,KAAKuC,GAAG;QACtB,MAAMK,QAAQ7K,2BAA2BkK,IAAI,CAAC,iBAAiB,EAAE/G,SAAS2H,KAAK;QAC/E,IAAID,MAAMhJ,OAAO,KAAK,MAAM,OAAOL,gBAAgB,KAAK,sBAAsBqJ,MAAMhJ,OAAO;QAC3F,MAAMkJ,iBAAiBF,MAAME,cAAc;QAC3C,wEAAwE;QACxE,qEAAqE;QACrE,MAAMC,YAAY,IAAI/E;QAItB,KAAK,MAAMyB,SAASI,MAAMC,OAAO,CAACmC,IAAI,CAAC,SAAS,IAAIA,IAAI,CAAC,SAAS,GAAG,EAAE,CAAE;YACvE,MAAMJ,SAAUpC,SAAS,OAAOA,UAAU,WAAWA,QAAQ,CAAC;YAC9D,MAAMJ,YAAY7F,uBAAuBqI,MAAM,CAAC,YAAY;YAC5D,MAAMvC,SAAS9F,uBAAuBqI,MAAM,CAAC,SAAS;YACtD,MAAMmB,MAAgC3D,YAClC;gBAAEiB,MAAM;gBAAWnD,IAAIkC;YAAU,IACjCC,SACE;gBAAEgB,MAAM;gBAAQnD,IAAImC;YAAO,IAC3B;YACN,IAAI,CAAC0D,OAAOD,UAAUL,GAAG,CAACM,IAAI7F,EAAE,GAAG;YACnC,qEAAqE;YACrE,kEAAkE;YAClE,sEAAsE;YACtE,MAAM8F,YAAYzK,iCAChBqJ,MAAM,CAAC,gBAAgB,EACvB3G,SAAS2H,KAAK,EACd;gBAAEtB,KAAK/F,OAAO+F,GAAG;gBAAErB;YAAM,GACzB4C;YAEF,IAAIvK,6BAA6B0K,YAAY;oBAIzCA;gBAHF,OAAO1J,gBACL,KACA,qBACA0J,qBAAAA,UAAUrJ,OAAO,YAAjBqJ,qBAAqB,4DACrB;oBAAEC,QAAQD,UAAUC,MAAM;gBAAC;YAE/B;YACAH,UAAUI,GAAG,CAACH,IAAI7F,EAAE,EAAE;gBACpB6F;gBACAI,cAAc,OAAOvB,MAAM,CAAC,eAAe,KAAK,WAAWA,MAAM,CAAC,eAAe,GAAG;gBACpFwB,cAAcxD,MAAMC,OAAO,CAAC+B,MAAM,CAAC,eAAe,IAAIA,MAAM,CAAC,eAAe,GAAG,EAAE;gBACjFoB;YACF;QACF;QACA,IAAI,CAACF,UAAUO,IAAI,EAAE,OAAO/J,gBAAgB,KAAK,mBAAmB;QACpE,IAAIwJ,UAAUO,IAAI,GAAG5K,2BAA2B;YAC9C,OAAOa,gBACL,KACA,mBACA,CAAC,eAAe,EAAEb,0BAA0B,kBAAkB,CAAC;QAEnE;QAEA,MAAM6K,kBAAkBrI,SAASgD,SAAS,GACtC,MAAMlF,sBAAsB2B,WAAWa,OAAOZ,KAAK,EAAE,aAAaI,GAAG,CAACE,SAASgD,SAAS,EAAEjD,GAAG,KAC7F;QACJ,MAAMoC,UAAUpE,0BACdiC,SAASgD,SAAS,EAClBqF,CAAAA,mCAAAA,gBAAiBpI,MAAM,IAAGoI,gBAAgBnI,IAAI,KAAKC;QAErD,IAAI,CAACgC,WAAWA,QAAQ/B,MAAM,KAAK,gBAAgB;YACjD,OAAO/B,gBACL,KACA,uBACA;QAEJ;QACA,IAAIzB,qBAAqB;YAAEoD;YAAUmC;YAASmG,cAActD;YAAOuD,QAAQzD,KAAKyD,MAAM;YAAEX;QAAe,OAAO,MAAM;YAClH,OAAOvJ,gBACL,KACA,uBACA;QAEJ;QAEA,MAAMmK,cAAc,MAAM/G,uBAAuBhC,WAAWa,OAAOZ,KAAK,EAAEM,UAAUmC,SAAS,CAACqB,YAC5F5G,qBAAqB;gBAAEoD;gBAAUmC,SAASqB;gBAAW8E,cAActD;gBAAOuD,QAAQzD,KAAKyD,MAAM;gBAAEX;YAAe,OAC9G;QAGF,MAAMxE,UAAU,MAAM/C,cAAcZ,WAAWa,QAAQN;QACvD,MAAM,EAAEuC,UAAU,EAAEmB,YAAY,EAAEQ,OAAO,EAAE,GAAG,MAAMf,WAClD1D,WACAa,QACAN,UACAoD,SACA;eAAIyE,UAAUnD,MAAM;SAAG,CAAC1C,GAAG,CAAC,CAACuC,QAAUA,MAAMuD,GAAG,GAChD;YACEX,WAAWrC,KAAKqC,SAAS;YACzBC,gBAAgBtC,KAAKsC,cAAc;YACnCpC;YACAsC,eAAe5J,6BAA6ByE;QAC9C;QAEF,MAAMG,cAAcxE,sBAAsB2B,WAAWa,OAAOZ,KAAK,EAAE;QACnE,MAAM+I,gBAAgB,MAAMhD,sBAAsBhG,WAAWa,OAAOZ,KAAK,EAAEM;QAC3E,sEAAsE;QACtE,yDAAyD;QACzD,IAAI0I,iBAAgD;QACpD,MAAMC,aAAa;YACjBD,yBAAAA,iBAAAA,iBAAmBjJ,UAChBkB,UAAU,CAAC,QACXb,GAAG,CAACQ,OAAOZ,KAAK,EAChBiB,UAAU,CAAC,WACXb,GAAG,CAACQ,OAAO+F,GAAG,EACdtG,GAAG,GACH6I,IAAI,CACH,CAACC;oBAEYA,aAAkDA;uBAD7D,AAACA,CAAAA,OAAO5I,MAAM,GACVuB,QAAOqH,cAAAA,OAAO9I,GAAG,CAAC,0BAAX8I,cAA6B,IAAIC,IAAI,MAAMtH,QAAOqH,eAAAA,OAAO9I,GAAG,CAAC,oBAAX8I,eAAuB,IAAIC,IAAI,KACxF,EAAC,KAAMxI,OAAO2D,KAAK;eAE1B8E,KAAK,CAAC,IAAMzI,OAAO2D,KAAK;YAC3B,OAAOyE;QACT;QACA,MAAMM,UAAmC,MAAMvI,QAAQC,GAAG,CACxD6B,WAAWP,GAAG,CAAC,OAAOwB;gBAOHqE,gBASJ3D;YAfb,MAAMgD,QAAQ;gBACZzD,UAAUD,UAAUC,QAAQ;gBAC5ByB,QAAQ1B,UAAU0B,MAAM;gBACxBf,WAAWX,UAAUW,SAAS;gBAC9BC,QAAQZ,UAAUY,MAAM;YAC1B;YACA,MAAM6E,YAAWpB,iBAAAA,UAAU9H,GAAG,CAACyD,UAAUC,QAAQ,aAAhCoE,iBAAqC;gBACpDK,cAAc;gBACdC,cAAc,EAAE;gBAChBJ,WAAW;oBAAEA,WAAW,CAAC;oBAAGC,QAAQ,EAAE;oBAAEtJ,SAAS;gBAAK;YACxD;YACA,MAAMwK,gBAAuCD,SAASlB,SAAS,CAACA,SAAS;YACzE,MAAMoB,WAAWlM,yBACf;gBACEuG;gBACAU,OAAO,GAAEA,eAAAA,QAAQnE,GAAG,CAACyD,UAAUC,QAAQ,aAA9BS,eAAmCrF;gBAC5CkC,UAAUqC,QAAQrC,QAAQ;gBAC1BE,gBAAgBmC,QAAQnC,cAAc;YACxC,GACA;gBAAEM,UAAU6B,QAAQ7B,QAAQ;gBAAEgG,mBAAmB7D,aAAa8D,GAAG,CAAChE,UAAUC,QAAQ;YAAE,GACtF;gBAAEyE,cAAce,SAASf,YAAY;gBAAEC,cAAcc,SAASd,YAAY;gBAAE9B,KAAK/F,OAAO+F,GAAG;gBAAErB;YAAM;YAErG,IAAI,CAACmE,SAASC,OAAO,IAAI,CAACD,SAASlF,KAAK,EAAE;gBACxC,OAAO,aAAKiD;oBAAOjD,OAAOT,UAAUS,KAAK;oBAAEsB,SAAS;oBAAW8D,QAAQF,SAASE,MAAM;;YACxF;YACA,MAAMpH,KAAKrE,qBAAqBoC,SAASiC,EAAE,EAAEuB,UAAUC,QAAQ;YAC/D,mEAAmE;YACnE,MAAM6F,WAAWd;YACjB,MAAMe,aAAa5M,wBAAwB;gBACzCsF;gBACAjC;gBACAmC,SAASmH;gBACTtG,WAAWsG,SAASrH,EAAE;gBACtBiD,QAAQ1B,UAAU0B,MAAM;gBACxBf,WAAWX,UAAUW,SAAS;gBAC9BC,QAAQZ,UAAUY,MAAM;gBACxBoF,aAAahG,UAAUoC,IAAI;gBAC3B3B,OAAOkF,SAASlF,KAAK;gBACrBwF,MAAMN,SAASM,IAAI;gBACnBvB,cAAce,SAASf,YAAY;gBACnCC,cAAcgB,SAAShB,YAAY;gBACnCuB,eAAepJ,OAAO+F,GAAG;gBACzBrB;gBACAuD,QAAQzD,KAAKyD,MAAM;gBACnBX;YACF;YACA,0DAA0D;YAC1D,mEAAmE;YACnE,6DAA6D;YAC7D,IAAIuB,SAASQ,IAAI,EAAE;gBACjBJ,WAAWK,WAAW,GAAG;oBACvBtK,SAAS6J,SAASQ,IAAI,CAACrK,OAAO;oBAC9BuK,UAAU;oBACVC,eAAexJ,OAAO+F,GAAG;oBACzB0D,cAAc/E;gBAChB;YACF;YACA,4DAA4D;YAC5D,0DAA0D;YAC1D,IAAIgF,OAAOC,IAAI,CAACf,eAAerH,MAAM,EAAE0H,WAAWL,aAAa,GAAGA;YAClE,gEAAgE;YAChE,6DAA6D;YAC7DK,WAAWW,YAAY,GAAGpN,+BAA+ByM;YACzD,uEAAuE;YACvE,gEAAgE;YAChEA,WAAWY,OAAO,GAAG;YACrB,IAAI;gBACF,MAAM7H,YAAYxC,GAAG,CAACmC,IAAImI,MAAM,CAACb;YACnC,EAAE,OAAOtG,OAAO;gBACd,IAAI,CAACA,yBAAD,AAACA,MAA8BoH,IAAI,MAAK9K,gBAAgB,MAAM0D;gBAClE,OAAO,aACFiE;oBACHjD,OAAOT,UAAUS,KAAK;oBACtBsB,SAAS;oBACT8D,QAAQ;wBACN;4BACEgB,MAAM;4BACN7D,QAAQ;wBACV;qBACD;;YAEL;YACA,MAAMzB,sBAAsBzE,OAAOZ,KAAK,EAAEM,UAAUwD,WAAWwB;YAC/D,mEAAmE;YACnE,gEAAgE;YAChE,2CAA2C;YAC3C,MAAM7I,kBAAkB;gBACtBuD,OAAOY,OAAOZ,KAAK;gBACnBkB,QAAQZ,SAASY,MAAM;gBACvBqD,OAAOkF,SAASlF,KAAK;gBACrBqG,QAAQ;YACV;YACA,oEAAoE;YACpE,gEAAgE;YAChE,MAAM/F,QAAQ7H,sBAAsB;gBAClC6N,cAActI;gBACduI,cAAcxK,SAAS4F,IAAI;gBAC3B6C;gBACAb;YACF;YACA,MAAMjK,iBAAiBmH,MAAM;gBAC3BpF,OAAOY,OAAOZ,KAAK;gBACnBkB,QAAQZ,SAASY,MAAM;gBACvB6J,MAAM5M,uBAAuB0L;gBAC7BmB,WAAWnG,MAAMmG,SAAS;gBAC1B3D,MAAMxC,MAAMwC,IAAI;gBAChBvB,MAAMR;YACR;YACA,gEAAgE;YAChE,4DAA4D;YAC5D,KAAK,MAAM,CAACvG,KAAKkM,SAAS,IAAIX,OAAOY,OAAO,CAAC1B,eAAgB;gBAC3D,MAAM2B,UAAUpO,qBAAqB;oBACnC8N,cAActI;oBACd6I,WAAWnI,OAAOlE;oBAClBqH,QAAQ6E,SAAS7E,MAAM;oBACvBiF,QAAQJ,SAASI,MAAM,KAAK;oBAC5BpC,YAAY,MAAMA;oBAClBnD,MAAMR;gBACR;gBACA,MAAMrH,iBAAiBmH,MAAM;oBAC3BpF,OAAOY,OAAOZ,KAAK;oBACnBkB,QAAQZ,SAASY,MAAM;oBACvB6J,MAAM5M,uBAAuB0L;oBAC7BmB,WAAWG,QAAQH,SAAS;oBAC5B3D,MAAM8D,QAAQ9D,IAAI;oBAClBvB,MAAMR;gBACR;YACF;YACA,OAAO,aAAKkC;gBAAOjD,OAAOkF,SAASlF,KAAK;gBAAEsB,SAAS;gBAAYgF,cAActI;;QAC/E;QAEF,MAAM+I,WAAWhC,QAAQ9G,MAAM,CAAC,CAAC+I,SAAWA,OAAO1F,OAAO,KAAK,YAAY1D,MAAM;QACjF,IAAImJ,UAAU;YACZ,MAAMlG,KAAKoG,cAAc,CACvB5K,OAAOZ,KAAK,EACZ;gBAAE2G,KAAK/F,OAAO+F,GAAG;gBAAEpC,OAAO3D,OAAO2D,KAAK;YAAC,GACvCtF,yBAAyBqM,WACzB;gBAAEG,MAAMlN;gBAAmCgE,IAAIjC,SAASiC,EAAE;gBAAE2D,MAAM5F,SAAS4F,IAAI;YAAC;QAEpF;QACA,OAAOxH,WAAW;YAAEmI,IAAI;YAAMyC;YAASgC;QAAS;IAClD;IAEA,OAAO;QAAEpE;QAASa;IAAQ;AAC5B"}
@@ -122,6 +122,7 @@ const issue = (path, code, message)=>({
122
122
  */ async function draftPlacementIssues(firestore, caller, // A draft, or a stored sequence being activated, which may predate the
123
123
  // campaign field (AGL-3254).
124
124
  draft, options = {}) {
125
+ var _draft_mailboxIds;
125
126
  const issues = [];
126
127
  const campaignIds = normalizeContainerIds(draft.campaignIds);
127
128
  if (draft.hostId) {
@@ -141,6 +142,13 @@ draft, options = {}) {
141
142
  issues.push(...(activation == null ? void 0 : activation.code) === 'mailbox_unknown' ? placement.filter((entry)=>entry.code !== 'mailbox_unknown') : placement);
142
143
  }
143
144
  if (activation && !issues.some((entry)=>entry.code === 'mailbox_not_yours')) issues.push(activation);
145
+ // The mailboxes it rotates through (AGL-3489) stand where its own does:
146
+ // connected here, and the caller's to send from. One that is paused or
147
+ // waiting to be reconnected is passed over at enroll time, not refused.
148
+ for (const rotationId of (_draft_mailboxIds = draft.mailboxIds) != null ? _draft_mailboxIds : []){
149
+ const rotation = await loadMailbox(firestore, caller.orgId, rotationId);
150
+ issues.push(...mailboxIssues(caller, rotation, 'mailboxIds'));
151
+ }
144
152
  const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId);
145
153
  const outside = draft.settings.allowedCountries.filter((code)=>!settings.allowedCountries.includes(code));
146
154
  if (outside.length) {
@@ -172,15 +180,15 @@ draft, options = {}) {
172
180
  const snapshot = readOutreachDocumentId(mailboxId) ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get() : null;
173
181
  return readStoredOutreachMailbox(mailboxId, (snapshot == null ? void 0 : snapshot.exists) ? snapshot.data() : undefined);
174
182
  }
175
- /** Whether the mailbox is connected here, and the caller may send from it. */ function mailboxIssues(caller, mailbox) {
183
+ /** Whether the mailbox is connected here, and the caller may send from it. */ function mailboxIssues(caller, mailbox, path = 'mailboxId') {
176
184
  if (!mailbox || mailbox.status === 'disconnected') {
177
185
  return [
178
- issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')
186
+ issue(path, 'mailbox_unknown', path === 'mailboxIds' ? 'One of the mailboxes it rotates through is not connected to this organization. Remove it.' : 'That mailbox is not connected to this organization.')
179
187
  ];
180
188
  }
181
189
  if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {
182
190
  return [
183
- issue('mailboxId', 'mailbox_not_yours', 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.')
191
+ issue(path, 'mailbox_not_yours', 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.')
184
192
  ];
185
193
  }
186
194
  return [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/sequence-routes.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 { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport type { OutreachRouteRefusalReason } from '../model/outreach-api'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n // A draft, or a stored sequence being activated, which may predate the\n // campaign field (AGL-3254).\n draft: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'settings'> & Pick<OutreachSequence, 'campaignIds'>,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n const campaignIds = normalizeContainerIds(draft.campaignIds)\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n /*\n * The campaigns must be the ORGANIZATION's live containers of the\n * `campaign` kind (AGL-3254): a request can claim any id,\n * and a sequence filed under another org's campaign, or under one the\n * console soft-deleted, would credit its outcomes to a page nobody here\n * can open. A sequence is an org record, so any live campaign of the org\n * qualifies, whichever sites it is placed on.\n */\n if (campaignIds.length) {\n const found = await readOrgContainers(\n firestore,\n 'campaign',\n caller.orgId,\n campaignIds.map((campaignId) => readOutreachDocumentId(campaignId) ?? ''),\n )\n const unknown = campaignIds.filter(\n (campaignId, index) => !readOutreachDocumentId(campaignId) || !found[index]?.live,\n )\n if (unknown.length) {\n issues.push(\n issue(\n 'campaignIds',\n 'campaign_unknown',\n unknown.length === 1\n ? \"One of the campaigns picked isn't one of this organization's campaigns any more. Pick it again.\"\n : \"Some of the campaigns picked aren't this organization's campaigns any more. Pick them again.\",\n ),\n )\n }\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(caller: OutreachRouteCaller, mailbox: OutreachMailbox | null): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n 'mailboxId',\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\n/** What saving a sequence came to: the stored sequence, or the refusal the route answers with. */\nexport type OutreachSequenceSaveOutcome =\n | { ok: true; sequence: OutreachSequence; created: boolean; warnings: OutreachSequenceIssue[] }\n | { ok: false; status: number; code: OutreachRouteRefusalReason; message: string; issues?: OutreachSequenceIssue[] }\n\n/**\n * Saves a sequence the way the editor's Save does (see \"Saving\" above):\n * the editor's validator, the organization's placement rules and — once\n * anyone is enrolled — the fixed steps, site and mailbox. A new sequence is\n * a draft; an edit keeps its status. The save route and the package import\n * (AGL-3535) both write through here. `newId` names a new sequence's id\n * (an import keeps the package's); `judgeOnly` answers the verdict without\n * writing, for an import's dry run.\n */\nexport async function saveOutreachSequence(\n deps: Pick<OutreachRouteDeps, 'firestore' | 'now' | 'logOrgActivity'>,\n caller: OutreachRouteCaller,\n input: { sequenceId: string | null; draft: OutreachSequenceDraft; newId?: string; judgeOnly?: boolean },\n): Promise<OutreachSequenceSaveOutcome> {\n const firestore = deps.firestore()\n const { sequenceId, draft } = input\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing && !input.newId) {\n return { ok: false, status: 404, code: 'sequence-not-found', message: 'That sequence no longer exists.' }\n }\n if (existing?.status === 'archived') {\n return { ok: false, status: 409, code: 'sequence-archived', message: \"An archived sequence can't be edited.\" }\n }\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return { ok: false, status: 403, code: 'permission', message: refused.message }\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return { ok: false, status: 403, code: 'permission', message: ownership.message }\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return { ok: false, status: 400, code: 'invalid-sequence', message: errors[0].message, issues: judged }\n }\n\n const nowMs = deps.now()\n const ref = outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(\n existing?.id ?? input.newId ?? createResourceUid(),\n )\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n /*\n * Carried through explicitly, because the write below REPLACES the\n * document (AGL-3239). The counters are the runtime's and the click\n * route's, never the editor's, and a member renaming a sequence must\n * not reset what it measured.\n */\n ...(existing?.stats ? { stats: existing.stats } : {}),\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n const warnings = judged.filter((entry) => entry.severity === 'warning')\n if (input.judgeOnly) return { ok: true, sequence, created: !existing, warnings }\n /*\n * The list's search fields ride with every write of the name (AGL-3321):\n * the sequences list asks Firestore for them, and a sequence missing\n * them would list normally and never be found.\n */\n await ref.set({ ...sequence, ...nameSearchFields(sequence.name) })\n if (!existing) {\n await deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, OUTREACH_SEQUENCE_ACTIVITY.create, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n }\n return { ok: true, sequence, created: !existing, warnings }\n}\n\n/**\n * Deletes a draft nobody was ever enrolled in — the only sequence that may\n * be deleted: what people were sent, and why they stopped, is the record of\n * what the organization did. The delete route and a package import's undo\n * (AGL-3535) both delete through here.\n */\nexport async function removeOutreachSequence(\n deps: Pick<OutreachRouteDeps, 'firestore' | 'logOrgActivity'>,\n caller: Pick<OutreachRouteCaller, 'orgId' | 'uid' | 'email'>,\n sequenceId: string,\n): Promise<{ ok: true } | { ok: false; status: number; code: OutreachRouteRefusalReason; message: string }> {\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return { ok: false, status: 404, code: 'sequence-not-found', message: 'That sequence no longer exists.' }\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n return {\n ok: false,\n status: 409,\n code: 'delete-refused',\n message: 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n }\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, OUTREACH_SEQUENCE_ACTIVITY.delete, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n return { ok: true }\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const outcome = await saveOutreachSequence(deps, caller, {\n sequenceId,\n draft: readOutreachSequenceDraft(body['sequence']),\n })\n if (outcome.ok === false) {\n return outreachRefusal(outcome.status, outcome.code, outcome.message, outcome.issues ? { issues: outcome.issues } : undefined)\n }\n return outreachOk({\n ok: true,\n sequence: outcome.sequence,\n created: outcome.created,\n warnings: outcome.warnings,\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const outcome = await removeOutreachSequence(deps, caller, sequenceId)\n if (outcome.ok === false) return outreachRefusal(outcome.status, outcome.code, outcome.message)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["normalizeContainerIds","createResourceUid","nameSearchFields","readOrgContainers","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","campaignIds","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","settings","outside","allowedCountries","includes","length","join","map","campaignId","unknown","index","live","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","saveOutreachSequence","deps","input","existing","newId","ok","stored","refused","find","judged","ownership","id","errors","nowMs","now","ref","sequence","stats","createdAtMs","updatedAtMs","warnings","judgeOnly","created","set","name","logOrgActivity","email","type","removeOutreachSequence","createOutreachSequenceRoutes","activity","action","save","request","method","body","gate","Response","rawId","outcome","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/sequence-routes.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 { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport type { OutreachRouteRefusalReason } from '../model/outreach-api'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n // A draft, or a stored sequence being activated, which may predate the\n // campaign field (AGL-3254).\n draft: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'mailboxIds' | 'settings'> &\n Pick<OutreachSequence, 'campaignIds'>,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n const campaignIds = normalizeContainerIds(draft.campaignIds)\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n // The mailboxes it rotates through (AGL-3489) stand where its own does:\n // connected here, and the caller's to send from. One that is paused or\n // waiting to be reconnected is passed over at enroll time, not refused.\n for (const rotationId of draft.mailboxIds ?? []) {\n const rotation = await loadMailbox(firestore, caller.orgId, rotationId)\n issues.push(...mailboxIssues(caller, rotation, 'mailboxIds'))\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n /*\n * The campaigns must be the ORGANIZATION's live containers of the\n * `campaign` kind (AGL-3254): a request can claim any id,\n * and a sequence filed under another org's campaign, or under one the\n * console soft-deleted, would credit its outcomes to a page nobody here\n * can open. A sequence is an org record, so any live campaign of the org\n * qualifies, whichever sites it is placed on.\n */\n if (campaignIds.length) {\n const found = await readOrgContainers(\n firestore,\n 'campaign',\n caller.orgId,\n campaignIds.map((campaignId) => readOutreachDocumentId(campaignId) ?? ''),\n )\n const unknown = campaignIds.filter(\n (campaignId, index) => !readOutreachDocumentId(campaignId) || !found[index]?.live,\n )\n if (unknown.length) {\n issues.push(\n issue(\n 'campaignIds',\n 'campaign_unknown',\n unknown.length === 1\n ? \"One of the campaigns picked isn't one of this organization's campaigns any more. Pick it again.\"\n : \"Some of the campaigns picked aren't this organization's campaigns any more. Pick them again.\",\n ),\n )\n }\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(\n caller: OutreachRouteCaller,\n mailbox: OutreachMailbox | null,\n path: 'mailboxId' | 'mailboxIds' = 'mailboxId',\n): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [\n issue(\n path,\n 'mailbox_unknown',\n path === 'mailboxIds'\n ? 'One of the mailboxes it rotates through is not connected to this organization. Remove it.'\n : 'That mailbox is not connected to this organization.',\n ),\n ]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n path,\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\n/** What saving a sequence came to: the stored sequence, or the refusal the route answers with. */\nexport type OutreachSequenceSaveOutcome =\n | { ok: true; sequence: OutreachSequence; created: boolean; warnings: OutreachSequenceIssue[] }\n | { ok: false; status: number; code: OutreachRouteRefusalReason; message: string; issues?: OutreachSequenceIssue[] }\n\n/**\n * Saves a sequence the way the editor's Save does (see \"Saving\" above):\n * the editor's validator, the organization's placement rules and — once\n * anyone is enrolled — the fixed steps, site and mailbox. A new sequence is\n * a draft; an edit keeps its status. The save route and the package import\n * (AGL-3535) both write through here. `newId` names a new sequence's id\n * (an import keeps the package's); `judgeOnly` answers the verdict without\n * writing, for an import's dry run.\n */\nexport async function saveOutreachSequence(\n deps: Pick<OutreachRouteDeps, 'firestore' | 'now' | 'logOrgActivity'>,\n caller: OutreachRouteCaller,\n input: { sequenceId: string | null; draft: OutreachSequenceDraft; newId?: string; judgeOnly?: boolean },\n): Promise<OutreachSequenceSaveOutcome> {\n const firestore = deps.firestore()\n const { sequenceId, draft } = input\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing && !input.newId) {\n return { ok: false, status: 404, code: 'sequence-not-found', message: 'That sequence no longer exists.' }\n }\n if (existing?.status === 'archived') {\n return { ok: false, status: 409, code: 'sequence-archived', message: \"An archived sequence can't be edited.\" }\n }\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return { ok: false, status: 403, code: 'permission', message: refused.message }\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return { ok: false, status: 403, code: 'permission', message: ownership.message }\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return { ok: false, status: 400, code: 'invalid-sequence', message: errors[0].message, issues: judged }\n }\n\n const nowMs = deps.now()\n const ref = outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(\n existing?.id ?? input.newId ?? createResourceUid(),\n )\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n /*\n * Carried through explicitly, because the write below REPLACES the\n * document (AGL-3239). The counters are the runtime's and the click\n * route's, never the editor's, and a member renaming a sequence must\n * not reset what it measured.\n */\n ...(existing?.stats ? { stats: existing.stats } : {}),\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n const warnings = judged.filter((entry) => entry.severity === 'warning')\n if (input.judgeOnly) return { ok: true, sequence, created: !existing, warnings }\n /*\n * The list's search fields ride with every write of the name (AGL-3321):\n * the sequences list asks Firestore for them, and a sequence missing\n * them would list normally and never be found.\n */\n await ref.set({ ...sequence, ...nameSearchFields(sequence.name) })\n if (!existing) {\n await deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, OUTREACH_SEQUENCE_ACTIVITY.create, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n }\n return { ok: true, sequence, created: !existing, warnings }\n}\n\n/**\n * Deletes a draft nobody was ever enrolled in — the only sequence that may\n * be deleted: what people were sent, and why they stopped, is the record of\n * what the organization did. The delete route and a package import's undo\n * (AGL-3535) both delete through here.\n */\nexport async function removeOutreachSequence(\n deps: Pick<OutreachRouteDeps, 'firestore' | 'logOrgActivity'>,\n caller: Pick<OutreachRouteCaller, 'orgId' | 'uid' | 'email'>,\n sequenceId: string,\n): Promise<{ ok: true } | { ok: false; status: number; code: OutreachRouteRefusalReason; message: string }> {\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return { ok: false, status: 404, code: 'sequence-not-found', message: 'That sequence no longer exists.' }\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n return {\n ok: false,\n status: 409,\n code: 'delete-refused',\n message: 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n }\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, OUTREACH_SEQUENCE_ACTIVITY.delete, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n return { ok: true }\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const outcome = await saveOutreachSequence(deps, caller, {\n sequenceId,\n draft: readOutreachSequenceDraft(body['sequence']),\n })\n if (outcome.ok === false) {\n return outreachRefusal(outcome.status, outcome.code, outcome.message, outcome.issues ? { issues: outcome.issues } : undefined)\n }\n return outreachOk({\n ok: true,\n sequence: outcome.sequence,\n created: outcome.created,\n warnings: outcome.warnings,\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const outcome = await removeOutreachSequence(deps, caller, sequenceId)\n if (outcome.ok === false) return outreachRefusal(outcome.status, outcome.code, outcome.message)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["normalizeContainerIds","createResourceUid","nameSearchFields","readOrgContainers","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","campaignIds","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","rotationId","mailboxIds","rotation","settings","outside","allowedCountries","includes","length","join","map","campaignId","unknown","index","live","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","saveOutreachSequence","deps","input","existing","newId","ok","stored","refused","find","judged","ownership","id","errors","nowMs","now","ref","sequence","stats","createdAtMs","updatedAtMs","warnings","judgeOnly","created","set","name","logOrgActivity","email","type","removeOutreachSequence","createOutreachSequenceRoutes","activity","action","save","request","method","body","gate","Response","rawId","outcome","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":"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"}
@@ -15,7 +15,8 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { GmailApiMessage, GmailApiMessagePart } from '../../engine/thread-message';
18
- import type { GmailClient, GmailMessageMetadata } from '../../transport/gmail-client';
18
+ import type { GmailClient } from '../../transport/gmail-client';
19
+ import { type OutreachMailSearch } from '../../transport/mail-client';
19
20
  /**
20
21
  * A MAILBOX IN MEMORY, for the runtime's specs (AGL-2981).
21
22
  *
@@ -42,6 +43,7 @@ export interface FakeGmailOptions {
42
43
  failNextSend?: Error | null;
43
44
  }
44
45
  export declare class FakeGmail implements GmailClient {
46
+ readonly provider: "google";
45
47
  readonly messages: Map<string, Stored>;
46
48
  readonly sent: Array<{
47
49
  raw: string;
@@ -94,7 +96,7 @@ export declare class FakeGmail implements GmailClient {
94
96
  getThread(threadId: string): Promise<{
95
97
  id: string;
96
98
  historyId: any;
97
- messages: GmailMessageMetadata[];
99
+ messages: import("../../transport/mail-client").OutreachMailMetadata[];
98
100
  }>;
99
101
  getFullThread(threadId: string): Promise<{
100
102
  id: string;
@@ -102,7 +104,7 @@ export declare class FakeGmail implements GmailClient {
102
104
  messages: GmailApiMessage[];
103
105
  }>;
104
106
  private metadataOf;
105
- getMessage(messageId: string): Promise<GmailMessageMetadata>;
107
+ getMessage(messageId: string): Promise<import("../../transport/mail-client").OutreachMailMetadata>;
106
108
  getFullMessage(messageId: string): Promise<GmailApiMessage>;
107
109
  listMessages(options: {
108
110
  q: string;
@@ -116,6 +118,19 @@ export declare class FakeGmail implements GmailClient {
116
118
  nextPageToken: any;
117
119
  resultSizeEstimate: number;
118
120
  }>;
121
+ searchMessages(search: OutreachMailSearch, page?: {
122
+ maxResults?: number;
123
+ }): Promise<{
124
+ messages: {
125
+ id: string;
126
+ threadId: string;
127
+ }[];
128
+ nextPageToken: any;
129
+ }>;
130
+ findMessageByMessageId(messageId: string): Promise<{
131
+ id: string;
132
+ threadId: string;
133
+ }>;
119
134
  revoke(): Promise<'revoked'>;
120
135
  }
121
136
  export {};
@@ -13,7 +13,8 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ const encode = (text)=>Buffer.from(text, 'utf8').toString('base64url');
16
+ */ import { bareMessageId, gmailSearchQuery } from "../../transport/mail-client.js";
17
+ const encode = (text)=>Buffer.from(text, 'utf8').toString('base64url');
17
18
  /** A plain header block and body out of a raw RFC 5322 message. */ function parseRaw(raw) {
18
19
  const text = Buffer.from(raw, 'base64url').toString('utf8');
19
20
  const split = text.indexOf('\r\n\r\n');
@@ -295,11 +296,30 @@ export class FakeGmail {
295
296
  resultSizeEstimate: found.length
296
297
  };
297
298
  }
299
+ async searchMessages(search, page) {
300
+ const list = await this.listMessages({
301
+ q: gmailSearchQuery(search),
302
+ maxResults: page == null ? void 0 : page.maxResults
303
+ });
304
+ return {
305
+ messages: list.messages,
306
+ nextPageToken: list.nextPageToken
307
+ };
308
+ }
309
+ async findMessageByMessageId(messageId) {
310
+ var _list_messages_;
311
+ const list = await this.listMessages({
312
+ q: `rfc822msgid:${bareMessageId(messageId)}`,
313
+ maxResults: 1
314
+ });
315
+ return (_list_messages_ = list.messages[0]) != null ? _list_messages_ : null;
316
+ }
298
317
  async revoke() {
299
318
  return 'revoked';
300
319
  }
301
320
  constructor(options){
302
321
  var _options_failNextSend;
322
+ this.provider = 'google';
303
323
  this.messages = new Map();
304
324
  this.sent = [];
305
325
  this.calls = [];