@aglyn/plugins-outreach 1.0.0-beta.274 → 1.0.0-beta.275

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 (52) hide show
  1. package/README.md +7 -2
  2. package/package.json +14 -14
  3. package/src/lib/components/enrollment-actions.d.ts +11 -4
  4. package/src/lib/components/enrollment-actions.js +16 -0
  5. package/src/lib/components/enrollment-actions.js.map +1 -1
  6. package/src/lib/components/enrollment-detail.js +40 -13
  7. package/src/lib/components/enrollment-detail.js.map +1 -1
  8. package/src/lib/components/mailbox-card.js +13 -2
  9. package/src/lib/components/mailbox-card.js.map +1 -1
  10. package/src/lib/components/record-reply-button.d.ts +52 -0
  11. package/src/lib/components/record-reply-button.js +92 -0
  12. package/src/lib/components/record-reply-button.js.map +1 -0
  13. package/src/lib/components/reply-dialog.d.ts +66 -0
  14. package/src/lib/components/reply-dialog.js +353 -0
  15. package/src/lib/components/reply-dialog.js.map +1 -0
  16. package/src/lib/components/use-outreach-api.d.ts +5 -1
  17. package/src/lib/components/use-outreach-api.js +10 -0
  18. package/src/lib/components/use-outreach-api.js.map +1 -1
  19. package/src/lib/constants/api-routes.d.ts +8 -0
  20. package/src/lib/constants/api-routes.js +6 -0
  21. package/src/lib/constants/api-routes.js.map +1 -1
  22. package/src/lib/engine/index.d.ts +1 -0
  23. package/src/lib/engine/index.js +1 -0
  24. package/src/lib/engine/index.js.map +1 -1
  25. package/src/lib/engine/manual-reply.d.ts +108 -0
  26. package/src/lib/engine/manual-reply.js +301 -0
  27. package/src/lib/engine/manual-reply.js.map +1 -0
  28. package/src/lib/model/enrollment-timeline.d.ts +3 -1
  29. package/src/lib/model/enrollment-timeline.js +83 -7
  30. package/src/lib/model/enrollment-timeline.js.map +1 -1
  31. package/src/lib/model/outreach-api.d.ts +66 -2
  32. package/src/lib/model/outreach-api.js.map +1 -1
  33. package/src/lib/model/outreach.types.d.ts +85 -3
  34. package/src/lib/model/outreach.types.js +6 -0
  35. package/src/lib/model/outreach.types.js.map +1 -1
  36. package/src/lib/model/stored-records.d.ts +7 -1
  37. package/src/lib/model/stored-records.js +37 -1
  38. package/src/lib/model/stored-records.js.map +1 -1
  39. package/src/lib/plugin.d.ts +4 -0
  40. package/src/lib/plugin.js +17 -0
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/routes/register-routes.d.ts +6 -0
  43. package/src/lib/routes/register-routes.js +18 -0
  44. package/src/lib/routes/register-routes.js.map +1 -1
  45. package/src/lib/routes/reply-routes.d.ts +76 -0
  46. package/src/lib/routes/reply-routes.js +563 -0
  47. package/src/lib/routes/reply-routes.js.map +1 -0
  48. package/src/lib/runtime/sync-job.js +14 -0
  49. package/src/lib/runtime/sync-job.js.map +1 -1
  50. package/src/lib/transport/send-message.d.ts +11 -0
  51. package/src/lib/transport/send-message.js +22 -0
  52. package/src/lib/transport/send-message.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach-api.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 CONTRACT OF OUTREACH'S SETTINGS, SEQUENCE AND ENROLLMENT ROUTES\n * (AGL-2980): what the console sends, what the routes answer, and every\n * reason one refuses. Client-safe — types and the batch limit only.\n *\n * Every request names its organization (`orgId`, in the query of a `GET`\n * and the JSON body of a `POST`): the routes serve an organization-level\n * surface that names no site.\n */\n\nimport type { OutreachComposeError } from '../engine/compose'\nimport type { OutreachGatewayStanding } from '../engine/mail-gateway'\nimport type { OutreachValidationIssue } from '../engine/sequence-validation'\nimport type { OutreachComplianceIssue } from './compliance-settings'\nimport type {\n OutreachAttestationKind,\n OutreachComplianceSettingsDocument,\n OutreachDoNotContactDomainEntry,\n OutreachEnrollment,\n OutreachSequence,\n OutreachStepOverrideSource,\n} from './outreach.types'\nimport type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft'\n\n/** Every reason these routes refuse with, for a caller to branch on. */\nexport type OutreachRouteRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'invalid-domain'\n | 'invalid-settings'\n | 'invalid-sequence'\n | 'sequence-not-found'\n | 'sequence-archived'\n | 'sequence-not-active'\n | 'activation-refused'\n | 'delete-refused'\n | 'mailbox-unavailable'\n | 'enrollment-not-found'\n | 'transition-refused'\n | 'view-not-found'\n | 'view-unsupported'\n | 'contact-not-found'\n | 'link-domain-refused'\n | 'rate-limited'\n /** The Google account has no Gmail service: off for it, or not yet provisioned. */\n | 'mail-service-unavailable'\n /** Google refused the account itself, such as an administrator barring Gmail API access. */\n | 'google-refused'\n | 'google-unavailable'\n | 'curation-unavailable'\n | 'curation-refused'\n | 'invalid-override'\n /** The step to begin enrolling at is not one the sequence has (AGL-3228). */\n | 'invalid-start-step'\n\n/** A refusal, in the one shape every route answers with. */\nexport interface OutreachRouteRefusal {\n /** A sentence written for the person reading it. */\n error: string\n reason: OutreachRouteRefusalReason\n /** The field-level reasons behind an `invalid-*` or `activation-refused` refusal. */\n issues?: ReadonlyArray<OutreachComplianceIssue | OutreachSequenceIssue>\n}\n\n/*==========================================\n * COMPLIANCE SETTINGS\n *==========================================*/\n\n/** `GET outreach/settings?orgId` — and the answer to a save. */\nexport interface OutreachSettingsResponse {\n ok: true\n settings: OutreachComplianceSettingsDocument\n}\n\n/** `POST outreach/settings` */\nexport interface OutreachSettingsSaveRequest {\n orgId: string\n legalName: string\n brandName: string\n postalAddress: string\n allowedCountries: string[]\n}\n\n/** The answer to a save: the settings as stored, and whether anything changed. */\nexport interface OutreachSettingsSaveResponse extends OutreachSettingsResponse {\n changed: boolean\n}\n\n/*==========================================\n * SEQUENCES\n *==========================================*/\n\n/** `POST outreach/sequences/save` — a new sequence without `sequenceId`, an edit with one. */\nexport interface OutreachSequenceSaveRequest {\n orgId: string\n sequenceId?: string\n sequence: OutreachSequenceDraft\n}\n\nexport interface OutreachSequenceSaveResponse {\n ok: true\n sequence: OutreachSequence\n created: boolean\n /** What the save allowed but probably isn't what was meant. */\n warnings: OutreachSequenceIssue[]\n}\n\n/** What `outreach/sequences/status` does to a sequence. */\nexport type OutreachSequenceAction = 'activate' | 'pause' | 'archive'\n\n/** `POST outreach/sequences/status` */\nexport interface OutreachSequenceStatusRequest {\n orgId: string\n sequenceId: string\n action: OutreachSequenceAction\n}\n\nexport interface OutreachSequenceStatusResponse {\n ok: true\n sequence: OutreachSequence\n /** The open enrollments an archive stopped; `0` for anything else. */\n stoppedEnrollments: number\n}\n\n/** `POST outreach/sequences/delete` — a draft nobody was ever enrolled in. */\nexport interface OutreachSequenceDeleteRequest {\n orgId: string\n sequenceId: string\n}\n\n/*==========================================\n * ENROLLING\n *==========================================*/\n\n/** The most people one preview or one enroll reads. */\nexport const OUTREACH_ENROLL_BATCH_MAX = 50\n\n/**\n * Which CRM record a person to enroll is (AGL-3234): a contact by its\n * document id, or a lead the sequence's site holds by its person key.\n */\nexport type OutreachPersonRef = { kind: 'contact'; id: string } | { kind: 'lead'; id: string }\n\n/**\n * Where the people to enroll come from: a saved Contacts or Leads view,\n * contacts picked by search, or leads picked from the sequence's site.\n */\nexport type OutreachEnrollSource =\n | { kind: 'view'; viewId: string }\n | { kind: 'contacts'; contactIds: string[] }\n | { kind: 'leads'; leadIds: string[] }\n\n/** `POST outreach/enroll/preview` */\nexport interface OutreachEnrollPreviewRequest {\n orgId: string\n sequenceId: string\n source: OutreachEnrollSource\n}\n\n/**\n * Where one person stands:\n *\n * - `eligible` — enrolled on Confirm as they are;\n * - `needs_confirmation` — enrolled once the rep writes the personal line\n * and ticks the attestations `requires` names;\n * - `blocked` — refused whatever the rep confirms, for the `blocks` given.\n */\nexport type OutreachEnrollPreviewStatus = 'eligible' | 'needs_confirmation' | 'blocked'\n\n/** One reason a person can't be enrolled, in a sentence a rep can act on. */\nexport interface OutreachEnrollBlock {\n code: string\n reason: string\n}\n\n/**\n * The mail gateway in front of one person's domain (AGL-3326), and what it\n * did with this organization's mail this week — what the Check-people chip\n * shows — with `hold` set when the engine would hold the send into it: the\n * gateway refused the sender twice in the last thirty days and delivered\n * nothing. A held person is not blocked; they default to un-ticked, and\n * ticking them is the member's say-so.\n */\nexport interface OutreachEnrollGateway extends OutreachGatewayStanding {\n hold: boolean\n}\n\nexport interface OutreachEnrollPreviewPerson {\n /** The key the dialog and the confirm name this person by: the record's own id. */\n personId: string\n /** The record the person is (AGL-3234). */\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n name: string\n /** The address the steps would go to, or `null` when the contact has none. */\n email: string | null\n /** No inbound capture behind them — the heavier rules apply. */\n cold: boolean\n /** ISO-3166-1 alpha-2, when anything names one. */\n country: string | null\n status: OutreachEnrollPreviewStatus\n /** Why a `blocked` person is refused; empty otherwise. */\n blocks: OutreachEnrollBlock[]\n /** What the rep must supply before a `needs_confirmation` person is enrolled. */\n requires: {\n personalLine: boolean\n attestations: OutreachAttestationKind[]\n }\n /** The mail gateway in front of their domain (AGL-3326); `null` when it could not be read. */\n gateway: OutreachEnrollGateway | null\n}\n\nexport interface OutreachEnrollPreviewResponse {\n ok: true\n people: OutreachEnrollPreviewPerson[]\n /** Everyone the source named, before the batch limit. */\n total: number\n /** True when the source named more people than one enrollment reads. */\n truncated: boolean\n}\n\n/**\n * One person's confirmed copy of one step, as the console sends it\n * (AGL-3324): what the member read and ticked the confirmation for. The\n * route validates it again and stamps who and when.\n */\nexport interface OutreachStepOverrideRequest {\n stepIndex: number\n /** Read only where the email starts a thread. */\n subject?: string\n body: string\n source: OutreachStepOverrideSource\n /** An AI draft the member changed before confirming it. */\n edited?: boolean\n /** The prompt the AI was given, returned by the draft route, for the audit log. */\n prompt?: string\n /** The model that drafted it, returned by the draft route. */\n model?: string\n}\n\n/** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */\nexport interface OutreachEnrollPersonRequest {\n contactId?: string\n leadId?: string\n personalLine?: string\n attestations?: OutreachAttestationKind[]\n /** The steps the member curated for this person before enrolling them (AGL-3324). */\n stepOverrides?: OutreachStepOverrideRequest[]\n}\n\n/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */\nexport interface OutreachEnrollRequest {\n orgId: string\n sequenceId: string\n people: OutreachEnrollPersonRequest[]\n /**\n * The step everyone begins at (AGL-3228), zero-based; the first when\n * absent. For people who already had the earlier steps by hand: those are\n * marked skipped, and this step goes out after its own delay, counted from\n * the enrollment. A step the sequence does not have refuses the request.\n */\n startStepIndex?: number\n}\n\ninterface OutreachEnrollOutcomePerson {\n personId: string\n target: 'contact' | 'lead'\n contactId: string\n leadId: string | null\n email: string | null\n}\n\nexport type OutreachEnrollOutcome =\n | (OutreachEnrollOutcomePerson & { outcome: 'enrolled'; enrollmentId: string })\n | (OutreachEnrollOutcomePerson & { outcome: 'blocked'; blocks: OutreachEnrollBlock[] })\n\nexport interface OutreachEnrollResponse {\n ok: true\n results: OutreachEnrollOutcome[]\n enrolled: number\n}\n\n/*==========================================\n * ENROLLMENTS\n *==========================================*/\n\n/** What a member may do to one enrollment. */\nexport type OutreachEnrollmentAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** `POST outreach/enrollments/action` */\nexport interface OutreachEnrollmentActionRequest {\n orgId: string\n enrollmentId: string\n action: OutreachEnrollmentAction\n /** Why, for a stop or a do-not-contact. */\n detail?: string\n}\n\nexport interface OutreachEnrollmentActionResponse {\n ok: true\n /** False when the enrollment was already where the action would put it. */\n changed: boolean\n enrollment: OutreachEnrollment\n /** Other open enrollments of the same address a do-not-contact stopped. */\n stoppedOthers: number\n}\n\n/*==========================================\n * DO NOT CONTACT: DOMAINS (AGL-3244)\n *==========================================*/\n\n/** `GET outreach/do-not-contact/domains?orgId` — and the answer to a change. */\nexport interface OutreachDoNotContactDomainsResponse {\n ok: true\n /** Every domain on the list, alphabetically. */\n domains: OutreachDoNotContactDomainEntry[]\n}\n\n/** `POST outreach/do-not-contact/domains` */\nexport interface OutreachDoNotContactDomainRequest {\n orgId: string\n action: 'add' | 'remove'\n /** The domain, or an address at it. */\n domain: string\n /** Why, for an add. */\n detail?: string\n}\n\n/** The answer to a change: the list as it stands, and whether anything changed. */\nexport interface OutreachDoNotContactDomainResponse extends OutreachDoNotContactDomainsResponse {\n changed: boolean\n /** The domain as the list spells it. */\n domain: string\n}\n\n/*==========================================\n * LINK DOMAINS (AGL-3306)\n *==========================================*/\n\n/** Where a mailbox domain's click-tracking host stands. */\nexport type OutreachLinkDomainStatus = 'not-set-up' | 'requested' | 'records-issued' | 'verified' | 'failed'\n\n/** One DNS record the member publishes, as the sending-domain cards print them. */\nexport interface OutreachLinkDomainRecord {\n type: string\n name: string\n value: string\n required: boolean\n note: string\n}\n\n/**\n * One sending domain of the organization's connected mailboxes, and its\n * `links.` host: what tracked links in mail from it read now, and what is\n * left to do for them to read `https://links.<domain>/<id>`.\n */\nexport interface OutreachLinkDomain {\n /** The mailboxes' sending domain. */\n domain: string\n /** `links.<domain>`. */\n host: string\n status: OutreachLinkDomainStatus\n /** The records to publish; empty until the host is set up. */\n records: OutreachLinkDomainRecord[]\n /** Why it is not verified, in a sentence; `null` when nothing is wrong. */\n detail: string | null\n /** How a tracked link in mail from this domain starts today. */\n linkPrefix: string | null\n checkedAtMs: number | null\n verifiedAtMs: number | null\n}\n\n/** `GET outreach/link-domains?orgId` */\nexport interface OutreachLinkDomainsResponse {\n ok: true\n domains: OutreachLinkDomain[]\n /** Whether the member may set a host up, check it, or remove it. */\n canManage: boolean\n}\n\n/** `POST outreach/link-domains` */\nexport interface OutreachLinkDomainRequest {\n orgId: string\n domain: string\n action: 'set-up' | 'check' | 'remove'\n}\n\nexport interface OutreachLinkDomainResponse extends OutreachLinkDomainsResponse {\n /** The domain the action was for. */\n domain: string\n}\n\n/*==========================================\n * PREVIEW\n *==========================================*/\n\n/** `POST outreach/preview` — one email of a saved sequence, as a person would get it. */\nexport interface OutreachPreviewRequest {\n orgId: string\n sequenceId: string\n /** The contact to write it to; a sample person when absent. */\n contactId?: string\n /** Or a lead the sequence's site holds (AGL-3234), written to as the contact it would be. */\n leadId?: string\n personalLine?: string\n /** Which step; the first email when absent. */\n stepIndex?: number\n /** The person's copies of steps as the dialog holds them (AGL-3324), so the preview shows the curated version. */\n stepOverrides?: OutreachStepOverrideRequest[]\n /**\n * The step the enrollment would begin at (AGL-3228), zero-based: the\n * first email written is the first one sent from there.\n */\n startStepIndex?: number\n}\n\nexport interface OutreachPreviewResponse {\n ok: true\n stepIndex: number\n subject: string\n /** The plain text, footer included. */\n text: string\n unresolvedFields: string[]\n /** Why it could not be written, when it could not. */\n error: OutreachComposeError | null\n}\n\n/*==========================================\n * A TEST OF ONE STEP (AGL-3325)\n *==========================================*/\n\n/**\n * `POST outreach/steps/test` — one step of a saved sequence, rendered\n * exactly as `outreach/preview` renders it (the same person, personal\n * line and step) and sent through the sequence's mailbox to the member.\n */\nexport interface OutreachStepTestRequest extends OutreachPreviewRequest {\n /** Where the test goes; the member's own address when absent or empty. */\n to?: string\n}\n\nexport interface OutreachStepTestResponse {\n ok: true\n stepIndex: number\n /** The address the test went to. */\n sentTo: string\n /** The subject as sent, `[Test]` prefix included. */\n subject: string\n sentAtMs: number\n /** How many tests the mailbox has sent on its local day, this one included. */\n testsToday: number\n}\n\n/*==========================================\n * CURATING (AGL-3324)\n *==========================================*/\n\n/**\n * `POST outreach/curate/draft` — this person's own copies of the sequence's\n * email steps, drafted by the workspace's AI. Names EITHER a person about to\n * be enrolled (the sequence, a contact or a lead, the personal line the rep\n * is writing) OR an enrollment, whose next email step is drafted. Nothing is\n * stored: the answer is what the member reads before confirming.\n */\nexport interface OutreachCurateDraftRequest {\n orgId: string\n sequenceId?: string\n contactId?: string\n leadId?: string\n personalLine?: string\n enrollmentId?: string\n /** Which email steps; every email step for a person to enroll, the next one for an enrollment, when absent. */\n stepIndexes?: number[]\n}\n\n/** One drafted step, with what the validator said about it. */\nexport interface OutreachCurateDraft {\n stepIndex: number\n /** `null` for an in-thread email, which is sent as `Re:` the thread's subject. */\n subject: string | null\n body: string\n /** Errors and warnings; a draft with an error cannot be confirmed as it stands. */\n issues: OutreachValidationIssue[]\n}\n\nexport interface OutreachCurateDraftResponse {\n ok: true\n drafts: OutreachCurateDraft[]\n /** The prompt the AI was given and the model that answered, for the override's audit record. */\n prompt: string\n model: string\n}\n\n/** `POST outreach/curate/save` — stores a confirmed copy of one step on an enrollment, or clears it with `override: null`. */\nexport interface OutreachCurateSaveRequest {\n orgId: string\n enrollmentId: string\n stepIndex: number\n override: Omit<OutreachStepOverrideRequest, 'stepIndex'> | null\n}\n\nexport interface OutreachCurateSaveResponse {\n ok: true\n enrollment: OutreachEnrollment\n}\n\n/**\n * `POST outreach/enrollments/gateway` (AGL-3332): the mail gateway in front\n * of one enrolled person's domain, for their page's Details.\n *\n * Read on the server because both halves are: the domain's MX lives in the\n * platform's `mailDomains` cache (AGL-3328), which no member reads, and its\n * standing is the ledger of the sequence mailbox's sending domain.\n */\nexport interface OutreachEnrollmentGatewayResponse {\n ok: true\n /** The person's domain; `null` for an address without one. */\n domain: string | null\n /** The domain the enrollment's mailbox sends from, whose ledger `standing` reads. */\n sendingDomain: string | null\n /** The domain's MX, classified; `null` when it could not be looked up. */\n intel: {\n status: 'mx' | 'implicit_mx' | 'null_mx' | 'no_mx'\n mx: string[]\n gateway: OutreachGatewayStanding['gateway']\n resolvedAtMs: number\n } | null\n /** What that gateway did with mail from the sending domain; `null` when unread. */\n standing: OutreachGatewayStanding | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAiID;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach-api.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 CONTRACT OF OUTREACH'S SETTINGS, SEQUENCE AND ENROLLMENT ROUTES\n * (AGL-2980): what the console sends, what the routes answer, and every\n * reason one refuses. Client-safe — types and the batch limit only.\n *\n * Every request names its organization (`orgId`, in the query of a `GET`\n * and the JSON body of a `POST`): the routes serve an organization-level\n * surface that names no site.\n */\n\nimport type { OutreachComposeError } from '../engine/compose'\nimport type { OutreachGatewayStanding } from '../engine/mail-gateway'\nimport type { OutreachManualReplyRefusalCode } from '../engine/manual-reply'\nimport type { OutreachValidationIssue } from '../engine/sequence-validation'\nimport type { OutreachComplianceIssue } from './compliance-settings'\nimport type {\n OutreachAttestationKind,\n OutreachComplianceSettingsDocument,\n OutreachDoNotContactDomainEntry,\n OutreachEnrollment,\n OutreachInboundMessage,\n OutreachMailbox,\n OutreachSequence,\n OutreachStepOverrideSource,\n OutreachStepRecord,\n} from './outreach.types'\nimport type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft'\n\n/** Every reason these routes refuse with, for a caller to branch on. */\nexport type OutreachRouteRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'invalid-domain'\n | 'invalid-settings'\n | 'invalid-sequence'\n | 'sequence-not-found'\n | 'sequence-archived'\n | 'sequence-not-active'\n | 'activation-refused'\n | 'delete-refused'\n | 'mailbox-unavailable'\n | 'enrollment-not-found'\n | 'transition-refused'\n | 'view-not-found'\n | 'view-unsupported'\n | 'contact-not-found'\n | 'link-domain-refused'\n | 'rate-limited'\n /** The Google account has no Gmail service: off for it, or not yet provisioned. */\n | 'mail-service-unavailable'\n /** Google refused the account itself, such as an administrator barring Gmail API access. */\n | 'google-refused'\n | 'google-unavailable'\n | 'curation-unavailable'\n | 'curation-refused'\n | 'invalid-override'\n /** The step to begin enrolling at is not one the sequence has (AGL-3228). */\n | 'invalid-start-step'\n /** An answer to a person who wrote back cannot go now: a list, their state, the mailbox (AGL-4020). */\n | 'reply-refused'\n /** The enrollment kept nothing the person wrote, and the mailbox held none either (AGL-4020). */\n | 'nothing-to-answer'\n /** An answer to them is already being sent (AGL-4020). */\n | 'reply-in-flight'\n\n/** A refusal, in the one shape every route answers with. */\nexport interface OutreachRouteRefusal {\n /** A sentence written for the person reading it. */\n error: string\n reason: OutreachRouteRefusalReason\n /** The field-level reasons behind an `invalid-*` or `activation-refused` refusal. */\n issues?: ReadonlyArray<OutreachComplianceIssue | OutreachSequenceIssue>\n}\n\n/*==========================================\n * COMPLIANCE SETTINGS\n *==========================================*/\n\n/** `GET outreach/settings?orgId` — and the answer to a save. */\nexport interface OutreachSettingsResponse {\n ok: true\n settings: OutreachComplianceSettingsDocument\n}\n\n/** `POST outreach/settings` */\nexport interface OutreachSettingsSaveRequest {\n orgId: string\n legalName: string\n brandName: string\n postalAddress: string\n allowedCountries: string[]\n}\n\n/** The answer to a save: the settings as stored, and whether anything changed. */\nexport interface OutreachSettingsSaveResponse extends OutreachSettingsResponse {\n changed: boolean\n}\n\n/*==========================================\n * SEQUENCES\n *==========================================*/\n\n/** `POST outreach/sequences/save` — a new sequence without `sequenceId`, an edit with one. */\nexport interface OutreachSequenceSaveRequest {\n orgId: string\n sequenceId?: string\n sequence: OutreachSequenceDraft\n}\n\nexport interface OutreachSequenceSaveResponse {\n ok: true\n sequence: OutreachSequence\n created: boolean\n /** What the save allowed but probably isn't what was meant. */\n warnings: OutreachSequenceIssue[]\n}\n\n/** What `outreach/sequences/status` does to a sequence. */\nexport type OutreachSequenceAction = 'activate' | 'pause' | 'archive'\n\n/** `POST outreach/sequences/status` */\nexport interface OutreachSequenceStatusRequest {\n orgId: string\n sequenceId: string\n action: OutreachSequenceAction\n}\n\nexport interface OutreachSequenceStatusResponse {\n ok: true\n sequence: OutreachSequence\n /** The open enrollments an archive stopped; `0` for anything else. */\n stoppedEnrollments: number\n}\n\n/** `POST outreach/sequences/delete` — a draft nobody was ever enrolled in. */\nexport interface OutreachSequenceDeleteRequest {\n orgId: string\n sequenceId: string\n}\n\n/*==========================================\n * ENROLLING\n *==========================================*/\n\n/** The most people one preview or one enroll reads. */\nexport const OUTREACH_ENROLL_BATCH_MAX = 50\n\n/**\n * Which CRM record a person to enroll is (AGL-3234): a contact by its\n * document id, or a lead the sequence's site holds by its person key.\n */\nexport type OutreachPersonRef = { kind: 'contact'; id: string } | { kind: 'lead'; id: string }\n\n/**\n * Where the people to enroll come from: a saved Contacts or Leads view,\n * contacts picked by search, or leads picked from the sequence's site.\n */\nexport type OutreachEnrollSource =\n | { kind: 'view'; viewId: string }\n | { kind: 'contacts'; contactIds: string[] }\n | { kind: 'leads'; leadIds: string[] }\n\n/** `POST outreach/enroll/preview` */\nexport interface OutreachEnrollPreviewRequest {\n orgId: string\n sequenceId: string\n source: OutreachEnrollSource\n}\n\n/**\n * Where one person stands:\n *\n * - `eligible` — enrolled on Confirm as they are;\n * - `needs_confirmation` — enrolled once the rep writes the personal line\n * and ticks the attestations `requires` names;\n * - `blocked` — refused whatever the rep confirms, for the `blocks` given.\n */\nexport type OutreachEnrollPreviewStatus = 'eligible' | 'needs_confirmation' | 'blocked'\n\n/** One reason a person can't be enrolled, in a sentence a rep can act on. */\nexport interface OutreachEnrollBlock {\n code: string\n reason: string\n}\n\n/**\n * The mail gateway in front of one person's domain (AGL-3326), and what it\n * did with this organization's mail this week — what the Check-people chip\n * shows — with `hold` set when the engine would hold the send into it: the\n * gateway refused the sender twice in the last thirty days and delivered\n * nothing. A held person is not blocked; they default to un-ticked, and\n * ticking them is the member's say-so.\n */\nexport interface OutreachEnrollGateway extends OutreachGatewayStanding {\n hold: boolean\n}\n\nexport interface OutreachEnrollPreviewPerson {\n /** The key the dialog and the confirm name this person by: the record's own id. */\n personId: string\n /** The record the person is (AGL-3234). */\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n name: string\n /** The address the steps would go to, or `null` when the contact has none. */\n email: string | null\n /** No inbound capture behind them — the heavier rules apply. */\n cold: boolean\n /** ISO-3166-1 alpha-2, when anything names one. */\n country: string | null\n status: OutreachEnrollPreviewStatus\n /** Why a `blocked` person is refused; empty otherwise. */\n blocks: OutreachEnrollBlock[]\n /** What the rep must supply before a `needs_confirmation` person is enrolled. */\n requires: {\n personalLine: boolean\n attestations: OutreachAttestationKind[]\n }\n /** The mail gateway in front of their domain (AGL-3326); `null` when it could not be read. */\n gateway: OutreachEnrollGateway | null\n}\n\nexport interface OutreachEnrollPreviewResponse {\n ok: true\n people: OutreachEnrollPreviewPerson[]\n /** Everyone the source named, before the batch limit. */\n total: number\n /** True when the source named more people than one enrollment reads. */\n truncated: boolean\n}\n\n/**\n * One person's confirmed copy of one step, as the console sends it\n * (AGL-3324): what the member read and ticked the confirmation for. The\n * route validates it again and stamps who and when.\n */\nexport interface OutreachStepOverrideRequest {\n stepIndex: number\n /** Read only where the email starts a thread. */\n subject?: string\n body: string\n source: OutreachStepOverrideSource\n /** An AI draft the member changed before confirming it. */\n edited?: boolean\n /** The prompt the AI was given, returned by the draft route, for the audit log. */\n prompt?: string\n /** The model that drafted it, returned by the draft route. */\n model?: string\n}\n\n/** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */\nexport interface OutreachEnrollPersonRequest {\n contactId?: string\n leadId?: string\n personalLine?: string\n attestations?: OutreachAttestationKind[]\n /** The steps the member curated for this person before enrolling them (AGL-3324). */\n stepOverrides?: OutreachStepOverrideRequest[]\n}\n\n/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */\nexport interface OutreachEnrollRequest {\n orgId: string\n sequenceId: string\n people: OutreachEnrollPersonRequest[]\n /**\n * The step everyone begins at (AGL-3228), zero-based; the first when\n * absent. For people who already had the earlier steps by hand: those are\n * marked skipped, and this step goes out after its own delay, counted from\n * the enrollment. A step the sequence does not have refuses the request.\n */\n startStepIndex?: number\n}\n\ninterface OutreachEnrollOutcomePerson {\n personId: string\n target: 'contact' | 'lead'\n contactId: string\n leadId: string | null\n email: string | null\n}\n\nexport type OutreachEnrollOutcome =\n | (OutreachEnrollOutcomePerson & { outcome: 'enrolled'; enrollmentId: string })\n | (OutreachEnrollOutcomePerson & { outcome: 'blocked'; blocks: OutreachEnrollBlock[] })\n\nexport interface OutreachEnrollResponse {\n ok: true\n results: OutreachEnrollOutcome[]\n enrolled: number\n}\n\n/*==========================================\n * ENROLLMENTS\n *==========================================*/\n\n/** What a member may do to one enrollment. */\nexport type OutreachEnrollmentAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** `POST outreach/enrollments/action` */\nexport interface OutreachEnrollmentActionRequest {\n orgId: string\n enrollmentId: string\n action: OutreachEnrollmentAction\n /** Why, for a stop or a do-not-contact. */\n detail?: string\n}\n\nexport interface OutreachEnrollmentActionResponse {\n ok: true\n /** False when the enrollment was already where the action would put it. */\n changed: boolean\n enrollment: OutreachEnrollment\n /** Other open enrollments of the same address a do-not-contact stopped. */\n stoppedOthers: number\n}\n\n/*==========================================\n * ANSWERING A PERSON WHO WROTE BACK (AGL-4020)\n *==========================================*/\n\n/** `POST outreach/enrollments/thread` — what the reply composer opens with. */\nexport interface OutreachEnrollmentThreadRequest {\n orgId: string\n enrollmentId: string\n}\n\n/** Where an answer would go, from what, under what subject. */\nexport interface OutreachReplyDraft {\n /** The provider id of the message it answers: the newest the person wrote. */\n answerId: string\n to: string\n subject: string\n from: { address: string; name: string; provider: OutreachMailbox['provider'] }\n}\n\nexport interface OutreachEnrollmentThreadResponse {\n ok: true\n /** The enrollment as stored, with anything the mailbox read added to it. */\n enrollment: OutreachEnrollment\n /** What the person wrote, oldest first. */\n inboundMessages: OutreachInboundMessage[]\n /** `null` when they wrote nothing that could be read. */\n draft: OutreachReplyDraft | null\n /** Why an answer cannot go now, in a sentence; `null` when it can. */\n refusal: { code: OutreachManualReplyRefusalCode; message: string } | null\n /** Whether the mailbox was read for messages the enrollment had not kept. */\n readMailbox: boolean\n}\n\n/** `POST outreach/enrollments/reply` — sends a member's answer in the thread. */\nexport interface OutreachEnrollmentReplyRequest {\n orgId: string\n enrollmentId: string\n /** What the member wrote: plain text. */\n body: string\n /** The message it answers, by its provider id; the newest the person wrote when omitted. */\n answerId?: string\n /**\n * A key the composer mints once per answer, which becomes the answer's\n * `Message-ID`: a retry of the same answer is recognised, never sent twice.\n */\n replyKey?: string\n}\n\nexport interface OutreachEnrollmentReplyResponse {\n ok: true\n enrollment: OutreachEnrollment\n /** The `manual_reply` record the answer was kept as. */\n record: OutreachStepRecord\n sentTo: string\n subject: string\n /** The same answer had already been sent: nothing went out again. */\n alreadySent: boolean\n}\n\n/*==========================================\n * DO NOT CONTACT: DOMAINS (AGL-3244)\n *==========================================*/\n\n/** `GET outreach/do-not-contact/domains?orgId` — and the answer to a change. */\nexport interface OutreachDoNotContactDomainsResponse {\n ok: true\n /** Every domain on the list, alphabetically. */\n domains: OutreachDoNotContactDomainEntry[]\n}\n\n/** `POST outreach/do-not-contact/domains` */\nexport interface OutreachDoNotContactDomainRequest {\n orgId: string\n action: 'add' | 'remove'\n /** The domain, or an address at it. */\n domain: string\n /** Why, for an add. */\n detail?: string\n}\n\n/** The answer to a change: the list as it stands, and whether anything changed. */\nexport interface OutreachDoNotContactDomainResponse extends OutreachDoNotContactDomainsResponse {\n changed: boolean\n /** The domain as the list spells it. */\n domain: string\n}\n\n/*==========================================\n * LINK DOMAINS (AGL-3306)\n *==========================================*/\n\n/** Where a mailbox domain's click-tracking host stands. */\nexport type OutreachLinkDomainStatus = 'not-set-up' | 'requested' | 'records-issued' | 'verified' | 'failed'\n\n/** One DNS record the member publishes, as the sending-domain cards print them. */\nexport interface OutreachLinkDomainRecord {\n type: string\n name: string\n value: string\n required: boolean\n note: string\n}\n\n/**\n * One sending domain of the organization's connected mailboxes, and its\n * `links.` host: what tracked links in mail from it read now, and what is\n * left to do for them to read `https://links.<domain>/<id>`.\n */\nexport interface OutreachLinkDomain {\n /** The mailboxes' sending domain. */\n domain: string\n /** `links.<domain>`. */\n host: string\n status: OutreachLinkDomainStatus\n /** The records to publish; empty until the host is set up. */\n records: OutreachLinkDomainRecord[]\n /** Why it is not verified, in a sentence; `null` when nothing is wrong. */\n detail: string | null\n /** How a tracked link in mail from this domain starts today. */\n linkPrefix: string | null\n checkedAtMs: number | null\n verifiedAtMs: number | null\n}\n\n/** `GET outreach/link-domains?orgId` */\nexport interface OutreachLinkDomainsResponse {\n ok: true\n domains: OutreachLinkDomain[]\n /** Whether the member may set a host up, check it, or remove it. */\n canManage: boolean\n}\n\n/** `POST outreach/link-domains` */\nexport interface OutreachLinkDomainRequest {\n orgId: string\n domain: string\n action: 'set-up' | 'check' | 'remove'\n}\n\nexport interface OutreachLinkDomainResponse extends OutreachLinkDomainsResponse {\n /** The domain the action was for. */\n domain: string\n}\n\n/*==========================================\n * PREVIEW\n *==========================================*/\n\n/** `POST outreach/preview` — one email of a saved sequence, as a person would get it. */\nexport interface OutreachPreviewRequest {\n orgId: string\n sequenceId: string\n /** The contact to write it to; a sample person when absent. */\n contactId?: string\n /** Or a lead the sequence's site holds (AGL-3234), written to as the contact it would be. */\n leadId?: string\n personalLine?: string\n /** Which step; the first email when absent. */\n stepIndex?: number\n /** The person's copies of steps as the dialog holds them (AGL-3324), so the preview shows the curated version. */\n stepOverrides?: OutreachStepOverrideRequest[]\n /**\n * The step the enrollment would begin at (AGL-3228), zero-based: the\n * first email written is the first one sent from there.\n */\n startStepIndex?: number\n}\n\nexport interface OutreachPreviewResponse {\n ok: true\n stepIndex: number\n subject: string\n /** The plain text, footer included. */\n text: string\n unresolvedFields: string[]\n /** Why it could not be written, when it could not. */\n error: OutreachComposeError | null\n}\n\n/*==========================================\n * A TEST OF ONE STEP (AGL-3325)\n *==========================================*/\n\n/**\n * `POST outreach/steps/test` — one step of a saved sequence, rendered\n * exactly as `outreach/preview` renders it (the same person, personal\n * line and step) and sent through the sequence's mailbox to the member.\n */\nexport interface OutreachStepTestRequest extends OutreachPreviewRequest {\n /** Where the test goes; the member's own address when absent or empty. */\n to?: string\n}\n\nexport interface OutreachStepTestResponse {\n ok: true\n stepIndex: number\n /** The address the test went to. */\n sentTo: string\n /** The subject as sent, `[Test]` prefix included. */\n subject: string\n sentAtMs: number\n /** How many tests the mailbox has sent on its local day, this one included. */\n testsToday: number\n}\n\n/*==========================================\n * CURATING (AGL-3324)\n *==========================================*/\n\n/**\n * `POST outreach/curate/draft` — this person's own copies of the sequence's\n * email steps, drafted by the workspace's AI. Names EITHER a person about to\n * be enrolled (the sequence, a contact or a lead, the personal line the rep\n * is writing) OR an enrollment, whose next email step is drafted. Nothing is\n * stored: the answer is what the member reads before confirming.\n */\nexport interface OutreachCurateDraftRequest {\n orgId: string\n sequenceId?: string\n contactId?: string\n leadId?: string\n personalLine?: string\n enrollmentId?: string\n /** Which email steps; every email step for a person to enroll, the next one for an enrollment, when absent. */\n stepIndexes?: number[]\n}\n\n/** One drafted step, with what the validator said about it. */\nexport interface OutreachCurateDraft {\n stepIndex: number\n /** `null` for an in-thread email, which is sent as `Re:` the thread's subject. */\n subject: string | null\n body: string\n /** Errors and warnings; a draft with an error cannot be confirmed as it stands. */\n issues: OutreachValidationIssue[]\n}\n\nexport interface OutreachCurateDraftResponse {\n ok: true\n drafts: OutreachCurateDraft[]\n /** The prompt the AI was given and the model that answered, for the override's audit record. */\n prompt: string\n model: string\n}\n\n/** `POST outreach/curate/save` — stores a confirmed copy of one step on an enrollment, or clears it with `override: null`. */\nexport interface OutreachCurateSaveRequest {\n orgId: string\n enrollmentId: string\n stepIndex: number\n override: Omit<OutreachStepOverrideRequest, 'stepIndex'> | null\n}\n\nexport interface OutreachCurateSaveResponse {\n ok: true\n enrollment: OutreachEnrollment\n}\n\n/**\n * `POST outreach/enrollments/gateway` (AGL-3332): the mail gateway in front\n * of one enrolled person's domain, for their page's Details.\n *\n * Read on the server because both halves are: the domain's MX lives in the\n * platform's `mailDomains` cache (AGL-3328), which no member reads, and its\n * standing is the ledger of the sequence mailbox's sending domain.\n */\nexport interface OutreachEnrollmentGatewayResponse {\n ok: true\n /** The person's domain; `null` for an address without one. */\n domain: string | null\n /** The domain the enrollment's mailbox sends from, whose ledger `standing` reads. */\n sendingDomain: string | null\n /** The domain's MX, classified; `null` when it could not be looked up. */\n intel: {\n status: 'mx' | 'implicit_mx' | 'null_mx' | 'no_mx'\n mx: string[]\n gateway: OutreachGatewayStanding['gateway']\n resolvedAtMs: number\n } | null\n /** What that gateway did with mail from the sending domain; `null` when unread. */\n standing: OutreachGatewayStanding | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GA2ID;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
@@ -158,6 +158,13 @@ export interface OutreachMailboxDailyHealth {
158
158
  * limits, but it is not a person emailed and never moves the daily cap.
159
159
  */
160
160
  tests?: number;
161
+ /**
162
+ * Answers a member wrote and sent in a person's thread (AGL-4020). Counted
163
+ * apart from `sent` for the same reasons as `tests`: a real send, but no
164
+ * step of a sequence, so it never spends the daily cap, never joins the
165
+ * sends a bounce rate is judged over, and never pauses the mailbox.
166
+ */
167
+ manualReplies?: number;
161
168
  }
162
169
  /**
163
170
  * The counters a mailbox's sending is judged by.
@@ -804,10 +811,27 @@ export interface OutreachEnrollment extends OutreachTimestamps {
804
811
  * or `null` while no run holds one.
805
812
  */
806
813
  sendClaim?: OutreachSendClaim | null;
807
- /** Every step the runtime ran, oldest first (AGL-2981). */
814
+ /**
815
+ * Every step the runtime ran, oldest first (AGL-2981), and every answer a
816
+ * member sent in the thread (`manual_reply`, AGL-4020).
817
+ */
808
818
  stepRecords?: OutreachStepRecord[];
809
819
  /** Gmail ids of this enrollment's thread messages the sync has handled, the last hundred. */
810
820
  syncedMessageIds?: string[];
821
+ /**
822
+ * What the person wrote back (AGL-4020): every reply, automatic answer and
823
+ * opt-out the sync read from them, oldest first, the last
824
+ * `OUTREACH_INBOUND_MESSAGES_MAX`. Absent until the first one is read —
825
+ * including on an enrollment whose reply was synced before they were kept,
826
+ * which the reply composer reads from the mailbox instead.
827
+ */
828
+ inboundMessages?: OutreachInboundMessage[];
829
+ /**
830
+ * A member's answer being sent (AGL-4020): taken in a transaction before
831
+ * the send, cleared when it is recorded, so a second click cannot send it
832
+ * twice. Absent or `null` while none is in flight.
833
+ */
834
+ replyClaim?: OutreachReplyClaim | null;
811
835
  /**
812
836
  * What this person did with the links they were sent (AGL-3239). Absent
813
837
  * until their first click, which is why the table reads an absence as
@@ -897,12 +921,70 @@ export interface OutreachSkippedStep {
897
921
  /** When it was marked skipped: the enrollment. */
898
922
  atMs: number;
899
923
  }
900
- /** One step the runtime ran for an enrollment (AGL-2981). */
924
+ /** The most of a person's messages one enrollment keeps (AGL-4020). */
925
+ export declare const OUTREACH_INBOUND_MESSAGES_MAX = 20;
926
+ /** What a message the person wrote is, as the sync classified it (AGL-4020). */
927
+ export declare const OUTREACH_INBOUND_KINDS: readonly ["reply", "auto_reply", "opt_out"];
928
+ export type OutreachInboundKind = (typeof OUTREACH_INBOUND_KINDS)[number];
929
+ /**
930
+ * One message the person wrote in the enrollment's threads (AGL-4020), kept
931
+ * so the console can show it and answer it in the same thread.
932
+ */
933
+ export interface OutreachInboundMessage {
934
+ /** The provider's id for it. */
935
+ id: string;
936
+ /** The provider's thread it is in: Gmail's thread id, Graph's conversation id. */
937
+ threadId: string | null;
938
+ /** Its `Message-ID`, angle brackets included, or `null` when it carried none. */
939
+ messageId: string | null;
940
+ /** The `Message-ID`s its `References` names, oldest first. */
941
+ references: string[];
942
+ /** The sender's address, normalized. */
943
+ from: string;
944
+ /** The sender's display name, `''` when the `From` named none. */
945
+ fromName: string;
946
+ /** The `Reply-To` address it asked answers to go to, when one differs from `from`. */
947
+ replyTo: string | null;
948
+ subject: string;
949
+ /** When the provider received it. */
950
+ atMs: number;
951
+ kind: OutreachInboundKind;
952
+ /** The plain text they wrote, the quoted history below it trimmed, bounded. */
953
+ body: string;
954
+ }
955
+ /** A member's answer in flight (AGL-4020) — see {@link OutreachEnrollment.replyClaim}. */
956
+ export interface OutreachReplyClaim {
957
+ /** The `Message-ID` the answer goes out with, minted before the send. */
958
+ messageId: string;
959
+ byUid: string;
960
+ atMs: number;
961
+ }
962
+ /**
963
+ * One step the runtime ran for an enrollment (AGL-2981) — or, as
964
+ * `manual_reply`, one answer a member wrote and sent in the person's thread
965
+ * (AGL-4020). A `manual_reply` is no step of the sequence: it never moves
966
+ * `stepIndex`, never counts as a send in the sequence's figures, and every
967
+ * reader that counts sends reads `kind === 'email'`.
968
+ */
901
969
  export interface OutreachStepRecord {
902
970
  stepIndex: number;
903
971
  stepId: string;
904
- kind: 'email' | 'task';
972
+ kind: 'email' | 'task' | 'manual_reply';
905
973
  atMs: number;
974
+ /** The member who wrote and sent a `manual_reply`. */
975
+ byUid?: string;
976
+ /** The address a `manual_reply` went from, and the one it went to. */
977
+ from?: string;
978
+ to?: string;
979
+ /** A `manual_reply`'s text as sent, bounded. */
980
+ body?: string;
981
+ /** The `Message-ID` a `manual_reply` answered. */
982
+ inReplyTo?: string;
983
+ /**
984
+ * The key the composer minted for a `manual_reply`, which a retry of the
985
+ * same answer is known by whatever `Message-ID` the provider reported.
986
+ */
987
+ replyKey?: string;
906
988
  /** Gmail's id for the sent message. */
907
989
  gmailMessageId?: string;
908
990
  /** Gmail's thread the message went into. */
@@ -311,6 +311,12 @@ export const OUTREACH_TASK_KIND_LABELS = {
311
311
  'contact',
312
312
  'lead'
313
313
  ];
314
+ /** The most of a person's messages one enrollment keeps (AGL-4020). */ export const OUTREACH_INBOUND_MESSAGES_MAX = 20;
315
+ /** What a message the person wrote is, as the sync classified it (AGL-4020). */ export const OUTREACH_INBOUND_KINDS = [
316
+ 'reply',
317
+ 'auto_reply',
318
+ 'opt_out'
319
+ ];
314
320
  /** The id of the compliance document in the `settings` collection. */ export const OUTREACH_COMPLIANCE_SETTINGS_ID = 'compliance';
315
321
  /*==========================================
316
322
  * DO NOT CONTACT (AGL-2980).
@@ -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\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport type { OutreachOpenSource } from '../engine/open-source'\nimport type { OutreachGatewayDayCounts, OutreachMailGateway } from '../engine/mail-gateway'\n\n/**\n * Where Outreach keeps its records.\n *\n * Eight collections under the organization, and two 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 ones — credentials, and the short\n * tracking links (AGL-3297) — are keyed by an `orgId` FIELD that a\n * path-scoped delete of `orgs/{orgId}` cannot see: the org erasure sweeps\n * them 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 * `orgs/{orgId}/outreachDomainIntel/{domain}` — a recipient domain's MX\n * as this organization first cached it (AGL-3326). Neither read nor\n * written any more: the platform-wide `mailDomains` cache replaced it\n * (AGL-3328), and it is retired with its rule once that has shipped.\n */\n domainIntel: 'outreachDomainIntel',\n /**\n * `orgs/{orgId}/outreachGatewayStats/{gateway}` — the organization's\n * ledger per mail gateway as first written (AGL-3326), with no sending\n * domain. READ THROUGH, never written: its last thirty days are summed\n * into the sending-domain ledger's standing\n * (`orgs/{orgId}/mailGatewayLedger`, AGL-3328) until they age out, and it\n * is retired with its rule after that.\n */\n gatewayStats: 'outreachGatewayStats',\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 /**\n * `outreachLinks/{linkId}` — what one short tracking link in a sent email\n * points at (AGL-3297): the organization, the enrollment, the step, the\n * link's index and the destination. TOP-LEVEL so the link's id alone\n * resolves it with one read, keyed by an `orgId` FIELD that the org\n * erasure sweeps, and closed to every client.\n */\n links: 'outreachLinks',\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' | 'links'\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/**\n * The mail providers a mailbox can be connected through: a Google Workspace\n * account (Gmail), or a Microsoft 365 account through Microsoft Graph\n * (AGL-3489).\n */\nexport const OUTREACH_MAILBOX_PROVIDERS = ['google', 'microsoft'] 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 * Tests of a sequence step sent to a member (AGL-3325). Counted apart from\n * `sent`: a test is a real Gmail send, so it spends the account's own\n * limits, but it is not a person emailed and never moves the daily cap.\n */\n tests?: 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 /**\n * `bounce_rate` is what the window rule wrote before AGL-3326 changed it\n * from a percentage to a count; a stored pause keeps the reason it was\n * written with, and the card shows `message` either way.\n */\n reason: 'bounces_today' | 'bounces_in_window' | '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 * short link of ours that forwards to it. Opens are the separate\n * {@link countOpens}, because a pixel needs an HTML part.\n */\n trackClicks: boolean\n /**\n * Whether this sequence's emails carry an HTML part with a tracking image,\n * so opens are counted (AGL-3395). Default `false`, and `false` on every\n * sequence written before the setting existed.\n *\n * On, each email goes out as `multipart/alternative`: the plain-text part\n * exactly as it would have been, and beside it the same words as HTML with\n * a 1×1 image on the short-link host. It costs the plain-text one-to-one\n * look, and some gateways score an HTML part with a remote image — see\n * `../engine/open-tracking.ts`. Off, nothing about the email changes and\n * the report says opens are not measured.\n */\n countOpens: boolean\n /**\n * Whether this sequence's emails carry `List-Unsubscribe` and\n * `List-Unsubscribe-Post` (RFC 8058) (AGL-3296). Default `false`, and\n * `false` on every sequence written before the setting existed.\n *\n * On, mail clients draw their own unsubscribe button — Apple Mail as \"This\n * message is from a mailing list\" — which is the easiest way out for the\n * recipient and makes a one-to-one email read as bulk. Off, the footer's\n * \"reply 'no'\" line is the way out; the footer and its postal address are\n * mandatory either way, and the reply classifier stops on a \"no\". The\n * Gmail/Yahoo one-click requirement binds bulk senders (5,000 a day), not a\n * mailbox sending a sequence.\n */\n listUnsubscribe: 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 `send-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 * At least one email of this sequence went out with a tracking image\n * (AGL-3395). Absent is \"never recorded\", and the report says opens are\n * not measured rather than showing 0%.\n */\n openTracked?: boolean\n /**\n * Distinct enrollments that have had at least one email carrying the\n * image — the open rate's denominator. Not `people`: a sequence that turned\n * the setting on halfway through emailed some people no image at all, and\n * they could never have been counted as opening.\n */\n openPeople?: number\n /** Opens judged a person's. One reader opening twice counts two. */\n opens?: number\n /** Enrollments whose FIRST human open was seen: the open rate's numerator. */\n uniqueOpens?: number\n /**\n * Fetches of the image a machine made — a mail privacy proxy prefetching\n * it, a gateway scanning the message — counted apart and never in the\n * rate (`../engine/open-tracking.ts`).\n */\n machineOpens?: number\n /** Of {@link machineOpens}, the ones a mail provider's image proxy made. */\n proxyOpens?: number\n /** When a person last opened one of this sequence's emails. */\n lastOpenAtMs?: 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 `sendLinkKey` reduces it. */\n lastClickUrl: string | null\n /** Clicks on this person's links that were a machine's. */\n machineClicks: number\n /**\n * Every distinct destination this person followed since the per-click\n * history began (AGL-3332), as `sendLinkKey` reduces it, oldest first\n * and at most {@link OUTREACH_ENGAGEMENT_LINKS_MAX}. What the table counts\n * as \"links\" and filters \"followed this link\" by, without a read of the\n * history. A click from before the history kept only `lastClickUrl`.\n */\n links?: string[]\n /**\n * How many of `clicks` and `machineClicks` have a row of their own in the\n * enrollment's history (AGL-3332). The rest were counted as totals only —\n * before the history began, or past {@link OUTREACH_ENROLLMENT_HISTORY_MAX}.\n */\n loggedClicks?: number\n loggedMachineClicks?: number\n /** Opens judged this person's, machines excluded (AGL-3395). */\n opens?: number\n /** The first, which is what makes them one of the sequence's `uniqueOpens`. */\n firstOpenAtMs?: number | null\n lastOpenAtMs?: number | null\n /** Fetches of this person's tracking image that were a machine's. */\n machineOpens?: number\n /** How many of `opens` and `machineOpens` have a row in the history. */\n loggedOpens?: number\n}\n\n/**\n * The most distinct destinations {@link OutreachEnrollmentEngagement.links}\n * holds. The enrollment is what the enrollments table listens to, so the\n * list is bounded; a sequence's emails carry a link or two each, and nobody\n * follows twenty different ones.\n */\nexport const OUTREACH_ENGAGEMENT_LINKS_MAX = 20\n\n/*==========================================\n * ONE PERSON'S HISTORY (AGL-3332).\n *\n * `orgs/{orgId}/outreachEnrollments/{id}/history/{entryId}`: a row for each\n * visit to a tracking link in this person's emails — which link, from which\n * step, when, and whether a person or a scanner made it — for each fetch\n * of the tracking image in a sequence that counts opens (AGL-3395), and for\n * each pause, resume, stop and do-not-contact a member applied. The enrollment\n * keeps totals; this keeps the events, and is read only when somebody opens\n * the person, so the table's listener never carries it.\n *\n * Server-written, read-only to members, and erased with the enrollment.\n *==========================================*/\n\n/** The history's subcollection under an enrollment. */\nexport const OUTREACH_ENROLLMENT_HISTORY = 'history'\n\n/**\n * The most rows one enrollment's history holds. Past it a click is still\n * counted on the engagement, and the detail view says how many went\n * unlisted — a scanner fetching the same links for a month must not grow a\n * person's history without end.\n */\nexport const OUTREACH_ENROLLMENT_HISTORY_MAX = 500\n\n/** A member's act on one enrollment, as its history records it. */\nexport type OutreachHistoryAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** One visit to a tracking link. */\nexport interface OutreachClickHistoryEntry {\n id: string\n kind: 'click'\n atMs: number\n /** The destination as `sendLinkKey` reduces it — the link rollup's key; `null` when unreadable. */\n url: string | null\n /** The step whose email carried the link. */\n stepIndex: number\n /** Whether it counts: a person's click, not a scanner's. */\n human: boolean\n /** Why it was read as a scanner's; `null` for a person's. */\n machineReason: OutreachClickMachineReason | null\n}\n\n/** One member's act on the enrollment. */\nexport interface OutreachActionHistoryEntry {\n id: string\n kind: 'action'\n atMs: number\n action: OutreachHistoryAction\n byUid: string\n /** The reason the member typed, if any. */\n detail: string | null\n}\n\n/** One fetch of the tracking image in this person's email (AGL-3395). */\nexport interface OutreachOpenHistoryEntry {\n id: string\n kind: 'open'\n atMs: number\n /** The step whose email carried the image. */\n stepIndex: number\n /** Whether it counts: a person's open, not a proxy's or a scanner's. */\n human: boolean\n /** Why it was read as a machine's; `null` for a person's. */\n machineReason: OutreachOpenMachineReason | null\n /** The agent the fetch sent (AGL-3488); `null` on rows from before it was kept. */\n userAgent?: string | null\n /** The network the fetch came from (AGL-3488); `null` when unknown or not kept. */\n source?: OutreachOpenSource | null\n}\n\nexport type OutreachEnrollmentHistoryEntry =\n | OutreachClickHistoryEntry\n | OutreachOpenHistoryEntry\n | OutreachActionHistoryEntry\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 /**\n * More mailboxes the sequence sends from in rotation (AGL-3489), after\n * `mailboxId`: each person enrolled is given one, and all of their steps\n * go from it (`engine/mailbox-rotation`). Absent when it sends from its\n * own alone.\n */\n mailboxIds?: string[]\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n status: OutreachSequenceStatus\n /**\n * The campaigns the sequence is in (AGL-3254): container ids from the\n * org's `emailCampaigns`, under the field every campaign member carries\n * (`containerMembershipField('campaign')`). Everyone enrolled gains them on their\n * own record at enroll time, and what the sequence produces is credited\n * to them. Absent on a sequence saved before it could join one.\n */\n campaignIds?: string[]\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 /**\n * The engine held the next send (AGL-3326): the recipient's mail gateway\n * refused this organization's sender twice in the last thirty days and\n * delivered nothing. A member resumes it to send anyway.\n */\n 'gateway_blocked_here',\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', 'gateway_blocked_here'],\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 * CURATED STEPS (AGL-3324).\n *\n * A step is one template for everyone, plus the personal line. Curating\n * rewrites ONE person's copy of a step — drafted by the workspace's AI from\n * their record and the step's own body as the skeleton, or written by the\n * member — and keeps it on the enrollment, never on the sequence. The send\n * job reads the override in place of the step's subject and body; the\n * tokens, the footer, the click tracking and the plain-text rules still\n * apply to it exactly as they apply to the step.\n *\n * Nothing is stored that a member has not read: a draft lands on the\n * enrollment only when the member confirms it, with who and when.\n *==========================================*/\n\n/** Who wrote the override: the AI, or the member by hand. */\nexport const OUTREACH_STEP_OVERRIDE_SOURCES = ['ai', 'member'] as const\nexport type OutreachStepOverrideSource = (typeof OUTREACH_STEP_OVERRIDE_SOURCES)[number]\n\n/** One person's own copy of one step. */\nexport interface OutreachStepOverride {\n /** The subject, merge fields allowed; absent keeps the step's own. Not read on an in-thread email. */\n subject?: string\n /** The plain-text body, merge fields allowed; absent keeps the step's own (or its template's). */\n body?: string\n source: OutreachStepOverrideSource\n /** When the member confirmed it — the moment it was stored, never before. */\n draftedAtMs: number\n /** The member who confirmed it. */\n draftedByUid?: string\n /** An AI draft the member changed before confirming it. */\n edited?: boolean\n /** The prompt the AI was given, for the audit log; absent on a member's own words. */\n prompt?: string\n /** The model that drafted it, for the audit log. */\n model?: string\n}\n\n/** The overrides an enrollment carries, keyed by the step's index as text. */\nexport type OutreachStepOverrides = Record<string, OutreachStepOverride>\n\n/** The sentence a member ticks before a curated draft is stored — a fourth attestation. */\nexport const OUTREACH_CURATION_CONFIRMATION_LABEL =\n \"I've read this draft, and it goes out to this person as written\"\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 /**\n * What the enrollments table's search box reads (AGL-3321): every prefix\n * of the name and of the address and its parts\n * (`outreachEnrollmentSearchTokens`), stamped by the enroll route beside\n * the two it is made of, which nothing rewrites afterwards.\n */\n searchTokens?: string[]\n hostId: string\n mailboxId: string\n /** The index into `steps` of the NEXT step to run. */\n stepIndex: number\n /**\n * The step the enrollment began at (AGL-3228), when it was not the first:\n * the member enrolled someone who already had the earlier steps by hand.\n * Absent on an enrollment that began at step 1. The first email sent from\n * here starts the enrollment's thread, whatever `replyInThread` says —\n * there is no earlier email of ours for it to answer.\n */\n startStepIndex?: number\n /**\n * The steps before {@link startStepIndex}, marked skipped at enrollment\n * and never run (AGL-3228). Absent when none were.\n */\n skippedSteps?: OutreachSkippedStep[]\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 * Whether the person clicked a link (AGL-3332): what the enrollments\n * table's \"Clicked\" filter asks Firestore. `false` from enrollment, set\n * by the click route on a person's click, never a scanner's.\n */\n clicked?: boolean\n /**\n * The sequence's campaigns as they stood when the person was enrolled\n * (AGL-3254): what every outcome of this enrollment is credited to. A\n * campaign the sequence joins later does not claim the people already\n * in it, and one it leaves keeps what it was credited with.\n */\n campaignIds?: string[]\n /**\n * This person's own copies of steps (AGL-3324), by step index. Absent on\n * an enrollment nobody curated, which sends the sequence's steps as\n * written; a step without an entry sends the same.\n */\n stepOverrides?: OutreachStepOverrides\n /**\n * The gateway hold (AGL-3326), once one applied: which gateway, when the\n * engine held the send, and the member who released it — by resuming the\n * held enrollment, or by ticking the person past the red chip when they\n * enrolled them. A released hold is not taken again; a send that the\n * engine held and nobody released waits.\n */\n gatewayHold?: OutreachGatewayHold | null\n /**\n * How many of this enrollment's email steps the sync has credited to the\n * gateway ledger as delivered (AGL-3326): a send with no bounce a day\n * later. The sync credits the next ones from here.\n */\n gatewayDeliveredSteps?: number\n}\n\n/** A gateway hold on one enrollment (AGL-3326) — see {@link OutreachEnrollment.gatewayHold}. */\nexport interface OutreachGatewayHold {\n gateway: OutreachMailGateway\n /** When the engine held a send; `null` when a member released it before one was. */\n heldAtMs: number | null\n releasedByUid: string | null\n releasedAtMs: number | null\n}\n\n\n/**\n * The organization's ledger for one mail gateway as first written\n * (`orgs/{orgId}/outreachGatewayStats/{gateway}`, AGL-3326): totals, and the\n * same counts by UTC day. Read through, never written — see\n * `OUTREACH_COLLECTIONS.gatewayStats`.\n */\nexport interface OutreachGatewayStats {\n gateway: OutreachMailGateway\n sent: number\n delivered: number\n blocked: number\n lastBlockedAtMs: number | null\n /** `YYYY-MM-DD` (UTC) → that day's counts, the last thirty days kept. */\n days: Record<string, OutreachGatewayDayCounts>\n updatedAtMs: number\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/**\n * One step an enrollment skipped because it began past it (AGL-3228): the\n * person already had it by hand, so the runtime never runs it. Kept apart\n * from {@link OutreachStepRecord}, which every reader counts as a send.\n */\nexport interface OutreachSkippedStep {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task'\n /** When it was marked skipped: the enrollment. */\n atMs: number\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 * The email carried a tracking image (AGL-3395). Absent on every email\n * sent without one — which is every email of a sequence that does not\n * count opens.\n */\n openTracked?: boolean\n /**\n * The email went out as this person's curated copy (AGL-3324), and who\n * wrote it. Absent on a step sent as the sequence wrote it.\n */\n curated?: OutreachStepOverrideSource\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/** Why an entry is on the list, as the Compliance page and an export say it. */\nexport const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string> = {\n manual: 'Added by a member',\n opt_out_reply: 'A reply asked not to be emailed',\n unsubscribe: 'Unsubscribed',\n hard_bounce: 'Mail bounced',\n gateway_block: 'Its mail gateway blocked the sender',\n}\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/** Who added an entry, as an export says it. */\nexport const OUTREACH_DO_NOT_CONTACT_SOURCE_LABELS: Record<OutreachDoNotContactSource, string> = {\n member: 'A member',\n runtime: 'Sequences, automatically',\n}\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 * What the domains list's search box reads (AGL-3321):\n * `outreachDomainSearchTokens`, stamped by the one writer that adds it.\n */\n searchTokens?: string[]\n}\n"],"names":["OUTREACH_COLLECTIONS","mailboxes","sequences","enrollments","settings","doNotContact","doNotContactDomains","domainIntel","gatewayStats","mailboxCredentials","links","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_ENGAGEMENT_LINKS_MAX","OUTREACH_ENROLLMENT_HISTORY","OUTREACH_ENROLLMENT_HISTORY_MAX","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_STEP_OVERRIDE_SOURCES","OUTREACH_CURATION_CONFIRMATION_LABEL","OUTREACH_ENROLLMENT_TARGETS","OUTREACH_COMPLIANCE_SETTINGS_ID","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","manual","opt_out_reply","unsubscribe","hard_bounce","gateway_block","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_SOURCE_LABELS","member","runtime"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;CAmBC,GAOD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMA,uBAAuB;IAClC,wEAAwE,GACxEC,WAAW;IACX,uEAAuE,GACvEC,WAAW;IACX,oFAAoF,GACpFC,aAAa;IACb;;;;GAIC,GACDC,UAAU;IACV;;;GAGC,GACDC,cAAc;IACd;;;;GAIC,GACDC,qBAAqB;IACrB;;;;;GAKC,GACDC,aAAa;IACb;;;;;;;GAOC,GACDC,cAAc;IACd;;;;GAIC,GACDC,oBAAoB;IACpB;;;;;;GAMC,GACDC,OAAO;AACT,EAAU;AAQV,uEAAuE,GACvE,OAAO,SAASC,0BACdC,KAAa,EACbC,UAAiC;IAEjC,OAAO,CAAC,KAAK,EAAED,MAAM,CAAC,EAAEZ,oBAAoB,CAACa,WAAW,EAAE;AAC5D;AAQA;;;;CAIC,GACD,OAAO,MAAMC,6BAA6B;IAAC;IAAU;CAAY,CAAS;AAI1E;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B;IACvC;IACA;IACA;IACA;CACD,CAAS;AAyLV,gFAAgF,GAChF,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;;;;4CAO4C,GAE5C,kEAAkE,GAClE,OAAO,MAAMC,qBAAqB,EAAC;AAEnC,gDAAgD,GAChD,OAAO,MAAMC,2BAA2B,EAAC;AAEzC,2DAA2D,GAC3D,OAAO,MAAMC,wCAAwC,GAAE;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,6CAA6C,EAAC;AAwC3D,gDAAgD,GAChD,OAAO,MAAMC,sBAAsB;IAAC;IAAY;IAAQ;CAAO,CAAS;AAGxE,OAAO,MAAMC,4BAA8D;IACzEC,UAAU;IACVC,MAAM;IACNC,MAAM;AACR,EAAC;AAeD,oEAAoE,GACpE,OAAO,MAAMC,qCAAwD;IAAC;CAAK,CAAA;AA2D3E;;;;;;;;;;;;;2CAa2C,GAE3C;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B;IAAC;IAAW;CAAQ,CAAS;AAmGtE;;;;;CAKC,GACD,OAAO,MAAMC,gCAAgC,GAAE;AAE/C;;;;;;;;;;;;4CAY4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMC,8BAA8B,UAAS;AAEpD;;;;;CAKC,GACD,OAAO,MAAMC,kCAAkC,IAAG;AA0FlD;;4CAE4C,GAE5C;;;;;;;;CAQC,GACD,OAAO,MAAMC,+BAA+B;IAC1C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB;IACnC,uCAAuC,GACvC;IACA,yDAAyD,GACzD;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,8DAA8D,GAC9D;IACA,sEAAsE,GACtE;IACA,mCAAmC,GACnC;IACA;;;;GAIC,GACD;IACA,2DAA2D,GAC3D;IACA,gFAAgF,GAChF;CACD,CAAS;AAGV,8EAA8E,GAC9E,OAAO,MAAMC,kCAKT;IACFC,QAAQ;QAAC;QAAU;KAAuB;IAC1CC,SAAS;QAAC;KAAQ;IAClBC,SAAS;QAAC;KAAc;IACxBC,WAAW;QAAC;QAAiB;QAAe;KAAiB;IAC7DC,SAAS;QAAC;QAAU;QAAQ;KAAoB;IAChDC,QAAQ;QAAC;KAAc;AACzB,EAAC;AAED;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;CACD,CAAS;AAGV,mDAAmD,GACnD,OAAO,MAAMC,8BAAuE;IAClFC,qBAAqB;IACrBC,oBACE;IACFC,sBAAsB;AACxB,EAAC;AAcD,+EAA+E,GAC/E,OAAO,MAAMC,6BAA6B,IAAG;AAE7C;;;;;;;;;;;;;4CAa4C,GAE5C,2DAA2D,GAC3D,OAAO,MAAMC,iCAAiC;IAAC;IAAM;CAAS,CAAS;AAyBvE,yFAAyF,GACzF,OAAO,MAAMC,uCACX,kEAAiE;AAEnE;;;;;CAKC,GACD,OAAO,MAAMC,8BAA8B;IAAC;IAAW;CAAO,CAAS;AAuQvE,oEAAoE,GACpE,OAAO,MAAMC,kCAAkC,aAAY;AAyB3D;;;;;;;;;;;;;;4CAc4C,GAE5C,mCAAmC,GACnC,OAAO,MAAMC,kCAAkC;IAC7C,2BAA2B,GAC3B;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,iCAAiC,GACjC;IACA;;;;GAIC,GACD;CACD,CAAS;AAIV,8EAA8E,GAC9E,OAAO,MAAMC,wCAAoF;IAC/FC,QAAQ;IACRC,eAAe;IACfC,aAAa;IACbC,aAAa;IACbC,eAAe;AACjB,EAAC;AAED,uEAAuE,GACvE,OAAO,MAAMC,kCAAkC;IAAC;IAAU;CAAU,CAAS;AAI7E,8CAA8C,GAC9C,OAAO,MAAMC,wCAAoF;IAC/FC,QAAQ;IACRC,SAAS;AACX,EAAC"}
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\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport type { OutreachOpenSource } from '../engine/open-source'\nimport type { OutreachGatewayDayCounts, OutreachMailGateway } from '../engine/mail-gateway'\n\n/**\n * Where Outreach keeps its records.\n *\n * Eight collections under the organization, and two 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 ones — credentials, and the short\n * tracking links (AGL-3297) — are keyed by an `orgId` FIELD that a\n * path-scoped delete of `orgs/{orgId}` cannot see: the org erasure sweeps\n * them 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 * `orgs/{orgId}/outreachDomainIntel/{domain}` — a recipient domain's MX\n * as this organization first cached it (AGL-3326). Neither read nor\n * written any more: the platform-wide `mailDomains` cache replaced it\n * (AGL-3328), and it is retired with its rule once that has shipped.\n */\n domainIntel: 'outreachDomainIntel',\n /**\n * `orgs/{orgId}/outreachGatewayStats/{gateway}` — the organization's\n * ledger per mail gateway as first written (AGL-3326), with no sending\n * domain. READ THROUGH, never written: its last thirty days are summed\n * into the sending-domain ledger's standing\n * (`orgs/{orgId}/mailGatewayLedger`, AGL-3328) until they age out, and it\n * is retired with its rule after that.\n */\n gatewayStats: 'outreachGatewayStats',\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 /**\n * `outreachLinks/{linkId}` — what one short tracking link in a sent email\n * points at (AGL-3297): the organization, the enrollment, the step, the\n * link's index and the destination. TOP-LEVEL so the link's id alone\n * resolves it with one read, keyed by an `orgId` FIELD that the org\n * erasure sweeps, and closed to every client.\n */\n links: 'outreachLinks',\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' | 'links'\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/**\n * The mail providers a mailbox can be connected through: a Google Workspace\n * account (Gmail), or a Microsoft 365 account through Microsoft Graph\n * (AGL-3489).\n */\nexport const OUTREACH_MAILBOX_PROVIDERS = ['google', 'microsoft'] 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 * Tests of a sequence step sent to a member (AGL-3325). Counted apart from\n * `sent`: a test is a real Gmail send, so it spends the account's own\n * limits, but it is not a person emailed and never moves the daily cap.\n */\n tests?: number\n /**\n * Answers a member wrote and sent in a person's thread (AGL-4020). Counted\n * apart from `sent` for the same reasons as `tests`: a real send, but no\n * step of a sequence, so it never spends the daily cap, never joins the\n * sends a bounce rate is judged over, and never pauses the mailbox.\n */\n manualReplies?: 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 /**\n * `bounce_rate` is what the window rule wrote before AGL-3326 changed it\n * from a percentage to a count; a stored pause keeps the reason it was\n * written with, and the card shows `message` either way.\n */\n reason: 'bounces_today' | 'bounces_in_window' | '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 * short link of ours that forwards to it. Opens are the separate\n * {@link countOpens}, because a pixel needs an HTML part.\n */\n trackClicks: boolean\n /**\n * Whether this sequence's emails carry an HTML part with a tracking image,\n * so opens are counted (AGL-3395). Default `false`, and `false` on every\n * sequence written before the setting existed.\n *\n * On, each email goes out as `multipart/alternative`: the plain-text part\n * exactly as it would have been, and beside it the same words as HTML with\n * a 1×1 image on the short-link host. It costs the plain-text one-to-one\n * look, and some gateways score an HTML part with a remote image — see\n * `../engine/open-tracking.ts`. Off, nothing about the email changes and\n * the report says opens are not measured.\n */\n countOpens: boolean\n /**\n * Whether this sequence's emails carry `List-Unsubscribe` and\n * `List-Unsubscribe-Post` (RFC 8058) (AGL-3296). Default `false`, and\n * `false` on every sequence written before the setting existed.\n *\n * On, mail clients draw their own unsubscribe button — Apple Mail as \"This\n * message is from a mailing list\" — which is the easiest way out for the\n * recipient and makes a one-to-one email read as bulk. Off, the footer's\n * \"reply 'no'\" line is the way out; the footer and its postal address are\n * mandatory either way, and the reply classifier stops on a \"no\". The\n * Gmail/Yahoo one-click requirement binds bulk senders (5,000 a day), not a\n * mailbox sending a sequence.\n */\n listUnsubscribe: 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 `send-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 * At least one email of this sequence went out with a tracking image\n * (AGL-3395). Absent is \"never recorded\", and the report says opens are\n * not measured rather than showing 0%.\n */\n openTracked?: boolean\n /**\n * Distinct enrollments that have had at least one email carrying the\n * image — the open rate's denominator. Not `people`: a sequence that turned\n * the setting on halfway through emailed some people no image at all, and\n * they could never have been counted as opening.\n */\n openPeople?: number\n /** Opens judged a person's. One reader opening twice counts two. */\n opens?: number\n /** Enrollments whose FIRST human open was seen: the open rate's numerator. */\n uniqueOpens?: number\n /**\n * Fetches of the image a machine made — a mail privacy proxy prefetching\n * it, a gateway scanning the message — counted apart and never in the\n * rate (`../engine/open-tracking.ts`).\n */\n machineOpens?: number\n /** Of {@link machineOpens}, the ones a mail provider's image proxy made. */\n proxyOpens?: number\n /** When a person last opened one of this sequence's emails. */\n lastOpenAtMs?: 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 `sendLinkKey` reduces it. */\n lastClickUrl: string | null\n /** Clicks on this person's links that were a machine's. */\n machineClicks: number\n /**\n * Every distinct destination this person followed since the per-click\n * history began (AGL-3332), as `sendLinkKey` reduces it, oldest first\n * and at most {@link OUTREACH_ENGAGEMENT_LINKS_MAX}. What the table counts\n * as \"links\" and filters \"followed this link\" by, without a read of the\n * history. A click from before the history kept only `lastClickUrl`.\n */\n links?: string[]\n /**\n * How many of `clicks` and `machineClicks` have a row of their own in the\n * enrollment's history (AGL-3332). The rest were counted as totals only —\n * before the history began, or past {@link OUTREACH_ENROLLMENT_HISTORY_MAX}.\n */\n loggedClicks?: number\n loggedMachineClicks?: number\n /** Opens judged this person's, machines excluded (AGL-3395). */\n opens?: number\n /** The first, which is what makes them one of the sequence's `uniqueOpens`. */\n firstOpenAtMs?: number | null\n lastOpenAtMs?: number | null\n /** Fetches of this person's tracking image that were a machine's. */\n machineOpens?: number\n /** How many of `opens` and `machineOpens` have a row in the history. */\n loggedOpens?: number\n}\n\n/**\n * The most distinct destinations {@link OutreachEnrollmentEngagement.links}\n * holds. The enrollment is what the enrollments table listens to, so the\n * list is bounded; a sequence's emails carry a link or two each, and nobody\n * follows twenty different ones.\n */\nexport const OUTREACH_ENGAGEMENT_LINKS_MAX = 20\n\n/*==========================================\n * ONE PERSON'S HISTORY (AGL-3332).\n *\n * `orgs/{orgId}/outreachEnrollments/{id}/history/{entryId}`: a row for each\n * visit to a tracking link in this person's emails — which link, from which\n * step, when, and whether a person or a scanner made it — for each fetch\n * of the tracking image in a sequence that counts opens (AGL-3395), and for\n * each pause, resume, stop and do-not-contact a member applied. The enrollment\n * keeps totals; this keeps the events, and is read only when somebody opens\n * the person, so the table's listener never carries it.\n *\n * Server-written, read-only to members, and erased with the enrollment.\n *==========================================*/\n\n/** The history's subcollection under an enrollment. */\nexport const OUTREACH_ENROLLMENT_HISTORY = 'history'\n\n/**\n * The most rows one enrollment's history holds. Past it a click is still\n * counted on the engagement, and the detail view says how many went\n * unlisted — a scanner fetching the same links for a month must not grow a\n * person's history without end.\n */\nexport const OUTREACH_ENROLLMENT_HISTORY_MAX = 500\n\n/** A member's act on one enrollment, as its history records it. */\nexport type OutreachHistoryAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** One visit to a tracking link. */\nexport interface OutreachClickHistoryEntry {\n id: string\n kind: 'click'\n atMs: number\n /** The destination as `sendLinkKey` reduces it — the link rollup's key; `null` when unreadable. */\n url: string | null\n /** The step whose email carried the link. */\n stepIndex: number\n /** Whether it counts: a person's click, not a scanner's. */\n human: boolean\n /** Why it was read as a scanner's; `null` for a person's. */\n machineReason: OutreachClickMachineReason | null\n}\n\n/** One member's act on the enrollment. */\nexport interface OutreachActionHistoryEntry {\n id: string\n kind: 'action'\n atMs: number\n action: OutreachHistoryAction\n byUid: string\n /** The reason the member typed, if any. */\n detail: string | null\n}\n\n/** One fetch of the tracking image in this person's email (AGL-3395). */\nexport interface OutreachOpenHistoryEntry {\n id: string\n kind: 'open'\n atMs: number\n /** The step whose email carried the image. */\n stepIndex: number\n /** Whether it counts: a person's open, not a proxy's or a scanner's. */\n human: boolean\n /** Why it was read as a machine's; `null` for a person's. */\n machineReason: OutreachOpenMachineReason | null\n /** The agent the fetch sent (AGL-3488); `null` on rows from before it was kept. */\n userAgent?: string | null\n /** The network the fetch came from (AGL-3488); `null` when unknown or not kept. */\n source?: OutreachOpenSource | null\n}\n\nexport type OutreachEnrollmentHistoryEntry =\n | OutreachClickHistoryEntry\n | OutreachOpenHistoryEntry\n | OutreachActionHistoryEntry\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 /**\n * More mailboxes the sequence sends from in rotation (AGL-3489), after\n * `mailboxId`: each person enrolled is given one, and all of their steps\n * go from it (`engine/mailbox-rotation`). Absent when it sends from its\n * own alone.\n */\n mailboxIds?: string[]\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n status: OutreachSequenceStatus\n /**\n * The campaigns the sequence is in (AGL-3254): container ids from the\n * org's `emailCampaigns`, under the field every campaign member carries\n * (`containerMembershipField('campaign')`). Everyone enrolled gains them on their\n * own record at enroll time, and what the sequence produces is credited\n * to them. Absent on a sequence saved before it could join one.\n */\n campaignIds?: string[]\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 /**\n * The engine held the next send (AGL-3326): the recipient's mail gateway\n * refused this organization's sender twice in the last thirty days and\n * delivered nothing. A member resumes it to send anyway.\n */\n 'gateway_blocked_here',\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', 'gateway_blocked_here'],\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 * CURATED STEPS (AGL-3324).\n *\n * A step is one template for everyone, plus the personal line. Curating\n * rewrites ONE person's copy of a step — drafted by the workspace's AI from\n * their record and the step's own body as the skeleton, or written by the\n * member — and keeps it on the enrollment, never on the sequence. The send\n * job reads the override in place of the step's subject and body; the\n * tokens, the footer, the click tracking and the plain-text rules still\n * apply to it exactly as they apply to the step.\n *\n * Nothing is stored that a member has not read: a draft lands on the\n * enrollment only when the member confirms it, with who and when.\n *==========================================*/\n\n/** Who wrote the override: the AI, or the member by hand. */\nexport const OUTREACH_STEP_OVERRIDE_SOURCES = ['ai', 'member'] as const\nexport type OutreachStepOverrideSource = (typeof OUTREACH_STEP_OVERRIDE_SOURCES)[number]\n\n/** One person's own copy of one step. */\nexport interface OutreachStepOverride {\n /** The subject, merge fields allowed; absent keeps the step's own. Not read on an in-thread email. */\n subject?: string\n /** The plain-text body, merge fields allowed; absent keeps the step's own (or its template's). */\n body?: string\n source: OutreachStepOverrideSource\n /** When the member confirmed it — the moment it was stored, never before. */\n draftedAtMs: number\n /** The member who confirmed it. */\n draftedByUid?: string\n /** An AI draft the member changed before confirming it. */\n edited?: boolean\n /** The prompt the AI was given, for the audit log; absent on a member's own words. */\n prompt?: string\n /** The model that drafted it, for the audit log. */\n model?: string\n}\n\n/** The overrides an enrollment carries, keyed by the step's index as text. */\nexport type OutreachStepOverrides = Record<string, OutreachStepOverride>\n\n/** The sentence a member ticks before a curated draft is stored — a fourth attestation. */\nexport const OUTREACH_CURATION_CONFIRMATION_LABEL =\n \"I've read this draft, and it goes out to this person as written\"\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 /**\n * What the enrollments table's search box reads (AGL-3321): every prefix\n * of the name and of the address and its parts\n * (`outreachEnrollmentSearchTokens`), stamped by the enroll route beside\n * the two it is made of, which nothing rewrites afterwards.\n */\n searchTokens?: string[]\n hostId: string\n mailboxId: string\n /** The index into `steps` of the NEXT step to run. */\n stepIndex: number\n /**\n * The step the enrollment began at (AGL-3228), when it was not the first:\n * the member enrolled someone who already had the earlier steps by hand.\n * Absent on an enrollment that began at step 1. The first email sent from\n * here starts the enrollment's thread, whatever `replyInThread` says —\n * there is no earlier email of ours for it to answer.\n */\n startStepIndex?: number\n /**\n * The steps before {@link startStepIndex}, marked skipped at enrollment\n * and never run (AGL-3228). Absent when none were.\n */\n skippedSteps?: OutreachSkippedStep[]\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 /**\n * Every step the runtime ran, oldest first (AGL-2981), and every answer a\n * member sent in the thread (`manual_reply`, AGL-4020).\n */\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 the person wrote back (AGL-4020): every reply, automatic answer and\n * opt-out the sync read from them, oldest first, the last\n * `OUTREACH_INBOUND_MESSAGES_MAX`. Absent until the first one is read —\n * including on an enrollment whose reply was synced before they were kept,\n * which the reply composer reads from the mailbox instead.\n */\n inboundMessages?: OutreachInboundMessage[]\n /**\n * A member's answer being sent (AGL-4020): taken in a transaction before\n * the send, cleared when it is recorded, so a second click cannot send it\n * twice. Absent or `null` while none is in flight.\n */\n replyClaim?: OutreachReplyClaim | null\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 * Whether the person clicked a link (AGL-3332): what the enrollments\n * table's \"Clicked\" filter asks Firestore. `false` from enrollment, set\n * by the click route on a person's click, never a scanner's.\n */\n clicked?: boolean\n /**\n * The sequence's campaigns as they stood when the person was enrolled\n * (AGL-3254): what every outcome of this enrollment is credited to. A\n * campaign the sequence joins later does not claim the people already\n * in it, and one it leaves keeps what it was credited with.\n */\n campaignIds?: string[]\n /**\n * This person's own copies of steps (AGL-3324), by step index. Absent on\n * an enrollment nobody curated, which sends the sequence's steps as\n * written; a step without an entry sends the same.\n */\n stepOverrides?: OutreachStepOverrides\n /**\n * The gateway hold (AGL-3326), once one applied: which gateway, when the\n * engine held the send, and the member who released it — by resuming the\n * held enrollment, or by ticking the person past the red chip when they\n * enrolled them. A released hold is not taken again; a send that the\n * engine held and nobody released waits.\n */\n gatewayHold?: OutreachGatewayHold | null\n /**\n * How many of this enrollment's email steps the sync has credited to the\n * gateway ledger as delivered (AGL-3326): a send with no bounce a day\n * later. The sync credits the next ones from here.\n */\n gatewayDeliveredSteps?: number\n}\n\n/** A gateway hold on one enrollment (AGL-3326) — see {@link OutreachEnrollment.gatewayHold}. */\nexport interface OutreachGatewayHold {\n gateway: OutreachMailGateway\n /** When the engine held a send; `null` when a member released it before one was. */\n heldAtMs: number | null\n releasedByUid: string | null\n releasedAtMs: number | null\n}\n\n\n/**\n * The organization's ledger for one mail gateway as first written\n * (`orgs/{orgId}/outreachGatewayStats/{gateway}`, AGL-3326): totals, and the\n * same counts by UTC day. Read through, never written — see\n * `OUTREACH_COLLECTIONS.gatewayStats`.\n */\nexport interface OutreachGatewayStats {\n gateway: OutreachMailGateway\n sent: number\n delivered: number\n blocked: number\n lastBlockedAtMs: number | null\n /** `YYYY-MM-DD` (UTC) → that day's counts, the last thirty days kept. */\n days: Record<string, OutreachGatewayDayCounts>\n updatedAtMs: number\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/**\n * One step an enrollment skipped because it began past it (AGL-3228): the\n * person already had it by hand, so the runtime never runs it. Kept apart\n * from {@link OutreachStepRecord}, which every reader counts as a send.\n */\nexport interface OutreachSkippedStep {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task'\n /** When it was marked skipped: the enrollment. */\n atMs: number\n}\n\n/** The most of a person's messages one enrollment keeps (AGL-4020). */\nexport const OUTREACH_INBOUND_MESSAGES_MAX = 20\n\n/** What a message the person wrote is, as the sync classified it (AGL-4020). */\nexport const OUTREACH_INBOUND_KINDS = ['reply', 'auto_reply', 'opt_out'] as const\n\nexport type OutreachInboundKind = (typeof OUTREACH_INBOUND_KINDS)[number]\n\n/**\n * One message the person wrote in the enrollment's threads (AGL-4020), kept\n * so the console can show it and answer it in the same thread.\n */\nexport interface OutreachInboundMessage {\n /** The provider's id for it. */\n id: string\n /** The provider's thread it is in: Gmail's thread id, Graph's conversation id. */\n threadId: string | null\n /** Its `Message-ID`, angle brackets included, or `null` when it carried none. */\n messageId: string | null\n /** The `Message-ID`s its `References` names, oldest first. */\n references: string[]\n /** The sender's address, normalized. */\n from: string\n /** The sender's display name, `''` when the `From` named none. */\n fromName: string\n /** The `Reply-To` address it asked answers to go to, when one differs from `from`. */\n replyTo: string | null\n subject: string\n /** When the provider received it. */\n atMs: number\n kind: OutreachInboundKind\n /** The plain text they wrote, the quoted history below it trimmed, bounded. */\n body: string\n}\n\n/** A member's answer in flight (AGL-4020) — see {@link OutreachEnrollment.replyClaim}. */\nexport interface OutreachReplyClaim {\n /** The `Message-ID` the answer goes out with, minted before the send. */\n messageId: string\n byUid: string\n atMs: number\n}\n\n/**\n * One step the runtime ran for an enrollment (AGL-2981) — or, as\n * `manual_reply`, one answer a member wrote and sent in the person's thread\n * (AGL-4020). A `manual_reply` is no step of the sequence: it never moves\n * `stepIndex`, never counts as a send in the sequence's figures, and every\n * reader that counts sends reads `kind === 'email'`.\n */\nexport interface OutreachStepRecord {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task' | 'manual_reply'\n atMs: number\n /** The member who wrote and sent a `manual_reply`. */\n byUid?: string\n /** The address a `manual_reply` went from, and the one it went to. */\n from?: string\n to?: string\n /** A `manual_reply`'s text as sent, bounded. */\n body?: string\n /** The `Message-ID` a `manual_reply` answered. */\n inReplyTo?: string\n /**\n * The key the composer minted for a `manual_reply`, which a retry of the\n * same answer is known by whatever `Message-ID` the provider reported.\n */\n replyKey?: string\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 * The email carried a tracking image (AGL-3395). Absent on every email\n * sent without one — which is every email of a sequence that does not\n * count opens.\n */\n openTracked?: boolean\n /**\n * The email went out as this person's curated copy (AGL-3324), and who\n * wrote it. Absent on a step sent as the sequence wrote it.\n */\n curated?: OutreachStepOverrideSource\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/** Why an entry is on the list, as the Compliance page and an export say it. */\nexport const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string> = {\n manual: 'Added by a member',\n opt_out_reply: 'A reply asked not to be emailed',\n unsubscribe: 'Unsubscribed',\n hard_bounce: 'Mail bounced',\n gateway_block: 'Its mail gateway blocked the sender',\n}\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/** Who added an entry, as an export says it. */\nexport const OUTREACH_DO_NOT_CONTACT_SOURCE_LABELS: Record<OutreachDoNotContactSource, string> = {\n member: 'A member',\n runtime: 'Sequences, automatically',\n}\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 * What the domains list's search box reads (AGL-3321):\n * `outreachDomainSearchTokens`, stamped by the one writer that adds it.\n */\n searchTokens?: string[]\n}\n"],"names":["OUTREACH_COLLECTIONS","mailboxes","sequences","enrollments","settings","doNotContact","doNotContactDomains","domainIntel","gatewayStats","mailboxCredentials","links","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_ENGAGEMENT_LINKS_MAX","OUTREACH_ENROLLMENT_HISTORY","OUTREACH_ENROLLMENT_HISTORY_MAX","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_STEP_OVERRIDE_SOURCES","OUTREACH_CURATION_CONFIRMATION_LABEL","OUTREACH_ENROLLMENT_TARGETS","OUTREACH_INBOUND_MESSAGES_MAX","OUTREACH_INBOUND_KINDS","OUTREACH_COMPLIANCE_SETTINGS_ID","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","manual","opt_out_reply","unsubscribe","hard_bounce","gateway_block","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_SOURCE_LABELS","member","runtime"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;CAmBC,GAOD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMA,uBAAuB;IAClC,wEAAwE,GACxEC,WAAW;IACX,uEAAuE,GACvEC,WAAW;IACX,oFAAoF,GACpFC,aAAa;IACb;;;;GAIC,GACDC,UAAU;IACV;;;GAGC,GACDC,cAAc;IACd;;;;GAIC,GACDC,qBAAqB;IACrB;;;;;GAKC,GACDC,aAAa;IACb;;;;;;;GAOC,GACDC,cAAc;IACd;;;;GAIC,GACDC,oBAAoB;IACpB;;;;;;GAMC,GACDC,OAAO;AACT,EAAU;AAQV,uEAAuE,GACvE,OAAO,SAASC,0BACdC,KAAa,EACbC,UAAiC;IAEjC,OAAO,CAAC,KAAK,EAAED,MAAM,CAAC,EAAEZ,oBAAoB,CAACa,WAAW,EAAE;AAC5D;AAQA;;;;CAIC,GACD,OAAO,MAAMC,6BAA6B;IAAC;IAAU;CAAY,CAAS;AAI1E;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B;IACvC;IACA;IACA;IACA;CACD,CAAS;AAgMV,gFAAgF,GAChF,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;;;;4CAO4C,GAE5C,kEAAkE,GAClE,OAAO,MAAMC,qBAAqB,EAAC;AAEnC,gDAAgD,GAChD,OAAO,MAAMC,2BAA2B,EAAC;AAEzC,2DAA2D,GAC3D,OAAO,MAAMC,wCAAwC,GAAE;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,6CAA6C,EAAC;AAwC3D,gDAAgD,GAChD,OAAO,MAAMC,sBAAsB;IAAC;IAAY;IAAQ;CAAO,CAAS;AAGxE,OAAO,MAAMC,4BAA8D;IACzEC,UAAU;IACVC,MAAM;IACNC,MAAM;AACR,EAAC;AAeD,oEAAoE,GACpE,OAAO,MAAMC,qCAAwD;IAAC;CAAK,CAAA;AA2D3E;;;;;;;;;;;;;2CAa2C,GAE3C;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B;IAAC;IAAW;CAAQ,CAAS;AAmGtE;;;;;CAKC,GACD,OAAO,MAAMC,gCAAgC,GAAE;AAE/C;;;;;;;;;;;;4CAY4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMC,8BAA8B,UAAS;AAEpD;;;;;CAKC,GACD,OAAO,MAAMC,kCAAkC,IAAG;AA0FlD;;4CAE4C,GAE5C;;;;;;;;CAQC,GACD,OAAO,MAAMC,+BAA+B;IAC1C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB;IACnC,uCAAuC,GACvC;IACA,yDAAyD,GACzD;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,8DAA8D,GAC9D;IACA,sEAAsE,GACtE;IACA,mCAAmC,GACnC;IACA;;;;GAIC,GACD;IACA,2DAA2D,GAC3D;IACA,gFAAgF,GAChF;CACD,CAAS;AAGV,8EAA8E,GAC9E,OAAO,MAAMC,kCAKT;IACFC,QAAQ;QAAC;QAAU;KAAuB;IAC1CC,SAAS;QAAC;KAAQ;IAClBC,SAAS;QAAC;KAAc;IACxBC,WAAW;QAAC;QAAiB;QAAe;KAAiB;IAC7DC,SAAS;QAAC;QAAU;QAAQ;KAAoB;IAChDC,QAAQ;QAAC;KAAc;AACzB,EAAC;AAED;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;CACD,CAAS;AAGV,mDAAmD,GACnD,OAAO,MAAMC,8BAAuE;IAClFC,qBAAqB;IACrBC,oBACE;IACFC,sBAAsB;AACxB,EAAC;AAcD,+EAA+E,GAC/E,OAAO,MAAMC,6BAA6B,IAAG;AAE7C;;;;;;;;;;;;;4CAa4C,GAE5C,2DAA2D,GAC3D,OAAO,MAAMC,iCAAiC;IAAC;IAAM;CAAS,CAAS;AAyBvE,yFAAyF,GACzF,OAAO,MAAMC,uCACX,kEAAiE;AAEnE;;;;;CAKC,GACD,OAAO,MAAMC,8BAA8B;IAAC;IAAW;CAAO,CAAS;AAsNvE,qEAAqE,GACrE,OAAO,MAAMC,gCAAgC,GAAE;AAE/C,8EAA8E,GAC9E,OAAO,MAAMC,yBAAyB;IAAC;IAAS;IAAc;CAAU,CAAS;AA6HjF,oEAAoE,GACpE,OAAO,MAAMC,kCAAkC,aAAY;AAyB3D;;;;;;;;;;;;;;4CAc4C,GAE5C,mCAAmC,GACnC,OAAO,MAAMC,kCAAkC;IAC7C,2BAA2B,GAC3B;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,iCAAiC,GACjC;IACA;;;;GAIC,GACD;CACD,CAAS;AAIV,8EAA8E,GAC9E,OAAO,MAAMC,wCAAoF;IAC/FC,QAAQ;IACRC,eAAe;IACfC,aAAa;IACbC,aAAa;IACbC,eAAe;AACjB,EAAC;AAED,uEAAuE,GACvE,OAAO,MAAMC,kCAAkC;IAAC;IAAU;CAAU,CAAS;AAI7E,8CAA8C,GAC9C,OAAO,MAAMC,wCAAoF;IAC/FC,QAAQ;IACRC,SAAS;AACX,EAAC"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type OutreachEnrollment, type OutreachMailbox, type OutreachSequence, type OutreachSequenceStats, type OutreachStepOverrides } from './outreach.types';
17
+ import { type OutreachEnrollment, type OutreachInboundMessage, type OutreachMailbox, type OutreachSequence, type OutreachSequenceStats, type OutreachStepOverrides } from './outreach.types';
18
18
  /** A stored sequence in its model shape, or `null` for no document. */
19
19
  export declare function readStoredOutreachSequence(id: string, data: Record<string, unknown> | undefined): OutreachSequence | null;
20
20
  /**
@@ -35,6 +35,12 @@ export declare function readOutreachSequenceStats(raw: unknown): OutreachSequenc
35
35
  * is dropped rather than sent as an empty email.
36
36
  */
37
37
  export declare function readOutreachStepOverrides(raw: unknown): OutreachStepOverrides | undefined;
38
+ /**
39
+ * The messages a stored enrollment kept from the person (AGL-4020), oldest
40
+ * first; one without an id or a sender is dropped, since it could be neither
41
+ * shown as theirs nor answered.
42
+ */
43
+ export declare function readOutreachInboundMessages(value: unknown): OutreachInboundMessage[];
38
44
  /** A stored enrollment in its model shape, or `null` for no document. */
39
45
  export declare function readStoredOutreachEnrollment(id: string, data: Record<string, unknown> | undefined): OutreachEnrollment | null;
40
46
  /** A stored mailbox with its id, or `null` for no document. */