@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
@@ -23,7 +23,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
23
23
  * what decides whether it is sent at all.
24
24
  */ import { normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
25
25
  import { readOutreachSequenceDraft } from "./sequence-draft.js";
26
- import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_SEQUENCE_STATUSES, OUTREACH_STEP_OVERRIDE_SOURCES, OUTREACH_STOP_REASONS } from "./outreach.types.js";
26
+ import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_INBOUND_KINDS, OUTREACH_SEQUENCE_STATUSES, OUTREACH_STEP_OVERRIDE_SOURCES, OUTREACH_STOP_REASONS } from "./outreach.types.js";
27
27
  const text = (value)=>typeof value === 'string' ? value : '';
28
28
  const ms = (value)=>typeof value === 'number' && Number.isFinite(value) ? value : null;
29
29
  const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry === 'string') : [];
@@ -138,6 +138,37 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
138
138
  }
139
139
  return steps;
140
140
  }
141
+ /**
142
+ * The messages a stored enrollment kept from the person (AGL-4020), oldest
143
+ * first; one without an id or a sender is dropped, since it could be neither
144
+ * shown as theirs nor answered.
145
+ */ export function readOutreachInboundMessages(value) {
146
+ if (!Array.isArray(value)) return [];
147
+ const messages = [];
148
+ for (const entry of value){
149
+ var _ms;
150
+ if (!entry || typeof entry !== 'object') continue;
151
+ const raw = entry;
152
+ const id = text(raw['id']);
153
+ const from = text(raw['from']);
154
+ if (!id || !from) continue;
155
+ const kind = raw['kind'];
156
+ messages.push({
157
+ id,
158
+ threadId: text(raw['threadId']) || null,
159
+ messageId: text(raw['messageId']) || null,
160
+ references: texts(raw['references']),
161
+ from,
162
+ fromName: text(raw['fromName']),
163
+ replyTo: text(raw['replyTo']) || null,
164
+ subject: text(raw['subject']),
165
+ atMs: (_ms = ms(raw['atMs'])) != null ? _ms : 0,
166
+ kind: OUTREACH_INBOUND_KINDS.includes(kind) ? kind : 'reply',
167
+ body: text(raw['body'])
168
+ });
169
+ }
170
+ return messages.sort((a, b)=>a.atMs - b.atMs);
171
+ }
141
172
  /** A stored enrollment in its model shape, or `null` for no document. */ export function readStoredOutreachEnrollment(id, data) {
142
173
  var _ms, _ms1;
143
174
  if (!data) return null;
@@ -196,6 +227,11 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
196
227
  const skippedSteps = readOutreachSkippedSteps(data['skippedSteps']);
197
228
  if (skippedSteps.length) enrollment.skippedSteps = skippedSteps;
198
229
  else delete enrollment.skippedSteps;
230
+ // What they wrote back (AGL-4020): absent until the sync, or the reply
231
+ // composer, first read a message of theirs.
232
+ if (Array.isArray(data['inboundMessages'])) {
233
+ enrollment.inboundMessages = readOutreachInboundMessages(data['inboundMessages']);
234
+ } else delete enrollment.inboundMessages;
199
235
  return enrollment;
200
236
  }
201
237
  /** A stored mailbox with its id, or `null` for no document. */ export function readStoredOutreachMailbox(id, data) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/stored-records.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 * A stored Outreach document read back into its model shape (AGL-2980),\n * for the server's routes and the console's listeners alike. Client-safe.\n *\n * Read DEFENSIVELY, a field at a time, because the readers act on what they\n * read: a sequence's steps are what gets sent, and an enrollment's status is\n * what decides whether it is sent at all.\n */\n\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STEP_OVERRIDE_SOURCES,\n OUTREACH_STOP_REASONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStats,\n type OutreachSequenceStatus,\n type OutreachSkippedStep,\n type OutreachStepOverride,\n type OutreachStepOverrides,\n type OutreachStopReason,\n} from './outreach.types'\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst texts = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : []\n\n/** A stored sequence in its model shape, or `null` for no document. */\nexport function readStoredOutreachSequence(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachSequence | null {\n if (!data) return null\n const draft = readOutreachSequenceDraft(data)\n const status = data['status']\n const stats = readOutreachSequenceStats(data['stats'])\n return {\n id,\n ...draft,\n status: (OUTREACH_SEQUENCE_STATUSES as readonly unknown[]).includes(status)\n ? (status as OutreachSequenceStatus)\n : 'draft',\n // Absent rather than an empty object for a sequence that has none\n // (AGL-3239): the report reads an absent counter as \"not recorded\" and\n // a present one as a number, and `{}` would be neither.\n ...(stats ? { stats } : {}),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A sequence's stored counters (AGL-3239), or `null` when it has none.\n *\n * Every field is carried through only when it is READABLE, so an absence\n * survives the round trip as an absence: the whole of the report's honesty\n * is that a counter nobody wrote is not a zero, and a normalizer that\n * defaulted these to `0` would destroy the distinction here, before any\n * reader could make it.\n */\nexport function readOutreachSequenceStats(\n raw: unknown,\n): OutreachSequenceStats | null {\n if (!raw || typeof raw !== 'object') return null\n const data = raw as Record<string, unknown>\n const stats: OutreachSequenceStats = {}\n for (const key of [\n 'sent',\n 'people',\n 'clicks',\n 'uniqueClicks',\n 'machineClicks',\n 'openPeople',\n 'opens',\n 'uniqueOpens',\n 'machineOpens',\n 'proxyOpens',\n ] as const) {\n const value = data[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {\n stats[key] = Math.floor(value)\n }\n }\n if (data['clickTracked'] === true) stats.clickTracked = true\n const last = data['lastClickAtMs']\n if (typeof last === 'number' && Number.isFinite(last)) stats.lastClickAtMs = last\n if (data['openTracked'] === true) stats.openTracked = true\n const lastOpen = data['lastOpenAtMs']\n if (typeof lastOpen === 'number' && Number.isFinite(lastOpen)) stats.lastOpenAtMs = lastOpen\n return Object.keys(stats).length ? stats : null\n}\n\n/**\n * A stored enrollment's curated steps (AGL-3324), or `undefined` when it\n * carries none worth reading. Read a field at a time, because what is read\n * here is what gets SENT in place of the step: an entry with neither a\n * subject nor a body, or one keyed by something that is not a step index,\n * is dropped rather than sent as an empty email.\n */\nexport function readOutreachStepOverrides(raw: unknown): OutreachStepOverrides | undefined {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined\n const overrides: OutreachStepOverrides = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!/^(0|[1-9]\\d*)$/.test(key) || !value || typeof value !== 'object') continue\n const entry = value as Record<string, unknown>\n const subject = typeof entry['subject'] === 'string' ? entry['subject'] : undefined\n const body = typeof entry['body'] === 'string' ? entry['body'] : undefined\n if (subject === undefined && body === undefined) continue\n const source = entry['source']\n const override: OutreachStepOverride = {\n ...(subject !== undefined ? { subject } : {}),\n ...(body !== undefined ? { body } : {}),\n // Words nobody can say the source of are read as the member's: the\n // audit line then claims no AI wrote them.\n source: (OUTREACH_STEP_OVERRIDE_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachStepOverride['source'])\n : 'member',\n draftedAtMs: ms(entry['draftedAtMs']) ?? 0,\n ...(typeof entry['draftedByUid'] === 'string' ? { draftedByUid: entry['draftedByUid'] } : {}),\n ...(entry['edited'] === true ? { edited: true } : {}),\n ...(typeof entry['prompt'] === 'string' ? { prompt: entry['prompt'] } : {}),\n ...(typeof entry['model'] === 'string' ? { model: entry['model'] } : {}),\n }\n overrides[key] = override\n }\n return Object.keys(overrides).length ? overrides : undefined\n}\n\n/** The steps a stored enrollment skipped (AGL-3228), each as the model reads one; the malformed are dropped. */\nfunction readOutreachSkippedSteps(value: unknown): OutreachSkippedStep[] {\n if (!Array.isArray(value)) return []\n const steps: OutreachSkippedStep[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const raw = entry as Record<string, unknown>\n const stepIndex = Number(raw['stepIndex'])\n if (!Number.isInteger(stepIndex) || stepIndex < 0) continue\n steps.push({\n stepIndex,\n stepId: text(raw['stepId']),\n kind: raw['kind'] === 'task' ? 'task' : 'email',\n atMs: ms(raw['atMs']) ?? 0,\n })\n }\n return steps\n}\n\n/** A stored enrollment in its model shape, or `null` for no document. */\nexport function readStoredOutreachEnrollment(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollment | null {\n if (!data) return null\n const status = data['status']\n const stopReason = data['stopReason']\n const stepIndex = Number(data['stepIndex'])\n // An enrollment written before leads could be sequenced (AGL-3234) names\n // no target and is a contact's; one that names a lead and no target is a\n // lead's, whatever else it says.\n const leadId = text(data['leadId']) || null\n const target: OutreachEnrollmentTarget =\n data['target'] === 'lead' || (data['target'] === undefined && leadId && !text(data['contactId']))\n ? 'lead'\n : 'contact'\n const enrollment: OutreachEnrollment = {\n ...(data as unknown as OutreachEnrollment),\n id,\n sequenceId: text(data['sequenceId']),\n target,\n contactId: text(data['contactId']),\n leadId,\n contactName: text(data['contactName']),\n email: text(data['email']),\n hostId: text(data['hostId']),\n mailboxId: text(data['mailboxId']),\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n nextDueAtMs: ms(data['nextDueAtMs']),\n // A status the model does not name is read as stopped: nothing sends\n // from a document nobody can say the state of.\n status: (OUTREACH_ENROLLMENT_STATUSES as readonly unknown[]).includes(\n status,\n )\n ? (status as OutreachEnrollmentStatus)\n : 'stopped',\n stopReason: (OUTREACH_STOP_REASONS as readonly unknown[]).includes(\n stopReason,\n )\n ? (stopReason as OutreachStopReason)\n : null,\n stopDetail:\n typeof data['stopDetail'] === 'string' ? data['stopDetail'] : null,\n stoppedAtMs: ms(data['stoppedAtMs']),\n stoppedByUid:\n typeof data['stoppedByUid'] === 'string' ? data['stoppedByUid'] : null,\n personalLine: text(data['personalLine']),\n cold: data['cold'] === true,\n enrolledByUid: text(data['enrolledByUid']),\n gmailThreadId:\n typeof data['gmailThreadId'] === 'string' ? data['gmailThreadId'] : null,\n gmailThreadIds: texts(data['gmailThreadIds']),\n threadSubject:\n typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,\n messageIds: texts(data['messageIds']),\n lastSentAtMs: ms(data['lastSentAtMs']),\n // Absent on an enrollment made before a sequence could join a campaign\n // (AGL-3254), and read as none: nothing is credited to a campaign the\n // enrollment does not name.\n campaignIds: normalizeContainerIds(data['campaignIds']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n // Absent rather than an empty map for an enrollment nobody curated\n // (AGL-3324): the spread above would otherwise carry whatever was stored.\n const stepOverrides = readOutreachStepOverrides(data['stepOverrides'])\n if (stepOverrides) enrollment.stepOverrides = stepOverrides\n else delete enrollment.stepOverrides\n // Where it began, and what it skipped (AGL-3228) — absent on one that\n // began at step 1, which is every enrollment made before either existed.\n const startStepIndex = Number(data['startStepIndex'])\n if (Number.isInteger(startStepIndex) && startStepIndex > 0) enrollment.startStepIndex = startStepIndex\n else delete enrollment.startStepIndex\n const skippedSteps = readOutreachSkippedSteps(data['skippedSteps'])\n if (skippedSteps.length) enrollment.skippedSteps = skippedSteps\n else delete enrollment.skippedSteps\n return enrollment\n}\n\n/** A stored mailbox with its id, or `null` for no document. */\nexport function readStoredOutreachMailbox(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n"],"names":["normalizeContainerIds","readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STEP_OVERRIDE_SOURCES","OUTREACH_STOP_REASONS","text","value","ms","Number","isFinite","texts","Array","isArray","filter","entry","readStoredOutreachSequence","id","data","draft","status","stats","readOutreachSequenceStats","includes","createdAtMs","updatedAtMs","raw","key","Math","floor","clickTracked","last","lastClickAtMs","openTracked","lastOpen","lastOpenAtMs","Object","keys","length","readOutreachStepOverrides","undefined","overrides","entries","test","subject","body","source","override","draftedAtMs","draftedByUid","edited","prompt","model","readOutreachSkippedSteps","steps","stepIndex","isInteger","push","stepId","kind","atMs","readStoredOutreachEnrollment","stopReason","leadId","target","enrollment","sequenceId","contactId","contactName","email","hostId","mailboxId","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","stepOverrides","startStepIndex","skippedSteps","readStoredOutreachMailbox"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/stored-records.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 * A stored Outreach document read back into its model shape (AGL-2980),\n * for the server's routes and the console's listeners alike. Client-safe.\n *\n * Read DEFENSIVELY, a field at a time, because the readers act on what they\n * read: a sequence's steps are what gets sent, and an enrollment's status is\n * what decides whether it is sent at all.\n */\n\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_INBOUND_KINDS,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STEP_OVERRIDE_SOURCES,\n OUTREACH_STOP_REASONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachInboundKind,\n type OutreachInboundMessage,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStats,\n type OutreachSequenceStatus,\n type OutreachSkippedStep,\n type OutreachStepOverride,\n type OutreachStepOverrides,\n type OutreachStopReason,\n} from './outreach.types'\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst texts = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : []\n\n/** A stored sequence in its model shape, or `null` for no document. */\nexport function readStoredOutreachSequence(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachSequence | null {\n if (!data) return null\n const draft = readOutreachSequenceDraft(data)\n const status = data['status']\n const stats = readOutreachSequenceStats(data['stats'])\n return {\n id,\n ...draft,\n status: (OUTREACH_SEQUENCE_STATUSES as readonly unknown[]).includes(status)\n ? (status as OutreachSequenceStatus)\n : 'draft',\n // Absent rather than an empty object for a sequence that has none\n // (AGL-3239): the report reads an absent counter as \"not recorded\" and\n // a present one as a number, and `{}` would be neither.\n ...(stats ? { stats } : {}),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A sequence's stored counters (AGL-3239), or `null` when it has none.\n *\n * Every field is carried through only when it is READABLE, so an absence\n * survives the round trip as an absence: the whole of the report's honesty\n * is that a counter nobody wrote is not a zero, and a normalizer that\n * defaulted these to `0` would destroy the distinction here, before any\n * reader could make it.\n */\nexport function readOutreachSequenceStats(\n raw: unknown,\n): OutreachSequenceStats | null {\n if (!raw || typeof raw !== 'object') return null\n const data = raw as Record<string, unknown>\n const stats: OutreachSequenceStats = {}\n for (const key of [\n 'sent',\n 'people',\n 'clicks',\n 'uniqueClicks',\n 'machineClicks',\n 'openPeople',\n 'opens',\n 'uniqueOpens',\n 'machineOpens',\n 'proxyOpens',\n ] as const) {\n const value = data[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {\n stats[key] = Math.floor(value)\n }\n }\n if (data['clickTracked'] === true) stats.clickTracked = true\n const last = data['lastClickAtMs']\n if (typeof last === 'number' && Number.isFinite(last)) stats.lastClickAtMs = last\n if (data['openTracked'] === true) stats.openTracked = true\n const lastOpen = data['lastOpenAtMs']\n if (typeof lastOpen === 'number' && Number.isFinite(lastOpen)) stats.lastOpenAtMs = lastOpen\n return Object.keys(stats).length ? stats : null\n}\n\n/**\n * A stored enrollment's curated steps (AGL-3324), or `undefined` when it\n * carries none worth reading. Read a field at a time, because what is read\n * here is what gets SENT in place of the step: an entry with neither a\n * subject nor a body, or one keyed by something that is not a step index,\n * is dropped rather than sent as an empty email.\n */\nexport function readOutreachStepOverrides(raw: unknown): OutreachStepOverrides | undefined {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined\n const overrides: OutreachStepOverrides = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!/^(0|[1-9]\\d*)$/.test(key) || !value || typeof value !== 'object') continue\n const entry = value as Record<string, unknown>\n const subject = typeof entry['subject'] === 'string' ? entry['subject'] : undefined\n const body = typeof entry['body'] === 'string' ? entry['body'] : undefined\n if (subject === undefined && body === undefined) continue\n const source = entry['source']\n const override: OutreachStepOverride = {\n ...(subject !== undefined ? { subject } : {}),\n ...(body !== undefined ? { body } : {}),\n // Words nobody can say the source of are read as the member's: the\n // audit line then claims no AI wrote them.\n source: (OUTREACH_STEP_OVERRIDE_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachStepOverride['source'])\n : 'member',\n draftedAtMs: ms(entry['draftedAtMs']) ?? 0,\n ...(typeof entry['draftedByUid'] === 'string' ? { draftedByUid: entry['draftedByUid'] } : {}),\n ...(entry['edited'] === true ? { edited: true } : {}),\n ...(typeof entry['prompt'] === 'string' ? { prompt: entry['prompt'] } : {}),\n ...(typeof entry['model'] === 'string' ? { model: entry['model'] } : {}),\n }\n overrides[key] = override\n }\n return Object.keys(overrides).length ? overrides : undefined\n}\n\n/** The steps a stored enrollment skipped (AGL-3228), each as the model reads one; the malformed are dropped. */\nfunction readOutreachSkippedSteps(value: unknown): OutreachSkippedStep[] {\n if (!Array.isArray(value)) return []\n const steps: OutreachSkippedStep[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const raw = entry as Record<string, unknown>\n const stepIndex = Number(raw['stepIndex'])\n if (!Number.isInteger(stepIndex) || stepIndex < 0) continue\n steps.push({\n stepIndex,\n stepId: text(raw['stepId']),\n kind: raw['kind'] === 'task' ? 'task' : 'email',\n atMs: ms(raw['atMs']) ?? 0,\n })\n }\n return steps\n}\n\n/**\n * The messages a stored enrollment kept from the person (AGL-4020), oldest\n * first; one without an id or a sender is dropped, since it could be neither\n * shown as theirs nor answered.\n */\nexport function readOutreachInboundMessages(value: unknown): OutreachInboundMessage[] {\n if (!Array.isArray(value)) return []\n const messages: OutreachInboundMessage[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const raw = entry as Record<string, unknown>\n const id = text(raw['id'])\n const from = text(raw['from'])\n if (!id || !from) continue\n const kind = raw['kind']\n messages.push({\n id,\n threadId: text(raw['threadId']) || null,\n messageId: text(raw['messageId']) || null,\n references: texts(raw['references']),\n from,\n fromName: text(raw['fromName']),\n replyTo: text(raw['replyTo']) || null,\n subject: text(raw['subject']),\n atMs: ms(raw['atMs']) ?? 0,\n kind: (OUTREACH_INBOUND_KINDS as readonly unknown[]).includes(kind) ? (kind as OutreachInboundKind) : 'reply',\n body: text(raw['body']),\n })\n }\n return messages.sort((a, b) => a.atMs - b.atMs)\n}\n\n/** A stored enrollment in its model shape, or `null` for no document. */\nexport function readStoredOutreachEnrollment(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollment | null {\n if (!data) return null\n const status = data['status']\n const stopReason = data['stopReason']\n const stepIndex = Number(data['stepIndex'])\n // An enrollment written before leads could be sequenced (AGL-3234) names\n // no target and is a contact's; one that names a lead and no target is a\n // lead's, whatever else it says.\n const leadId = text(data['leadId']) || null\n const target: OutreachEnrollmentTarget =\n data['target'] === 'lead' || (data['target'] === undefined && leadId && !text(data['contactId']))\n ? 'lead'\n : 'contact'\n const enrollment: OutreachEnrollment = {\n ...(data as unknown as OutreachEnrollment),\n id,\n sequenceId: text(data['sequenceId']),\n target,\n contactId: text(data['contactId']),\n leadId,\n contactName: text(data['contactName']),\n email: text(data['email']),\n hostId: text(data['hostId']),\n mailboxId: text(data['mailboxId']),\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n nextDueAtMs: ms(data['nextDueAtMs']),\n // A status the model does not name is read as stopped: nothing sends\n // from a document nobody can say the state of.\n status: (OUTREACH_ENROLLMENT_STATUSES as readonly unknown[]).includes(\n status,\n )\n ? (status as OutreachEnrollmentStatus)\n : 'stopped',\n stopReason: (OUTREACH_STOP_REASONS as readonly unknown[]).includes(\n stopReason,\n )\n ? (stopReason as OutreachStopReason)\n : null,\n stopDetail:\n typeof data['stopDetail'] === 'string' ? data['stopDetail'] : null,\n stoppedAtMs: ms(data['stoppedAtMs']),\n stoppedByUid:\n typeof data['stoppedByUid'] === 'string' ? data['stoppedByUid'] : null,\n personalLine: text(data['personalLine']),\n cold: data['cold'] === true,\n enrolledByUid: text(data['enrolledByUid']),\n gmailThreadId:\n typeof data['gmailThreadId'] === 'string' ? data['gmailThreadId'] : null,\n gmailThreadIds: texts(data['gmailThreadIds']),\n threadSubject:\n typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,\n messageIds: texts(data['messageIds']),\n lastSentAtMs: ms(data['lastSentAtMs']),\n // Absent on an enrollment made before a sequence could join a campaign\n // (AGL-3254), and read as none: nothing is credited to a campaign the\n // enrollment does not name.\n campaignIds: normalizeContainerIds(data['campaignIds']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n // Absent rather than an empty map for an enrollment nobody curated\n // (AGL-3324): the spread above would otherwise carry whatever was stored.\n const stepOverrides = readOutreachStepOverrides(data['stepOverrides'])\n if (stepOverrides) enrollment.stepOverrides = stepOverrides\n else delete enrollment.stepOverrides\n // Where it began, and what it skipped (AGL-3228) — absent on one that\n // began at step 1, which is every enrollment made before either existed.\n const startStepIndex = Number(data['startStepIndex'])\n if (Number.isInteger(startStepIndex) && startStepIndex > 0) enrollment.startStepIndex = startStepIndex\n else delete enrollment.startStepIndex\n const skippedSteps = readOutreachSkippedSteps(data['skippedSteps'])\n if (skippedSteps.length) enrollment.skippedSteps = skippedSteps\n else delete enrollment.skippedSteps\n // What they wrote back (AGL-4020): absent until the sync, or the reply\n // composer, first read a message of theirs.\n if (Array.isArray(data['inboundMessages'])) {\n enrollment.inboundMessages = readOutreachInboundMessages(data['inboundMessages'])\n } else delete enrollment.inboundMessages\n return enrollment\n}\n\n/** A stored mailbox with its id, or `null` for no document. */\nexport function readStoredOutreachMailbox(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n"],"names":["normalizeContainerIds","readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_INBOUND_KINDS","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STEP_OVERRIDE_SOURCES","OUTREACH_STOP_REASONS","text","value","ms","Number","isFinite","texts","Array","isArray","filter","entry","readStoredOutreachSequence","id","data","draft","status","stats","readOutreachSequenceStats","includes","createdAtMs","updatedAtMs","raw","key","Math","floor","clickTracked","last","lastClickAtMs","openTracked","lastOpen","lastOpenAtMs","Object","keys","length","readOutreachStepOverrides","undefined","overrides","entries","test","subject","body","source","override","draftedAtMs","draftedByUid","edited","prompt","model","readOutreachSkippedSteps","steps","stepIndex","isInteger","push","stepId","kind","atMs","readOutreachInboundMessages","messages","from","threadId","messageId","references","fromName","replyTo","sort","a","b","readStoredOutreachEnrollment","stopReason","leadId","target","enrollment","sequenceId","contactId","contactName","email","hostId","mailboxId","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","stepOverrides","startStepIndex","skippedSteps","inboundMessages","readStoredOutreachMailbox"],"mappings":"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"}
@@ -42,5 +42,9 @@
42
42
  * answered by the route with the refusal that applies, and the words for the
43
43
  * entitlement one are this extension's own: the shell's derived sentence
44
44
  * calls a feature no plan carries "a paid add-on", which this is not.
45
+ *
46
+ * ONE WIDGET ELSEWHERE (AGL-4020): "Reply in thread from <mailbox>" in a CRM
47
+ * contact's or lead's header, before its Send email, for a person who wrote
48
+ * back to a sequence — behind the same gates as the tab.
45
49
  */
46
50
  export declare function registerOutreachConsole(): void;
package/src/lib/plugin.js CHANGED
@@ -21,6 +21,11 @@ import { OUTREACH_CONSOLE_SECTIONS } from "./components/outreach-console-section
21
21
  import { OUTREACH_PERMISSIONS, OUTREACH_PLUGIN_ID, OUTREACH_USE_PERMISSION } from "./constants/bundle-common.js";
22
22
  import { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY, OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL } from "./constants/transfer-resources.js";
23
23
  /** Code-split: the hub only loads when opened. */ const OutreachConsolePage = lazy(()=>import("./components/outreach-console-page.js"));
24
+ /** Code-split: loads with a CRM record's header, where the CRM hosts its zone. */ const OutreachRecordReplyButton = lazy(()=>import("./components/record-reply-button.js"));
25
+ /**
26
+ * The CRM's record-header zone for another way to write to the person
27
+ * (AGL-4020), restated by id: a plugin never imports another plugin.
28
+ */ const OUTREACH_RECORD_EMAIL_ACTIONS_SLOT = 'crmRecordEmailActions';
24
29
  /**
25
30
  * The console half of this plugin (AGL-2974), named in `plugins.config.json`
26
31
  * as `console`. Console and console API only, like the CRM: a sequence has no
@@ -49,6 +54,10 @@ import { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY, OUTREACH_DO_NOT_CONTACT_TRANSFER_
49
54
  * answered by the route with the refusal that applies, and the words for the
50
55
  * entitlement one are this extension's own: the shell's derived sentence
51
56
  * calls a feature no plan carries "a paid add-on", which this is not.
57
+ *
58
+ * ONE WIDGET ELSEWHERE (AGL-4020): "Reply in thread from <mailbox>" in a CRM
59
+ * contact's or lead's header, before its Send email, for a person who wrote
60
+ * back to a sequence — behind the same gates as the tab.
52
61
  */ export function registerOutreachConsole() {
53
62
  registerPluginPermissions(OUTREACH_PERMISSIONS);
54
63
  // How the Import & export hub names sequences in a workspace package (AGL-3535).
@@ -90,6 +99,14 @@ import { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY, OUTREACH_DO_NOT_CONTACT_TRANSFER_
90
99
  },
91
100
  Component: OutreachConsolePage
92
101
  }
102
+ ],
103
+ widgets: [
104
+ {
105
+ slot: OUTREACH_RECORD_EMAIL_ACTIONS_SLOT,
106
+ widgetId: 'outreach-record-reply',
107
+ title: 'Reply in thread',
108
+ Component: OutreachRecordReplyButton
109
+ }
93
110
  ]
94
111
  });
95
112
  // The do-not-contact list's Import and Export, opened from the
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerConsoleExtension,\n registerPluginPermissions,\n} from '@aglyn/aglyn'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { mdiEmailFastOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { OUTREACH_CONSOLE_SECTIONS } from './components/outreach-console-sections'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n OUTREACH_USE_PERMISSION,\n} from './constants/bundle-common'\nimport {\n OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY,\n OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL,\n} from './constants/transfer-resources'\n\n/** Code-split: the hub only loads when opened. */\nconst OutreachConsolePage = lazy(\n () => import('./components/outreach-console-page'),\n)\n\n/**\n * The console half of this plugin (AGL-2974), named in `plugins.config.json`\n * as `console`. Console and console API only, like the CRM: a sequence has no\n * canvas element, so there is no site bundle and no tenant half.\n *\n * THE NAME PEOPLE READ IS \"SEQUENCES\" (AGL-3199); the plugin id, the lib, the\n * package, the flag, the entitlement, the permission, the collections and the\n * API prefix all stay `outreach`. Renaming those would move stored data and\n * break the unsubscribe links already sent, and no reader sees them.\n *\n * ONE ORGANIZATION-LEVEL SURFACE. Sequences are a rep's work across every site\n * the organization sells for, sent from that rep's own mailbox, so they live\n * beside the organization's other tabs rather than under a site. It is\n * declared in `orgNavItems`, which the console serves at\n * `/[orgSlug]/outreach/<section>` through its generic org route.\n *\n * THREE GATES, each answered by the shell, never by this page:\n *\n * - the RELEASE flag `release_outreach`, reached through the nav item's\n * `navTabId` — off by default, so only staff preview it;\n * - the ENTITLEMENT `features.outreach`, which no plan carries, so an\n * organization has it only through its per-org override;\n * - the PERMISSION `outreach.use`, which owners and admins hold by default.\n *\n * The org tab strip hides the tab unless all three hold. A deep link is\n * answered by the route with the refusal that applies, and the words for the\n * entitlement one are this extension's own: the shell's derived sentence\n * calls a feature no plan carries \"a paid add-on\", which this is not.\n */\nexport function registerOutreachConsole(): void {\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n // How the Import & export hub names sequences in a workspace package (AGL-3535).\n registerPluginTransferResourceUi(\n 'outreach.sequences',\n { label: 'Sequences', icon: { path: mdiEmailFastOutline.path } },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n registerConsoleExtension({\n pluginId: OUTREACH_PLUGIN_ID,\n displayName: 'Sequences',\n permission: OUTREACH_USE_PERMISSION,\n featureFlag: 'outreach',\n upgradeNotice: {\n message: \"Sequences isn't available to this workspace yet.\",\n },\n orgNavItems: [\n {\n label: 'Sequences',\n // The URL slug stays `outreach`: it is the plugin id the console\n // resolves the surface by, and this label is the name its tab carries.\n href: '/outreach',\n sections: OUTREACH_CONSOLE_SECTIONS,\n // The release flag's tab id: `release_outreach` names it, and the org\n // strip hides the tab from customers while the flag is off.\n navTabId: 'nav-tab-org-outreach',\n icon: { path: mdiEmailFastOutline.path },\n header: {\n title: 'Sequences',\n icon: { path: mdiEmailFastOutline.path },\n docsTopic: 'sequences',\n },\n Component: OutreachConsolePage,\n },\n ],\n })\n // The do-not-contact list's Import and Export, opened from the\n // Compliance section's do-not-contact card.\n registerPluginTransferResourceUi(\n OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY,\n { label: OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n}\n"],"names":["registerConsoleExtension","registerPluginPermissions","registerPluginTransferResourceUi","mdiEmailFastOutline","lazy","OUTREACH_CONSOLE_SECTIONS","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","OUTREACH_USE_PERMISSION","OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY","OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL","OutreachConsolePage","registerOutreachConsole","label","icon","path","pluginId","displayName","permission","featureFlag","upgradeNotice","message","orgNavItems","href","sections","navTabId","header","title","docsTopic","Component"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,yBAAyB,QACpB,eAAc;AACrB,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,yBAAyB,QAAQ,4CAAwC;AAClF,SACEC,oBAAoB,EACpBC,kBAAkB,EAClBC,uBAAuB,QAClB,+BAA2B;AAClC,SACEC,oCAAoC,EACpCC,sCAAsC,QACjC,oCAAgC;AAEvC,gDAAgD,GAChD,MAAMC,sBAAsBP,KAC1B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASQ;IACdX,0BAA0BK;IAC1B,iFAAiF;IACjFJ,iCACE,sBACA;QAAEW,OAAO;QAAaC,MAAM;YAAEC,MAAMZ,oBAAoBY,IAAI;QAAC;IAAE,GAC/D;QAAEC,UAAUT;IAAmB;IAEjCP,yBAAyB;QACvBgB,UAAUT;QACVU,aAAa;QACbC,YAAYV;QACZW,aAAa;QACbC,eAAe;YACbC,SAAS;QACX;QACAC,aAAa;YACX;gBACET,OAAO;gBACP,iEAAiE;gBACjE,uEAAuE;gBACvEU,MAAM;gBACNC,UAAUnB;gBACV,sEAAsE;gBACtE,4DAA4D;gBAC5DoB,UAAU;gBACVX,MAAM;oBAAEC,MAAMZ,oBAAoBY,IAAI;gBAAC;gBACvCW,QAAQ;oBACNC,OAAO;oBACPb,MAAM;wBAAEC,MAAMZ,oBAAoBY,IAAI;oBAAC;oBACvCa,WAAW;gBACb;gBACAC,WAAWlB;YACb;SACD;IACH;IACA,+DAA+D;IAC/D,4CAA4C;IAC5CT,iCACEO,sCACA;QAAEI,OAAOH;IAAuC,GAChD;QAAEM,UAAUT;IAAmB;AAEnC"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerConsoleExtension,\n registerPluginPermissions,\n} from '@aglyn/aglyn'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { mdiEmailFastOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { OUTREACH_CONSOLE_SECTIONS } from './components/outreach-console-sections'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n OUTREACH_USE_PERMISSION,\n} from './constants/bundle-common'\nimport {\n OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY,\n OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL,\n} from './constants/transfer-resources'\n\n/** Code-split: the hub only loads when opened. */\nconst OutreachConsolePage = lazy(\n () => import('./components/outreach-console-page'),\n)\n\n/** Code-split: loads with a CRM record's header, where the CRM hosts its zone. */\nconst OutreachRecordReplyButton = lazy(\n () => import('./components/record-reply-button'),\n)\n\n/**\n * The CRM's record-header zone for another way to write to the person\n * (AGL-4020), restated by id: a plugin never imports another plugin.\n */\nconst OUTREACH_RECORD_EMAIL_ACTIONS_SLOT = 'crmRecordEmailActions'\n\n/**\n * The console half of this plugin (AGL-2974), named in `plugins.config.json`\n * as `console`. Console and console API only, like the CRM: a sequence has no\n * canvas element, so there is no site bundle and no tenant half.\n *\n * THE NAME PEOPLE READ IS \"SEQUENCES\" (AGL-3199); the plugin id, the lib, the\n * package, the flag, the entitlement, the permission, the collections and the\n * API prefix all stay `outreach`. Renaming those would move stored data and\n * break the unsubscribe links already sent, and no reader sees them.\n *\n * ONE ORGANIZATION-LEVEL SURFACE. Sequences are a rep's work across every site\n * the organization sells for, sent from that rep's own mailbox, so they live\n * beside the organization's other tabs rather than under a site. It is\n * declared in `orgNavItems`, which the console serves at\n * `/[orgSlug]/outreach/<section>` through its generic org route.\n *\n * THREE GATES, each answered by the shell, never by this page:\n *\n * - the RELEASE flag `release_outreach`, reached through the nav item's\n * `navTabId` — off by default, so only staff preview it;\n * - the ENTITLEMENT `features.outreach`, which no plan carries, so an\n * organization has it only through its per-org override;\n * - the PERMISSION `outreach.use`, which owners and admins hold by default.\n *\n * The org tab strip hides the tab unless all three hold. A deep link is\n * answered by the route with the refusal that applies, and the words for the\n * entitlement one are this extension's own: the shell's derived sentence\n * calls a feature no plan carries \"a paid add-on\", which this is not.\n *\n * ONE WIDGET ELSEWHERE (AGL-4020): \"Reply in thread from <mailbox>\" in a CRM\n * contact's or lead's header, before its Send email, for a person who wrote\n * back to a sequence — behind the same gates as the tab.\n */\nexport function registerOutreachConsole(): void {\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n // How the Import & export hub names sequences in a workspace package (AGL-3535).\n registerPluginTransferResourceUi(\n 'outreach.sequences',\n { label: 'Sequences', icon: { path: mdiEmailFastOutline.path } },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n registerConsoleExtension({\n pluginId: OUTREACH_PLUGIN_ID,\n displayName: 'Sequences',\n permission: OUTREACH_USE_PERMISSION,\n featureFlag: 'outreach',\n upgradeNotice: {\n message: \"Sequences isn't available to this workspace yet.\",\n },\n orgNavItems: [\n {\n label: 'Sequences',\n // The URL slug stays `outreach`: it is the plugin id the console\n // resolves the surface by, and this label is the name its tab carries.\n href: '/outreach',\n sections: OUTREACH_CONSOLE_SECTIONS,\n // The release flag's tab id: `release_outreach` names it, and the org\n // strip hides the tab from customers while the flag is off.\n navTabId: 'nav-tab-org-outreach',\n icon: { path: mdiEmailFastOutline.path },\n header: {\n title: 'Sequences',\n icon: { path: mdiEmailFastOutline.path },\n docsTopic: 'sequences',\n },\n Component: OutreachConsolePage,\n },\n ],\n widgets: [\n {\n slot: OUTREACH_RECORD_EMAIL_ACTIONS_SLOT,\n widgetId: 'outreach-record-reply',\n title: 'Reply in thread',\n Component: OutreachRecordReplyButton,\n },\n ],\n })\n // The do-not-contact list's Import and Export, opened from the\n // Compliance section's do-not-contact card.\n registerPluginTransferResourceUi(\n OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY,\n { label: OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n}\n"],"names":["registerConsoleExtension","registerPluginPermissions","registerPluginTransferResourceUi","mdiEmailFastOutline","lazy","OUTREACH_CONSOLE_SECTIONS","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","OUTREACH_USE_PERMISSION","OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY","OUTREACH_DO_NOT_CONTACT_TRANSFER_LABEL","OutreachConsolePage","OutreachRecordReplyButton","OUTREACH_RECORD_EMAIL_ACTIONS_SLOT","registerOutreachConsole","label","icon","path","pluginId","displayName","permission","featureFlag","upgradeNotice","message","orgNavItems","href","sections","navTabId","header","title","docsTopic","Component","widgets","slot","widgetId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,yBAAyB,QACpB,eAAc;AACrB,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,yBAAyB,QAAQ,4CAAwC;AAClF,SACEC,oBAAoB,EACpBC,kBAAkB,EAClBC,uBAAuB,QAClB,+BAA2B;AAClC,SACEC,oCAAoC,EACpCC,sCAAsC,QACjC,oCAAgC;AAEvC,gDAAgD,GAChD,MAAMC,sBAAsBP,KAC1B,IAAM,MAAM,CAAC;AAGf,gFAAgF,GAChF,MAAMQ,4BAA4BR,KAChC,IAAM,MAAM,CAAC;AAGf;;;CAGC,GACD,MAAMS,qCAAqC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,SAASC;IACdb,0BAA0BK;IAC1B,iFAAiF;IACjFJ,iCACE,sBACA;QAAEa,OAAO;QAAaC,MAAM;YAAEC,MAAMd,oBAAoBc,IAAI;QAAC;IAAE,GAC/D;QAAEC,UAAUX;IAAmB;IAEjCP,yBAAyB;QACvBkB,UAAUX;QACVY,aAAa;QACbC,YAAYZ;QACZa,aAAa;QACbC,eAAe;YACbC,SAAS;QACX;QACAC,aAAa;YACX;gBACET,OAAO;gBACP,iEAAiE;gBACjE,uEAAuE;gBACvEU,MAAM;gBACNC,UAAUrB;gBACV,sEAAsE;gBACtE,4DAA4D;gBAC5DsB,UAAU;gBACVX,MAAM;oBAAEC,MAAMd,oBAAoBc,IAAI;gBAAC;gBACvCW,QAAQ;oBACNC,OAAO;oBACPb,MAAM;wBAAEC,MAAMd,oBAAoBc,IAAI;oBAAC;oBACvCa,WAAW;gBACb;gBACAC,WAAWpB;YACb;SACD;QACDqB,SAAS;YACP;gBACEC,MAAMpB;gBACNqB,UAAU;gBACVL,OAAO;gBACPE,WAAWnB;YACb;SACD;IACH;IACA,+DAA+D;IAC/D,4CAA4C;IAC5CV,iCACEO,sCACA;QAAEM,OAAOL;IAAuC,GAChD;QAAEQ,UAAUX;IAAmB;AAEnC"}
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { type OutreachEnrollRouteDeps } from './enroll-routes';
18
18
  import { type OutreachLinkDomainRouteDeps } from './link-domain-routes';
19
+ import { type OutreachReplyDeps } from './reply-routes';
19
20
  import type { OutreachRouteGateDeps } from './route-gate';
20
21
  import { type OutreachStepTestDeps } from './step-test-routes';
21
22
  /** The gate's reach into the platform. */
@@ -29,4 +30,9 @@ export declare function defaultOutreachLinkDomainDeps(): OutreachLinkDomainRoute
29
30
  * and the short links a real send would mint.
30
31
  */
31
32
  export declare function defaultOutreachStepTestDeps(): OutreachStepTestDeps;
33
+ /**
34
+ * The answer's reach (AGL-4020): the enrollment's mailbox through the same
35
+ * door every send uses, and the platform's rate limit.
36
+ */
37
+ export declare function defaultOutreachReplyDeps(): OutreachReplyDeps;
32
38
  export declare function registerOutreachRoutes(deps?: OutreachEnrollRouteDeps): void;
@@ -30,6 +30,7 @@ import { createOutreachEnrollmentActionRoute, createOutreachEnrollmentGatewayRou
30
30
  import { canonicalConsoleOrigin } from "../mailboxes/oauth-redirect.js";
31
31
  import { createOutreachLinkDomainsRoute } from "./link-domain-routes.js";
32
32
  import { createOutreachPreviewRoute } from "./preview-routes.js";
33
+ import { createOutreachEnrollmentReplyRoute, createOutreachEnrollmentThreadRoute } from "./reply-routes.js";
33
34
  import { createOutreachSequenceRoutes } from "./sequence-routes.js";
34
35
  import { createOutreachSettingsRoute } from "./settings-routes.js";
35
36
  import { createOutreachStepTestRoute } from "./step-test-routes.js";
@@ -138,6 +139,17 @@ export function defaultOutreachRouteDeps() {
138
139
  clickLinkOrigin: async ({ orgId, senderAddress })=>(await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress)
139
140
  };
140
141
  }
142
+ /**
143
+ * The answer's reach (AGL-4020): the enrollment's mailbox through the same
144
+ * door every send uses, and the platform's rate limit.
145
+ */ export function defaultOutreachReplyDeps() {
146
+ return {
147
+ openMailbox: (mailboxId)=>openOutreachMailboxClient(firebaseAdmin.app().firestore(), {
148
+ mailboxId
149
+ }),
150
+ consumeRateLimit: async (key, options)=>(await import("@aglyn/tenant-data-admin/server/rate-limit-store")).consumeRateLimit(key, options)
151
+ };
152
+ }
141
153
  export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
142
154
  const sequences = createOutreachSequenceRoutes(deps);
143
155
  const enroll = createOutreachEnrollRoutes(deps);
@@ -169,6 +181,12 @@ export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
169
181
  registerPluginApiRoute(OUTREACH_API_ROUTES.enrollmentsGateway, {
170
182
  web: createOutreachEnrollmentGatewayRoute(deps)
171
183
  }, orgSubject);
184
+ registerPluginApiRoute(OUTREACH_API_ROUTES.enrollmentsThread, {
185
+ web: createOutreachEnrollmentThreadRoute(deps, defaultOutreachReplyDeps())
186
+ }, orgSubject);
187
+ registerPluginApiRoute(OUTREACH_API_ROUTES.enrollmentsReply, {
188
+ web: createOutreachEnrollmentReplyRoute(deps, defaultOutreachReplyDeps())
189
+ }, orgSubject);
172
190
  registerPluginApiRoute(OUTREACH_API_ROUTES.preview, {
173
191
  web: createOutreachPreviewRoute(deps)
174
192
  }, orgSubject);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { creditConversionOutcome } from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { pluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { outreachShortLinkUrl } from '../runtime/click-link'\nimport { createOutreachCurateRoutes } from './curate-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport {\n createOutreachEnrollmentActionRoute,\n createOutreachEnrollmentGatewayRoute,\n} from './enrollment-routes'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport { createOutreachLinkDomainsRoute, type OutreachLinkDomainRouteDeps } from './link-domain-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\nimport { createOutreachStepTestRoute, type OutreachStepTestDeps } from './step-test-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n // Through the plugin that credits outcomes (`plugin-conversion-credit`),\n // which never throws and answers nothing where no plugin credits them.\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await creditConversionOutcome({ hostId, orgId, containerIds: campaignIds, outcome, atMs })\n },\n // The record system's writer (AGL-3274), resolved per call the way the\n // runtime resolves it: the CRM registers at boot, and a request that\n // arrives before it did files nothing rather than caching a `null`.\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n // The workspace's text generator (AGL-3324), resolved per call the same\n // way: the AI plugin registers at boot, and a request before it did is\n // told drafting is unavailable rather than caching a `null`.\n textGenerator: () => pluginTextGenerator()?.generator ?? null,\n }\n}\n\n/** The link domains route's reach (AGL-3306): the tracking-host store, loaded on first use. */\nexport function defaultOutreachLinkDomainDeps(): OutreachLinkDomainRouteDeps {\n return {\n hosts: async () => {\n const loaded = await platform()\n return {\n list: loaded.listTrackingHosts,\n setUp: loaded.setUpTrackingHost,\n verify: loaded.verifyTrackingHost,\n remove: loaded.removeTrackingHost,\n }\n },\n consoleOrigin: canonicalConsoleOrigin,\n }\n}\n\n/**\n * The test send's reach (AGL-3325): the mailbox's Gmail through the same\n * door the runtime and the Mailboxes test use, the platform's rate limit,\n * and the short links a real send would mint.\n */\nexport function defaultOutreachStepTestDeps(): OutreachStepTestDeps {\n return {\n openMailbox: (mailboxId) => openOutreachMailboxClient(firebaseAdmin.app().firestore(), { mailboxId }),\n consumeRateLimit: async (key, options) =>\n (await import('@aglyn/tenant-data-admin/server/rate-limit-store')).consumeRateLimit(key, options),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n clickLinkOrigin: async ({ orgId, senderAddress }) =>\n (await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress),\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const curate = createOutreachCurateRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsGateway,\n { web: createOutreachEnrollmentGatewayRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateDraft, { web: curate.draft }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateSave, { web: curate.save }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.stepTest,\n { web: createOutreachStepTestRoute(deps, defaultOutreachStepTestDeps()) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.linkDomains,\n { web: createOutreachLinkDomainsRoute(deps, defaultOutreachLinkDomainDeps()) },\n orgSubject,\n )\n}\n"],"names":["creditConversionOutcome","pluginRecordTimelineWriter","pluginTextGenerator","registerPluginApiRoute","firebaseAdmin","platformMailDnsResolver","OUTREACH_API_ROUTES","openOutreachMailboxClient","outreachOrgSubject","outreachShortLinkUrl","createOutreachCurateRoutes","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachEnrollmentGatewayRoute","canonicalConsoleOrigin","createOutreachLinkDomainsRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","createOutreachStepTestRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","containerIds","timeline","writer","resolveMx","domain","resolveAddress","Promise","resolve","textGenerator","generator","defaultOutreachLinkDomainDeps","hosts","loaded","list","listTrackingHosts","setUp","setUpTrackingHost","verify","verifyTrackingHost","remove","removeTrackingHost","consoleOrigin","defaultOutreachStepTestDeps","openMailbox","mailboxId","consumeRateLimit","clickLinkUrl","linkId","trackingOrigin","origin","clickLinkOrigin","senderAddress","resolveTrackingLinkOrigin","registerOutreachRoutes","deps","sequences","enroll","curate","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","enrollPreview","preview","confirm","enrollmentsAction","enrollmentsGateway","curateDraft","draft","curateSave","stepTest","doNotContactDomains","linkDomains"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { creditConversionOutcome } from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { pluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { outreachShortLinkUrl } from '../runtime/click-link'\nimport { createOutreachCurateRoutes } from './curate-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport {\n createOutreachEnrollmentActionRoute,\n createOutreachEnrollmentGatewayRoute,\n} from './enrollment-routes'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport { createOutreachLinkDomainsRoute, type OutreachLinkDomainRouteDeps } from './link-domain-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport {\n createOutreachEnrollmentReplyRoute,\n createOutreachEnrollmentThreadRoute,\n type OutreachReplyDeps,\n} from './reply-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\nimport { createOutreachStepTestRoute, type OutreachStepTestDeps } from './step-test-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n // Through the plugin that credits outcomes (`plugin-conversion-credit`),\n // which never throws and answers nothing where no plugin credits them.\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await creditConversionOutcome({ hostId, orgId, containerIds: campaignIds, outcome, atMs })\n },\n // The record system's writer (AGL-3274), resolved per call the way the\n // runtime resolves it: the CRM registers at boot, and a request that\n // arrives before it did files nothing rather than caching a `null`.\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n // The workspace's text generator (AGL-3324), resolved per call the same\n // way: the AI plugin registers at boot, and a request before it did is\n // told drafting is unavailable rather than caching a `null`.\n textGenerator: () => pluginTextGenerator()?.generator ?? null,\n }\n}\n\n/** The link domains route's reach (AGL-3306): the tracking-host store, loaded on first use. */\nexport function defaultOutreachLinkDomainDeps(): OutreachLinkDomainRouteDeps {\n return {\n hosts: async () => {\n const loaded = await platform()\n return {\n list: loaded.listTrackingHosts,\n setUp: loaded.setUpTrackingHost,\n verify: loaded.verifyTrackingHost,\n remove: loaded.removeTrackingHost,\n }\n },\n consoleOrigin: canonicalConsoleOrigin,\n }\n}\n\n/**\n * The test send's reach (AGL-3325): the mailbox's Gmail through the same\n * door the runtime and the Mailboxes test use, the platform's rate limit,\n * and the short links a real send would mint.\n */\nexport function defaultOutreachStepTestDeps(): OutreachStepTestDeps {\n return {\n openMailbox: (mailboxId) => openOutreachMailboxClient(firebaseAdmin.app().firestore(), { mailboxId }),\n consumeRateLimit: async (key, options) =>\n (await import('@aglyn/tenant-data-admin/server/rate-limit-store')).consumeRateLimit(key, options),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n clickLinkOrigin: async ({ orgId, senderAddress }) =>\n (await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress),\n }\n}\n\n/**\n * The answer's reach (AGL-4020): the enrollment's mailbox through the same\n * door every send uses, and the platform's rate limit.\n */\nexport function defaultOutreachReplyDeps(): OutreachReplyDeps {\n return {\n openMailbox: (mailboxId) => openOutreachMailboxClient(firebaseAdmin.app().firestore(), { mailboxId }),\n consumeRateLimit: async (key, options) =>\n (await import('@aglyn/tenant-data-admin/server/rate-limit-store')).consumeRateLimit(key, options),\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const curate = createOutreachCurateRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsGateway,\n { web: createOutreachEnrollmentGatewayRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsThread,\n { web: createOutreachEnrollmentThreadRoute(deps, defaultOutreachReplyDeps()) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsReply,\n { web: createOutreachEnrollmentReplyRoute(deps, defaultOutreachReplyDeps()) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateDraft, { web: curate.draft }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateSave, { web: curate.save }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.stepTest,\n { web: createOutreachStepTestRoute(deps, defaultOutreachStepTestDeps()) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.linkDomains,\n { web: createOutreachLinkDomainsRoute(deps, defaultOutreachLinkDomainDeps()) },\n orgSubject,\n )\n}\n"],"names":["creditConversionOutcome","pluginRecordTimelineWriter","pluginTextGenerator","registerPluginApiRoute","firebaseAdmin","platformMailDnsResolver","OUTREACH_API_ROUTES","openOutreachMailboxClient","outreachOrgSubject","outreachShortLinkUrl","createOutreachCurateRoutes","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachEnrollmentGatewayRoute","canonicalConsoleOrigin","createOutreachLinkDomainsRoute","createOutreachPreviewRoute","createOutreachEnrollmentReplyRoute","createOutreachEnrollmentThreadRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","createOutreachStepTestRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","containerIds","timeline","writer","resolveMx","domain","resolveAddress","Promise","resolve","textGenerator","generator","defaultOutreachLinkDomainDeps","hosts","loaded","list","listTrackingHosts","setUp","setUpTrackingHost","verify","verifyTrackingHost","remove","removeTrackingHost","consoleOrigin","defaultOutreachStepTestDeps","openMailbox","mailboxId","consumeRateLimit","clickLinkUrl","linkId","trackingOrigin","origin","clickLinkOrigin","senderAddress","resolveTrackingLinkOrigin","defaultOutreachReplyDeps","registerOutreachRoutes","deps","sequences","enroll","curate","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","enrollPreview","preview","confirm","enrollmentsAction","enrollmentsGateway","enrollmentsThread","enrollmentsReply","curateDraft","draft","curateSave","stepTest","doNotContactDomains","linkDomains"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
18
+ import { type OpenedOutreachMailbox } from '../mailboxes/mailbox-transport';
19
+ import type { OutreachEnrollRouteDeps } from './enroll-routes';
20
+ /**
21
+ * ANSWERING A PERSON WHO WROTE BACK (AGL-4020): `outreach/enrollments/thread`
22
+ * and `outreach/enrollments/reply`.
23
+ *
24
+ * A person who answered a sequence email is answered by a member, from the
25
+ * enrollment's own mailbox, in the same thread — never from the platform's
26
+ * mail service, which carries no prospecting, and never as a new thread.
27
+ * The engine writes the answer (`engine/manual-reply.ts`); these routes read
28
+ * what it answers, check what still refuses, and send it.
29
+ *
30
+ * ## What the person wrote
31
+ *
32
+ * The sync keeps every message of theirs on the enrollment. A reply synced
33
+ * before they were kept is on the enrollment only as an id, so when the
34
+ * enrollment says someone wrote back and keeps nothing they wrote, the
35
+ * `thread` route reads the enrollment's threads in the mailbox — and, when
36
+ * they answered in a thread of their own, the messages from their address
37
+ * since they were enrolled — and keeps what it finds. That read is made only
38
+ * for a member who may send from the mailbox: nobody reads another member's
39
+ * mail through these routes.
40
+ *
41
+ * ## The answer is no step of the sequence
42
+ *
43
+ * It is kept as a `manual_reply` step record and filed on the person's CRM
44
+ * record. It never restarts the sequence or moves its step; it never counts
45
+ * against the mailbox's daily cap, joins the sends a bounce rate is judged
46
+ * over, or bumps the sequence's figures. The mailbox counts it apart, on
47
+ * `health.daily.<day>.manualReplies`, so it can never pause the mailbox.
48
+ * Its `Message-ID` joins the enrollment's when it went into the current
49
+ * thread, so a later step of an enrollment still running answers it, and
50
+ * the enrollment's `lastSentAtMs` moves, which keeps the thread watched for
51
+ * what the person writes next.
52
+ *
53
+ * ## Once
54
+ *
55
+ * The composer mints a key per answer, which becomes its `Message-ID`. The
56
+ * answer is claimed in a transaction before it is sent: a retry of the same
57
+ * answer that already went is answered with its record, and a second answer
58
+ * while one is in flight is refused. A send that fails hands the claim back.
59
+ */
60
+ /** Answers one mailbox may send in an hour: a conversation, not a campaign. */
61
+ export declare const OUTREACH_MANUAL_REPLIES_PER_HOUR = 30;
62
+ /** The routes' reach beyond the shared route deps: the mailbox, and the platform's rate limit. */
63
+ export interface OutreachReplyDeps {
64
+ /** Opens a mailbox's client from its sealed grant. */
65
+ openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>;
66
+ consumeRateLimit(key: string, options: {
67
+ limit: number;
68
+ windowMs: number;
69
+ }): Promise<{
70
+ allowed: boolean;
71
+ }>;
72
+ }
73
+ /** `outreach/enrollments/thread` — see the module note. */
74
+ export declare function createOutreachEnrollmentThreadRoute(deps: OutreachEnrollRouteDeps, replyDeps: OutreachReplyDeps): PluginWebApiHandler;
75
+ /** `outreach/enrollments/reply` — see the module note. */
76
+ export declare function createOutreachEnrollmentReplyRoute(deps: OutreachEnrollRouteDeps, replyDeps: OutreachReplyDeps): PluginWebApiHandler;