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