@aglyn/plugins-outreach 1.0.0-beta.195 → 1.0.0-beta.197

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 (90) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/curate-step-dialog.d.ts +50 -0
  3. package/src/lib/components/curate-step-dialog.js +235 -0
  4. package/src/lib/components/curate-step-dialog.js.map +1 -0
  5. package/src/lib/components/curated-step-editor.d.ts +80 -0
  6. package/src/lib/components/curated-step-editor.js +255 -0
  7. package/src/lib/components/curated-step-editor.js.map +1 -0
  8. package/src/lib/components/enroll-dialog.d.ts +15 -0
  9. package/src/lib/components/enroll-dialog.js +214 -14
  10. package/src/lib/components/enroll-dialog.js.map +1 -1
  11. package/src/lib/components/enrollments-table.d.ts +2 -0
  12. package/src/lib/components/enrollments-table.js +62 -8
  13. package/src/lib/components/enrollments-table.js.map +1 -1
  14. package/src/lib/components/mailbox-card.js +11 -2
  15. package/src/lib/components/mailbox-card.js.map +1 -1
  16. package/src/lib/components/sequence-detail.js +1 -0
  17. package/src/lib/components/sequence-detail.js.map +1 -1
  18. package/src/lib/components/sequence-editor.d.ts +2 -0
  19. package/src/lib/components/sequence-editor.js +26 -2
  20. package/src/lib/components/sequence-editor.js.map +1 -1
  21. package/src/lib/components/sequence-step-card.d.ts +7 -0
  22. package/src/lib/components/sequence-step-card.js +37 -14
  23. package/src/lib/components/sequence-step-card.js.map +1 -1
  24. package/src/lib/components/sequences-section.js +12 -3
  25. package/src/lib/components/sequences-section.js.map +1 -1
  26. package/src/lib/components/step-test-dialog.d.ts +31 -0
  27. package/src/lib/components/step-test-dialog.js +247 -0
  28. package/src/lib/components/step-test-dialog.js.map +1 -0
  29. package/src/lib/components/use-outreach-api.d.ts +15 -1
  30. package/src/lib/components/use-outreach-api.js +16 -0
  31. package/src/lib/components/use-outreach-api.js.map +1 -1
  32. package/src/lib/components/use-outreach-data.d.ts +6 -1
  33. package/src/lib/components/use-outreach-data.js +6 -3
  34. package/src/lib/components/use-outreach-data.js.map +1 -1
  35. package/src/lib/constants/api-routes.d.ts +15 -0
  36. package/src/lib/constants/api-routes.js +12 -0
  37. package/src/lib/constants/api-routes.js.map +1 -1
  38. package/src/lib/engine/compose.d.ts +12 -2
  39. package/src/lib/engine/compose.js +32 -6
  40. package/src/lib/engine/compose.js.map +1 -1
  41. package/src/lib/engine/curation-prompt.d.ts +73 -0
  42. package/src/lib/engine/curation-prompt.js +151 -0
  43. package/src/lib/engine/curation-prompt.js.map +1 -0
  44. package/src/lib/engine/enrollment-activity.d.ts +24 -0
  45. package/src/lib/engine/enrollment-activity.js +25 -0
  46. package/src/lib/engine/enrollment-activity.js.map +1 -1
  47. package/src/lib/engine/sequence-validation.d.ts +10 -1
  48. package/src/lib/engine/sequence-validation.js +54 -0
  49. package/src/lib/engine/sequence-validation.js.map +1 -1
  50. package/src/lib/enrollment/step-overrides.d.ts +53 -0
  51. package/src/lib/enrollment/step-overrides.js +140 -0
  52. package/src/lib/enrollment/step-overrides.js.map +1 -0
  53. package/src/lib/model/outreach-api.d.ts +89 -2
  54. package/src/lib/model/outreach-api.js.map +1 -1
  55. package/src/lib/model/outreach.types.d.ts +42 -0
  56. package/src/lib/model/outreach.types.js +18 -0
  57. package/src/lib/model/outreach.types.js.map +1 -1
  58. package/src/lib/model/step-preview.d.ts +10 -2
  59. package/src/lib/model/step-preview.js +9 -5
  60. package/src/lib/model/step-preview.js.map +1 -1
  61. package/src/lib/model/stored-records.d.ts +9 -1
  62. package/src/lib/model/stored-records.js +47 -2
  63. package/src/lib/model/stored-records.js.map +1 -1
  64. package/src/lib/routes/curate-routes.d.ts +63 -0
  65. package/src/lib/routes/curate-routes.js +424 -0
  66. package/src/lib/routes/curate-routes.js.map +1 -0
  67. package/src/lib/routes/enroll-routes.d.ts +7 -0
  68. package/src/lib/routes/enroll-routes.js +58 -4
  69. package/src/lib/routes/enroll-routes.js.map +1 -1
  70. package/src/lib/routes/platform-deps.d.ts +2 -2
  71. package/src/lib/routes/platform-deps.js +2 -2
  72. package/src/lib/routes/platform-deps.js.map +1 -1
  73. package/src/lib/routes/preview-routes.d.ts +24 -0
  74. package/src/lib/routes/preview-routes.js +103 -85
  75. package/src/lib/routes/preview-routes.js.map +1 -1
  76. package/src/lib/routes/register-routes.d.ts +7 -0
  77. package/src/lib/routes/register-routes.js +41 -0
  78. package/src/lib/routes/register-routes.js.map +1 -1
  79. package/src/lib/routes/step-test-routes.d.ts +88 -0
  80. package/src/lib/routes/step-test-routes.js +275 -0
  81. package/src/lib/routes/step-test-routes.js.map +1 -0
  82. package/src/lib/runtime/click-events.js +10 -0
  83. package/src/lib/runtime/click-events.js.map +1 -1
  84. package/src/lib/runtime/click-link.d.ts +11 -0
  85. package/src/lib/runtime/click-link.js +9 -4
  86. package/src/lib/runtime/click-link.js.map +1 -1
  87. package/src/lib/runtime/click-route.js +3 -0
  88. package/src/lib/runtime/click-route.js.map +1 -1
  89. package/src/lib/runtime/send-job.js +2 -0
  90. package/src/lib/runtime/send-job.js.map +1 -1
@@ -20,6 +20,7 @@ import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-temp
20
20
  import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
21
21
  import { firstEmailStepIndex } from "../engine/sequence-validation.js";
22
22
  import { leadAsContact } from "../enrollment/enroll-people.js";
23
+ import { outreachStepOverridesRefused, readOutreachStepOverrideRequests } from "../enrollment/step-overrides.js";
23
24
  import { OUTREACH_SAMPLE_PERSON, outreachSampleMergeContext, previewOutreachStep } from "../model/step-preview.js";
24
25
  import { readOutreachComplianceSettingsDoc } from "../storage/compliance-settings-store.js";
25
26
  import { outreachOrgCollection, readStoredOutreachMailbox, readStoredOutreachSequence } from "../storage/outreach-records.js";
@@ -27,100 +28,117 @@ import { OUTREACH_READS_PEOPLE } from "./enroll-routes.js";
27
28
  import { outreachRouteGate } from "./route-gate.js";
28
29
  import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocumentId, readOutreachJsonBody } from "./route-http.js";
29
30
  /**
30
- * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):
31
- * `outreach/preview`.
32
- *
33
- * Written from what is stored — the sequence, the CRM template a step names,
34
- * the mailbox it sends as, the organization's footer — by the engine's own
35
- * composer (`previewOutreachStep`), for a real contact with the personal
36
- * line the rep is writing, or for a sample person. The enroll dialog shows
37
- * it beside each person before Confirm, so the rep reads the sentence they
38
- * wrote in the email it goes into.
39
- *
40
- * A real contact is read from the CRM, so naming one asks for `data.manage`
41
- * as enrolling does; the sample person asks for nothing more.
42
- */ export function createOutreachPreviewRoute(deps) {
43
- return async (request)=>{
44
- var _host_get, _normalizeContactEmail, _ref, _ref1;
45
- var _result_email, _result_email1;
46
- if (request.method !== 'POST') return outreachMethodNotAllowed('POST');
47
- const body = await readOutreachJsonBody(request);
48
- const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId']);
49
- if (body['contactId'] !== undefined && !contactId) {
50
- return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.');
51
- }
52
- // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.
53
- const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId']);
54
- if (body['leadId'] !== undefined && !leadId) {
55
- return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.');
56
- }
57
- const caller = await outreachRouteGate(request, body['orgId'], deps.gate, contactId || leadId ? [
58
- OUTREACH_READS_PEOPLE
59
- ] : []);
60
- if (caller instanceof Response) return caller;
61
- const sequenceId = readOutreachDocumentId(body['sequenceId']);
62
- if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.');
63
- const firestore = deps.firestore();
64
- const org = firestore.collection('orgs').doc(caller.orgId);
65
- const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get();
66
- const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined);
67
- if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.');
68
- const requested = body['stepIndex'];
69
- const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps);
70
- const step = sequence.steps[stepIndex];
71
- if ((step == null ? void 0 : step.kind) !== 'email') {
72
- return outreachRefusal(400, 'invalid-request', 'That step does not send an email.');
73
- }
74
- const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([
75
- readOutreachComplianceSettingsDoc(firestore, caller.orgId),
76
- sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,
77
- sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,
78
- step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,
79
- contactId ? org.collection('contacts').doc(contactId).get() : null,
80
- // One org row (AGL-3275); the site is still required because a lead
81
- // preview is rendered in a site's context.
82
- leadId && sequence.hostId ? org.collection('leads').doc(leadId).get() : null
83
- ]);
84
- const leadData = (lead == null ? void 0 : lead.exists) ? lead.data() : null;
85
- if (leadId && !leadData) {
86
- return outreachRefusal(404, 'contact-not-found', "That lead isn't in this sequence's site's CRM.");
87
- }
88
- const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId;
89
- const contactData = (contact == null ? void 0 : contact.exists) ? contact.data() : leadData ? leadAsContact(leadData, contactGroupId) : null;
90
- if (contactId && (!contactData || !visibleToHost(contactData['visibleTo'], sequence.hostId))) {
91
- return outreachRefusal(404, 'contact-not-found', "That contact isn't in this sequence's site's CRM.");
92
- }
93
- const mailbox = readStoredOutreachMailbox(sequence.mailboxId, (mailboxSnapshot == null ? void 0 : mailboxSnapshot.exists) ? mailboxSnapshot.data() : undefined);
94
- const sender = mailbox ? {
95
- name: mailbox.displayName,
96
- email: mailbox.sendAs || mailbox.email
97
- } : null;
98
- const siteName = (host == null ? void 0 : host.exists) ? String((_host_get = host.get('name')) != null ? _host_get : '') : '';
99
- const merge = contactData ? {
100
- contact: contactData,
101
- contactGroupId,
102
- lead: leadData,
103
- sender,
104
- site: {
105
- name: siteName
106
- }
107
- } : outreachSampleMergeContext({
108
- sender,
109
- siteName
31
+ * The gate, then the sequence, the step and everything the step is rendered
32
+ * from — as the request body names them — or the refusal that stops it.
33
+ */ export async function readOutreachStepRender(deps, request, body) {
34
+ var _host_get, _normalizeContactEmail;
35
+ const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId']);
36
+ if (body['contactId'] !== undefined && !contactId) {
37
+ return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.');
38
+ }
39
+ // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.
40
+ const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId']);
41
+ if (body['leadId'] !== undefined && !leadId) {
42
+ return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.');
43
+ }
44
+ const caller = await outreachRouteGate(request, body['orgId'], deps.gate, contactId || leadId ? [
45
+ OUTREACH_READS_PEOPLE
46
+ ] : []);
47
+ if (caller instanceof Response) return caller;
48
+ const sequenceId = readOutreachDocumentId(body['sequenceId']);
49
+ if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.');
50
+ const firestore = deps.firestore();
51
+ const org = firestore.collection('orgs').doc(caller.orgId);
52
+ const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get();
53
+ const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined);
54
+ if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.');
55
+ const requested = body['stepIndex'];
56
+ const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps);
57
+ const step = sequence.steps[stepIndex];
58
+ if ((step == null ? void 0 : step.kind) !== 'email') {
59
+ return outreachRefusal(400, 'invalid-request', 'That step does not send an email.');
60
+ }
61
+ // The person's copies of steps as the dialog holds them (AGL-3324): the
62
+ // preview shows the curated version, held to the rules a stored one is —
63
+ // and a test send (AGL-3325) sends it.
64
+ const overrides = readOutreachStepOverrideRequests(body['stepOverrides'], sequence.steps, {
65
+ uid: caller.uid,
66
+ nowMs: deps.now()
67
+ });
68
+ if (outreachStepOverridesRefused(overrides)) {
69
+ var _overrides_refusal;
70
+ return outreachRefusal(400, 'invalid-override', (_overrides_refusal = overrides.refusal) != null ? _overrides_refusal : 'A curated step breaks a rule every sequence email keeps.', {
71
+ issues: overrides.issues
110
72
  });
111
- const personalLine = typeof body['personalLine'] === 'string' ? body['personalLine'] : contactData ? '' : OUTREACH_SAMPLE_PERSON.personalLine;
112
- const result = previewOutreachStep({
73
+ }
74
+ const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([
75
+ readOutreachComplianceSettingsDoc(firestore, caller.orgId),
76
+ sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,
77
+ sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,
78
+ step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,
79
+ contactId ? org.collection('contacts').doc(contactId).get() : null,
80
+ // One org row (AGL-3275); the site is still required because a lead
81
+ // preview is rendered in a site's context.
82
+ leadId && sequence.hostId ? org.collection('leads').doc(leadId).get() : null
83
+ ]);
84
+ const leadData = (lead == null ? void 0 : lead.exists) ? lead.data() : null;
85
+ if (leadId && !leadData) {
86
+ return outreachRefusal(404, 'contact-not-found', "That lead isn't in this sequence's site's CRM.");
87
+ }
88
+ const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId;
89
+ const contactData = (contact == null ? void 0 : contact.exists) ? contact.data() : leadData ? leadAsContact(leadData, contactGroupId) : null;
90
+ if (contactId && (!contactData || !visibleToHost(contactData['visibleTo'], sequence.hostId))) {
91
+ return outreachRefusal(404, 'contact-not-found', "That contact isn't in this sequence's site's CRM.");
92
+ }
93
+ const mailbox = readStoredOutreachMailbox(sequence.mailboxId, (mailboxSnapshot == null ? void 0 : mailboxSnapshot.exists) ? mailboxSnapshot.data() : undefined);
94
+ const sender = mailbox ? {
95
+ name: mailbox.displayName,
96
+ email: mailbox.sendAs || mailbox.email
97
+ } : null;
98
+ const siteName = (host == null ? void 0 : host.exists) ? String((_host_get = host.get('name')) != null ? _host_get : '') : '';
99
+ const merge = contactData ? {
100
+ contact: contactData,
101
+ contactGroupId,
102
+ lead: leadData,
103
+ sender,
104
+ site: {
105
+ name: siteName
106
+ }
107
+ } : outreachSampleMergeContext({
108
+ sender,
109
+ siteName
110
+ });
111
+ const personalLine = typeof body['personalLine'] === 'string' ? body['personalLine'] : contactData ? '' : OUTREACH_SAMPLE_PERSON.personalLine;
112
+ return {
113
+ caller,
114
+ sequence,
115
+ stepIndex,
116
+ step,
117
+ mailbox,
118
+ input: {
113
119
  steps: sequence.steps,
114
120
  stepIndex,
115
121
  orgSettings,
116
122
  merge,
117
123
  email: contactData ? (_normalizeContactEmail = normalizeContactEmail(contactData['email'])) != null ? _normalizeContactEmail : '' : OUTREACH_SAMPLE_PERSON.email,
118
124
  personalLine,
119
- templateBody: (template == null ? void 0 : template.exists) ? normalizeCrmEmailTemplate(template.data()).body : null
120
- });
125
+ templateBody: (template == null ? void 0 : template.exists) ? normalizeCrmEmailTemplate(template.data()).body : null,
126
+ stepOverrides: overrides.overrides
127
+ }
128
+ };
129
+ }
130
+ export function createOutreachPreviewRoute(deps) {
131
+ return async (request)=>{
132
+ var _ref, _ref1;
133
+ var _result_email, _result_email1;
134
+ if (request.method !== 'POST') return outreachMethodNotAllowed('POST');
135
+ const body = await readOutreachJsonBody(request);
136
+ const loaded = await readOutreachStepRender(deps, request, body);
137
+ if (loaded instanceof Response) return loaded;
138
+ const result = previewOutreachStep(loaded.input);
121
139
  return outreachOk({
122
140
  ok: true,
123
- stepIndex,
141
+ stepIndex: loaded.stepIndex,
124
142
  subject: (_ref = (_result_email = result.email) == null ? void 0 : _result_email.subject) != null ? _ref : '',
125
143
  text: (_ref1 = (_result_email1 = result.email) == null ? void 0 : _result_email1.text) != null ? _ref1 : '',
126
144
  unresolvedFields: result.unresolvedFields,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/preview-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { normalizeContactEmail } 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 { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { firstEmailStepIndex } from '../engine/sequence-validation'\nimport { leadAsContact } from '../enrollment/enroll-people'\nimport type { OutreachPreviewResponse } from '../model/outreach-api'\nimport {\n OUTREACH_SAMPLE_PERSON,\n outreachSampleMergeContext,\n previewOutreachStep,\n} from '../model/step-preview'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE } from './enroll-routes'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):\n * `outreach/preview`.\n *\n * Written from what is stored — the sequence, the CRM template a step names,\n * the mailbox it sends as, the organization's footer — by the engine's own\n * composer (`previewOutreachStep`), for a real contact with the personal\n * line the rep is writing, or for a sample person. The enroll dialog shows\n * it beside each person before Confirm, so the rep reads the sentence they\n * wrote in the email it goes into.\n *\n * A real contact is read from the CRM, so naming one asks for `data.manage`\n * as enrolling does; the sample person asks for nothing more.\n */\n\nexport function createOutreachPreviewRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n if (body['contactId'] !== undefined && !contactId) {\n return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.')\n }\n // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n if (body['leadId'] !== undefined && !leadId) {\n return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.')\n }\n const caller = await outreachRouteGate(\n request,\n body['orgId'],\n deps.gate,\n contactId || leadId ? [OUTREACH_READS_PEOPLE] : [],\n )\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.')\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(caller.orgId)\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\n const requested = body['stepIndex']\n const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps)\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'That step does not send an email.')\n }\n\n const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,\n sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,\n step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,\n contactId ? org.collection('contacts').doc(contactId).get() : null,\n // One org row (AGL-3275); the site is still required because a lead\n // preview is rendered in a site's context.\n leadId && sequence.hostId ? org.collection('leads').doc(leadId).get() : null,\n ])\n const leadData = lead?.exists ? (lead.data() as Record<string, unknown>) : null\n if (leadId && !leadData) {\n return outreachRefusal(404, 'contact-not-found', \"That lead isn't in this sequence's site's CRM.\")\n }\n const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId\n const contactData = contact?.exists\n ? (contact.data() as Record<string, unknown>)\n : leadData\n ? leadAsContact(leadData, contactGroupId)\n : null\n if (\n contactId &&\n (!contactData || !visibleToHost(contactData['visibleTo'] as string[] | undefined, sequence.hostId))\n ) {\n return outreachRefusal(404, 'contact-not-found', \"That contact isn't in this sequence's site's CRM.\")\n }\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n const sender = mailbox ? { name: mailbox.displayName, email: mailbox.sendAs || mailbox.email } : null\n const siteName = host?.exists ? String(host.get('name') ?? '') : ''\n const merge = contactData\n ? {\n contact: contactData,\n contactGroupId,\n lead: leadData,\n sender,\n site: { name: siteName },\n }\n : outreachSampleMergeContext({ sender, siteName })\n const personalLine =\n typeof body['personalLine'] === 'string'\n ? body['personalLine']\n : contactData\n ? ''\n : OUTREACH_SAMPLE_PERSON.personalLine\n const result = previewOutreachStep({\n steps: sequence.steps,\n stepIndex,\n orgSettings,\n merge,\n email: contactData ? (normalizeContactEmail(contactData['email']) ?? '') : OUTREACH_SAMPLE_PERSON.email,\n personalLine,\n templateBody: template?.exists\n ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body\n : null,\n })\n return outreachOk({\n ok: true,\n stepIndex,\n subject: result.email?.subject ?? '',\n text: result.email?.text ?? '',\n unresolvedFields: result.unresolvedFields,\n error: result.error,\n } satisfies OutreachPreviewResponse)\n }\n}\n"],"names":["consentGroupForHost","normalizeContactEmail","CRM_COLLECTIONS","normalizeCrmEmailTemplate","visibleToHost","firstEmailStepIndex","leadAsContact","OUTREACH_SAMPLE_PERSON","outreachSampleMergeContext","previewOutreachStep","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","createOutreachPreviewRoute","deps","request","host","result","method","body","contactId","undefined","leadId","caller","gate","Response","sequenceId","firestore","org","collection","doc","orgId","snapshot","get","sequence","exists","data","requested","stepIndex","steps","step","kind","orgSettings","mailboxSnapshot","template","contact","lead","Promise","all","mailboxId","hostId","templateId","emailTemplates","leadData","contactGroupId","groupId","contactData","mailbox","sender","name","displayName","email","sendAs","siteName","String","merge","site","personalLine","templateBody","ok","subject","text","unresolvedFields","error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/preview-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { normalizeContactEmail } 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 { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { firstEmailStepIndex } from '../engine/sequence-validation'\nimport { leadAsContact } from '../enrollment/enroll-people'\nimport { outreachStepOverridesRefused, readOutreachStepOverrideRequests } from '../enrollment/step-overrides'\nimport type { OutreachPreviewResponse } from '../model/outreach-api'\nimport type { OutreachEmailStep, OutreachMailbox, OutreachSequence } from '../model/outreach.types'\nimport {\n OUTREACH_SAMPLE_PERSON,\n outreachSampleMergeContext,\n previewOutreachStep,\n type OutreachStepPreviewInput,\n} from '../model/step-preview'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE } from './enroll-routes'\nimport type { OutreachRouteDeps } 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 * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):\n * `outreach/preview`.\n *\n * Written from what is stored — the sequence, the CRM template a step names,\n * the mailbox it sends as, the organization's footer — by the engine's own\n * composer (`previewOutreachStep`), for a real contact with the personal\n * line the rep is writing, or for a sample person. The enroll dialog shows\n * it beside each person before Confirm, so the rep reads the sentence they\n * wrote in the email it goes into.\n *\n * A real contact is read from the CRM, so naming one asks for `data.manage`\n * as enrolling does; the sample person asks for nothing more.\n *\n * The reading is {@link readOutreachStepRender}, shared with the test send\n * (`step-test-routes.ts`, AGL-3325): a test is the preview's own email,\n * sent — so both routes read the same records through the same gate, and\n * what a member reads on screen is what arrives in their inbox.\n */\n\n/** What a preview and a test both read before the composer runs. */\nexport interface OutreachStepRender {\n caller: OutreachRouteCaller\n sequence: OutreachSequence\n stepIndex: number\n step: OutreachEmailStep\n /** The sequence's mailbox as stored, or `null` when it names none or the mailbox is gone. */\n mailbox: OutreachMailbox | null\n /** The composer's input: the person, the line, the footer, the template. */\n input: OutreachStepPreviewInput\n}\n\n/**\n * The gate, then the sequence, the step and everything the step is rendered\n * from — as the request body names them — or the refusal that stops it.\n */\nexport async function readOutreachStepRender(\n deps: OutreachRouteDeps,\n request: Request,\n body: Record<string, unknown>,\n): Promise<Response | OutreachStepRender> {\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n if (body['contactId'] !== undefined && !contactId) {\n return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.')\n }\n // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n if (body['leadId'] !== undefined && !leadId) {\n return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.')\n }\n const caller = await outreachRouteGate(\n request,\n body['orgId'],\n deps.gate,\n contactId || leadId ? [OUTREACH_READS_PEOPLE] : [],\n )\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.')\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(caller.orgId)\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\n const requested = body['stepIndex']\n const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps)\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'That step does not send an email.')\n }\n // The person's copies of steps as the dialog holds them (AGL-3324): the\n // preview shows the curated version, held to the rules a stored one is —\n // and a test send (AGL-3325) sends it.\n const overrides = readOutreachStepOverrideRequests(body['stepOverrides'], sequence.steps, {\n uid: caller.uid,\n nowMs: deps.now(),\n })\n if (outreachStepOverridesRefused(overrides)) {\n return outreachRefusal(\n 400,\n 'invalid-override',\n overrides.refusal ?? 'A curated step breaks a rule every sequence email keeps.',\n { issues: overrides.issues },\n )\n }\n\n const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,\n sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,\n step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,\n contactId ? org.collection('contacts').doc(contactId).get() : null,\n // One org row (AGL-3275); the site is still required because a lead\n // preview is rendered in a site's context.\n leadId && sequence.hostId ? org.collection('leads').doc(leadId).get() : null,\n ])\n const leadData = lead?.exists ? (lead.data() as Record<string, unknown>) : null\n if (leadId && !leadData) {\n return outreachRefusal(404, 'contact-not-found', \"That lead isn't in this sequence's site's CRM.\")\n }\n const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId\n const contactData = contact?.exists\n ? (contact.data() as Record<string, unknown>)\n : leadData\n ? leadAsContact(leadData, contactGroupId)\n : null\n if (\n contactId &&\n (!contactData || !visibleToHost(contactData['visibleTo'] as string[] | undefined, sequence.hostId))\n ) {\n return outreachRefusal(404, 'contact-not-found', \"That contact isn't in this sequence's site's CRM.\")\n }\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n const sender = mailbox ? { name: mailbox.displayName, email: mailbox.sendAs || mailbox.email } : null\n const siteName = host?.exists ? String(host.get('name') ?? '') : ''\n const merge = contactData\n ? {\n contact: contactData,\n contactGroupId,\n lead: leadData,\n sender,\n site: { name: siteName },\n }\n : outreachSampleMergeContext({ sender, siteName })\n const personalLine =\n typeof body['personalLine'] === 'string'\n ? body['personalLine']\n : contactData\n ? ''\n : OUTREACH_SAMPLE_PERSON.personalLine\n return {\n caller,\n sequence,\n stepIndex,\n step,\n mailbox,\n input: {\n steps: sequence.steps,\n stepIndex,\n orgSettings,\n merge,\n email: contactData ? (normalizeContactEmail(contactData['email']) ?? '') : OUTREACH_SAMPLE_PERSON.email,\n personalLine,\n templateBody: template?.exists\n ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body\n : null,\n stepOverrides: overrides.overrides,\n },\n }\n}\n\nexport function createOutreachPreviewRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const loaded = await readOutreachStepRender(deps, request, body)\n if (loaded instanceof Response) return loaded\n const result = previewOutreachStep(loaded.input)\n return outreachOk({\n ok: true,\n stepIndex: loaded.stepIndex,\n subject: result.email?.subject ?? '',\n text: result.email?.text ?? '',\n unresolvedFields: result.unresolvedFields,\n error: result.error,\n } satisfies OutreachPreviewResponse)\n }\n}\n"],"names":["consentGroupForHost","normalizeContactEmail","CRM_COLLECTIONS","normalizeCrmEmailTemplate","visibleToHost","firstEmailStepIndex","leadAsContact","outreachStepOverridesRefused","readOutreachStepOverrideRequests","OUTREACH_SAMPLE_PERSON","outreachSampleMergeContext","previewOutreachStep","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","readOutreachStepRender","deps","request","body","host","contactId","undefined","leadId","caller","gate","Response","sequenceId","firestore","org","collection","doc","orgId","snapshot","get","sequence","exists","data","requested","stepIndex","steps","step","kind","overrides","uid","nowMs","now","refusal","issues","orgSettings","mailboxSnapshot","template","contact","lead","Promise","all","mailboxId","hostId","templateId","emailTemplates","leadData","contactGroupId","groupId","contactData","mailbox","sender","name","displayName","email","sendAs","siteName","String","merge","site","personalLine","input","templateBody","stepOverrides","createOutreachPreviewRoute","result","method","loaded","ok","subject","text","unresolvedFields","error"],"mappings":"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"}
@@ -17,9 +17,16 @@
17
17
  import { type OutreachEnrollRouteDeps } from './enroll-routes';
18
18
  import { type OutreachLinkDomainRouteDeps } from './link-domain-routes';
19
19
  import type { OutreachRouteGateDeps } from './route-gate';
20
+ import { type OutreachStepTestDeps } from './step-test-routes';
20
21
  /** The gate's reach into the platform. */
21
22
  export declare function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps;
22
23
  export declare function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps;
23
24
  /** The link domains route's reach (AGL-3306): the tracking-host store, loaded on first use. */
24
25
  export declare function defaultOutreachLinkDomainDeps(): OutreachLinkDomainRouteDeps;
26
+ /**
27
+ * The test send's reach (AGL-3325): the mailbox's Gmail through the same
28
+ * door the runtime and the Mailboxes test use, the platform's rate limit,
29
+ * and the short links a real send would mint.
30
+ */
31
+ export declare function defaultOutreachStepTestDeps(): OutreachStepTestDeps;
25
32
  export declare function registerOutreachRoutes(deps?: OutreachEnrollRouteDeps): void;
@@ -14,10 +14,14 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { pluginRecordTimelineWriter } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
17
+ import { pluginTextGenerator } from "@aglyn/aglyn/plugin-manager/plugin-text-generation";
17
18
  import { registerPluginApiRoute } from "@aglyn/aglyn/server";
18
19
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
19
20
  import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
21
+ import { openOutreachMailboxClient } from "../mailboxes/mailbox-transport.js";
20
22
  import { outreachOrgSubject } from "../mailboxes/register-mailbox-routes.js";
23
+ import { outreachShortLinkUrl } from "../runtime/click-link.js";
24
+ import { createOutreachCurateRoutes } from "./curate-routes.js";
21
25
  import { createOutreachDoNotContactDomainsRoute } from "./do-not-contact-routes.js";
22
26
  import { createOutreachEnrollRoutes } from "./enroll-routes.js";
23
27
  import { createOutreachEnrollmentActionRoute } from "./enrollment-routes.js";
@@ -26,6 +30,7 @@ import { createOutreachLinkDomainsRoute } from "./link-domain-routes.js";
26
30
  import { createOutreachPreviewRoute } from "./preview-routes.js";
27
31
  import { createOutreachSequenceRoutes } from "./sequence-routes.js";
28
32
  import { createOutreachSettingsRoute } from "./settings-routes.js";
33
+ import { createOutreachStepTestRoute } from "./step-test-routes.js";
29
34
  /**
30
35
  * Wires the settings, sequence and enrollment routes into the console
31
36
  * dispatcher (AGL-2980) with the platform's own dependencies. Specs build
@@ -78,6 +83,14 @@ export function defaultOutreachRouteDeps() {
78
83
  var _ref;
79
84
  var _pluginRecordTimelineWriter;
80
85
  return (_ref = (_pluginRecordTimelineWriter = pluginRecordTimelineWriter()) == null ? void 0 : _pluginRecordTimelineWriter.writer) != null ? _ref : null;
86
+ },
87
+ // The workspace's text generator (AGL-3324), resolved per call the same
88
+ // way: the AI plugin registers at boot, and a request before it did is
89
+ // told drafting is unavailable rather than caching a `null`.
90
+ textGenerator: ()=>{
91
+ var _ref;
92
+ var _pluginTextGenerator;
93
+ return (_ref = (_pluginTextGenerator = pluginTextGenerator()) == null ? void 0 : _pluginTextGenerator.generator) != null ? _ref : null;
81
94
  }
82
95
  };
83
96
  }
@@ -95,9 +108,28 @@ export function defaultOutreachRouteDeps() {
95
108
  consoleOrigin: canonicalConsoleOrigin
96
109
  };
97
110
  }
111
+ /**
112
+ * The test send's reach (AGL-3325): the mailbox's Gmail through the same
113
+ * door the runtime and the Mailboxes test use, the platform's rate limit,
114
+ * and the short links a real send would mint.
115
+ */ export function defaultOutreachStepTestDeps() {
116
+ return {
117
+ openMailbox: (mailboxId)=>openOutreachMailboxClient(firebaseAdmin.app().firestore(), {
118
+ mailboxId
119
+ }),
120
+ consumeRateLimit: async (key, options)=>(await import("@aglyn/tenant-data-admin/server/rate-limit-store")).consumeRateLimit(key, options),
121
+ clickLinkUrl: (linkId, trackingOrigin)=>outreachShortLinkUrl({
122
+ origin: canonicalConsoleOrigin(),
123
+ linkId,
124
+ trackingOrigin
125
+ }),
126
+ clickLinkOrigin: async ({ orgId, senderAddress })=>(await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress)
127
+ };
128
+ }
98
129
  export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
99
130
  const sequences = createOutreachSequenceRoutes(deps);
100
131
  const enroll = createOutreachEnrollRoutes(deps);
132
+ const curate = createOutreachCurateRoutes(deps);
101
133
  const orgSubject = {
102
134
  subject: outreachOrgSubject
103
135
  };
@@ -125,6 +157,15 @@ export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
125
157
  registerPluginApiRoute(OUTREACH_API_ROUTES.preview, {
126
158
  web: createOutreachPreviewRoute(deps)
127
159
  }, orgSubject);
160
+ registerPluginApiRoute(OUTREACH_API_ROUTES.curateDraft, {
161
+ web: curate.draft
162
+ }, orgSubject);
163
+ registerPluginApiRoute(OUTREACH_API_ROUTES.curateSave, {
164
+ web: curate.save
165
+ }, orgSubject);
166
+ registerPluginApiRoute(OUTREACH_API_ROUTES.stepTest, {
167
+ web: createOutreachStepTestRoute(deps, defaultOutreachStepTestDeps())
168
+ }, orgSubject);
128
169
  registerPluginApiRoute(OUTREACH_API_ROUTES.doNotContactDomains, {
129
170
  web: createOutreachDoNotContactDomainsRoute(deps)
130
171
  }, 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 { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { createOutreachEnrollmentActionRoute } from './enrollment-routes'\nimport { 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'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await (await platform()).creditCampaignSequenceOutcome(\n { hostId, orgId, campaignIds, outcome, atMs },\n firebaseAdmin.app().firestore(),\n )\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 }\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\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.linkDomains,\n { web: createOutreachLinkDomainsRoute(deps, defaultOutreachLinkDomainDeps()) },\n orgSubject,\n )\n}\n"],"names":["pluginRecordTimelineWriter","registerPluginApiRoute","firebaseAdmin","OUTREACH_API_ROUTES","outreachOrgSubject","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","canonicalConsoleOrigin","createOutreachLinkDomainsRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","crmViewEmails","input","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","creditCampaignSequenceOutcome","timeline","writer","defaultOutreachLinkDomainDeps","hosts","loaded","list","listTrackingHosts","setUp","setUpTrackingHost","verify","verifyTrackingHost","remove","removeTrackingHost","consoleOrigin","registerOutreachRoutes","deps","sequences","enroll","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","enrollPreview","preview","confirm","enrollmentsAction","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 { 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 { 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 { createOutreachEnrollmentActionRoute } 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 crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await (await platform()).creditCampaignSequenceOutcome(\n { hostId, orgId, campaignIds, outcome, atMs },\n firebaseAdmin.app().firestore(),\n )\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 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(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":["pluginRecordTimelineWriter","pluginTextGenerator","registerPluginApiRoute","firebaseAdmin","OUTREACH_API_ROUTES","openOutreachMailboxClient","outreachOrgSubject","outreachShortLinkUrl","createOutreachCurateRoutes","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","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","crmViewEmails","input","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","creditCampaignSequenceOutcome","timeline","writer","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","curateDraft","draft","curateSave","stepTest","doNotContactDomains","linkDomains"],"mappings":"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"}
@@ -0,0 +1,88 @@
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 { OutreachStepTestResponse } from '../model/outreach-api';
20
+ import type { OutreachRouteDeps } from './route-deps';
21
+ /**
22
+ * A TEST OF ONE STEP, IN A REAL INBOX (AGL-3325): `outreach/steps/test`.
23
+ *
24
+ * The Mailboxes page's test proves a mailbox can send; this proves what a
25
+ * step LOOKS like once it has been through Gmail. It is the preview's email
26
+ * — the same reader, the same person or sample person, the same personal
27
+ * line, the same footer — sent through the sequence's mailbox to the member
28
+ * who asked, or to an address they typed, with `[Test] ` in front of the
29
+ * subject so it never reads as real mail.
30
+ *
31
+ * ## Rendered as a send, sent as a test
32
+ *
33
+ * A later step in a thread is written as `Re:` the thread's subject, as the
34
+ * preview writes it, but sent as a new message: there is no thread to
35
+ * answer, and the preview's placeholder message id must not leave the
36
+ * server. When the sequence counts clicks, every link is rewritten to a
37
+ * short link as a real send's would be, so the member sees the link their
38
+ * recipients will see — but each is stored as a TEST link, which the click
39
+ * route follows and never records (`click-link.ts`). A member clicking their
40
+ * own test must not move the sequence's `clicks` or `machineClicks`.
41
+ *
42
+ * Merge fields the person has nothing for are sent empty and named in the
43
+ * answer, where a real send would pause: the point of a test is to see the
44
+ * gap, not to be refused it.
45
+ *
46
+ * ## Counted on nothing but the mailbox's tests
47
+ *
48
+ * A test is a real Gmail send and spends the account's own limits. It is
49
+ * not a person emailed: it moves neither the sequence's `stats` nor the
50
+ * mailbox's `health.daily.<day>.sent`, it reserves nothing of the daily cap,
51
+ * it files nothing on anyone's record, and it enrolls nobody. It is tallied
52
+ * on `health.daily.<day>.tests`, which the Mailboxes card shows as "Tests
53
+ * today", and held to {@link OUTREACH_STEP_TESTS_PER_HOUR} a mailbox.
54
+ *
55
+ * ## Who, and from what
56
+ *
57
+ * The mailbox's own member, or an organization owner or admin — the people
58
+ * who may edit and activate the sequence. A paused mailbox, or one waiting
59
+ * to be reconnected, refuses and says which; a member who cannot send is
60
+ * told where to fix it rather than shown a Google error.
61
+ */
62
+ /** Tests one mailbox may send in an hour: a few steps for a few people. */
63
+ export declare const OUTREACH_STEP_TESTS_PER_HOUR = 20;
64
+ /** What every test's subject starts with. */
65
+ export declare const OUTREACH_TEST_SUBJECT_PREFIX = "[Test] ";
66
+ /** The route's reach beyond the shared route deps: the mailbox's Gmail, the limit, the links. */
67
+ export interface OutreachStepTestDeps {
68
+ /** Opens a mailbox's Gmail client from its sealed grant. */
69
+ openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>;
70
+ consumeRateLimit(key: string, options: {
71
+ limit: number;
72
+ windowMs: number;
73
+ }): Promise<{
74
+ allowed: boolean;
75
+ }>;
76
+ /** The short tracking link for a stored link's id, or `null` when none can be made. */
77
+ clickLinkUrl(linkId: string, trackingOrigin?: string | null): string | null;
78
+ /** The click-tracking origin for mail sent as `senderAddress` (AGL-3306), or `null` for the console's. */
79
+ clickLinkOrigin?(input: {
80
+ orgId: string;
81
+ senderAddress: string;
82
+ }): Promise<string | null>;
83
+ }
84
+ /** What the route answers: the response the console reads, and the fields the test sent empty. */
85
+ export interface OutreachStepTestAnswer extends OutreachStepTestResponse {
86
+ unresolvedFields: string[];
87
+ }
88
+ export declare function createOutreachStepTestRoute(deps: OutreachRouteDeps, testDeps: OutreachStepTestDeps): PluginWebApiHandler;