@aglyn/plugins-outreach 1.0.0-beta.218 → 1.0.0-beta.220

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 (112) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/compliance-section.d.ts +5 -0
  3. package/src/lib/components/compliance-section.js +302 -180
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enroll-dialog.d.ts +12 -2
  6. package/src/lib/components/enroll-dialog.js +72 -18
  7. package/src/lib/components/enroll-dialog.js.map +1 -1
  8. package/src/lib/components/enrollment-detail.d.ts +5 -1
  9. package/src/lib/components/enrollment-detail.js +14 -8
  10. package/src/lib/components/enrollment-detail.js.map +1 -1
  11. package/src/lib/components/outreach-console-sections.d.ts +1 -1
  12. package/src/lib/components/outreach-console-sections.js +1 -1
  13. package/src/lib/components/outreach-console-sections.js.map +1 -1
  14. package/src/lib/components/sequence-editor.d.ts +13 -0
  15. package/src/lib/components/sequence-editor.js +586 -335
  16. package/src/lib/components/sequence-editor.js.map +1 -1
  17. package/src/lib/components/sequence-preview.js +25 -10
  18. package/src/lib/components/sequence-preview.js.map +1 -1
  19. package/src/lib/components/sequence-step-card.d.ts +10 -0
  20. package/src/lib/components/sequence-step-card.js +320 -202
  21. package/src/lib/components/sequence-step-card.js.map +1 -1
  22. package/src/lib/components/use-outreach-api.d.ts +2 -1
  23. package/src/lib/components/use-outreach-api.js +5 -3
  24. package/src/lib/components/use-outreach-api.js.map +1 -1
  25. package/src/lib/engine/compose.d.ts +1 -1
  26. package/src/lib/engine/compose.js +10 -6
  27. package/src/lib/engine/compose.js.map +1 -1
  28. package/src/lib/engine/delivery-status.js +1 -1
  29. package/src/lib/engine/delivery-status.js.map +1 -1
  30. package/src/lib/engine/enrollment-activity.d.ts +9 -1
  31. package/src/lib/engine/enrollment-activity.js +15 -2
  32. package/src/lib/engine/enrollment-activity.js.map +1 -1
  33. package/src/lib/engine/enrollment-state.d.ts +33 -2
  34. package/src/lib/engine/enrollment-state.js +68 -9
  35. package/src/lib/engine/enrollment-state.js.map +1 -1
  36. package/src/lib/engine/gates.d.ts +2 -2
  37. package/src/lib/engine/gates.js.map +1 -1
  38. package/src/lib/engine/opt-out-intent.js +1 -1
  39. package/src/lib/engine/opt-out-intent.js.map +1 -1
  40. package/src/lib/engine/sending-capacity.d.ts +1 -1
  41. package/src/lib/engine/sending-capacity.js +1 -1
  42. package/src/lib/engine/sending-capacity.js.map +1 -1
  43. package/src/lib/engine/sequence-validation.d.ts +20 -3
  44. package/src/lib/engine/sequence-validation.js +32 -6
  45. package/src/lib/engine/sequence-validation.js.map +1 -1
  46. package/src/lib/engine/thread-classification.js +3 -3
  47. package/src/lib/engine/thread-classification.js.map +1 -1
  48. package/src/lib/enrollment/enroll-people.js +6 -4
  49. package/src/lib/enrollment/enroll-people.js.map +1 -1
  50. package/src/lib/enrollment/gate-lookups.js +5 -4
  51. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  52. package/src/lib/enrollment/step-overrides.d.ts +4 -2
  53. package/src/lib/enrollment/step-overrides.js +11 -4
  54. package/src/lib/enrollment/step-overrides.js.map +1 -1
  55. package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
  56. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  57. package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
  58. package/src/lib/mailboxes/mailbox-routes.js +10 -7
  59. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  60. package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
  61. package/src/lib/mailboxes/mailbox-settings.js +1 -1
  62. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  63. package/src/lib/model/enrollment-timeline.d.ts +1 -1
  64. package/src/lib/model/enrollment-timeline.js +22 -3
  65. package/src/lib/model/enrollment-timeline.js.map +1 -1
  66. package/src/lib/model/outreach-api.d.ts +19 -1
  67. package/src/lib/model/outreach-api.js.map +1 -1
  68. package/src/lib/model/outreach.types.d.ts +26 -1
  69. package/src/lib/model/outreach.types.js.map +1 -1
  70. package/src/lib/model/sales-topic.d.ts +27 -0
  71. package/src/lib/model/sales-topic.js +27 -0
  72. package/src/lib/model/sales-topic.js.map +1 -0
  73. package/src/lib/model/sequence-draft.d.ts +1 -1
  74. package/src/lib/model/sequence-draft.js +2 -2
  75. package/src/lib/model/sequence-draft.js.map +1 -1
  76. package/src/lib/model/step-preview.d.ts +2 -0
  77. package/src/lib/model/step-preview.js +11 -7
  78. package/src/lib/model/step-preview.js.map +1 -1
  79. package/src/lib/model/stored-records.js +28 -2
  80. package/src/lib/model/stored-records.js.map +1 -1
  81. package/src/lib/plugin.js +1 -1
  82. package/src/lib/plugin.js.map +1 -1
  83. package/src/lib/routes/curate-routes.js +7 -7
  84. package/src/lib/routes/curate-routes.js.map +1 -1
  85. package/src/lib/routes/enroll-routes.d.ts +6 -0
  86. package/src/lib/routes/enroll-routes.js +31 -19
  87. package/src/lib/routes/enroll-routes.js.map +1 -1
  88. package/src/lib/routes/preview-routes.js +14 -5
  89. package/src/lib/routes/preview-routes.js.map +1 -1
  90. package/src/lib/routes/sequence-routes.js +9 -9
  91. package/src/lib/routes/sequence-routes.js.map +1 -1
  92. package/src/lib/routes/step-test-routes.js +3 -3
  93. package/src/lib/routes/step-test-routes.js.map +1 -1
  94. package/src/lib/runtime/campaign-credit.js +2 -2
  95. package/src/lib/runtime/campaign-credit.js.map +1 -1
  96. package/src/lib/runtime/campaign-detach.js +3 -2
  97. package/src/lib/runtime/campaign-detach.js.map +1 -1
  98. package/src/lib/runtime/lead-records.d.ts +6 -1
  99. package/src/lib/runtime/lead-records.js +25 -6
  100. package/src/lib/runtime/lead-records.js.map +1 -1
  101. package/src/lib/runtime/platform-runtime-deps.js +3 -3
  102. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  103. package/src/lib/runtime/send-job.js +20 -4
  104. package/src/lib/runtime/send-job.js.map +1 -1
  105. package/src/lib/runtime/sync-job.js +3 -3
  106. package/src/lib/runtime/sync-job.js.map +1 -1
  107. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  108. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  109. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
  110. package/src/lib/transport/gmail-errors.d.ts +27 -0
  111. package/src/lib/transport/gmail-errors.js +56 -0
  112. package/src/lib/transport/gmail-errors.js.map +1 -1
@@ -21,7 +21,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
21
21
  * Read DEFENSIVELY, a field at a time, because the readers act on what they
22
22
  * read: a sequence's steps are what gets sent, and an enrollment's status is
23
23
  * what decides whether it is sent at all.
24
- */ import { normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
24
+ */ import { normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
25
25
  import { readOutreachSequenceDraft } from "./sequence-draft.js";
26
26
  import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_SEQUENCE_STATUSES, OUTREACH_STEP_OVERRIDE_SOURCES, OUTREACH_STOP_REASONS } from "./outreach.types.js";
27
27
  const text = (value)=>typeof value === 'string' ? value : '';
@@ -120,6 +120,24 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
120
120
  }
121
121
  return Object.keys(overrides).length ? overrides : undefined;
122
122
  }
123
+ /** The steps a stored enrollment skipped (AGL-3228), each as the model reads one; the malformed are dropped. */ function readOutreachSkippedSteps(value) {
124
+ if (!Array.isArray(value)) return [];
125
+ const steps = [];
126
+ for (const entry of value){
127
+ var _ms;
128
+ if (!entry || typeof entry !== 'object') continue;
129
+ const raw = entry;
130
+ const stepIndex = Number(raw['stepIndex']);
131
+ if (!Number.isInteger(stepIndex) || stepIndex < 0) continue;
132
+ steps.push({
133
+ stepIndex,
134
+ stepId: text(raw['stepId']),
135
+ kind: raw['kind'] === 'task' ? 'task' : 'email',
136
+ atMs: (_ms = ms(raw['atMs'])) != null ? _ms : 0
137
+ });
138
+ }
139
+ return steps;
140
+ }
123
141
  /** A stored enrollment in its model shape, or `null` for no document. */ export function readStoredOutreachEnrollment(id, data) {
124
142
  var _ms, _ms1;
125
143
  if (!data) return null;
@@ -161,7 +179,7 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
161
179
  // Absent on an enrollment made before a sequence could join a campaign
162
180
  // (AGL-3254), and read as none: nothing is credited to a campaign the
163
181
  // enrollment does not name.
164
- campaignIds: normalizeCampaignIds(data['campaignIds']),
182
+ campaignIds: normalizeContainerIds(data['campaignIds']),
165
183
  createdAtMs: (_ms = ms(data['createdAtMs'])) != null ? _ms : 0,
166
184
  updatedAtMs: (_ms1 = ms(data['updatedAtMs'])) != null ? _ms1 : 0
167
185
  });
@@ -170,6 +188,14 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
170
188
  const stepOverrides = readOutreachStepOverrides(data['stepOverrides']);
171
189
  if (stepOverrides) enrollment.stepOverrides = stepOverrides;
172
190
  else delete enrollment.stepOverrides;
191
+ // Where it began, and what it skipped (AGL-3228) — absent on one that
192
+ // began at step 1, which is every enrollment made before either existed.
193
+ const startStepIndex = Number(data['startStepIndex']);
194
+ if (Number.isInteger(startStepIndex) && startStepIndex > 0) enrollment.startStepIndex = startStepIndex;
195
+ else delete enrollment.startStepIndex;
196
+ const skippedSteps = readOutreachSkippedSteps(data['skippedSteps']);
197
+ if (skippedSteps.length) enrollment.skippedSteps = skippedSteps;
198
+ else delete enrollment.skippedSteps;
173
199
  return enrollment;
174
200
  }
175
201
  /** 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 { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-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 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/** 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: normalizeCampaignIds(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 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":["normalizeCampaignIds","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","readStoredOutreachEnrollment","stopReason","stepIndex","leadId","target","enrollment","sequenceId","contactId","contactName","email","hostId","mailboxId","isInteger","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","stepOverrides","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_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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"}
package/src/lib/plugin.js CHANGED
@@ -61,7 +61,7 @@ import { OUTREACH_PERMISSIONS, OUTREACH_PLUGIN_ID, OUTREACH_USE_PERMISSION } fro
61
61
  {
62
62
  label: 'Sequences',
63
63
  // The URL slug stays `outreach`: it is the plugin id the console
64
- // resolves the surface by, and `PLUGIN_TITLES` carries the name.
64
+ // resolves the surface by, and this label is the name its tab carries.
65
65
  href: '/outreach',
66
66
  sections: OUTREACH_CONSOLE_SECTIONS,
67
67
  // The release flag's tab id: `release_outreach` names it, and the org
@@ -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 { 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'\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 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 `PLUGIN_TITLES` carries the name.\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}\n"],"names":["registerConsoleExtension","registerPluginPermissions","mdiEmailFastOutline","lazy","OUTREACH_CONSOLE_SECTIONS","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","OUTREACH_USE_PERMISSION","OutreachConsolePage","registerOutreachConsole","pluginId","displayName","permission","featureFlag","upgradeNotice","message","orgNavItems","label","href","sections","navTabId","icon","path","header","title","docsTopic","Component"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,yBAAyB,QACpB,eAAc;AACrB,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,yBAAyB,QAAQ,4CAAwC;AAClF,SACEC,oBAAoB,EACpBC,kBAAkB,EAClBC,uBAAuB,QAClB,+BAA2B;AAElC,gDAAgD,GAChD,MAAMC,sBAAsBL,KAC1B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASM;IACdR,0BAA0BI;IAC1BL,yBAAyB;QACvBU,UAAUJ;QACVK,aAAa;QACbC,YAAYL;QACZM,aAAa;QACbC,eAAe;YACbC,SAAS;QACX;QACAC,aAAa;YACX;gBACEC,OAAO;gBACP,iEAAiE;gBACjE,iEAAiE;gBACjEC,MAAM;gBACNC,UAAUf;gBACV,sEAAsE;gBACtE,4DAA4D;gBAC5DgB,UAAU;gBACVC,MAAM;oBAAEC,MAAMpB,oBAAoBoB,IAAI;gBAAC;gBACvCC,QAAQ;oBACNC,OAAO;oBACPH,MAAM;wBAAEC,MAAMpB,oBAAoBoB,IAAI;oBAAC;oBACvCG,WAAW;gBACb;gBACAC,WAAWlB;YACb;SACD;IACH;AACF"}
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 { 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'\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 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}\n"],"names":["registerConsoleExtension","registerPluginPermissions","mdiEmailFastOutline","lazy","OUTREACH_CONSOLE_SECTIONS","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","OUTREACH_USE_PERMISSION","OutreachConsolePage","registerOutreachConsole","pluginId","displayName","permission","featureFlag","upgradeNotice","message","orgNavItems","label","href","sections","navTabId","icon","path","header","title","docsTopic","Component"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,yBAAyB,QACpB,eAAc;AACrB,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,yBAAyB,QAAQ,4CAAwC;AAClF,SACEC,oBAAoB,EACpBC,kBAAkB,EAClBC,uBAAuB,QAClB,+BAA2B;AAElC,gDAAgD,GAChD,MAAMC,sBAAsBL,KAC1B,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASM;IACdR,0BAA0BI;IAC1BL,yBAAyB;QACvBU,UAAUJ;QACVK,aAAa;QACbC,YAAYL;QACZM,aAAa;QACbC,eAAe;YACbC,SAAS;QACX;QACAC,aAAa;YACX;gBACEC,OAAO;gBACP,iEAAiE;gBACjE,uEAAuE;gBACvEC,MAAM;gBACNC,UAAUf;gBACV,sEAAsE;gBACtE,4DAA4D;gBAC5DgB,UAAU;gBACVC,MAAM;oBAAEC,MAAMpB,oBAAoBoB,IAAI;gBAAC;gBACvCC,QAAQ;oBACNC,OAAO;oBACPH,MAAM;wBAAEC,MAAMpB,oBAAoBoB,IAAI;oBAAC;oBACvCG,WAAW;gBACb;gBACAC,WAAWlB;YACb;SACD;IACH;AACF"}
@@ -14,11 +14,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { readCampaignIds, readContactCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
17
+ */ import { readContainerIds, readContactContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
18
18
  import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
19
19
  import { readContactFacet } from "@aglyn/aglyn/app-utils/contacts";
20
20
  import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
21
21
  import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
22
+ import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
22
23
  import { FieldValue } from "firebase-admin/firestore";
23
24
  import { OUTREACH_CURATION_SYSTEM, outreachCurationPrompt, parseOutreachCurationAnswer } from "../engine/curation-prompt.js";
24
25
  import { outreachCuratedEntry } from "../engine/enrollment-activity.js";
@@ -158,14 +159,13 @@ const text = (value)=>typeof value === 'string' ? value.trim() : '';
158
159
  const texts = (value)=>Array.isArray(value) ? value.map(text).filter(Boolean) : [];
159
160
  /**
160
161
  * The names of the campaigns the person is filed under — the org's
161
- * containers, `orgs/{orgId}/emailCampaigns`. A container that is gone names
162
- * nothing; a read that fails names none, and the draft goes without.
162
+ * containers of the `campaign` kind. A container that is gone names nothing;
163
+ * a read that fails names none, and the draft goes without.
163
164
  */ async function campaignNames(firestore, orgId, campaignIds) {
164
165
  if (!campaignIds.length) return [];
165
166
  try {
166
- const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns');
167
- const found = await firestore.getAll(...campaignIds.map((id)=>containers.doc(id)));
168
- return found.map((snapshot)=>text(snapshot.get('name'))).filter(Boolean);
167
+ const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds);
168
+ return found.map((container)=>container.name).filter(Boolean);
169
169
  } catch (error) {
170
170
  console.error('[outreach] the person’s campaigns could not be named for the draft', error);
171
171
  return [];
@@ -177,7 +177,7 @@ const texts = (value)=>Array.isArray(value) ? value.map(text).filter(Boolean) :
177
177
  const facet = readContactFacet(contact, contactGroupId);
178
178
  const lead = (_candidate_lead = candidate.lead) != null ? _candidate_lead : {};
179
179
  const company = (_candidate_company = candidate.company) != null ? _candidate_company : {};
180
- const campaignIds = candidate.target === 'lead' ? readCampaignIds(lead) : readContactCampaignIds(contact, contactGroupId);
180
+ const campaignIds = candidate.target === 'lead' ? readContainerIds(lead, 'campaign') : readContactContainerIds(contact, contactGroupId, 'campaign');
181
181
  const [mailboxSnapshot, host, campaigns] = await Promise.all([
182
182
  subject.sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get() : null,
183
183
  subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/curate-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 { readCampaignIds, readContactCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n OUTREACH_CURATION_SYSTEM,\n outreachCurationPrompt,\n parseOutreachCurationAnswer,\n type OutreachCurationFacts,\n type OutreachCurationStep,\n} from '../engine/curation-prompt'\nimport { outreachCuratedEntry } from '../engine/enrollment-activity'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n} from '../engine/sequence-validation'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachStepOverrideRequest } from '../enrollment/step-overrides'\nimport type {\n OutreachCurateDraft,\n OutreachCurateDraftResponse,\n OutreachCurateSaveResponse,\n OutreachPersonRef,\n} from '../model/outreach-api'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n type OutreachEnrollment,\n type OutreachSequence,\n type OutreachStepOverride,\n} from '../model/outreach.types'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * CURATING ONE PERSON'S EMAILS (AGL-3324): `outreach/curate/draft` and\n * `outreach/curate/save`.\n *\n * A sequence's step is one template for everyone plus the personal line;\n * what an agent-run outbound tool does instead is write each send for each\n * prospect from what it knows about them, and let the seller approve or\n * edit. These two routes are that, with the member's confirmation kept as\n * the one thing that stores anything.\n *\n * DRAFT reads the person the way enrolling reads them — the contact, or the\n * lead as the contact it would be, with the company filed beside them — and\n * asks the workspace's text generator, on the core's seam, for this\n * person's copy of the email steps named: every email step for a person\n * about to be enrolled, the next one for an enrollment. The answer is\n * validated against the playbook's rules and returned with each draft's\n * issues, the prompt and the model. NOTHING IS STORED: the member reads it\n * in the dialog, edits it, ticks the confirmation, and then either enrolls\n * (the enroll route stores the copies with the enrollment) or saves.\n *\n * SAVE stores one confirmed copy on an open enrollment, for a step that\n * has not gone out, validated again — the request is not trusted to have\n * shown the member what it sends — and files \"Curated step N\" on the\n * person's record with who wrote the words. `override: null` clears a copy,\n * so the step goes back to the sequence's own words.\n *\n * Both read the CRM, so both ask for `data.manage` beside `outreach.use`;\n * the generator applies its own rules — the generation permission, the\n * plan, the switches, the allotments — to the member named, and meters the\n * draft on the member's month and the org's. Outreach never reaches the AI\n * plugin: the seam is the platform's, and a workspace where no plugin\n * generates text is told so in the route's words.\n */\n\n/** The purpose the generator's ledger files a draft under. */\nexport const OUTREACH_CURATION_PURPOSE = 'outreach-curate'\n\n/** The most tokens one drafting answer may run to: four short emails, with room. */\nconst CURATION_MAX_TOKENS = 2_400\n\n/** The org activity line a saved copy writes. */\nexport function outreachCuratedActivity(stepIndex: number, cleared: boolean): string {\n return cleared\n ? `Cleared the curated copy of step ${stepIndex + 1} of a sequence`\n : `Curated step ${stepIndex + 1} of a sequence for one person`\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person's copies of steps a draft covers, and the enrollment they belong to when one exists. */\ninterface DraftSubject {\n sequence: OutreachSequence\n person: OutreachPersonRef\n personalLine: string\n enrollment: OutreachEnrollment | null\n /** The steps to draft, email steps only, unique, in order. */\n stepIndexes: number[]\n}\n\n/** Every email step's index, in order. */\nfunction emailStepIndexes(sequence: Pick<OutreachSequence, 'steps'>): number[] {\n return sequence.steps.map((step, index) => (step.kind === 'email' ? index : -1)).filter((index) => index >= 0)\n}\n\n/** The step indexes a request names, or the default for its subject; a refusal for anything else. */\nfunction readStepIndexes(\n raw: unknown,\n sequence: OutreachSequence,\n fallback: number[],\n): number[] | Response {\n if (raw === undefined) return fallback\n if (!Array.isArray(raw) || !raw.length) {\n return outreachRefusal(400, 'invalid-request', 'Name the email steps to draft.')\n }\n const wanted = [...new Set(raw.map((value) => Number(value)))]\n const emails = emailStepIndexes(sequence)\n if (wanted.some((index) => !emails.includes(index))) {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n if (wanted.length > OUTREACH_MAX_EMAIL_STEPS) {\n return outreachRefusal(400, 'invalid-request', `Draft at most ${OUTREACH_MAX_EMAIL_STEPS} emails at a time.`)\n }\n return wanted.sort((a, b) => a - b)\n}\n\n/** What the request is about: a person to enroll, or an enrollment's next step. */\nasync function draftSubject(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n body: Record<string, unknown>,\n): Promise<DraftSubject | Response> {\n const enrollmentId = body['enrollmentId'] === undefined ? null : readOutreachDocumentId(body['enrollmentId'])\n if (body['enrollmentId'] !== undefined && !enrollmentId) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment to curate.')\n }\n if (enrollmentId) {\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.')\n }\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(enrollment.sequenceId)\n .get()\n const sequence = readStoredOutreachSequence(\n enrollment.sequenceId,\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n // The next email step that has not gone out: the one the enrollment is\n // on when it is an email, else the first email after it.\n const next = emailStepIndexes(sequence).find((index) => index >= enrollment.stepIndex)\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, next === undefined ? [] : [next])\n if (stepIndexes instanceof Response) return stepIndexes\n if (stepIndexes.some((index) => index < enrollment.stepIndex)) {\n return outreachRefusal(409, 'transition-refused', 'That step already went out to this person.')\n }\n if (!stepIndexes.length) {\n return outreachRefusal(409, 'transition-refused', 'Every email in this sequence has gone out to this person.')\n }\n return {\n sequence,\n person: outreachEnrollmentPerson(enrollment),\n personalLine: enrollment.personalLine,\n enrollment,\n stepIndexes,\n }\n }\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to draft for.')\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n const person: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!person) return outreachRefusal(400, 'invalid-request', 'Name the contact or the lead to draft for.')\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, emailStepIndexes(sequence))\n if (stepIndexes instanceof Response) return stepIndexes\n if (!stepIndexes.length) {\n return outreachRefusal(400, 'invalid-request', 'This sequence sends no email, so there is nothing to draft.')\n }\n return {\n sequence,\n person,\n personalLine: typeof body['personalLine'] === 'string' ? body['personalLine'] : '',\n enrollment: null,\n stepIndexes,\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(text).filter(Boolean) : []\n\n/**\n * The names of the campaigns the person is filed under — the org's\n * containers, `orgs/{orgId}/emailCampaigns`. A container that is gone names\n * nothing; a read that fails names none, and the draft goes without.\n */\nasync function campaignNames(firestore: Firestore, orgId: string, campaignIds: readonly string[]): Promise<string[]> {\n if (!campaignIds.length) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => text(snapshot.get('name'))).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the person’s campaigns could not be named for the draft', error)\n return []\n }\n}\n\n/** What the CRM holds about the person, for the prompt. */\nasync function curationFacts(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n subject: DraftSubject,\n candidate: OutreachEnrollCandidate,\n contactGroupId: string,\n): Promise<OutreachCurationFacts> {\n const contact = candidate.contact ?? {}\n const facet = readContactFacet(contact, contactGroupId) as unknown as Record<string, unknown>\n const lead = candidate.lead ?? {}\n const company = candidate.company ?? {}\n const campaignIds =\n candidate.target === 'lead' ? readCampaignIds(lead) : readContactCampaignIds(contact, contactGroupId)\n const [mailboxSnapshot, host, campaigns] = await Promise.all([\n subject.sequence.mailboxId\n ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get()\n : null,\n subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,\n campaignNames(firestore, caller.orgId, campaignIds),\n ])\n const mailbox = readStoredOutreachMailbox(\n subject.sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n return {\n name: candidate.name || text(contact['name']),\n company: text(facet['companyName']) || text(lead['company']) || text(company['name']),\n title: text(facet['jobTitle']) || text(lead['jobTitle']),\n website: text(company['website']) || text(company['domain']) || text(lead['website']) || text(lead['domain']),\n tags: [...new Set([...texts(facet['tags']), ...texts(company['tags'])])],\n sources: Object.entries((facet['sources'] ?? {}) as Record<string, unknown>)\n .filter(([, on]) => on === true)\n .map(([source]) => source)\n .sort(),\n campaigns,\n notes: candidate.target === 'lead' ? text(lead['notes']) : '',\n record: candidate.target,\n siteName: host?.exists ? text(host.get('name')) : '',\n senderName: mailbox?.displayName || '',\n personalLine: subject.personalLine,\n }\n}\n\n/** The email steps to draft, as the prompt states them: the step's words, or its template's. */\nasync function curationSteps(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n stepIndexes: readonly number[],\n): Promise<OutreachCurationStep[]> {\n const emails = emailStepIndexes(sequence)\n const templates = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.emailTemplates)\n return Promise.all(\n stepIndexes.map(async (stepIndex) => {\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') throw new Error(`step ${stepIndex} is not an email`)\n let body = step.body\n if (step.templateId) {\n const template = await templates.doc(step.templateId).get()\n body = template.exists ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body : ''\n }\n return {\n stepIndex,\n position: emails.indexOf(stepIndex) + 1,\n count: emails.length,\n subject: step.subject,\n body,\n startsThread: !isInThreadEmailStep(sequence.steps, stepIndex),\n }\n }),\n )\n}\n\n/** The member's name for the record's entry: as the roster lists them, else their address. */\nasync function memberName(firestore: Firestore, caller: OutreachRouteCaller): Promise<string | null> {\n try {\n const member = await firestore.collection('orgs').doc(caller.orgId).collection('members').doc(caller.uid).get()\n return (member.exists ? text(member.get('displayName')) || text(member.get('email')) : '') || caller.email\n } catch {\n return caller.email\n }\n}\n\nexport interface OutreachCurateRoutes {\n draft: PluginWebApiHandler\n save: PluginWebApiHandler\n}\n\nexport function createOutreachCurateRoutes(deps: OutreachEnrollRouteDeps): OutreachCurateRoutes {\n const draft: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const generator = deps.textGenerator()\n if (!generator) {\n return outreachRefusal(503, 'curation-unavailable', 'AI drafting is not available in this workspace.')\n }\n const firestore = deps.firestore()\n const subject = await draftSubject(firestore, caller, body)\n if (subject instanceof Response) return subject\n const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId\n const [candidate] = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: subject.sequence.hostId,\n contactGroupId,\n people: [subject.person],\n })\n if (!candidate?.contact || !candidate.visible) {\n return outreachRefusal(404, 'contact-not-found', \"That person isn't in this sequence's site's CRM.\")\n }\n const [facts, steps] = await Promise.all([\n curationFacts(firestore, caller, subject, candidate, contactGroupId),\n curationSteps(firestore, caller.orgId, subject.sequence, subject.stepIndexes),\n ])\n const prompt = outreachCurationPrompt(facts, steps)\n const answer = await generator.generate({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId || null,\n uid: caller.uid,\n staff: caller.staff,\n org: caller.org,\n purpose: OUTREACH_CURATION_PURPOSE,\n system: OUTREACH_CURATION_SYSTEM,\n prompt,\n maxTokens: CURATION_MAX_TOKENS,\n now: new Date(deps.now()),\n })\n // Compared to `false` rather than negated: the union narrows on the\n // literal, and this tree compiles without strict null checks.\n if (answer.ok === false) {\n return outreachRefusal(\n answer.status,\n answer.reason === 'unavailable' ? 'curation-unavailable' : 'curation-refused',\n answer.error,\n )\n }\n const parsed = parseOutreachCurationAnswer(answer.text, steps)\n if (!parsed) {\n return outreachRefusal(\n 502,\n 'curation-refused',\n 'The draft came back in a shape that can’t be read. Try again.',\n )\n }\n const drafts: OutreachCurateDraft[] = parsed.map((entry) => {\n const step = steps.find((candidateStep) => candidateStep.stepIndex === entry.stepIndex)\n return {\n ...entry,\n issues: validateOutreachCuratedStep(\n { subject: entry.subject, body: entry.body, startsThread: step?.startsThread === true },\n `stepOverrides.${entry.stepIndex}`,\n ),\n }\n })\n return outreachOk({ ok: true, drafts, prompt, model: answer.model } satisfies OutreachCurateDraftResponse)\n }\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const stepIndex = Number(body['stepIndex'])\n if (!enrollmentId || !Number.isInteger(stepIndex) || stepIndex < 0) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment and the step to curate.')\n }\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const current = await enrollments.doc(enrollmentId).get()\n const stored = readStoredOutreachEnrollment(enrollmentId, current.exists ? current.data() : undefined)\n if (!stored) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(stored.sequenceId).get()\n const sequence = readStoredOutreachSequence(stored.sequenceId, sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.steps[stepIndex]?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n\n const cleared = body['override'] === null\n let override: OutreachStepOverride | null = null\n if (!cleared) {\n const read = readOutreachStepOverrideRequest({ ...(body['override'] as object), stepIndex }, sequence.steps)\n if ('refusal' in read) return outreachRefusal(400, 'invalid-override', read.refusal)\n if (hasOutreachValidationErrors(read.issues)) {\n return outreachRefusal(400, 'invalid-override', 'This draft breaks a rule every sequence email keeps.', {\n issues: read.issues,\n })\n }\n override = { ...read.override, draftedAtMs: deps.now(), draftedByUid: caller.uid }\n }\n\n const nowMs = deps.now()\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(enrollments.doc(enrollmentId))\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { refusal: outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.') }\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return {\n refusal: outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.'),\n }\n }\n // A step that already went out, or is going out now, is not rewritten\n // under the run: what was sent is what was sent.\n if (stepIndex < enrollment.stepIndex || (enrollment.sendClaim && enrollment.sendClaim.stepIndex === stepIndex)) {\n return { refusal: outreachRefusal(409, 'transition-refused', 'That step already went out to this person.') }\n }\n transaction.update(enrollments.doc(enrollmentId), {\n [`stepOverrides.${stepIndex}`]: override ?? FieldValue.delete(),\n updatedAtMs: nowMs,\n })\n const stepOverrides = { ...(enrollment.stepOverrides ?? {}) }\n if (override) stepOverrides[String(stepIndex)] = override\n else delete stepOverrides[String(stepIndex)]\n // Absent rather than empty once the last copy is cleared, as the\n // stored reader answers it.\n const next: OutreachEnrollment = { ...enrollment, updatedAtMs: nowMs }\n delete next.stepOverrides\n if (Object.keys(stepOverrides).length) next.stepOverrides = stepOverrides\n return { enrollment: next }\n })\n if ('refusal' in written) return written.refusal\n const { enrollment } = written\n\n if (override) {\n // The person's record says so (AGL-3274, AGL-3324): who wrote this\n // person's copy of the step, under the send it shapes.\n const entry = outreachCuratedEntry({\n enrollmentId,\n stepIndex,\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(firestore, caller),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n }\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachCuratedActivity(stepIndex, cleared),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n return outreachOk({ ok: true, enrollment } satisfies OutreachCurateSaveResponse)\n }\n\n return { draft, save }\n}\n\n/** The text generator on the core's seam, for the default route deps; `null` when no plugin generates. */\nexport type OutreachTextGeneratorReader = () => PluginTextGenerator | null\n"],"names":["readCampaignIds","readContactCampaignIds","consentGroupForHost","readContactFacet","CRM_COLLECTIONS","normalizeCrmEmailTemplate","FieldValue","OUTREACH_CURATION_SYSTEM","outreachCurationPrompt","parseOutreachCurationAnswer","outreachCuratedEntry","OUTREACH_OPEN_ENROLLMENT_STATUSES","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","readOutreachEnrollCandidates","readOutreachStepOverrideRequest","OUTREACH_MAX_EMAIL_STEPS","fileOutreachNote","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_CURATION_PURPOSE","CURATION_MAX_TOKENS","outreachCuratedActivity","stepIndex","cleared","emailStepIndexes","sequence","steps","map","step","index","kind","filter","readStepIndexes","raw","fallback","undefined","Array","isArray","length","wanted","Set","value","Number","emails","some","includes","sort","a","b","draftSubject","firestore","caller","body","enrollmentId","snapshot","orgId","doc","get","enrollment","exists","data","status","sequenceSnapshot","sequenceId","next","find","stepIndexes","Response","person","personalLine","contactId","leadId","id","text","trim","texts","Boolean","campaignNames","campaignIds","containers","collection","found","getAll","error","console","curationFacts","subject","candidate","contactGroupId","facet","contact","lead","company","target","mailboxSnapshot","host","campaigns","Promise","all","mailboxId","hostId","mailbox","name","title","website","tags","sources","Object","entries","on","source","notes","record","siteName","senderName","displayName","curationSteps","templates","emailTemplates","Error","templateId","template","position","indexOf","count","startsThread","memberName","member","uid","email","createOutreachCurateRoutes","deps","draft","request","method","gate","generator","textGenerator","org","groupId","people","visible","facts","prompt","answer","generate","staff","purpose","system","maxTokens","now","Date","ok","reason","parsed","drafts","entry","candidateStep","issues","model","save","isInteger","enrollments","current","stored","override","read","refusal","draftedAtMs","draftedByUid","nowMs","written","runTransaction","transaction","sendClaim","update","delete","updatedAtMs","stepOverrides","String","keys","edited","atMs","link","dedupeKey","logOrgActivity","type"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/curate-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 { readContainerIds, readContactContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n OUTREACH_CURATION_SYSTEM,\n outreachCurationPrompt,\n parseOutreachCurationAnswer,\n type OutreachCurationFacts,\n type OutreachCurationStep,\n} from '../engine/curation-prompt'\nimport { outreachCuratedEntry } from '../engine/enrollment-activity'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n} from '../engine/sequence-validation'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachStepOverrideRequest } from '../enrollment/step-overrides'\nimport type {\n OutreachCurateDraft,\n OutreachCurateDraftResponse,\n OutreachCurateSaveResponse,\n OutreachPersonRef,\n} from '../model/outreach-api'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n type OutreachEnrollment,\n type OutreachSequence,\n type OutreachStepOverride,\n} from '../model/outreach.types'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * CURATING ONE PERSON'S EMAILS (AGL-3324): `outreach/curate/draft` and\n * `outreach/curate/save`.\n *\n * A sequence's step is one template for everyone plus the personal line;\n * what an agent-run outbound tool does instead is write each send for each\n * prospect from what it knows about them, and let the seller approve or\n * edit. These two routes are that, with the member's confirmation kept as\n * the one thing that stores anything.\n *\n * DRAFT reads the person the way enrolling reads them — the contact, or the\n * lead as the contact it would be, with the company filed beside them — and\n * asks the workspace's text generator, on the core's seam, for this\n * person's copy of the email steps named: every email step for a person\n * about to be enrolled, the next one for an enrollment. The answer is\n * validated against the playbook's rules and returned with each draft's\n * issues, the prompt and the model. NOTHING IS STORED: the member reads it\n * in the dialog, edits it, ticks the confirmation, and then either enrolls\n * (the enroll route stores the copies with the enrollment) or saves.\n *\n * SAVE stores one confirmed copy on an open enrollment, for a step that\n * has not gone out, validated again — the request is not trusted to have\n * shown the member what it sends — and files \"Curated step N\" on the\n * person's record with who wrote the words. `override: null` clears a copy,\n * so the step goes back to the sequence's own words.\n *\n * Both read the CRM, so both ask for `data.manage` beside `outreach.use`;\n * the generator applies its own rules — the generation permission, the\n * plan, the switches, the allotments — to the member named, and meters the\n * draft on the member's month and the org's. Outreach never reaches the AI\n * plugin: the seam is the platform's, and a workspace where no plugin\n * generates text is told so in the route's words.\n */\n\n/** The purpose the generator's ledger files a draft under. */\nexport const OUTREACH_CURATION_PURPOSE = 'outreach-curate'\n\n/** The most tokens one drafting answer may run to: four short emails, with room. */\nconst CURATION_MAX_TOKENS = 2_400\n\n/** The org activity line a saved copy writes. */\nexport function outreachCuratedActivity(stepIndex: number, cleared: boolean): string {\n return cleared\n ? `Cleared the curated copy of step ${stepIndex + 1} of a sequence`\n : `Curated step ${stepIndex + 1} of a sequence for one person`\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person's copies of steps a draft covers, and the enrollment they belong to when one exists. */\ninterface DraftSubject {\n sequence: OutreachSequence\n person: OutreachPersonRef\n personalLine: string\n enrollment: OutreachEnrollment | null\n /** The steps to draft, email steps only, unique, in order. */\n stepIndexes: number[]\n}\n\n/** Every email step's index, in order. */\nfunction emailStepIndexes(sequence: Pick<OutreachSequence, 'steps'>): number[] {\n return sequence.steps.map((step, index) => (step.kind === 'email' ? index : -1)).filter((index) => index >= 0)\n}\n\n/** The step indexes a request names, or the default for its subject; a refusal for anything else. */\nfunction readStepIndexes(\n raw: unknown,\n sequence: OutreachSequence,\n fallback: number[],\n): number[] | Response {\n if (raw === undefined) return fallback\n if (!Array.isArray(raw) || !raw.length) {\n return outreachRefusal(400, 'invalid-request', 'Name the email steps to draft.')\n }\n const wanted = [...new Set(raw.map((value) => Number(value)))]\n const emails = emailStepIndexes(sequence)\n if (wanted.some((index) => !emails.includes(index))) {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n if (wanted.length > OUTREACH_MAX_EMAIL_STEPS) {\n return outreachRefusal(400, 'invalid-request', `Draft at most ${OUTREACH_MAX_EMAIL_STEPS} emails at a time.`)\n }\n return wanted.sort((a, b) => a - b)\n}\n\n/** What the request is about: a person to enroll, or an enrollment's next step. */\nasync function draftSubject(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n body: Record<string, unknown>,\n): Promise<DraftSubject | Response> {\n const enrollmentId = body['enrollmentId'] === undefined ? null : readOutreachDocumentId(body['enrollmentId'])\n if (body['enrollmentId'] !== undefined && !enrollmentId) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment to curate.')\n }\n if (enrollmentId) {\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.')\n }\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(enrollment.sequenceId)\n .get()\n const sequence = readStoredOutreachSequence(\n enrollment.sequenceId,\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n // The next email step that has not gone out: the one the enrollment is\n // on when it is an email, else the first email after it.\n const next = emailStepIndexes(sequence).find((index) => index >= enrollment.stepIndex)\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, next === undefined ? [] : [next])\n if (stepIndexes instanceof Response) return stepIndexes\n if (stepIndexes.some((index) => index < enrollment.stepIndex)) {\n return outreachRefusal(409, 'transition-refused', 'That step already went out to this person.')\n }\n if (!stepIndexes.length) {\n return outreachRefusal(409, 'transition-refused', 'Every email in this sequence has gone out to this person.')\n }\n return {\n sequence,\n person: outreachEnrollmentPerson(enrollment),\n personalLine: enrollment.personalLine,\n enrollment,\n stepIndexes,\n }\n }\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to draft for.')\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n const person: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!person) return outreachRefusal(400, 'invalid-request', 'Name the contact or the lead to draft for.')\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, emailStepIndexes(sequence))\n if (stepIndexes instanceof Response) return stepIndexes\n if (!stepIndexes.length) {\n return outreachRefusal(400, 'invalid-request', 'This sequence sends no email, so there is nothing to draft.')\n }\n return {\n sequence,\n person,\n personalLine: typeof body['personalLine'] === 'string' ? body['personalLine'] : '',\n enrollment: null,\n stepIndexes,\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(text).filter(Boolean) : []\n\n/**\n * The names of the campaigns the person is filed under — the org's\n * containers of the `campaign` kind. A container that is gone names nothing;\n * a read that fails names none, and the draft goes without.\n */\nasync function campaignNames(firestore: Firestore, orgId: string, campaignIds: readonly string[]): Promise<string[]> {\n if (!campaignIds.length) return []\n try {\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => container.name).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the person’s campaigns could not be named for the draft', error)\n return []\n }\n}\n\n/** What the CRM holds about the person, for the prompt. */\nasync function curationFacts(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n subject: DraftSubject,\n candidate: OutreachEnrollCandidate,\n contactGroupId: string,\n): Promise<OutreachCurationFacts> {\n const contact = candidate.contact ?? {}\n const facet = readContactFacet(contact, contactGroupId) as unknown as Record<string, unknown>\n const lead = candidate.lead ?? {}\n const company = candidate.company ?? {}\n const campaignIds =\n candidate.target === 'lead' ? readContainerIds(lead, 'campaign') : readContactContainerIds(contact, contactGroupId, 'campaign')\n const [mailboxSnapshot, host, campaigns] = await Promise.all([\n subject.sequence.mailboxId\n ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get()\n : null,\n subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,\n campaignNames(firestore, caller.orgId, campaignIds),\n ])\n const mailbox = readStoredOutreachMailbox(\n subject.sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n return {\n name: candidate.name || text(contact['name']),\n company: text(facet['companyName']) || text(lead['company']) || text(company['name']),\n title: text(facet['jobTitle']) || text(lead['jobTitle']),\n website: text(company['website']) || text(company['domain']) || text(lead['website']) || text(lead['domain']),\n tags: [...new Set([...texts(facet['tags']), ...texts(company['tags'])])],\n sources: Object.entries((facet['sources'] ?? {}) as Record<string, unknown>)\n .filter(([, on]) => on === true)\n .map(([source]) => source)\n .sort(),\n campaigns,\n notes: candidate.target === 'lead' ? text(lead['notes']) : '',\n record: candidate.target,\n siteName: host?.exists ? text(host.get('name')) : '',\n senderName: mailbox?.displayName || '',\n personalLine: subject.personalLine,\n }\n}\n\n/** The email steps to draft, as the prompt states them: the step's words, or its template's. */\nasync function curationSteps(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n stepIndexes: readonly number[],\n): Promise<OutreachCurationStep[]> {\n const emails = emailStepIndexes(sequence)\n const templates = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.emailTemplates)\n return Promise.all(\n stepIndexes.map(async (stepIndex) => {\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') throw new Error(`step ${stepIndex} is not an email`)\n let body = step.body\n if (step.templateId) {\n const template = await templates.doc(step.templateId).get()\n body = template.exists ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body : ''\n }\n return {\n stepIndex,\n position: emails.indexOf(stepIndex) + 1,\n count: emails.length,\n subject: step.subject,\n body,\n startsThread: !isInThreadEmailStep(sequence.steps, stepIndex),\n }\n }),\n )\n}\n\n/** The member's name for the record's entry: as the roster lists them, else their address. */\nasync function memberName(firestore: Firestore, caller: OutreachRouteCaller): Promise<string | null> {\n try {\n const member = await firestore.collection('orgs').doc(caller.orgId).collection('members').doc(caller.uid).get()\n return (member.exists ? text(member.get('displayName')) || text(member.get('email')) : '') || caller.email\n } catch {\n return caller.email\n }\n}\n\nexport interface OutreachCurateRoutes {\n draft: PluginWebApiHandler\n save: PluginWebApiHandler\n}\n\nexport function createOutreachCurateRoutes(deps: OutreachEnrollRouteDeps): OutreachCurateRoutes {\n const draft: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const generator = deps.textGenerator()\n if (!generator) {\n return outreachRefusal(503, 'curation-unavailable', 'AI drafting is not available in this workspace.')\n }\n const firestore = deps.firestore()\n const subject = await draftSubject(firestore, caller, body)\n if (subject instanceof Response) return subject\n const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId\n const [candidate] = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: subject.sequence.hostId,\n contactGroupId,\n people: [subject.person],\n })\n if (!candidate?.contact || !candidate.visible) {\n return outreachRefusal(404, 'contact-not-found', \"That person isn't in this sequence's site's CRM.\")\n }\n const [facts, steps] = await Promise.all([\n curationFacts(firestore, caller, subject, candidate, contactGroupId),\n curationSteps(firestore, caller.orgId, subject.sequence, subject.stepIndexes),\n ])\n const prompt = outreachCurationPrompt(facts, steps)\n const answer = await generator.generate({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId || null,\n uid: caller.uid,\n staff: caller.staff,\n org: caller.org,\n purpose: OUTREACH_CURATION_PURPOSE,\n system: OUTREACH_CURATION_SYSTEM,\n prompt,\n maxTokens: CURATION_MAX_TOKENS,\n now: new Date(deps.now()),\n })\n // Compared to `false` rather than negated: the union narrows on the\n // literal, and this tree compiles without strict null checks.\n if (answer.ok === false) {\n return outreachRefusal(\n answer.status,\n answer.reason === 'unavailable' ? 'curation-unavailable' : 'curation-refused',\n answer.error,\n )\n }\n const parsed = parseOutreachCurationAnswer(answer.text, steps)\n if (!parsed) {\n return outreachRefusal(\n 502,\n 'curation-refused',\n 'The draft came back in a shape that can’t be read. Try again.',\n )\n }\n const drafts: OutreachCurateDraft[] = parsed.map((entry) => {\n const step = steps.find((candidateStep) => candidateStep.stepIndex === entry.stepIndex)\n return {\n ...entry,\n issues: validateOutreachCuratedStep(\n { subject: entry.subject, body: entry.body, startsThread: step?.startsThread === true },\n `stepOverrides.${entry.stepIndex}`,\n ),\n }\n })\n return outreachOk({ ok: true, drafts, prompt, model: answer.model } satisfies OutreachCurateDraftResponse)\n }\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const stepIndex = Number(body['stepIndex'])\n if (!enrollmentId || !Number.isInteger(stepIndex) || stepIndex < 0) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment and the step to curate.')\n }\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const current = await enrollments.doc(enrollmentId).get()\n const stored = readStoredOutreachEnrollment(enrollmentId, current.exists ? current.data() : undefined)\n if (!stored) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(stored.sequenceId).get()\n const sequence = readStoredOutreachSequence(stored.sequenceId, sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.steps[stepIndex]?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n\n const cleared = body['override'] === null\n let override: OutreachStepOverride | null = null\n if (!cleared) {\n const read = readOutreachStepOverrideRequest({ ...(body['override'] as object), stepIndex }, sequence.steps)\n if ('refusal' in read) return outreachRefusal(400, 'invalid-override', read.refusal)\n if (hasOutreachValidationErrors(read.issues)) {\n return outreachRefusal(400, 'invalid-override', 'This draft breaks a rule every sequence email keeps.', {\n issues: read.issues,\n })\n }\n override = { ...read.override, draftedAtMs: deps.now(), draftedByUid: caller.uid }\n }\n\n const nowMs = deps.now()\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(enrollments.doc(enrollmentId))\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { refusal: outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.') }\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return {\n refusal: outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.'),\n }\n }\n // A step that already went out, or is going out now, is not rewritten\n // under the run: what was sent is what was sent.\n if (stepIndex < enrollment.stepIndex || (enrollment.sendClaim && enrollment.sendClaim.stepIndex === stepIndex)) {\n return { refusal: outreachRefusal(409, 'transition-refused', 'That step already went out to this person.') }\n }\n transaction.update(enrollments.doc(enrollmentId), {\n [`stepOverrides.${stepIndex}`]: override ?? FieldValue.delete(),\n updatedAtMs: nowMs,\n })\n const stepOverrides = { ...(enrollment.stepOverrides ?? {}) }\n if (override) stepOverrides[String(stepIndex)] = override\n else delete stepOverrides[String(stepIndex)]\n // Absent rather than empty once the last copy is cleared, as the\n // stored reader answers it.\n const next: OutreachEnrollment = { ...enrollment, updatedAtMs: nowMs }\n delete next.stepOverrides\n if (Object.keys(stepOverrides).length) next.stepOverrides = stepOverrides\n return { enrollment: next }\n })\n if ('refusal' in written) return written.refusal\n const { enrollment } = written\n\n if (override) {\n // The person's record says so (AGL-3274, AGL-3324): who wrote this\n // person's copy of the step, under the send it shapes.\n const entry = outreachCuratedEntry({\n enrollmentId,\n stepIndex,\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(firestore, caller),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n }\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachCuratedActivity(stepIndex, cleared),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n return outreachOk({ ok: true, enrollment } satisfies OutreachCurateSaveResponse)\n }\n\n return { draft, save }\n}\n\n/** The text generator on the core's seam, for the default route deps; `null` when no plugin generates. */\nexport type OutreachTextGeneratorReader = () => PluginTextGenerator | null\n"],"names":["readContainerIds","readContactContainerIds","consentGroupForHost","readContactFacet","CRM_COLLECTIONS","normalizeCrmEmailTemplate","readOrgContainers","FieldValue","OUTREACH_CURATION_SYSTEM","outreachCurationPrompt","parseOutreachCurationAnswer","outreachCuratedEntry","OUTREACH_OPEN_ENROLLMENT_STATUSES","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","readOutreachEnrollCandidates","readOutreachStepOverrideRequest","OUTREACH_MAX_EMAIL_STEPS","fileOutreachNote","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_CURATION_PURPOSE","CURATION_MAX_TOKENS","outreachCuratedActivity","stepIndex","cleared","emailStepIndexes","sequence","steps","map","step","index","kind","filter","readStepIndexes","raw","fallback","undefined","Array","isArray","length","wanted","Set","value","Number","emails","some","includes","sort","a","b","draftSubject","firestore","caller","body","enrollmentId","snapshot","orgId","doc","get","enrollment","exists","data","status","sequenceSnapshot","sequenceId","next","find","stepIndexes","Response","person","personalLine","contactId","leadId","id","text","trim","texts","Boolean","campaignNames","campaignIds","found","container","name","error","console","curationFacts","subject","candidate","contactGroupId","facet","contact","lead","company","target","mailboxSnapshot","host","campaigns","Promise","all","mailboxId","hostId","collection","mailbox","title","website","tags","sources","Object","entries","on","source","notes","record","siteName","senderName","displayName","curationSteps","templates","emailTemplates","Error","templateId","template","position","indexOf","count","startsThread","memberName","member","uid","email","createOutreachCurateRoutes","deps","draft","request","method","gate","generator","textGenerator","org","groupId","people","visible","facts","prompt","answer","generate","staff","purpose","system","maxTokens","now","Date","ok","reason","parsed","drafts","entry","candidateStep","issues","model","save","isInteger","enrollments","current","stored","override","read","refusal","draftedAtMs","draftedByUid","nowMs","written","runTransaction","transaction","sendClaim","update","delete","updatedAtMs","stepOverrides","String","keys","edited","atMs","link","dedupeKey","logOrgActivity","type"],"mappings":"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"}
@@ -39,6 +39,12 @@ import { type OutreachRouteExtraPermission } from './route-gate';
39
39
  * Only an ACTIVE sequence takes people: enrolling schedules the first step
40
40
  * in the mailbox's hours, and a draft's steps and a paused sequence's timing
41
41
  * are not yet what will be sent.
42
+ *
43
+ * Confirm may name `startStepIndex` (AGL-3228) for people who already had
44
+ * the earlier steps by hand: everyone in the request begins at that step,
45
+ * scheduled by its own delay from now, and the steps before it are marked
46
+ * skipped on their enrollments. Absent, it is the first step; a step the
47
+ * sequence does not have refuses the whole request.
42
48
  */
43
49
  /** The permission enrolling reads the CRM with. */
44
50
  export declare const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission;
@@ -14,16 +14,17 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { CAMPAIGN_MEMBERSHIP_FIELD, contactCampaignFieldPath, normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
17
+ */ import { containerMembershipField, contactContainerFieldPath, normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
18
18
  import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
19
- import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
19
+ import { scopeTokensForHost, seenOnlyThroughGrant } from "@aglyn/aglyn/app-utils/scope-tokens";
20
20
  import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
21
21
  import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
22
22
  import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
23
+ import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
23
24
  import { restampCrmListFieldsAt } from "@aglyn/tenant-data-admin/server/crm-records";
24
25
  import { FieldValue } from "firebase-admin/firestore";
25
26
  import { outreachCuratedEntry, outreachEnrolledEntry } from "../engine/enrollment-activity.js";
26
- import { buildOutreachEnrollment, planOutreachFirstDue } from "../engine/enrollment-state.js";
27
+ import { buildOutreachEnrollment, planOutreachFirstDue, readOutreachStartStepIndex } from "../engine/enrollment-state.js";
27
28
  import { outreachEnrollmentSearchTokens } from "../enrollment/enrollment-search.js";
28
29
  import { decideOutreachEnrollment, previewOutreachPerson, readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
29
30
  import { readOutreachGateLookups } from "../enrollment/gate-lookups.js";
@@ -57,6 +58,12 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
57
58
  * Only an ACTIVE sequence takes people: enrolling schedules the first step
58
59
  * in the mailbox's hours, and a draft's steps and a paused sequence's timing
59
60
  * are not yet what will be sent.
61
+ *
62
+ * Confirm may name `startStepIndex` (AGL-3228) for people who already had
63
+ * the earlier steps by hand: everyone in the request begins at that step,
64
+ * scheduled by its own delay from now, and the steps before it are marked
65
+ * skipped on their enrollments. Absent, it is the first step; a step the
66
+ * sequence does not have refuses the whole request.
60
67
  */ /** The permission enrolling reads the CRM with. */ export const OUTREACH_READS_PEOPLE = {
61
68
  key: 'data.manage',
62
69
  refusal: "Enrolling reads your CRM's contacts, and your role does not include Manage data."
@@ -195,6 +202,9 @@ const uniqueIds = (values)=>Array.isArray(values) ? [
195
202
  */ const window = await firestore.collection('orgs').doc(orgId).collection('leads').where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId)).orderBy('lastSeenAtMs', 'desc').limit(LEADS_VIEW_WINDOW + 1).get();
196
203
  const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc)=>{
197
204
  const lead = doc.data();
205
+ // Held, not merely seen: a lead shared with this site (AGL-3336) is not
206
+ // its to sequence.
207
+ if (seenOnlyThroughGrant(lead, sequence.hostId)) return false;
198
208
  if (wanted === 'all') return true;
199
209
  if (wanted === 'open') return isCrmLeadOpen(lead) && !lead['convertedContactId'];
200
210
  return crmLeadStatus(lead) === wanted;
@@ -219,12 +229,12 @@ export function createOutreachEnrollRoutes(deps) {
219
229
  * enrollment, which already exists; a membership that could not be
220
230
  * stamped is one the list's bulk bar can add by hand.
221
231
  */ async function joinSequenceCampaigns(firestore, orgId, sequence, contactGroupId, candidate, nowMs) {
222
- const campaignIds = normalizeCampaignIds(sequence.campaignIds);
232
+ const campaignIds = normalizeContainerIds(sequence.campaignIds);
223
233
  if (!campaignIds.length) return;
224
234
  try {
225
235
  const ref = candidate.target === 'lead' && candidate.leadId ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId) : candidate.contactId ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId) : null;
226
236
  if (ref) {
227
- const field = candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId);
237
+ const field = candidate.target === 'lead' ? containerMembershipField('campaign') : contactContainerFieldPath(contactGroupId, 'campaign');
228
238
  await ref.update({
229
239
  [field]: FieldValue.arrayUnion(...campaignIds),
230
240
  updatedAt: FieldValue.serverTimestamp()
@@ -250,19 +260,15 @@ export function createOutreachEnrollRoutes(deps) {
250
260
  }
251
261
  /**
252
262
  * The names of the sequence's campaigns as they stand, for the entry the
253
- * enroll files on the person's record (AGL-3274) — the org's containers,
254
- * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a
255
- * container that is gone answers nothing and the entry names the rest.
263
+ * enroll files on the person's record (AGL-3274) — the org's containers of
264
+ * the `campaign` kind. Read once per request, not per person; a container
265
+ * that is gone answers nothing and the entry names the rest.
256
266
  */ async function sequenceCampaignNames(firestore, orgId, sequence) {
257
- const campaignIds = normalizeCampaignIds(sequence.campaignIds);
267
+ const campaignIds = normalizeContainerIds(sequence.campaignIds);
258
268
  if (!campaignIds.length || !orgId) return [];
259
269
  try {
260
- const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns');
261
- const found = await firestore.getAll(...campaignIds.map((id)=>containers.doc(id)));
262
- return found.map((snapshot)=>{
263
- var _snapshot_get;
264
- return String((_snapshot_get = snapshot.get('name')) != null ? _snapshot_get : '').trim();
265
- }).filter(Boolean);
270
+ const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds);
271
+ return found.map((container)=>container.name).filter(Boolean);
266
272
  } catch (error) {
267
273
  console.error('[outreach] the sequence’s campaigns could not be named for the record', error);
268
274
  return [];
@@ -386,6 +392,9 @@ export function createOutreachEnrollRoutes(deps) {
386
392
  const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId']);
387
393
  if (sequence instanceof Response) return sequence;
388
394
  const nowMs = deps.now();
395
+ const start = readOutreachStartStepIndex(body['startStepIndex'], sequence.steps);
396
+ if (start.refusal !== null) return outreachRefusal(400, 'invalid-start-step', start.refusal);
397
+ const startStepIndex = start.startStepIndex;
389
398
  // Each person once, by whichever record they are: a contact by id, or a
390
399
  // lead by its key (AGL-3234). A person named both ways is a contact.
391
400
  const requested = new Map();
@@ -407,7 +416,7 @@ export function createOutreachEnrollRoutes(deps) {
407
416
  const overrides = readOutreachStepOverrideRequests(person['stepOverrides'], sequence.steps, {
408
417
  uid: caller.uid,
409
418
  nowMs
410
- });
419
+ }, startStepIndex);
411
420
  if (outreachStepOverridesRefused(overrides)) {
412
421
  var _overrides_refusal;
413
422
  return outreachRefusal(400, 'invalid-override', (_overrides_refusal = overrides.refusal) != null ? _overrides_refusal : 'A curated step breaks a rule every sequence email keeps.', {
@@ -434,7 +443,8 @@ export function createOutreachEnrollRoutes(deps) {
434
443
  sequence,
435
444
  mailbox,
436
445
  enrolledAtMs: nowMs,
437
- random: deps.random
446
+ random: deps.random,
447
+ startStepIndex
438
448
  }) === null) {
439
449
  return outreachRefusal(409, 'mailbox-unavailable', "This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.");
440
450
  }
@@ -513,7 +523,8 @@ export function createOutreachEnrollRoutes(deps) {
513
523
  attestations: decision.attestations,
514
524
  enrolledByUid: caller.uid,
515
525
  nowMs,
516
- random: deps.random
526
+ random: deps.random,
527
+ startStepIndex
517
528
  });
518
529
  // The member ticked this person past the red gateway chip
519
530
  // (AGL-3326): that is their say-so, stamped as the hold's release,
@@ -556,7 +567,8 @@ export function createOutreachEnrollRoutes(deps) {
556
567
  const entry = outreachEnrolledEntry({
557
568
  enrollmentId: id,
558
569
  sequenceName: sequence.name,
559
- campaignNames
570
+ campaignNames,
571
+ startStepIndex
560
572
  });
561
573
  await fileOutreachNote(deps, {
562
574
  orgId: caller.orgId,