@aglyn/plugins-outreach 1.0.0-beta.160 → 1.0.0-beta.161

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 (72) hide show
  1. package/README.md +4 -0
  2. package/package.json +13 -13
  3. package/src/lib/components/compliance-section.d.ts +3 -2
  4. package/src/lib/components/compliance-section.js +7 -2
  5. package/src/lib/components/compliance-section.js.map +1 -1
  6. package/src/lib/components/do-not-contact-domains.d.ts +29 -0
  7. package/src/lib/components/do-not-contact-domains.js +208 -0
  8. package/src/lib/components/do-not-contact-domains.js.map +1 -0
  9. package/src/lib/components/use-outreach-api.d.ts +5 -1
  10. package/src/lib/components/use-outreach-api.js +12 -0
  11. package/src/lib/components/use-outreach-api.js.map +1 -1
  12. package/src/lib/components/use-outreach-data.d.ts +8 -1
  13. package/src/lib/components/use-outreach-data.js +32 -0
  14. package/src/lib/components/use-outreach-data.js.map +1 -1
  15. package/src/lib/constants/api-routes.d.ts +5 -0
  16. package/src/lib/constants/api-routes.js +4 -0
  17. package/src/lib/constants/api-routes.js.map +1 -1
  18. package/src/lib/engine/do-not-contact-domain.d.ts +39 -0
  19. package/src/lib/engine/do-not-contact-domain.js +51 -0
  20. package/src/lib/engine/do-not-contact-domain.js.map +1 -0
  21. package/src/lib/engine/gates.d.ts +6 -1
  22. package/src/lib/engine/gates.js +6 -0
  23. package/src/lib/engine/gates.js.map +1 -1
  24. package/src/lib/engine/gateway-block.d.ts +31 -0
  25. package/src/lib/engine/gateway-block.js +54 -0
  26. package/src/lib/engine/gateway-block.js.map +1 -0
  27. package/src/lib/engine/index.d.ts +3 -0
  28. package/src/lib/engine/index.js +3 -0
  29. package/src/lib/engine/index.js.map +1 -1
  30. package/src/lib/engine/mailbox-health.d.ts +5 -0
  31. package/src/lib/engine/mailbox-health.js +12 -5
  32. package/src/lib/engine/mailbox-health.js.map +1 -1
  33. package/src/lib/engine/mailbox-notice.d.ts +46 -0
  34. package/src/lib/engine/mailbox-notice.js +64 -0
  35. package/src/lib/engine/mailbox-notice.js.map +1 -0
  36. package/src/lib/enrollment/gate-lookups.js +7 -3
  37. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  38. package/src/lib/model/outreach-api.d.ts +23 -2
  39. package/src/lib/model/outreach-api.js.map +1 -1
  40. package/src/lib/model/outreach.types.d.ts +33 -1
  41. package/src/lib/model/outreach.types.js +11 -1
  42. package/src/lib/model/outreach.types.js.map +1 -1
  43. package/src/lib/routes/do-not-contact-routes.d.ts +35 -0
  44. package/src/lib/routes/do-not-contact-routes.js +92 -0
  45. package/src/lib/routes/do-not-contact-routes.js.map +1 -0
  46. package/src/lib/routes/enroll-routes.js +1 -0
  47. package/src/lib/routes/enroll-routes.js.map +1 -1
  48. package/src/lib/routes/register-routes.js +4 -0
  49. package/src/lib/routes/register-routes.js.map +1 -1
  50. package/src/lib/runtime/mailbox-health-store.d.ts +1 -1
  51. package/src/lib/runtime/mailbox-health-store.js +17 -4
  52. package/src/lib/runtime/mailbox-health-store.js.map +1 -1
  53. package/src/lib/runtime/mailbox-notices.d.ts +39 -0
  54. package/src/lib/runtime/mailbox-notices.js +117 -0
  55. package/src/lib/runtime/mailbox-notices.js.map +1 -0
  56. package/src/lib/runtime/person-erasure.d.ts +4 -0
  57. package/src/lib/runtime/person-erasure.js.map +1 -1
  58. package/src/lib/runtime/platform-runtime-deps.js +25 -0
  59. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  60. package/src/lib/runtime/runtime-deps.d.ts +21 -0
  61. package/src/lib/runtime/runtime-deps.js.map +1 -1
  62. package/src/lib/runtime/send-job.js +6 -6
  63. package/src/lib/runtime/send-job.js.map +1 -1
  64. package/src/lib/runtime/sync-job.d.ts +2 -0
  65. package/src/lib/runtime/sync-job.js +34 -7
  66. package/src/lib/runtime/sync-job.js.map +1 -1
  67. package/src/lib/server.d.ts +1 -1
  68. package/src/lib/server.js +1 -1
  69. package/src/lib/server.js.map +1 -1
  70. package/src/lib/storage/do-not-contact-store.d.ts +28 -1
  71. package/src/lib/storage/do-not-contact-store.js +117 -9
  72. package/src/lib/storage/do-not-contact-store.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE OUTREACH DOCUMENT MODEL (AGL-2974).\n *\n * One file both halves import — the console reads these documents, the server\n * routes write them — so a field is spelled once. Client-safe: types and\n * constants only, nothing that reaches Firestore.\n *\n * EVERY DOCUMENT HERE IS SERVER-WRITTEN. The Firestore rules let an\n * org-wide member holding `outreach.use` READ the org collections on a\n * workspace that carries `features.outreach`, and refuse every client write,\n * because these documents are the sending engine's state: a client that\n * could write a mailbox could lift its daily cap, and one that could write\n * an enrollment could send a step twice. The credential collection is closed\n * to clients entirely.\n *\n * Lean on purpose. Each type carries what the mailbox and sequence work is\n * known to need and nothing speculative; a field is added where the code\n * that reads it lands. Times are epoch milliseconds throughout, the unit\n * `nextDueAtMs` has to be compared in.\n */\n\n/**\n * Where Outreach keeps its records.\n *\n * Five collections under the organization, and one at the top level. The\n * org-scoped ones are erased with the org, whose erasure deletes the whole\n * `orgs/{orgId}` tree. The top-level one holds credentials, so it is keyed by\n * an `orgId` FIELD that a path-scoped delete of `orgs/{orgId}` cannot see:\n * the org erasure sweeps it by that field\n * (`libs/tenant/data/admin/src/lib/server/erase.ts` names it as a literal,\n * which the boundary forces, and `outreach.types.spec.ts` holds the two\n * spellings together, along with the rules').\n */\nexport const OUTREACH_COLLECTIONS = {\n /** `orgs/{orgId}/outreachMailboxes/{mailboxId}` — a connected mailbox. */\n mailboxes: 'outreachMailboxes',\n /** `orgs/{orgId}/outreachSequences/{sequenceId}` — the steps to send. */\n sequences: 'outreachSequences',\n /** `orgs/{orgId}/outreachEnrollments/{enrollmentId}` — one person in one sequence. */\n enrollments: 'outreachEnrollments',\n /**\n * `orgs/{orgId}/outreachSettings/{settingsId}` — the organization's own\n * Outreach settings, one document per concern. `compliance` holds who\n * every email says sent it and the countries Outreach may send to.\n */\n settings: 'outreachSettings',\n /**\n * `orgs/{orgId}/outreachDoNotContact/{key}` — the addresses Outreach never\n * emails for this organization, keyed by `outreachDoNotContactKey`.\n */\n doNotContact: 'outreachDoNotContact',\n /**\n * `outreachMailboxCredentials/{mailboxId}` — the provider grant behind a\n * mailbox. TOP-LEVEL and closed to every client, staff included, so the\n * token material never shares a readable path with the mailbox it serves.\n */\n mailboxCredentials: 'outreachMailboxCredentials',\n} as const\n\n/** The org-scoped collections, by their {@link OUTREACH_COLLECTIONS} key. */\nexport type OutreachOrgCollection = Exclude<\n keyof typeof OUTREACH_COLLECTIONS,\n 'mailboxCredentials'\n>\n\n/** `orgs/{orgId}/<collection>` for one of the org-scoped collections. */\nexport function outreachOrgCollectionPath(\n orgId: string,\n collection: OutreachOrgCollection,\n): string {\n return `orgs/${orgId}/${OUTREACH_COLLECTIONS[collection]}`\n}\n\n/** Stamped by the server on every write. */\nexport interface OutreachTimestamps {\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** The mail providers a mailbox can be connected through. */\nexport const OUTREACH_MAILBOX_PROVIDERS = ['google'] as const\nexport type OutreachMailboxProvider =\n (typeof OUTREACH_MAILBOX_PROVIDERS)[number]\n\n/**\n * Where a mailbox stands.\n *\n * - `connected` — the grant works and the mailbox may send.\n * - `paused` — a member stopped it; enrollments on it wait.\n * - `reconnect_required` — the provider refused the grant (revoked, expired,\n * password changed); nothing sends until the rep connects it again.\n * - `disconnected` — a member removed it; its credential is gone.\n */\nexport const OUTREACH_MAILBOX_STATUSES = [\n 'connected',\n 'paused',\n 'reconnect_required',\n 'disconnected',\n] as const\nexport type OutreachMailboxStatus = (typeof OUTREACH_MAILBOX_STATUSES)[number]\n\n/**\n * The hours a mailbox sends in, in its own {@link OutreachMailbox.timezone}.\n * A step that comes due outside the window waits for the next opening.\n */\nexport interface OutreachSendWindow {\n /** Days of the week it sends on, `0` Sunday through `6` Saturday. */\n days: number[]\n /** Minutes after local midnight the window opens, inclusive. */\n startMinute: number\n /** Minutes after local midnight the window closes, exclusive. */\n endMinute: number\n}\n\n/**\n * One mailbox-local day's sending, as the health panel sums it (AGL-2978).\n */\nexport interface OutreachMailboxDailyHealth {\n sent: number\n bounces: number\n replies: number\n}\n\n/**\n * The counters a mailbox's sending is judged by.\n *\n * `sentToday` is what `dailyCap` refuses against, and it belongs to one\n * mailbox-local day — `sentOnDay` — so a counter from yesterday is read as\n * zero rather than as today's spend.\n */\nexport interface OutreachMailboxHealth {\n sentToday: number\n /** The local day (`YYYY-MM-DD` in the mailbox's timezone) `sentToday` counts. */\n sentOnDay: string | null\n /** Hard bounces on mail this mailbox sent. */\n bounces: number\n /** Replies detected on Outreach threads. */\n replies: number\n lastSentAtMs: number | null\n /** The last provider error, kept until a later send succeeds. */\n lastErrorAtMs: number | null\n lastErrorCode: string | null\n /**\n * Per mailbox-local day (`YYYY-MM-DD`), the sends, bounces and replies\n * counted on it (AGL-2978). The Mailboxes panel shows the last seven days\n * summed; the sending runtime increments the day it counts on and may drop\n * days older than a week. Absent, or empty, until the first send.\n */\n daily?: Record<string, OutreachMailboxDailyHealth>\n /**\n * The mailbox's most recent sends, newest last, at most\n * `OUTREACH_BOUNCE_RATE_WINDOW_SENDS` of them, each marked when a hard\n * bounce came back for it (AGL-2981): the window the bounce-rate pause is\n * judged over. A send is named by a digest of its `Message-ID`, never by\n * its recipient.\n */\n recentSends?: OutreachRecentSend[]\n /** When a reply last called this mailbox's email spam (AGL-2981). */\n lastComplaintAtMs?: number | null\n}\n\n/** One send in {@link OutreachMailboxHealth.recentSends}. */\nexport interface OutreachRecentSend {\n /** A digest of the send's `Message-ID`: what a bounce is matched to. */\n id: string\n atMs: number\n bounced: boolean\n}\n\n/** Why a mailbox paused itself (AGL-2981): the engine's health decision, kept. */\nexport interface OutreachMailboxAutoPause {\n reason: 'bounces_today' | 'bounce_rate' | 'complaint'\n /** The sentence the mailbox's card shows. */\n message: string\n atMs: number\n /** For a complaint, when resuming stops being premature; `null` otherwise. */\n untilMs: number | null\n}\n\n/** Where a mailbox's reply and bounce sync has read to (AGL-2981). */\nexport interface OutreachMailboxSync {\n /** Everything received before this was read by an earlier run, epoch ms. */\n throughMs: number\n /** Gmail ids of messages outside any enrollment's thread already handled. */\n handledMessageIds: string[]\n}\n\n/**\n * An address a connected Google account may send as, which Gmail has\n * verified — its own address, or an alias whose ownership Gmail confirmed\n * (AGL-2978). Only these are offered as a mailbox's `sendAs`.\n */\nexport interface OutreachSendAsAddress {\n email: string\n /** The name Gmail holds for the address, `''` when it holds none. */\n displayName: string\n /** The account's own address. */\n isPrimary: boolean\n /** The address Gmail sends as by default. */\n isDefault: boolean\n}\n\n/**\n * A rep's own mailbox, connected to send Outreach mail\n * (`orgs/{orgId}/outreachMailboxes/{id}`).\n *\n * Its grant lives in {@link OutreachMailboxCredentials} under the same id.\n */\nexport interface OutreachMailbox extends OutreachTimestamps {\n id: string\n provider: OutreachMailboxProvider\n /** The account's own address. */\n email: string\n /** The address mail goes out as: the account's, or an alias it may send as. */\n sendAs: string\n /**\n * The verified addresses `sendAs` may be, as Gmail listed them at the last\n * connect (AGL-2978). A changed alias shows up here on the next reconnect.\n */\n sendAsOptions: OutreachSendAsAddress[]\n /** The From name recipients see. */\n displayName: string\n status: OutreachMailboxStatus\n /** The most mail it sends in one local day, ramp included. */\n dailyCap: number\n window: OutreachSendWindow\n /** IANA zone the window and the day boundary are read in. */\n timezone: string\n /**\n * When the warm-up ramp began, or `null` for a mailbox sending at its full\n * cap. The ramp is measured from here, not from connection, so a paused\n * mailbox can restart it.\n */\n rampStartedAtMs: number | null\n health: OutreachMailboxHealth\n /** The member who connected it, and whose mail it is. */\n connectedByUid: string\n /** When the grant behind it was last connected (AGL-2978). */\n connectedAtMs: number\n /**\n * Set when the mailbox paused ITSELF on its health (AGL-2981), and cleared\n * when a member pauses or resumes it; absent otherwise.\n */\n autoPause?: OutreachMailboxAutoPause | null\n /** The reply and bounce sync's place in the mailbox (AGL-2981). */\n sync?: OutreachMailboxSync | null\n}\n\n/**\n * The provider grant behind one mailbox\n * (`outreachMailboxCredentials/{mailboxId}`).\n *\n * Only the fields every reader relies on are declared here; the token\n * material is added by the code that stores it. `orgId` is REQUIRED, not\n * decorative: the org erasure finds these documents by it, and a credential\n * written without one would outlive its workspace.\n *\n * NAME EVERY SEALED FIELD WITH A SECRET WORD — `refreshToken`,\n * `tokenCiphertext`. The personal-data export discloses these documents with\n * secrets redacted, and its redaction reads field names first: a name\n * carrying `token`, `secret` or `credential` is withheld whatever its value,\n * while a neutral name such as `ciphertext` is judged by the value's shape\n * alone and can be disclosed.\n */\nexport interface OutreachMailboxCredentials extends OutreachTimestamps {\n /** The mailbox id, which is also this document's id. */\n id: string\n orgId: string\n mailboxId: string\n provider: OutreachMailboxProvider\n}\n\n/** A sequence's lifecycle. Only an `active` sequence advances its enrollments. */\nexport const OUTREACH_SEQUENCE_STATUSES = [\n 'draft',\n 'active',\n 'paused',\n 'archived',\n] as const\nexport type OutreachSequenceStatus =\n (typeof OUTREACH_SEQUENCE_STATUSES)[number]\n\n/*==========================================\n * SEQUENCES (AGL-2979).\n *\n * The limits are the outbound playbook's: a person gets at most four emails\n * from one sequence, spaced by business days, and the tasks between them are\n * the rep's own touches — a LinkedIn note, a call. The engine\n * (`../engine/sequence-validation.ts`) holds a stored sequence to them.\n *==========================================*/\n\n/** The most steps one sequence holds, emails and tasks together. */\nexport const OUTREACH_MAX_STEPS = 8\n\n/** The most of those steps that send an email. */\nexport const OUTREACH_MAX_EMAIL_STEPS = 4\n\n/** The longest wait one step may carry, in business days. */\nexport const OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS = 30\n\n/**\n * The shortest wait before an email that follows an earlier email. The first\n * email may wait `0` — \"the next opening of the window\", or the same day as\n * a task before it — and so may a task, but each later email waits at least\n * this long after the step before it.\n */\nexport const OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS = 1\n\n/**\n * One email step.\n *\n * Steps are a union on `kind`, so a later kind joins without reshaping the\n * ones already stored.\n */\nexport interface OutreachEmailStep {\n /** Stable within the sequence, so an edit that reorders steps is traceable. */\n id: string\n kind: 'email'\n /**\n * Business days to wait after the previous step, counted in the mailbox's\n * zone — after enrollment for the first step, where `0` means the next\n * opening of the sending window.\n */\n delayBusinessDays: number\n /**\n * The subject, merge fields allowed. Required on the first email and on\n * any email that starts a new thread; an in-thread email is sent as `Re:`\n * the thread's own subject, so its field is not read.\n */\n subject: string\n /**\n * Whether the email goes out as a reply in the thread the earlier emails\n * started. True by default for every email after the first; the first\n * email always starts the thread, whatever this says.\n */\n replyInThread: boolean\n /** Plain text with merge fields, as its author wrote it; `''` when `templateId` is set. */\n body: string\n /**\n * A CRM email template (`orgs/{orgId}/crmEmailTemplates/{id}`) whose body\n * is sent instead of `body`, or `null`. Only the body is taken from it: the\n * step's own subject is the one that is sent.\n */\n templateId: string | null\n}\n\n/** The rep's own touches a task step asks for. */\nexport const OUTREACH_TASK_KINDS = ['linkedin', 'call', 'todo'] as const\nexport type OutreachTaskKind = (typeof OUTREACH_TASK_KINDS)[number]\n\nexport const OUTREACH_TASK_KIND_LABELS: Record<OutreachTaskKind, string> = {\n linkedin: 'LinkedIn',\n call: 'Call',\n todo: 'To-do',\n}\n\n/** One task step: a CRM task for the rep, created when the step comes due. */\nexport interface OutreachTaskStep {\n id: string\n kind: 'task'\n taskKind: OutreachTaskKind\n /** What the task says, e.g. \"Connect on LinkedIn\". */\n title: string\n /** Business days after the previous step; `0` is the same day. */\n delayBusinessDays: number\n}\n\nexport type OutreachSequenceStep = OutreachEmailStep | OutreachTaskStep\n\n/** The countries a new sequence sends to: the United States alone. */\nexport const OUTREACH_DEFAULT_ALLOWED_COUNTRIES: readonly string[] = ['US']\n\n/** Behavior a sequence applies to every enrollment in it. */\nexport interface OutreachSequenceSettings {\n /**\n * The hours this sequence sends in, replacing its mailbox's own window;\n * `null` sends in the mailbox's. Read in the mailbox's zone either way.\n */\n window: OutreachSendWindow | null\n /**\n * ISO-3166-1 alpha-2 codes a recipient may be in, uppercase. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}: Canada, the United Kingdom\n * and most of the EU need a consent basis a cold email does not have.\n */\n allowedCountries: string[]\n /** Whether a contact who is already a customer may be enrolled. Default `false`. */\n allowCustomers: boolean\n /**\n * Whether the links in this sequence's emails are rewritten so clicks are\n * counted (AGL-3239). Default `false`, and `false` on every sequence\n * written before the setting existed: turning it on changes what the\n * recipient sees in the body, which is not a change to make on anyone's\n * behalf.\n *\n * It buys the one engagement number a plain-text sequence can honestly\n * report. It costs a visible link: the destination in the body becomes a\n * signed link on the console that forwards to it. There is no equivalent\n * setting for opens, because a pixel needs an HTML part — see\n * `../engine/click-tracking.ts`.\n */\n trackClicks: boolean\n}\n\n/*==========================================\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Counters on the sequence document, incremented by the sending runtime and\n * by the click route. They are NOT derived from the enrollments on read: the\n * console lists enrollments a page at a time, so a rollup taken over what is\n * loaded would report the first page's numbers as the sequence's.\n *\n * Every field is optional and every absence means \"not recorded\", never\n * zero. The distinction is the whole of `campaign-report.ts`'s honesty and\n * it is kept here for the same reason: a sequence that ran before this\n * existed has no counters, and reporting its click rate as 0% would publish\n * a fact about our schema as a fact about its recipients.\n *=========================================*/\n\n/**\n * The per-destination click rollup under one sequence:\n * `orgs/{orgId}/outreachSequences/{id}/reports/links`.\n *\n * One document per sequence, holding a bounded map — the campaign rollup's\n * own shape and its own cap, so \"a link\" means the same thing in a rep's\n * report and a marketer's. Here rather than beside the writer, because the\n * console's listener is a browser module and the writer is not.\n */\nexport const OUTREACH_LINK_ROLLUP_PATH = ['reports', 'links'] as const\n\n/** The counters one sequence is judged by. */\nexport interface OutreachSequenceStats {\n /** Email steps that left. One person getting four emails counts four. */\n sent?: number\n /**\n * Distinct enrollments that have had at least one email — the denominator\n * of every engagement rate, and the thing `sent` is not.\n */\n people?: number\n /**\n * At least one email of this sequence went out with its links rewritten.\n *\n * Absent is NOT false; it is \"never recorded\", which is what every\n * sequence sent before the setting existed reads as. Either way the report\n * withholds the click rate rather than showing 0% — see\n * {@link OutreachSequenceStats} above.\n */\n clickTracked?: boolean\n /** Click EVENTS judged a person's. One reader clicking twice counts two. */\n clicks?: number\n /** Enrollments whose FIRST human click was seen: the rate's numerator. */\n uniqueClicks?: number\n /**\n * Clicks a link scanner or a security gateway made, counted apart and\n * never in the rate (`../engine/click-tracking.ts`). Shown, not hidden:\n * a large number here is the reader's evidence that the small number\n * beside it is the real one.\n */\n machineClicks?: number\n /** When a person last followed a link. */\n lastClickAtMs?: number | null\n}\n\n/** What one enrollment did with the links it was sent (AGL-3239). */\nexport interface OutreachEnrollmentEngagement {\n /** Click events judged this person's, machines excluded. */\n clicks: number\n /** The first, which is what makes them one of the sequence's `uniqueClicks`. */\n firstClickAtMs: number | null\n lastClickAtMs: number | null\n /** The destination they followed last, as `campaignLinkKey` reduces it. */\n lastClickUrl: string | null\n /** Clicks on this person's links that were a machine's. */\n machineClicks: number\n}\n\n/** An ordered set of steps sent from one mailbox (`orgs/{orgId}/outreachSequences/{id}`). */\nexport interface OutreachSequence extends OutreachTimestamps {\n id: string\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /**\n * The mailbox every email step is sent from. A setting of the sequence,\n * kept beside `settings` rather than inside it because enrollments and a\n * mailbox's own screens select sequences by it.\n */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n status: OutreachSequenceStatus\n /**\n * What it measured (AGL-3239). Absent until the first email leaves, and\n * absent forever on a sequence that finished before the counters existed.\n */\n stats?: OutreachSequenceStats\n}\n\n/*==========================================\n * ENROLLMENTS (AGL-2979).\n *==========================================*/\n\n/**\n * Where one person's run through a sequence stands.\n *\n * `active` sends and `paused` waits. The rest end the sending, and each says\n * why: `finished` sent every step, `replied` heard back, `bounced` reached a\n * mailbox that does not exist, `opted_out` was asked to stop, `stopped` was\n * ended by a member or a gate, `failed` could not be sent. The transitions\n * between them are the engine's (`../engine/enrollment-state.ts`).\n */\nexport const OUTREACH_ENROLLMENT_STATUSES = [\n 'active',\n 'paused',\n 'finished',\n 'replied',\n 'bounced',\n 'opted_out',\n 'stopped',\n 'failed',\n] as const\nexport type OutreachEnrollmentStatus =\n (typeof OUTREACH_ENROLLMENT_STATUSES)[number]\n\n/**\n * Why an enrollment is not `active`, recorded beside the status. `finished`\n * needs none; each other status allows the reasons\n * `OUTREACH_STOP_REASONS_BY_STATUS` names.\n */\nexport const OUTREACH_STOP_REASONS = [\n /** A person wrote back in the thread. */\n 'reply',\n /** The recipient's server refused the address for good. */\n 'hard_bounce',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** The address is on the organization's do-not-contact list. */\n 'do_not_contact',\n /** A gate refused the next send — the person became a customer, say. */\n 'gate',\n /** A member paused or stopped it. */\n 'manual',\n /** The sequence was archived with the person still in it. */\n 'sequence_archived',\n /** The provider refused the send for good, or the email could not be composed. */\n 'send_failed',\n] as const\nexport type OutreachStopReason = (typeof OUTREACH_STOP_REASONS)[number]\n\n/** The statuses a stop reason is recorded with, and the reasons each allows. */\nexport const OUTREACH_STOP_REASONS_BY_STATUS: Readonly<\n Record<\n Exclude<OutreachEnrollmentStatus, 'active' | 'finished'>,\n readonly OutreachStopReason[]\n >\n> = {\n paused: ['manual'],\n replied: ['reply'],\n bounced: ['hard_bounce'],\n opted_out: ['opt_out_reply', 'unsubscribe', 'do_not_contact'],\n stopped: ['manual', 'gate', 'sequence_archived'],\n failed: ['send_failed'],\n}\n\n/**\n * What a rep confirms before a COLD contact is enrolled — one with no\n * inbound capture behind them — each recorded with who confirmed it and\n * when, because each is a fact only the rep can know:\n *\n * - `us_business_address`: the address is a business address in the US;\n * - `published_or_given`: they or their company published it, or they gave\n * it to us — never guessed from a name or bought on a list;\n * - `verified_deliverable`: a verifier said it accepts mail.\n */\nexport const OUTREACH_ATTESTATION_KINDS = [\n 'us_business_address',\n 'published_or_given',\n 'verified_deliverable',\n] as const\nexport type OutreachAttestationKind = (typeof OUTREACH_ATTESTATION_KINDS)[number]\n\n/** The sentence a rep ticks for each attestation. */\nexport const OUTREACH_ATTESTATION_LABELS: Record<OutreachAttestationKind, string> = {\n us_business_address: 'This is a US business address',\n published_or_given:\n 'They or their company published this address, or they gave it to us',\n verified_deliverable: 'This address was verified as deliverable',\n}\n\n/** One confirmation, as stored. */\nexport interface OutreachAttestation {\n /** The member who confirmed it. */\n uid: string\n atMs: number\n}\n\n/** The confirmations an enrollment carries, by kind. */\nexport type OutreachAttestations = Partial<\n Record<OutreachAttestationKind, OutreachAttestation>\n>\n\n/** The longest personal line an enrollment keeps: one sentence, not a letter. */\nexport const OUTREACH_PERSONAL_LINE_MAX = 300\n\n/**\n * Which CRM record an enrollment names (AGL-3234). A person is a LEAD until\n * somebody qualifies them and a CONTACT after, and a sequence works either:\n * an enrollment made on a lead follows the lead to the contact it becomes,\n * so the thread and the steps carry on as one enrollment.\n */\nexport const OUTREACH_ENROLLMENT_TARGETS = ['contact', 'lead'] as const\nexport type OutreachEnrollmentTarget = (typeof OUTREACH_ENROLLMENT_TARGETS)[number]\n\n/** One person in one sequence (`orgs/{orgId}/outreachEnrollments/{id}`). */\nexport interface OutreachEnrollment extends OutreachTimestamps {\n id: string\n sequenceId: string\n /**\n * The record the person is (AGL-3234): `contact` for a CRM contact,\n * `lead` for a lead the sequence's site holds. A stored enrollment with\n * no target is a contact's — every enrollment was, before leads could be\n * sequenced.\n */\n target: OutreachEnrollmentTarget\n /**\n * The CRM contact the person is — `''` while the enrollment targets a\n * lead that has not converted. Filled in, beside `target: 'contact'`, the\n * moment the lead becomes a contact.\n */\n contactId: string\n /**\n * `hosts/{hostId}/leads/{leadId}` — the person key — for an enrollment\n * made on a lead; kept once the lead converts, so the lead's page still\n * lists the sequence. `null` for an enrollment made on a contact.\n */\n leadId: string | null\n /**\n * The person's name as the sending site knew it at enrollment, `''` when\n * it had none — what the enrollments table shows beside the address,\n * without a read of every record on the page (AGL-2980).\n */\n contactName: string\n /** The address the steps go to, normalized, captured at enrollment. */\n email: string\n hostId: string\n mailboxId: string\n /** The index into `steps` of the NEXT step to run. */\n stepIndex: number\n /** When that step comes due, or `null` when nothing is waiting. */\n nextDueAtMs: number | null\n status: OutreachEnrollmentStatus\n /** Why the status is not `active`; `null` while active and once finished. */\n stopReason: OutreachStopReason | null\n /** Plain-language detail: the gate's reason, the bounce's diagnostic. */\n stopDetail: string | null\n stoppedAtMs: number | null\n /** The member who paused or stopped it; `null` when the engine did. */\n stoppedByUid: string | null\n /**\n * The rep's signal sentence — why this person, now — merged into the\n * steps as `{{enrollment.personalLine}}`. Required for a cold contact.\n */\n personalLine: string\n /** Whether the contact was cold (no inbound capture) when enrolled. */\n cold: boolean\n attestations: OutreachAttestations\n /** The member who enrolled the person. */\n enrolledByUid: string\n /** The provider thread the next in-thread email is sent into, once one exists. */\n gmailThreadId: string | null\n /** Every provider thread this enrollment has sent into, oldest first — what reply sync watches. */\n gmailThreadIds: string[]\n /** The subject the current thread was started with, as sent. */\n threadSubject: string | null\n /**\n * The `Message-ID`s of the emails sent into the current thread, oldest\n * first, angle brackets included: the next in-thread email answers the\n * last and names them all in `References`.\n */\n messageIds: string[]\n /** When the last step ran. */\n lastSentAtMs: number | null\n /**\n * A sending run's hold on the step it is running (AGL-2981): taken in a\n * transaction before the step runs, cleared when it is recorded. Absent\n * or `null` while no run holds one.\n */\n sendClaim?: OutreachSendClaim | null\n /** Every step the runtime ran, oldest first (AGL-2981). */\n stepRecords?: OutreachStepRecord[]\n /** Gmail ids of this enrollment's thread messages the sync has handled, the last hundred. */\n syncedMessageIds?: string[]\n /**\n * What this person did with the links they were sent (AGL-3239). Absent\n * until their first click, which is why the table reads an absence as\n * \"no clicks\" only for a sequence that tracks them at all.\n */\n engagement?: OutreachEnrollmentEngagement\n}\n\n/** A sending run's claim on one enrollment's step (AGL-2981). */\nexport interface OutreachSendClaim {\n /** The run that holds it. */\n token: string\n atMs: number\n stepIndex: number\n /**\n * The `Message-ID` an email step goes out with, minted before the send, so\n * a claim a run died holding is settled by looking for the message rather\n * than by sending it again. `null` for a task step.\n */\n messageId: string | null\n}\n\n/** One step the runtime ran for an enrollment (AGL-2981). */\nexport interface OutreachStepRecord {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task'\n atMs: number\n /** Gmail's id for the sent message. */\n gmailMessageId?: string\n /** Gmail's thread the message went into. */\n gmailThreadId?: string\n /** The `Message-ID` it went out with, angle brackets included. */\n messageId?: string\n /** The subject as sent. */\n subject?: string\n /** The record system's task for a task step; `null` when none could be filed. */\n taskId?: string | null\n /**\n * The destinations this email's links were rewritten to point at, in the\n * order a click token indexes them (AGL-3239). Absent on a step that\n * carried no links, and on every step sent before tracking existed.\n *\n * Kept so the console can say WHICH link a person followed without the\n * token having to carry the URL back to us, and so a rewritten body can be\n * read afterwards for what it actually offered them.\n */\n links?: string[]\n}\n\n/*==========================================\n * ORGANIZATION SETTINGS (AGL-2979).\n *==========================================*/\n\n/**\n * What every Outreach email's footer says about who sent it — the\n * identification and the postal address CAN-SPAM requires on commercial\n * mail. The composer refuses to write an email without a legal name and a\n * postal address, and a sequence cannot be activated without them.\n */\nexport interface OutreachOrgSettings {\n /** The organization's legal name, as the footer prints it: \"Example Co LLC\". */\n legalName: string\n /**\n * The name the solicitation sentence uses when it is not the legal name:\n * \"This is a sales email from Example Co.\" `''` uses the legal\n * name.\n */\n brandName: string\n /**\n * A valid physical postal address: a street address, a USPS PO box, or a\n * private mailbox registered with the USPS. Line breaks are printed as\n * commas.\n */\n postalAddress: string\n}\n\n/** The id of the compliance document in the `settings` collection. */\nexport const OUTREACH_COMPLIANCE_SETTINGS_ID = 'compliance'\n\n/**\n * The organization's Outreach compliance settings\n * (`orgs/{orgId}/outreachSettings/compliance`): the footer's sender identity,\n * and the countries Outreach may send to at all.\n *\n * `allowedCountries` is a CEILING over every sequence's own list — a\n * sequence sends only to the countries both name — so narrowing it here\n * narrows every sequence at once, without editing any of them. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}.\n */\nexport interface OutreachComplianceSettings extends OutreachOrgSettings {\n /** ISO-3166-1 alpha-2 codes, uppercase, in the order they were chosen. */\n allowedCountries: string[]\n}\n\n/** The compliance settings as stored, with who changed them last. */\nexport interface OutreachComplianceSettingsDocument\n extends OutreachComplianceSettings {\n /** `0` until the organization first saves them. */\n updatedAtMs: number\n updatedByUid: string | null\n}\n\n/*==========================================\n * DO NOT CONTACT (AGL-2980).\n *\n * The organization's own list of addresses Outreach never emails, whoever\n * enrolls them and from whichever site. A member adds one by hand; the\n * sending runtime adds one when a reply asks to be left alone, when the\n * unsubscribe link is used, and when an address bounces for good.\n *\n * AN ENTRY CARRIES NO ADDRESS. Its id is `outreachDoNotContactKey(email)` —\n * the same hash the platform's suppression lists key by — and that is all a\n * lookup needs, since every caller holds the address it is about to use.\n * So the list keeps working after a person is erased from the workspace,\n * which is exactly when a promise not to email them must still hold, while\n * holding nothing that identifies them.\n *==========================================*/\n\n/** Why an address is on the list. */\nexport const OUTREACH_DO_NOT_CONTACT_REASONS = [\n /** A member put it there. */\n 'manual',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** Mail to it bounced for good. */\n 'hard_bounce',\n] as const\nexport type OutreachDoNotContactReason =\n (typeof OUTREACH_DO_NOT_CONTACT_REASONS)[number]\n\n/** What put an address on the list: a member, or the sending runtime. */\nexport const OUTREACH_DO_NOT_CONTACT_SOURCES = ['member', 'runtime'] as const\nexport type OutreachDoNotContactSource =\n (typeof OUTREACH_DO_NOT_CONTACT_SOURCES)[number]\n\n/** One address on the list (`orgs/{orgId}/outreachDoNotContact/{key}`). */\nexport interface OutreachDoNotContactEntry {\n /** `outreachDoNotContactKey` of the address, which is also the document id. */\n key: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member who added it; `null` when the runtime did. */\n addedByUid: string | null\n addedAtMs: number\n /** The enrollment that led here, when one did. */\n enrollmentId: string | null\n /** That enrollment's sequence. */\n sequenceId: string | null\n /** Plain-language detail: why the member added it, the bounce's diagnostic. */\n detail: string | null\n}\n"],"names":["OUTREACH_COLLECTIONS","mailboxes","sequences","enrollments","settings","doNotContact","mailboxCredentials","outreachOrgCollectionPath","orgId","collection","OUTREACH_MAILBOX_PROVIDERS","OUTREACH_MAILBOX_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_MAX_STEPS","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS","OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS","OUTREACH_TASK_KINDS","OUTREACH_TASK_KIND_LABELS","linkedin","call","todo","OUTREACH_DEFAULT_ALLOWED_COUNTRIES","OUTREACH_LINK_ROLLUP_PATH","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_STOP_REASONS","OUTREACH_STOP_REASONS_BY_STATUS","paused","replied","bounced","opted_out","stopped","failed","OUTREACH_ATTESTATION_KINDS","OUTREACH_ATTESTATION_LABELS","us_business_address","published_or_given","verified_deliverable","OUTREACH_PERSONAL_LINE_MAX","OUTREACH_ENROLLMENT_TARGETS","OUTREACH_COMPLIANCE_SETTINGS_ID","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE OUTREACH DOCUMENT MODEL (AGL-2974).\n *\n * One file both halves import — the console reads these documents, the server\n * routes write them — so a field is spelled once. Client-safe: types and\n * constants only, nothing that reaches Firestore.\n *\n * EVERY DOCUMENT HERE IS SERVER-WRITTEN. The Firestore rules let an\n * org-wide member holding `outreach.use` READ the org collections on a\n * workspace that carries `features.outreach`, and refuse every client write,\n * because these documents are the sending engine's state: a client that\n * could write a mailbox could lift its daily cap, and one that could write\n * an enrollment could send a step twice. The credential collection is closed\n * to clients entirely.\n *\n * Lean on purpose. Each type carries what the mailbox and sequence work is\n * known to need and nothing speculative; a field is added where the code\n * that reads it lands. Times are epoch milliseconds throughout, the unit\n * `nextDueAtMs` has to be compared in.\n */\n\n/**\n * Where Outreach keeps its records.\n *\n * Five collections under the organization, and one at the top level. The\n * org-scoped ones are erased with the org, whose erasure deletes the whole\n * `orgs/{orgId}` tree. The top-level one holds credentials, so it is keyed by\n * an `orgId` FIELD that a path-scoped delete of `orgs/{orgId}` cannot see:\n * the org erasure sweeps it by that field\n * (`libs/tenant/data/admin/src/lib/server/erase.ts` names it as a literal,\n * which the boundary forces, and `outreach.types.spec.ts` holds the two\n * spellings together, along with the rules').\n */\nexport const OUTREACH_COLLECTIONS = {\n /** `orgs/{orgId}/outreachMailboxes/{mailboxId}` — a connected mailbox. */\n mailboxes: 'outreachMailboxes',\n /** `orgs/{orgId}/outreachSequences/{sequenceId}` — the steps to send. */\n sequences: 'outreachSequences',\n /** `orgs/{orgId}/outreachEnrollments/{enrollmentId}` — one person in one sequence. */\n enrollments: 'outreachEnrollments',\n /**\n * `orgs/{orgId}/outreachSettings/{settingsId}` — the organization's own\n * Outreach settings, one document per concern. `compliance` holds who\n * every email says sent it and the countries Outreach may send to.\n */\n settings: 'outreachSettings',\n /**\n * `orgs/{orgId}/outreachDoNotContact/{key}` — the addresses Outreach never\n * emails for this organization, keyed by `outreachDoNotContactKey`.\n */\n doNotContact: 'outreachDoNotContact',\n /**\n * `orgs/{orgId}/outreachDoNotContactDomains/{domain}` — the domains\n * Outreach never emails for this organization (AGL-3244), keyed by the\n * domain itself: a domain names a company, not a person.\n */\n doNotContactDomains: 'outreachDoNotContactDomains',\n /**\n * `outreachMailboxCredentials/{mailboxId}` — the provider grant behind a\n * mailbox. TOP-LEVEL and closed to every client, staff included, so the\n * token material never shares a readable path with the mailbox it serves.\n */\n mailboxCredentials: 'outreachMailboxCredentials',\n} as const\n\n/** The org-scoped collections, by their {@link OUTREACH_COLLECTIONS} key. */\nexport type OutreachOrgCollection = Exclude<\n keyof typeof OUTREACH_COLLECTIONS,\n 'mailboxCredentials'\n>\n\n/** `orgs/{orgId}/<collection>` for one of the org-scoped collections. */\nexport function outreachOrgCollectionPath(\n orgId: string,\n collection: OutreachOrgCollection,\n): string {\n return `orgs/${orgId}/${OUTREACH_COLLECTIONS[collection]}`\n}\n\n/** Stamped by the server on every write. */\nexport interface OutreachTimestamps {\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** The mail providers a mailbox can be connected through. */\nexport const OUTREACH_MAILBOX_PROVIDERS = ['google'] as const\nexport type OutreachMailboxProvider =\n (typeof OUTREACH_MAILBOX_PROVIDERS)[number]\n\n/**\n * Where a mailbox stands.\n *\n * - `connected` — the grant works and the mailbox may send.\n * - `paused` — a member stopped it; enrollments on it wait.\n * - `reconnect_required` — the provider refused the grant (revoked, expired,\n * password changed); nothing sends until the rep connects it again.\n * - `disconnected` — a member removed it; its credential is gone.\n */\nexport const OUTREACH_MAILBOX_STATUSES = [\n 'connected',\n 'paused',\n 'reconnect_required',\n 'disconnected',\n] as const\nexport type OutreachMailboxStatus = (typeof OUTREACH_MAILBOX_STATUSES)[number]\n\n/**\n * The hours a mailbox sends in, in its own {@link OutreachMailbox.timezone}.\n * A step that comes due outside the window waits for the next opening.\n */\nexport interface OutreachSendWindow {\n /** Days of the week it sends on, `0` Sunday through `6` Saturday. */\n days: number[]\n /** Minutes after local midnight the window opens, inclusive. */\n startMinute: number\n /** Minutes after local midnight the window closes, exclusive. */\n endMinute: number\n}\n\n/**\n * One mailbox-local day's sending, as the health panel sums it (AGL-2978).\n */\nexport interface OutreachMailboxDailyHealth {\n sent: number\n bounces: number\n replies: number\n}\n\n/**\n * The counters a mailbox's sending is judged by.\n *\n * `sentToday` is what `dailyCap` refuses against, and it belongs to one\n * mailbox-local day — `sentOnDay` — so a counter from yesterday is read as\n * zero rather than as today's spend.\n */\nexport interface OutreachMailboxHealth {\n sentToday: number\n /** The local day (`YYYY-MM-DD` in the mailbox's timezone) `sentToday` counts. */\n sentOnDay: string | null\n /** Hard bounces on mail this mailbox sent. */\n bounces: number\n /** Replies detected on Outreach threads. */\n replies: number\n lastSentAtMs: number | null\n /** The last provider error, kept until a later send succeeds. */\n lastErrorAtMs: number | null\n lastErrorCode: string | null\n /**\n * Per mailbox-local day (`YYYY-MM-DD`), the sends, bounces and replies\n * counted on it (AGL-2978). The Mailboxes panel shows the last seven days\n * summed; the sending runtime increments the day it counts on and may drop\n * days older than a week. Absent, or empty, until the first send.\n */\n daily?: Record<string, OutreachMailboxDailyHealth>\n /**\n * The mailbox's most recent sends, newest last, at most\n * `OUTREACH_BOUNCE_RATE_WINDOW_SENDS` of them, each marked when a hard\n * bounce came back for it (AGL-2981): the window the bounce-rate pause is\n * judged over. A send is named by a digest of its `Message-ID`, never by\n * its recipient.\n */\n recentSends?: OutreachRecentSend[]\n /** When a reply last called this mailbox's email spam (AGL-2981). */\n lastComplaintAtMs?: number | null\n}\n\n/** One send in {@link OutreachMailboxHealth.recentSends}. */\nexport interface OutreachRecentSend {\n /** A digest of the send's `Message-ID`: what a bounce is matched to. */\n id: string\n atMs: number\n bounced: boolean\n}\n\n/** Why a mailbox paused itself (AGL-2981): the engine's health decision, kept. */\nexport interface OutreachMailboxAutoPause {\n reason: 'bounces_today' | 'bounce_rate' | 'complaint'\n /** The sentence the mailbox's card shows. */\n message: string\n atMs: number\n /** For a complaint, when resuming stops being premature; `null` otherwise. */\n untilMs: number | null\n}\n\n/** Where a mailbox's reply and bounce sync has read to (AGL-2981). */\nexport interface OutreachMailboxSync {\n /** Everything received before this was read by an earlier run, epoch ms. */\n throughMs: number\n /** Gmail ids of messages outside any enrollment's thread already handled. */\n handledMessageIds: string[]\n}\n\n/**\n * An address a connected Google account may send as, which Gmail has\n * verified — its own address, or an alias whose ownership Gmail confirmed\n * (AGL-2978). Only these are offered as a mailbox's `sendAs`.\n */\nexport interface OutreachSendAsAddress {\n email: string\n /** The name Gmail holds for the address, `''` when it holds none. */\n displayName: string\n /** The account's own address. */\n isPrimary: boolean\n /** The address Gmail sends as by default. */\n isDefault: boolean\n}\n\n/**\n * A rep's own mailbox, connected to send Outreach mail\n * (`orgs/{orgId}/outreachMailboxes/{id}`).\n *\n * Its grant lives in {@link OutreachMailboxCredentials} under the same id.\n */\nexport interface OutreachMailbox extends OutreachTimestamps {\n id: string\n provider: OutreachMailboxProvider\n /** The account's own address. */\n email: string\n /** The address mail goes out as: the account's, or an alias it may send as. */\n sendAs: string\n /**\n * The verified addresses `sendAs` may be, as Gmail listed them at the last\n * connect (AGL-2978). A changed alias shows up here on the next reconnect.\n */\n sendAsOptions: OutreachSendAsAddress[]\n /** The From name recipients see. */\n displayName: string\n status: OutreachMailboxStatus\n /** The most mail it sends in one local day, ramp included. */\n dailyCap: number\n window: OutreachSendWindow\n /** IANA zone the window and the day boundary are read in. */\n timezone: string\n /**\n * When the warm-up ramp began, or `null` for a mailbox sending at its full\n * cap. The ramp is measured from here, not from connection, so a paused\n * mailbox can restart it.\n */\n rampStartedAtMs: number | null\n health: OutreachMailboxHealth\n /** The member who connected it, and whose mail it is. */\n connectedByUid: string\n /** When the grant behind it was last connected (AGL-2978). */\n connectedAtMs: number\n /**\n * Set when the mailbox paused ITSELF on its health (AGL-2981), and cleared\n * when a member pauses or resumes it; absent otherwise.\n */\n autoPause?: OutreachMailboxAutoPause | null\n /** The reply and bounce sync's place in the mailbox (AGL-2981). */\n sync?: OutreachMailboxSync | null\n}\n\n/**\n * The provider grant behind one mailbox\n * (`outreachMailboxCredentials/{mailboxId}`).\n *\n * Only the fields every reader relies on are declared here; the token\n * material is added by the code that stores it. `orgId` is REQUIRED, not\n * decorative: the org erasure finds these documents by it, and a credential\n * written without one would outlive its workspace.\n *\n * NAME EVERY SEALED FIELD WITH A SECRET WORD — `refreshToken`,\n * `tokenCiphertext`. The personal-data export discloses these documents with\n * secrets redacted, and its redaction reads field names first: a name\n * carrying `token`, `secret` or `credential` is withheld whatever its value,\n * while a neutral name such as `ciphertext` is judged by the value's shape\n * alone and can be disclosed.\n */\nexport interface OutreachMailboxCredentials extends OutreachTimestamps {\n /** The mailbox id, which is also this document's id. */\n id: string\n orgId: string\n mailboxId: string\n provider: OutreachMailboxProvider\n}\n\n/** A sequence's lifecycle. Only an `active` sequence advances its enrollments. */\nexport const OUTREACH_SEQUENCE_STATUSES = [\n 'draft',\n 'active',\n 'paused',\n 'archived',\n] as const\nexport type OutreachSequenceStatus =\n (typeof OUTREACH_SEQUENCE_STATUSES)[number]\n\n/*==========================================\n * SEQUENCES (AGL-2979).\n *\n * The limits are the outbound playbook's: a person gets at most four emails\n * from one sequence, spaced by business days, and the tasks between them are\n * the rep's own touches — a LinkedIn note, a call. The engine\n * (`../engine/sequence-validation.ts`) holds a stored sequence to them.\n *==========================================*/\n\n/** The most steps one sequence holds, emails and tasks together. */\nexport const OUTREACH_MAX_STEPS = 8\n\n/** The most of those steps that send an email. */\nexport const OUTREACH_MAX_EMAIL_STEPS = 4\n\n/** The longest wait one step may carry, in business days. */\nexport const OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS = 30\n\n/**\n * The shortest wait before an email that follows an earlier email. The first\n * email may wait `0` — \"the next opening of the window\", or the same day as\n * a task before it — and so may a task, but each later email waits at least\n * this long after the step before it.\n */\nexport const OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS = 1\n\n/**\n * One email step.\n *\n * Steps are a union on `kind`, so a later kind joins without reshaping the\n * ones already stored.\n */\nexport interface OutreachEmailStep {\n /** Stable within the sequence, so an edit that reorders steps is traceable. */\n id: string\n kind: 'email'\n /**\n * Business days to wait after the previous step, counted in the mailbox's\n * zone — after enrollment for the first step, where `0` means the next\n * opening of the sending window.\n */\n delayBusinessDays: number\n /**\n * The subject, merge fields allowed. Required on the first email and on\n * any email that starts a new thread; an in-thread email is sent as `Re:`\n * the thread's own subject, so its field is not read.\n */\n subject: string\n /**\n * Whether the email goes out as a reply in the thread the earlier emails\n * started. True by default for every email after the first; the first\n * email always starts the thread, whatever this says.\n */\n replyInThread: boolean\n /** Plain text with merge fields, as its author wrote it; `''` when `templateId` is set. */\n body: string\n /**\n * A CRM email template (`orgs/{orgId}/crmEmailTemplates/{id}`) whose body\n * is sent instead of `body`, or `null`. Only the body is taken from it: the\n * step's own subject is the one that is sent.\n */\n templateId: string | null\n}\n\n/** The rep's own touches a task step asks for. */\nexport const OUTREACH_TASK_KINDS = ['linkedin', 'call', 'todo'] as const\nexport type OutreachTaskKind = (typeof OUTREACH_TASK_KINDS)[number]\n\nexport const OUTREACH_TASK_KIND_LABELS: Record<OutreachTaskKind, string> = {\n linkedin: 'LinkedIn',\n call: 'Call',\n todo: 'To-do',\n}\n\n/** One task step: a CRM task for the rep, created when the step comes due. */\nexport interface OutreachTaskStep {\n id: string\n kind: 'task'\n taskKind: OutreachTaskKind\n /** What the task says, e.g. \"Connect on LinkedIn\". */\n title: string\n /** Business days after the previous step; `0` is the same day. */\n delayBusinessDays: number\n}\n\nexport type OutreachSequenceStep = OutreachEmailStep | OutreachTaskStep\n\n/** The countries a new sequence sends to: the United States alone. */\nexport const OUTREACH_DEFAULT_ALLOWED_COUNTRIES: readonly string[] = ['US']\n\n/** Behavior a sequence applies to every enrollment in it. */\nexport interface OutreachSequenceSettings {\n /**\n * The hours this sequence sends in, replacing its mailbox's own window;\n * `null` sends in the mailbox's. Read in the mailbox's zone either way.\n */\n window: OutreachSendWindow | null\n /**\n * ISO-3166-1 alpha-2 codes a recipient may be in, uppercase. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}: Canada, the United Kingdom\n * and most of the EU need a consent basis a cold email does not have.\n */\n allowedCountries: string[]\n /** Whether a contact who is already a customer may be enrolled. Default `false`. */\n allowCustomers: boolean\n /**\n * Whether the links in this sequence's emails are rewritten so clicks are\n * counted (AGL-3239). Default `false`, and `false` on every sequence\n * written before the setting existed: turning it on changes what the\n * recipient sees in the body, which is not a change to make on anyone's\n * behalf.\n *\n * It buys the one engagement number a plain-text sequence can honestly\n * report. It costs a visible link: the destination in the body becomes a\n * signed link on the console that forwards to it. There is no equivalent\n * setting for opens, because a pixel needs an HTML part — see\n * `../engine/click-tracking.ts`.\n */\n trackClicks: boolean\n}\n\n/*==========================================\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Counters on the sequence document, incremented by the sending runtime and\n * by the click route. They are NOT derived from the enrollments on read: the\n * console lists enrollments a page at a time, so a rollup taken over what is\n * loaded would report the first page's numbers as the sequence's.\n *\n * Every field is optional and every absence means \"not recorded\", never\n * zero. The distinction is the whole of `campaign-report.ts`'s honesty and\n * it is kept here for the same reason: a sequence that ran before this\n * existed has no counters, and reporting its click rate as 0% would publish\n * a fact about our schema as a fact about its recipients.\n *=========================================*/\n\n/**\n * The per-destination click rollup under one sequence:\n * `orgs/{orgId}/outreachSequences/{id}/reports/links`.\n *\n * One document per sequence, holding a bounded map — the campaign rollup's\n * own shape and its own cap, so \"a link\" means the same thing in a rep's\n * report and a marketer's. Here rather than beside the writer, because the\n * console's listener is a browser module and the writer is not.\n */\nexport const OUTREACH_LINK_ROLLUP_PATH = ['reports', 'links'] as const\n\n/** The counters one sequence is judged by. */\nexport interface OutreachSequenceStats {\n /** Email steps that left. One person getting four emails counts four. */\n sent?: number\n /**\n * Distinct enrollments that have had at least one email — the denominator\n * of every engagement rate, and the thing `sent` is not.\n */\n people?: number\n /**\n * At least one email of this sequence went out with its links rewritten.\n *\n * Absent is NOT false; it is \"never recorded\", which is what every\n * sequence sent before the setting existed reads as. Either way the report\n * withholds the click rate rather than showing 0% — see\n * {@link OutreachSequenceStats} above.\n */\n clickTracked?: boolean\n /** Click EVENTS judged a person's. One reader clicking twice counts two. */\n clicks?: number\n /** Enrollments whose FIRST human click was seen: the rate's numerator. */\n uniqueClicks?: number\n /**\n * Clicks a link scanner or a security gateway made, counted apart and\n * never in the rate (`../engine/click-tracking.ts`). Shown, not hidden:\n * a large number here is the reader's evidence that the small number\n * beside it is the real one.\n */\n machineClicks?: number\n /** When a person last followed a link. */\n lastClickAtMs?: number | null\n}\n\n/** What one enrollment did with the links it was sent (AGL-3239). */\nexport interface OutreachEnrollmentEngagement {\n /** Click events judged this person's, machines excluded. */\n clicks: number\n /** The first, which is what makes them one of the sequence's `uniqueClicks`. */\n firstClickAtMs: number | null\n lastClickAtMs: number | null\n /** The destination they followed last, as `campaignLinkKey` reduces it. */\n lastClickUrl: string | null\n /** Clicks on this person's links that were a machine's. */\n machineClicks: number\n}\n\n/** An ordered set of steps sent from one mailbox (`orgs/{orgId}/outreachSequences/{id}`). */\nexport interface OutreachSequence extends OutreachTimestamps {\n id: string\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /**\n * The mailbox every email step is sent from. A setting of the sequence,\n * kept beside `settings` rather than inside it because enrollments and a\n * mailbox's own screens select sequences by it.\n */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n status: OutreachSequenceStatus\n /**\n * What it measured (AGL-3239). Absent until the first email leaves, and\n * absent forever on a sequence that finished before the counters existed.\n */\n stats?: OutreachSequenceStats\n}\n\n/*==========================================\n * ENROLLMENTS (AGL-2979).\n *==========================================*/\n\n/**\n * Where one person's run through a sequence stands.\n *\n * `active` sends and `paused` waits. The rest end the sending, and each says\n * why: `finished` sent every step, `replied` heard back, `bounced` reached a\n * mailbox that does not exist, `opted_out` was asked to stop, `stopped` was\n * ended by a member or a gate, `failed` could not be sent. The transitions\n * between them are the engine's (`../engine/enrollment-state.ts`).\n */\nexport const OUTREACH_ENROLLMENT_STATUSES = [\n 'active',\n 'paused',\n 'finished',\n 'replied',\n 'bounced',\n 'opted_out',\n 'stopped',\n 'failed',\n] as const\nexport type OutreachEnrollmentStatus =\n (typeof OUTREACH_ENROLLMENT_STATUSES)[number]\n\n/**\n * Why an enrollment is not `active`, recorded beside the status. `finished`\n * needs none; each other status allows the reasons\n * `OUTREACH_STOP_REASONS_BY_STATUS` names.\n */\nexport const OUTREACH_STOP_REASONS = [\n /** A person wrote back in the thread. */\n 'reply',\n /** The recipient's server refused the address for good. */\n 'hard_bounce',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** The address is on the organization's do-not-contact list. */\n 'do_not_contact',\n /** A gate refused the next send — the person became a customer, say. */\n 'gate',\n /** A member paused or stopped it. */\n 'manual',\n /** The sequence was archived with the person still in it. */\n 'sequence_archived',\n /** The provider refused the send for good, or the email could not be composed. */\n 'send_failed',\n] as const\nexport type OutreachStopReason = (typeof OUTREACH_STOP_REASONS)[number]\n\n/** The statuses a stop reason is recorded with, and the reasons each allows. */\nexport const OUTREACH_STOP_REASONS_BY_STATUS: Readonly<\n Record<\n Exclude<OutreachEnrollmentStatus, 'active' | 'finished'>,\n readonly OutreachStopReason[]\n >\n> = {\n paused: ['manual'],\n replied: ['reply'],\n bounced: ['hard_bounce'],\n opted_out: ['opt_out_reply', 'unsubscribe', 'do_not_contact'],\n stopped: ['manual', 'gate', 'sequence_archived'],\n failed: ['send_failed'],\n}\n\n/**\n * What a rep confirms before a COLD contact is enrolled — one with no\n * inbound capture behind them — each recorded with who confirmed it and\n * when, because each is a fact only the rep can know:\n *\n * - `us_business_address`: the address is a business address in the US;\n * - `published_or_given`: they or their company published it, or they gave\n * it to us — never guessed from a name or bought on a list;\n * - `verified_deliverable`: a verifier said it accepts mail.\n */\nexport const OUTREACH_ATTESTATION_KINDS = [\n 'us_business_address',\n 'published_or_given',\n 'verified_deliverable',\n] as const\nexport type OutreachAttestationKind = (typeof OUTREACH_ATTESTATION_KINDS)[number]\n\n/** The sentence a rep ticks for each attestation. */\nexport const OUTREACH_ATTESTATION_LABELS: Record<OutreachAttestationKind, string> = {\n us_business_address: 'This is a US business address',\n published_or_given:\n 'They or their company published this address, or they gave it to us',\n verified_deliverable: 'This address was verified as deliverable',\n}\n\n/** One confirmation, as stored. */\nexport interface OutreachAttestation {\n /** The member who confirmed it. */\n uid: string\n atMs: number\n}\n\n/** The confirmations an enrollment carries, by kind. */\nexport type OutreachAttestations = Partial<\n Record<OutreachAttestationKind, OutreachAttestation>\n>\n\n/** The longest personal line an enrollment keeps: one sentence, not a letter. */\nexport const OUTREACH_PERSONAL_LINE_MAX = 300\n\n/**\n * Which CRM record an enrollment names (AGL-3234). A person is a LEAD until\n * somebody qualifies them and a CONTACT after, and a sequence works either:\n * an enrollment made on a lead follows the lead to the contact it becomes,\n * so the thread and the steps carry on as one enrollment.\n */\nexport const OUTREACH_ENROLLMENT_TARGETS = ['contact', 'lead'] as const\nexport type OutreachEnrollmentTarget = (typeof OUTREACH_ENROLLMENT_TARGETS)[number]\n\n/** One person in one sequence (`orgs/{orgId}/outreachEnrollments/{id}`). */\nexport interface OutreachEnrollment extends OutreachTimestamps {\n id: string\n sequenceId: string\n /**\n * The record the person is (AGL-3234): `contact` for a CRM contact,\n * `lead` for a lead the sequence's site holds. A stored enrollment with\n * no target is a contact's — every enrollment was, before leads could be\n * sequenced.\n */\n target: OutreachEnrollmentTarget\n /**\n * The CRM contact the person is — `''` while the enrollment targets a\n * lead that has not converted. Filled in, beside `target: 'contact'`, the\n * moment the lead becomes a contact.\n */\n contactId: string\n /**\n * `hosts/{hostId}/leads/{leadId}` — the person key — for an enrollment\n * made on a lead; kept once the lead converts, so the lead's page still\n * lists the sequence. `null` for an enrollment made on a contact.\n */\n leadId: string | null\n /**\n * The person's name as the sending site knew it at enrollment, `''` when\n * it had none — what the enrollments table shows beside the address,\n * without a read of every record on the page (AGL-2980).\n */\n contactName: string\n /** The address the steps go to, normalized, captured at enrollment. */\n email: string\n hostId: string\n mailboxId: string\n /** The index into `steps` of the NEXT step to run. */\n stepIndex: number\n /** When that step comes due, or `null` when nothing is waiting. */\n nextDueAtMs: number | null\n status: OutreachEnrollmentStatus\n /** Why the status is not `active`; `null` while active and once finished. */\n stopReason: OutreachStopReason | null\n /** Plain-language detail: the gate's reason, the bounce's diagnostic. */\n stopDetail: string | null\n stoppedAtMs: number | null\n /** The member who paused or stopped it; `null` when the engine did. */\n stoppedByUid: string | null\n /**\n * The rep's signal sentence — why this person, now — merged into the\n * steps as `{{enrollment.personalLine}}`. Required for a cold contact.\n */\n personalLine: string\n /** Whether the contact was cold (no inbound capture) when enrolled. */\n cold: boolean\n attestations: OutreachAttestations\n /** The member who enrolled the person. */\n enrolledByUid: string\n /** The provider thread the next in-thread email is sent into, once one exists. */\n gmailThreadId: string | null\n /** Every provider thread this enrollment has sent into, oldest first — what reply sync watches. */\n gmailThreadIds: string[]\n /** The subject the current thread was started with, as sent. */\n threadSubject: string | null\n /**\n * The `Message-ID`s of the emails sent into the current thread, oldest\n * first, angle brackets included: the next in-thread email answers the\n * last and names them all in `References`.\n */\n messageIds: string[]\n /** When the last step ran. */\n lastSentAtMs: number | null\n /**\n * A sending run's hold on the step it is running (AGL-2981): taken in a\n * transaction before the step runs, cleared when it is recorded. Absent\n * or `null` while no run holds one.\n */\n sendClaim?: OutreachSendClaim | null\n /** Every step the runtime ran, oldest first (AGL-2981). */\n stepRecords?: OutreachStepRecord[]\n /** Gmail ids of this enrollment's thread messages the sync has handled, the last hundred. */\n syncedMessageIds?: string[]\n /**\n * What this person did with the links they were sent (AGL-3239). Absent\n * until their first click, which is why the table reads an absence as\n * \"no clicks\" only for a sequence that tracks them at all.\n */\n engagement?: OutreachEnrollmentEngagement\n}\n\n/** A sending run's claim on one enrollment's step (AGL-2981). */\nexport interface OutreachSendClaim {\n /** The run that holds it. */\n token: string\n atMs: number\n stepIndex: number\n /**\n * The `Message-ID` an email step goes out with, minted before the send, so\n * a claim a run died holding is settled by looking for the message rather\n * than by sending it again. `null` for a task step.\n */\n messageId: string | null\n}\n\n/** One step the runtime ran for an enrollment (AGL-2981). */\nexport interface OutreachStepRecord {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task'\n atMs: number\n /** Gmail's id for the sent message. */\n gmailMessageId?: string\n /** Gmail's thread the message went into. */\n gmailThreadId?: string\n /** The `Message-ID` it went out with, angle brackets included. */\n messageId?: string\n /** The subject as sent. */\n subject?: string\n /** The record system's task for a task step; `null` when none could be filed. */\n taskId?: string | null\n /**\n * The destinations this email's links were rewritten to point at, in the\n * order a click token indexes them (AGL-3239). Absent on a step that\n * carried no links, and on every step sent before tracking existed.\n *\n * Kept so the console can say WHICH link a person followed without the\n * token having to carry the URL back to us, and so a rewritten body can be\n * read afterwards for what it actually offered them.\n */\n links?: string[]\n}\n\n/*==========================================\n * ORGANIZATION SETTINGS (AGL-2979).\n *==========================================*/\n\n/**\n * What every Outreach email's footer says about who sent it — the\n * identification and the postal address CAN-SPAM requires on commercial\n * mail. The composer refuses to write an email without a legal name and a\n * postal address, and a sequence cannot be activated without them.\n */\nexport interface OutreachOrgSettings {\n /** The organization's legal name, as the footer prints it: \"Example Co LLC\". */\n legalName: string\n /**\n * The name the solicitation sentence uses when it is not the legal name:\n * \"This is a sales email from Example Co.\" `''` uses the legal\n * name.\n */\n brandName: string\n /**\n * A valid physical postal address: a street address, a USPS PO box, or a\n * private mailbox registered with the USPS. Line breaks are printed as\n * commas.\n */\n postalAddress: string\n}\n\n/** The id of the compliance document in the `settings` collection. */\nexport const OUTREACH_COMPLIANCE_SETTINGS_ID = 'compliance'\n\n/**\n * The organization's Outreach compliance settings\n * (`orgs/{orgId}/outreachSettings/compliance`): the footer's sender identity,\n * and the countries Outreach may send to at all.\n *\n * `allowedCountries` is a CEILING over every sequence's own list — a\n * sequence sends only to the countries both name — so narrowing it here\n * narrows every sequence at once, without editing any of them. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}.\n */\nexport interface OutreachComplianceSettings extends OutreachOrgSettings {\n /** ISO-3166-1 alpha-2 codes, uppercase, in the order they were chosen. */\n allowedCountries: string[]\n}\n\n/** The compliance settings as stored, with who changed them last. */\nexport interface OutreachComplianceSettingsDocument\n extends OutreachComplianceSettings {\n /** `0` until the organization first saves them. */\n updatedAtMs: number\n updatedByUid: string | null\n}\n\n/*==========================================\n * DO NOT CONTACT (AGL-2980).\n *\n * The organization's own list of addresses Outreach never emails, whoever\n * enrolls them and from whichever site. A member adds one by hand; the\n * sending runtime adds one when a reply asks to be left alone, when the\n * unsubscribe link is used, and when an address bounces for good.\n *\n * AN ENTRY CARRIES NO ADDRESS. Its id is `outreachDoNotContactKey(email)` —\n * the same hash the platform's suppression lists key by — and that is all a\n * lookup needs, since every caller holds the address it is about to use.\n * So the list keeps working after a person is erased from the workspace,\n * which is exactly when a promise not to email them must still hold, while\n * holding nothing that identifies them.\n *==========================================*/\n\n/** Why an address is on the list. */\nexport const OUTREACH_DO_NOT_CONTACT_REASONS = [\n /** A member put it there. */\n 'manual',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** Mail to it bounced for good. */\n 'hard_bounce',\n /**\n * The recipient organization's mail gateway refused the sender outright\n * (AGL-3244) — a Barracuda, Proofpoint or Mimecast policy block — which is\n * a verdict on the whole domain, and is filed against it.\n */\n 'gateway_block',\n] as const\nexport type OutreachDoNotContactReason =\n (typeof OUTREACH_DO_NOT_CONTACT_REASONS)[number]\n\n/** What put an address on the list: a member, or the sending runtime. */\nexport const OUTREACH_DO_NOT_CONTACT_SOURCES = ['member', 'runtime'] as const\nexport type OutreachDoNotContactSource =\n (typeof OUTREACH_DO_NOT_CONTACT_SOURCES)[number]\n\n/** One address on the list (`orgs/{orgId}/outreachDoNotContact/{key}`). */\nexport interface OutreachDoNotContactEntry {\n /** `outreachDoNotContactKey` of the address, which is also the document id. */\n key: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member who added it; `null` when the runtime did. */\n addedByUid: string | null\n addedAtMs: number\n /** The enrollment that led here, when one did. */\n enrollmentId: string | null\n /** That enrollment's sequence. */\n sequenceId: string | null\n /** Plain-language detail: why the member added it, the bounce's diagnostic. */\n detail: string | null\n}\n\n/**\n * One domain on the list (`orgs/{orgId}/outreachDoNotContactDomains/{domain}`)\n * (AGL-3244): no address at it is emailed, whoever enrolls them.\n *\n * Unlike an address entry it CARRIES THE DOMAIN, in clear, because the\n * Compliance page lists it and a member takes it off by name; a domain is a\n * company's, not a person's, and a person erasure leaves it alone. A member\n * adds one by hand; the sending runtime adds one when a hard bounce reads\n * as the domain's mail gateway refusing the sender rather than one address\n * being unknown.\n */\nexport interface OutreachDoNotContactDomainEntry {\n /** The domain, lower-cased, which is also the document id. */\n domain: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member who added it; `null` when the runtime did. */\n addedByUid: string | null\n addedAtMs: number\n /** The enrollment whose bounce led here, when one did. */\n enrollmentId: string | null\n /** That enrollment's sequence. */\n sequenceId: string | null\n /** Plain-language detail: the member's note, or the bounce's diagnostic. */\n detail: string | null\n}\n"],"names":["OUTREACH_COLLECTIONS","mailboxes","sequences","enrollments","settings","doNotContact","doNotContactDomains","mailboxCredentials","outreachOrgCollectionPath","orgId","collection","OUTREACH_MAILBOX_PROVIDERS","OUTREACH_MAILBOX_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_MAX_STEPS","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS","OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS","OUTREACH_TASK_KINDS","OUTREACH_TASK_KIND_LABELS","linkedin","call","todo","OUTREACH_DEFAULT_ALLOWED_COUNTRIES","OUTREACH_LINK_ROLLUP_PATH","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_STOP_REASONS","OUTREACH_STOP_REASONS_BY_STATUS","paused","replied","bounced","opted_out","stopped","failed","OUTREACH_ATTESTATION_KINDS","OUTREACH_ATTESTATION_LABELS","us_business_address","published_or_given","verified_deliverable","OUTREACH_PERSONAL_LINE_MAX","OUTREACH_ENROLLMENT_TARGETS","OUTREACH_COMPLIANCE_SETTINGS_ID","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
18
+ import type { OutreachRouteDeps } from './route-deps';
19
+ /**
20
+ * THE DO-NOT-CONTACT DOMAINS ROUTE (AGL-3244): `outreach/do-not-contact/domains`.
21
+ *
22
+ * `GET ?orgId` lists the domains; `POST` adds one, with the member as its
23
+ * author, or takes one off. The address half of the list has no route of
24
+ * its own — an address entry carries no address and is listed nowhere — so
25
+ * this is the one place the list is edited by hand. Both directions write a
26
+ * line to the organization's activity feed: taking a domain off is what
27
+ * lets a sequence email a company that once blocked the sender, and who did
28
+ * that is worth knowing.
29
+ */
30
+ /** The activity lines a change writes. */
31
+ export declare const OUTREACH_DO_NOT_CONTACT_DOMAIN_ACTIVITY: {
32
+ readonly add: (domain: string) => string;
33
+ readonly remove: (domain: string) => string;
34
+ };
35
+ export declare function createOutreachDoNotContactDomainsRoute(deps: OutreachRouteDeps): PluginWebApiHandler;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { normalizeOutreachDomain } from "../engine/do-not-contact-domain.js";
17
+ import { addOutreachDoNotContactDomain, listOutreachDoNotContactDomains, removeOutreachDoNotContactDomain } from "../storage/do-not-contact-store.js";
18
+ import { outreachRouteGate } from "./route-gate.js";
19
+ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachJsonBody } from "./route-http.js";
20
+ /**
21
+ * THE DO-NOT-CONTACT DOMAINS ROUTE (AGL-3244): `outreach/do-not-contact/domains`.
22
+ *
23
+ * `GET ?orgId` lists the domains; `POST` adds one, with the member as its
24
+ * author, or takes one off. The address half of the list has no route of
25
+ * its own — an address entry carries no address and is listed nowhere — so
26
+ * this is the one place the list is edited by hand. Both directions write a
27
+ * line to the organization's activity feed: taking a domain off is what
28
+ * lets a sequence email a company that once blocked the sender, and who did
29
+ * that is worth knowing.
30
+ */ /** The activity lines a change writes. */ export const OUTREACH_DO_NOT_CONTACT_DOMAIN_ACTIVITY = {
31
+ add: (domain)=>`Added ${domain} to the Sequences do-not-contact list`,
32
+ remove: (domain)=>`Removed ${domain} from the Sequences do-not-contact list`
33
+ };
34
+ export function createOutreachDoNotContactDomainsRoute(deps) {
35
+ return async (request)=>{
36
+ if (request.method === 'GET') {
37
+ const gate = await outreachRouteGate(request, new URL(request.url).searchParams.get('orgId'), deps.gate);
38
+ if (gate instanceof Response) return gate;
39
+ const domains = await listOutreachDoNotContactDomains(deps.firestore(), gate.orgId);
40
+ return outreachOk({
41
+ ok: true,
42
+ domains
43
+ });
44
+ }
45
+ if (request.method !== 'POST') return outreachMethodNotAllowed('GET, POST');
46
+ const body = await readOutreachJsonBody(request);
47
+ const gate = await outreachRouteGate(request, body['orgId'], deps.gate);
48
+ if (gate instanceof Response) return gate;
49
+ const action = body['action'];
50
+ if (action !== 'add' && action !== 'remove') {
51
+ return outreachRefusal(400, 'invalid-request', 'Say whether to add the domain or remove it.');
52
+ }
53
+ const domain = normalizeOutreachDomain(body['domain']);
54
+ if (!domain) {
55
+ return outreachRefusal(400, 'invalid-domain', 'Type a domain, such as example.com.');
56
+ }
57
+ const firestore = deps.firestore();
58
+ let changed;
59
+ if (action === 'add') {
60
+ const added = await addOutreachDoNotContactDomain(firestore, {
61
+ orgId: gate.orgId,
62
+ domain,
63
+ reason: 'manual',
64
+ source: 'member',
65
+ addedByUid: gate.uid,
66
+ nowMs: deps.now(),
67
+ detail: typeof body['detail'] === 'string' ? body['detail'] : null
68
+ });
69
+ changed = added.created;
70
+ } else {
71
+ changed = await removeOutreachDoNotContactDomain(firestore, gate.orgId, domain);
72
+ }
73
+ if (changed) {
74
+ await deps.logOrgActivity(gate.orgId, {
75
+ uid: gate.uid,
76
+ email: gate.email
77
+ }, OUTREACH_DO_NOT_CONTACT_DOMAIN_ACTIVITY[action](domain), {
78
+ type: 'org',
79
+ id: gate.orgId
80
+ });
81
+ }
82
+ const domains = await listOutreachDoNotContactDomains(firestore, gate.orgId);
83
+ return outreachOk({
84
+ ok: true,
85
+ domains,
86
+ changed,
87
+ domain
88
+ });
89
+ };
90
+ }
91
+
92
+ //# sourceMappingURL=do-not-contact-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/do-not-contact-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 type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport type {\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n} from '../model/outreach-api'\nimport {\n addOutreachDoNotContactDomain,\n listOutreachDoNotContactDomains,\n removeOutreachDoNotContactDomain,\n} from '../storage/do-not-contact-store'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE DO-NOT-CONTACT DOMAINS ROUTE (AGL-3244): `outreach/do-not-contact/domains`.\n *\n * `GET ?orgId` lists the domains; `POST` adds one, with the member as its\n * author, or takes one off. The address half of the list has no route of\n * its own — an address entry carries no address and is listed nowhere — so\n * this is the one place the list is edited by hand. Both directions write a\n * line to the organization's activity feed: taking a domain off is what\n * lets a sequence email a company that once blocked the sender, and who did\n * that is worth knowing.\n */\n\n/** The activity lines a change writes. */\nexport const OUTREACH_DO_NOT_CONTACT_DOMAIN_ACTIVITY = {\n add: (domain: string) => `Added ${domain} to the Sequences do-not-contact list`,\n remove: (domain: string) => `Removed ${domain} from the Sequences do-not-contact list`,\n} as const\n\nexport function createOutreachDoNotContactDomainsRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method === 'GET') {\n const gate = await outreachRouteGate(\n request,\n new URL(request.url).searchParams.get('orgId'),\n deps.gate,\n )\n if (gate instanceof Response) return gate\n const domains = await listOutreachDoNotContactDomains(deps.firestore(), gate.orgId)\n return outreachOk({ ok: true, domains } satisfies OutreachDoNotContactDomainsResponse)\n }\n if (request.method !== 'POST') return outreachMethodNotAllowed('GET, POST')\n\n const body = await readOutreachJsonBody(request)\n const gate = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const action = body['action'] as OutreachDoNotContactDomainRequest['action']\n if (action !== 'add' && action !== 'remove') {\n return outreachRefusal(400, 'invalid-request', 'Say whether to add the domain or remove it.')\n }\n const domain = normalizeOutreachDomain(body['domain'])\n if (!domain) {\n return outreachRefusal(400, 'invalid-domain', 'Type a domain, such as example.com.')\n }\n const firestore = deps.firestore()\n let changed: boolean\n if (action === 'add') {\n const added = await addOutreachDoNotContactDomain(firestore, {\n orgId: gate.orgId,\n domain,\n reason: 'manual',\n source: 'member',\n addedByUid: gate.uid,\n nowMs: deps.now(),\n detail: typeof body['detail'] === 'string' ? body['detail'] : null,\n })\n changed = added.created\n } else {\n changed = await removeOutreachDoNotContactDomain(firestore, gate.orgId, domain)\n }\n if (changed) {\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n OUTREACH_DO_NOT_CONTACT_DOMAIN_ACTIVITY[action](domain),\n { type: 'org', id: gate.orgId },\n )\n }\n const domains = await listOutreachDoNotContactDomains(firestore, gate.orgId)\n return outreachOk({ ok: true, domains, changed, domain } satisfies OutreachDoNotContactDomainResponse)\n }\n}\n"],"names":["normalizeOutreachDomain","addOutreachDoNotContactDomain","listOutreachDoNotContactDomains","removeOutreachDoNotContactDomain","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachJsonBody","OUTREACH_DO_NOT_CONTACT_DOMAIN_ACTIVITY","add","domain","remove","createOutreachDoNotContactDomainsRoute","deps","request","method","gate","URL","url","searchParams","get","Response","domains","firestore","orgId","ok","body","action","changed","added","reason","source","addedByUid","uid","nowMs","now","detail","created","logOrgActivity","email","type","id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,uBAAuB,QAAQ,qCAAiC;AAMzE,SACEC,6BAA6B,EAC7BC,+BAA+B,EAC/BC,gCAAgC,QAC3B,qCAAiC;AAExC,SAASC,iBAAiB,QAAQ,kBAAc;AAChD,SACEC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,oBAAoB,QACf,kBAAc;AAErB;;;;;;;;;;CAUC,GAED,wCAAwC,GACxC,OAAO,MAAMC,0CAA0C;IACrDC,KAAK,CAACC,SAAmB,CAAC,MAAM,EAAEA,OAAO,qCAAqC,CAAC;IAC/EC,QAAQ,CAACD,SAAmB,CAAC,QAAQ,EAAEA,OAAO,uCAAuC,CAAC;AACxF,EAAU;AAEV,OAAO,SAASE,uCAAuCC,IAAuB;IAC5E,OAAO,OAAOC;QACZ,IAAIA,QAAQC,MAAM,KAAK,OAAO;YAC5B,MAAMC,OAAO,MAAMb,kBACjBW,SACA,IAAIG,IAAIH,QAAQI,GAAG,EAAEC,YAAY,CAACC,GAAG,CAAC,UACtCP,KAAKG,IAAI;YAEX,IAAIA,gBAAgBK,UAAU,OAAOL;YACrC,MAAMM,UAAU,MAAMrB,gCAAgCY,KAAKU,SAAS,IAAIP,KAAKQ,KAAK;YAClF,OAAOnB,WAAW;gBAAEoB,IAAI;gBAAMH;YAAQ;QACxC;QACA,IAAIR,QAAQC,MAAM,KAAK,QAAQ,OAAOX,yBAAyB;QAE/D,MAAMsB,OAAO,MAAMnB,qBAAqBO;QACxC,MAAME,OAAO,MAAMb,kBAAkBW,SAASY,IAAI,CAAC,QAAQ,EAAEb,KAAKG,IAAI;QACtE,IAAIA,gBAAgBK,UAAU,OAAOL;QACrC,MAAMW,SAASD,IAAI,CAAC,SAAS;QAC7B,IAAIC,WAAW,SAASA,WAAW,UAAU;YAC3C,OAAOrB,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAMI,SAASX,wBAAwB2B,IAAI,CAAC,SAAS;QACrD,IAAI,CAAChB,QAAQ;YACX,OAAOJ,gBAAgB,KAAK,kBAAkB;QAChD;QACA,MAAMiB,YAAYV,KAAKU,SAAS;QAChC,IAAIK;QACJ,IAAID,WAAW,OAAO;YACpB,MAAME,QAAQ,MAAM7B,8BAA8BuB,WAAW;gBAC3DC,OAAOR,KAAKQ,KAAK;gBACjBd;gBACAoB,QAAQ;gBACRC,QAAQ;gBACRC,YAAYhB,KAAKiB,GAAG;gBACpBC,OAAOrB,KAAKsB,GAAG;gBACfC,QAAQ,OAAOV,IAAI,CAAC,SAAS,KAAK,WAAWA,IAAI,CAAC,SAAS,GAAG;YAChE;YACAE,UAAUC,MAAMQ,OAAO;QACzB,OAAO;YACLT,UAAU,MAAM1B,iCAAiCqB,WAAWP,KAAKQ,KAAK,EAAEd;QAC1E;QACA,IAAIkB,SAAS;YACX,MAAMf,KAAKyB,cAAc,CACvBtB,KAAKQ,KAAK,EACV;gBAAES,KAAKjB,KAAKiB,GAAG;gBAAEM,OAAOvB,KAAKuB,KAAK;YAAC,GACnC/B,uCAAuC,CAACmB,OAAO,CAACjB,SAChD;gBAAE8B,MAAM;gBAAOC,IAAIzB,KAAKQ,KAAK;YAAC;QAElC;QACA,MAAMF,UAAU,MAAMrB,gCAAgCsB,WAAWP,KAAKQ,KAAK;QAC3E,OAAOnB,WAAW;YAAEoB,IAAI;YAAMH;YAASM;YAASlB;QAAO;IACzD;AACF"}
@@ -60,6 +60,7 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
60
60
  hostSuppressed: null,
61
61
  salesTopicState: null,
62
62
  doNotContact: null,
63
+ doNotContactDomain: null,
63
64
  workspaceMembers: null,
64
65
  openEnrollments: null,
65
66
  hasInboundEmail: null
@@ -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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\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 { 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 } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentId,\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\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 addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\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 workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: 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 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 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: consentGroupForHost(caller.org, sequence.hostId).groupId,\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 */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\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 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\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. A lead is the\n * site's own, so there is no address to resolve: the person key IS the id.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n const window = await firestore\n .collection('hosts')\n .doc(sequence.hostId)\n .collection('leads')\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\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 firestore: Firestore,\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 const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\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 ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\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(firestore, 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 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 // 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[] }\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 requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\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 const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === 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 const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\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) ?? { personalLine: '', attestations: [] }\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 })\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 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":["consentGroupForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachEnrollmentId","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","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","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","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","found","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","ref","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","nowMs","now","enrolledAtMs","random","results","target","supplied","decision","allowed","outcome","blocks","enrollment","contactName","name","cold","enrolledByUid","create","error","code","reason","enrollmentId","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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\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 { 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 } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentId,\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\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 addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\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}\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 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 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: consentGroupForHost(caller.org, sequence.hostId).groupId,\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 */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\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 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\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. A lead is the\n * site's own, so there is no address to resolve: the person key IS the id.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n const window = await firestore\n .collection('hosts')\n .doc(sequence.hostId)\n .collection('leads')\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\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 firestore: Firestore,\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 const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\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 ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\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(firestore, 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 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 // 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[] }\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 requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\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 const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === 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 const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\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) ?? { personalLine: '', attestations: [] }\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 })\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 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":["consentGroupForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachEnrollmentId","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","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","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","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","found","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","ref","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","nowMs","now","enrolledAtMs","random","results","target","supplied","decision","allowed","outcome","blocks","enrollment","contactName","name","cold","enrolledByUid","create","error","code","reason","enrollmentId","enrolled","result","logOrgActivity","type"],"mappings":"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"}
@@ -17,6 +17,7 @@
17
17
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
18
18
  import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
19
19
  import { outreachOrgSubject } from "../mailboxes/register-mailbox-routes.js";
20
+ import { createOutreachDoNotContactDomainsRoute } from "./do-not-contact-routes.js";
20
21
  import { createOutreachEnrollRoutes } from "./enroll-routes.js";
21
22
  import { createOutreachEnrollmentActionRoute } from "./enrollment-routes.js";
22
23
  import { createOutreachPreviewRoute } from "./preview-routes.js";
@@ -87,6 +88,9 @@ export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
87
88
  registerPluginApiRoute(OUTREACH_API_ROUTES.preview, {
88
89
  web: createOutreachPreviewRoute(deps)
89
90
  }, orgSubject);
91
+ registerPluginApiRoute(OUTREACH_API_ROUTES.doNotContactDomains, {
92
+ web: createOutreachDoNotContactDomainsRoute(deps)
93
+ }, orgSubject);
90
94
  }
91
95
 
92
96
  //# sourceMappingURL=register-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-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 { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { createOutreachEnrollmentActionRoute } from './enrollment-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n}\n"],"names":["registerPluginApiRoute","firebaseAdmin","OUTREACH_API_ROUTES","outreachOrgSubject","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","crmViewEmails","input","registerOutreachRoutes","deps","sequences","enroll","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","remove","enrollPreview","preview","confirm","enrollmentsAction"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,sBAAqB;AAC5D,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,kBAAkB,QAAQ,0CAAsC;AACzE,SAASC,0BAA0B,QAAsC,qBAAiB;AAC1F,SAASC,mCAAmC,QAAQ,yBAAqB;AACzE,SAASC,0BAA0B,QAAQ,sBAAkB;AAE7D,SAASC,4BAA4B,QAAQ,uBAAmB;AAChE,SAASC,2BAA2B,QAAQ,uBAAmB;AAE/D;;;;;;;;;;;CAWC,GAED,+EAA+E,GAC/E,MAAMC,WAAW,IAAM,MAAM,CAAC;AAE9B,wCAAwC,GACxC,OAAO,SAASC;IACd,OAAO;QACLC,eAAe,CAACC,UAAYX,cAAcY,GAAG,GAAGC,IAAI,GAAGH,aAAa,CAACC;QACrEG,uBAAuB,OAAOC,KAAKC,UAAY,AAAC,CAAA,MAAMR,UAAS,EAAGM,qBAAqB,CAACC,KAAKC;QAC7FC,2BAA2B,OAAOF,KAAKG,OAAOC,MAC5C,AAAC,CAAA,MAAMX,UAAS,EAAGS,yBAAyB,CAACF,KAAKG,OAAOC;QAC3DC,SAAS,OAAOF;gBAEYG;YAD1B,MAAMA,WAAW,MAAMrB,cAAcY,GAAG,GAAGU,SAAS,GAAGC,UAAU,CAAC,QAAQC,GAAG,CAACN,OAAOO,GAAG;YACxF,OAAOJ,SAASK,MAAM,IAAIL,iBAAAA,SAASM,IAAI,cAAbN,iBAAmB,CAAC,IAAK;QACrD;QACAO,iBAAiB,OAAOC,UAAY,AAAC,CAAA,MAAMrB,UAAS,EAAGoB,eAAe,CAACC;IACzE;AACF;AAEA,OAAO,SAASC;IACd,OAAO;QACLR,WAAW,IAAMtB,cAAcY,GAAG,GAAGU,SAAS;QAC9CS,MAAMtB;QACNuB,KAAKC,KAAKD,GAAG;QACbE,QAAQC,KAAKD,MAAM;QACnBE,gBAAgB,OAAOlB,OAAOmB,OAAOC,QAAQC,SAC3C,AAAC,CAAA,MAAM/B,UAAS,EAAG4B,cAAc,CAAClB,OAAOmB,OAAOC,QAAQC;QAC1DC,eAAe,OAAOC,QAAU,AAAC,CAAA,MAAMjC,UAAS,EAAGgC,aAAa,CAACC;IACnE;AACF;AAEA,OAAO,SAASC,uBACdC,OAAgCb,0BAA0B;IAE1D,MAAMc,YAAYtC,6BAA6BqC;IAC/C,MAAME,SAAS1C,2BAA2BwC;IAC1C,MAAMG,aAAa;QAAEC,SAAS7C;IAAmB;IACjDH,uBAAuBE,oBAAoB+C,QAAQ,EAAE;QAAEC,KAAK1C,4BAA4BoC;IAAM,GAAGG;IACjG/C,uBAAuBE,oBAAoBiD,aAAa,EAAE;QAAED,KAAKL,UAAUO,IAAI;IAAC,GAAGL;IACnF/C,uBAAuBE,oBAAoBmD,eAAe,EAAE;QAAEH,KAAKL,UAAUS,MAAM;IAAC,GAAGP;IACvF/C,uBAAuBE,oBAAoBqD,eAAe,EAAE;QAAEL,KAAKL,UAAUW,MAAM;IAAC,GAAGT;IACvF/C,uBAAuBE,oBAAoBuD,aAAa,EAAE;QAAEP,KAAKJ,OAAOY,OAAO;IAAC,GAAGX;IACnF/C,uBAAuBE,oBAAoB4C,MAAM,EAAE;QAAEI,KAAKJ,OAAOa,OAAO;IAAC,GAAGZ;IAC5E/C,uBACEE,oBAAoB0D,iBAAiB,EACrC;QAAEV,KAAK7C,oCAAoCuC;IAAM,GACjDG;IAEF/C,uBAAuBE,oBAAoBwD,OAAO,EAAE;QAAER,KAAK5C,2BAA2BsC;IAAM,GAAGG;AACjG"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-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 { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { createOutreachEnrollmentActionRoute } from './enrollment-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n}\n"],"names":["registerPluginApiRoute","firebaseAdmin","OUTREACH_API_ROUTES","outreachOrgSubject","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","crmViewEmails","input","registerOutreachRoutes","deps","sequences","enroll","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","remove","enrollPreview","preview","confirm","enrollmentsAction","doNotContactDomains"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,sBAAqB;AAC5D,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,kBAAkB,QAAQ,0CAAsC;AACzE,SAASC,sCAAsC,QAAQ,6BAAyB;AAChF,SAASC,0BAA0B,QAAsC,qBAAiB;AAC1F,SAASC,mCAAmC,QAAQ,yBAAqB;AACzE,SAASC,0BAA0B,QAAQ,sBAAkB;AAE7D,SAASC,4BAA4B,QAAQ,uBAAmB;AAChE,SAASC,2BAA2B,QAAQ,uBAAmB;AAE/D;;;;;;;;;;;CAWC,GAED,+EAA+E,GAC/E,MAAMC,WAAW,IAAM,MAAM,CAAC;AAE9B,wCAAwC,GACxC,OAAO,SAASC;IACd,OAAO;QACLC,eAAe,CAACC,UAAYZ,cAAca,GAAG,GAAGC,IAAI,GAAGH,aAAa,CAACC;QACrEG,uBAAuB,OAAOC,KAAKC,UAAY,AAAC,CAAA,MAAMR,UAAS,EAAGM,qBAAqB,CAACC,KAAKC;QAC7FC,2BAA2B,OAAOF,KAAKG,OAAOC,MAC5C,AAAC,CAAA,MAAMX,UAAS,EAAGS,yBAAyB,CAACF,KAAKG,OAAOC;QAC3DC,SAAS,OAAOF;gBAEYG;YAD1B,MAAMA,WAAW,MAAMtB,cAAca,GAAG,GAAGU,SAAS,GAAGC,UAAU,CAAC,QAAQC,GAAG,CAACN,OAAOO,GAAG;YACxF,OAAOJ,SAASK,MAAM,IAAIL,iBAAAA,SAASM,IAAI,cAAbN,iBAAmB,CAAC,IAAK;QACrD;QACAO,iBAAiB,OAAOC,UAAY,AAAC,CAAA,MAAMrB,UAAS,EAAGoB,eAAe,CAACC;IACzE;AACF;AAEA,OAAO,SAASC;IACd,OAAO;QACLR,WAAW,IAAMvB,cAAca,GAAG,GAAGU,SAAS;QAC9CS,MAAMtB;QACNuB,KAAKC,KAAKD,GAAG;QACbE,QAAQC,KAAKD,MAAM;QACnBE,gBAAgB,OAAOlB,OAAOmB,OAAOC,QAAQC,SAC3C,AAAC,CAAA,MAAM/B,UAAS,EAAG4B,cAAc,CAAClB,OAAOmB,OAAOC,QAAQC;QAC1DC,eAAe,OAAOC,QAAU,AAAC,CAAA,MAAMjC,UAAS,EAAGgC,aAAa,CAACC;IACnE;AACF;AAEA,OAAO,SAASC,uBACdC,OAAgCb,0BAA0B;IAE1D,MAAMc,YAAYtC,6BAA6BqC;IAC/C,MAAME,SAAS1C,2BAA2BwC;IAC1C,MAAMG,aAAa;QAAEC,SAAS9C;IAAmB;IACjDH,uBAAuBE,oBAAoBgD,QAAQ,EAAE;QAAEC,KAAK1C,4BAA4BoC;IAAM,GAAGG;IACjGhD,uBAAuBE,oBAAoBkD,aAAa,EAAE;QAAED,KAAKL,UAAUO,IAAI;IAAC,GAAGL;IACnFhD,uBAAuBE,oBAAoBoD,eAAe,EAAE;QAAEH,KAAKL,UAAUS,MAAM;IAAC,GAAGP;IACvFhD,uBAAuBE,oBAAoBsD,eAAe,EAAE;QAAEL,KAAKL,UAAUW,MAAM;IAAC,GAAGT;IACvFhD,uBAAuBE,oBAAoBwD,aAAa,EAAE;QAAEP,KAAKJ,OAAOY,OAAO;IAAC,GAAGX;IACnFhD,uBAAuBE,oBAAoB6C,MAAM,EAAE;QAAEI,KAAKJ,OAAOa,OAAO;IAAC,GAAGZ;IAC5EhD,uBACEE,oBAAoB2D,iBAAiB,EACrC;QAAEV,KAAK7C,oCAAoCuC;IAAM,GACjDG;IAEFhD,uBAAuBE,oBAAoByD,OAAO,EAAE;QAAER,KAAK5C,2BAA2BsC;IAAM,GAAGG;IAC/FhD,uBACEE,oBAAoB4D,mBAAmB,EACvC;QAAEX,KAAK/C,uCAAuCyC;IAAM,GACpDG;AAEJ"}