@aglyn/plugins-outreach 1.0.0-beta.196 → 1.0.0-beta.198
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/package.json +14 -14
- package/src/lib/components/compliance-section.d.ts +3 -1
- package/src/lib/components/compliance-section.js +110 -45
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/enroll-dialog.d.ts +7 -1
- package/src/lib/components/enroll-dialog.js +106 -3
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/mailbox-card.js +11 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/outreach-ui.js +1 -0
- package/src/lib/components/outreach-ui.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.d.ts +2 -0
- package/src/lib/components/sequence-editor.js +51 -26
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-step-card.d.ts +7 -0
- package/src/lib/components/sequence-step-card.js +37 -14
- package/src/lib/components/sequence-step-card.js.map +1 -1
- package/src/lib/components/sequences-section.js +12 -3
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/step-test-dialog.d.ts +31 -0
- package/src/lib/components/step-test-dialog.js +247 -0
- package/src/lib/components/step-test-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +8 -1
- package/src/lib/components/use-outreach-api.js +4 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +6 -1
- package/src/lib/components/use-outreach-data.js +6 -3
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +7 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/delivery-status.d.ts +8 -0
- package/src/lib/engine/delivery-status.js +10 -5
- package/src/lib/engine/delivery-status.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +2 -0
- package/src/lib/engine/enrollment-state.js +2 -2
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/gates.d.ts +28 -1
- package/src/lib/engine/gates.js +28 -1
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +97 -0
- package/src/lib/engine/mail-gateway.js +226 -0
- package/src/lib/engine/mail-gateway.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +3 -3
- package/src/lib/engine/mailbox-health.js +17 -11
- package/src/lib/engine/mailbox-health.js.map +1 -1
- package/src/lib/engine/thread-classification.d.ts +2 -0
- package/src/lib/engine/thread-classification.js +2 -1
- package/src/lib/engine/thread-classification.js.map +1 -1
- package/src/lib/enrollment/enroll-people.d.ts +6 -1
- package/src/lib/enrollment/enroll-people.js +18 -4
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.d.ts +7 -0
- package/src/lib/enrollment/gate-lookups.js +11 -2
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +35 -1
- 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 +21 -2
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +7 -1
- package/src/lib/model/step-preview.js +3 -2
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +23 -4
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.js +15 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +2 -2
- package/src/lib/routes/platform-deps.js +2 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.d.ts +24 -0
- package/src/lib/routes/preview-routes.js +99 -95
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +7 -0
- package/src/lib/routes/register-routes.js +27 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +6 -0
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/step-test-routes.d.ts +88 -0
- package/src/lib/routes/step-test-routes.js +275 -0
- package/src/lib/routes/step-test-routes.js.map +1 -0
- package/src/lib/runtime/click-events.js +10 -0
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +11 -0
- package/src/lib/runtime/click-link.js +9 -4
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/click-route.js +3 -0
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +46 -0
- package/src/lib/runtime/gateway-ledger.js +113 -0
- package/src/lib/runtime/gateway-ledger.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.js +3 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +6 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +48 -0
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.d.ts +2 -0
- package/src/lib/runtime/sync-job.js +40 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +107 -0
- package/src/lib/storage/domain-intel-store.js +320 -0
- package/src/lib/storage/domain-intel-store.js.map +1 -0
|
@@ -28,101 +28,94 @@ import { OUTREACH_READS_PEOPLE } from "./enroll-routes.js";
|
|
|
28
28
|
import { outreachRouteGate } from "./route-gate.js";
|
|
29
29
|
import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocumentId, readOutreachJsonBody } from "./route-http.js";
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if ((step == null ? void 0 : step.kind) !== 'email') {
|
|
73
|
-
return outreachRefusal(400, 'invalid-request', 'That step does not send an email.');
|
|
74
|
-
}
|
|
75
|
-
// The person's copies of steps as the dialog holds them (AGL-3324): the
|
|
76
|
-
// preview shows the curated version, held to the rules a stored one is.
|
|
77
|
-
const overrides = readOutreachStepOverrideRequests(body['stepOverrides'], sequence.steps, {
|
|
78
|
-
uid: caller.uid,
|
|
79
|
-
nowMs: deps.now()
|
|
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
|
|
80
72
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
name: siteName
|
|
119
|
-
}
|
|
120
|
-
} : outreachSampleMergeContext({
|
|
121
|
-
sender,
|
|
122
|
-
siteName
|
|
123
|
-
});
|
|
124
|
-
const personalLine = typeof body['personalLine'] === 'string' ? body['personalLine'] : contactData ? '' : OUTREACH_SAMPLE_PERSON.personalLine;
|
|
125
|
-
const result = previewOutreachStep({
|
|
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: {
|
|
126
119
|
steps: sequence.steps,
|
|
127
120
|
stepIndex,
|
|
128
121
|
orgSettings,
|
|
@@ -131,10 +124,21 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
131
124
|
personalLine,
|
|
132
125
|
templateBody: (template == null ? void 0 : template.exists) ? normalizeCrmEmailTemplate(template.data()).body : null,
|
|
133
126
|
stepOverrides: overrides.overrides
|
|
134
|
-
}
|
|
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);
|
|
135
139
|
return outreachOk({
|
|
136
140
|
ok: true,
|
|
137
|
-
stepIndex,
|
|
141
|
+
stepIndex: loaded.stepIndex,
|
|
138
142
|
subject: (_ref = (_result_email = result.email) == null ? void 0 : _result_email.subject) != null ? _ref : '',
|
|
139
143
|
text: (_ref1 = (_result_email1 = result.email) == null ? void 0 : _result_email1.text) != null ? _ref1 : '',
|
|
140
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 { outreachStepOverridesRefused, readOutreachStepOverrideRequests } from '../enrollment/step-overrides'\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 // 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 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 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 stepOverrides: overrides.overrides,\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","outreachStepOverridesRefused","readOutreachStepOverrideRequests","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","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","templateBody","stepOverrides","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;
|
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
import { pluginTextGenerator } from "@aglyn/aglyn/plugin-manager/plugin-text-generation";
|
|
18
18
|
import { registerPluginApiRoute } from "@aglyn/aglyn/server";
|
|
19
19
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
20
|
+
import { resolveMx } from "node:dns/promises";
|
|
20
21
|
import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
22
|
+
import { openOutreachMailboxClient } from "../mailboxes/mailbox-transport.js";
|
|
21
23
|
import { outreachOrgSubject } from "../mailboxes/register-mailbox-routes.js";
|
|
24
|
+
import { outreachShortLinkUrl } from "../runtime/click-link.js";
|
|
22
25
|
import { createOutreachCurateRoutes } from "./curate-routes.js";
|
|
23
26
|
import { createOutreachDoNotContactDomainsRoute } from "./do-not-contact-routes.js";
|
|
24
27
|
import { createOutreachEnrollRoutes } from "./enroll-routes.js";
|
|
@@ -28,6 +31,7 @@ import { createOutreachLinkDomainsRoute } from "./link-domain-routes.js";
|
|
|
28
31
|
import { createOutreachPreviewRoute } from "./preview-routes.js";
|
|
29
32
|
import { createOutreachSequenceRoutes } from "./sequence-routes.js";
|
|
30
33
|
import { createOutreachSettingsRoute } from "./settings-routes.js";
|
|
34
|
+
import { createOutreachStepTestRoute } from "./step-test-routes.js";
|
|
31
35
|
/**
|
|
32
36
|
* Wires the settings, sequence and enrollment routes into the console
|
|
33
37
|
* dispatcher (AGL-2980) with the platform's own dependencies. Specs build
|
|
@@ -81,6 +85,8 @@ export function defaultOutreachRouteDeps() {
|
|
|
81
85
|
var _pluginRecordTimelineWriter;
|
|
82
86
|
return (_ref = (_pluginRecordTimelineWriter = pluginRecordTimelineWriter()) == null ? void 0 : _pluginRecordTimelineWriter.writer) != null ? _ref : null;
|
|
83
87
|
},
|
|
88
|
+
// The domain's MX (AGL-3326), from the console's own resolver.
|
|
89
|
+
resolveMx: (domain)=>resolveMx(domain),
|
|
84
90
|
// The workspace's text generator (AGL-3324), resolved per call the same
|
|
85
91
|
// way: the AI plugin registers at boot, and a request before it did is
|
|
86
92
|
// told drafting is unavailable rather than caching a `null`.
|
|
@@ -105,6 +111,24 @@ export function defaultOutreachRouteDeps() {
|
|
|
105
111
|
consoleOrigin: canonicalConsoleOrigin
|
|
106
112
|
};
|
|
107
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* The test send's reach (AGL-3325): the mailbox's Gmail through the same
|
|
116
|
+
* door the runtime and the Mailboxes test use, the platform's rate limit,
|
|
117
|
+
* and the short links a real send would mint.
|
|
118
|
+
*/ export function defaultOutreachStepTestDeps() {
|
|
119
|
+
return {
|
|
120
|
+
openMailbox: (mailboxId)=>openOutreachMailboxClient(firebaseAdmin.app().firestore(), {
|
|
121
|
+
mailboxId
|
|
122
|
+
}),
|
|
123
|
+
consumeRateLimit: async (key, options)=>(await import("@aglyn/tenant-data-admin/server/rate-limit-store")).consumeRateLimit(key, options),
|
|
124
|
+
clickLinkUrl: (linkId, trackingOrigin)=>outreachShortLinkUrl({
|
|
125
|
+
origin: canonicalConsoleOrigin(),
|
|
126
|
+
linkId,
|
|
127
|
+
trackingOrigin
|
|
128
|
+
}),
|
|
129
|
+
clickLinkOrigin: async ({ orgId, senderAddress })=>(await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
108
132
|
export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
|
|
109
133
|
const sequences = createOutreachSequenceRoutes(deps);
|
|
110
134
|
const enroll = createOutreachEnrollRoutes(deps);
|
|
@@ -142,6 +166,9 @@ export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
|
|
|
142
166
|
registerPluginApiRoute(OUTREACH_API_ROUTES.curateSave, {
|
|
143
167
|
web: curate.save
|
|
144
168
|
}, orgSubject);
|
|
169
|
+
registerPluginApiRoute(OUTREACH_API_ROUTES.stepTest, {
|
|
170
|
+
web: createOutreachStepTestRoute(deps, defaultOutreachStepTestDeps())
|
|
171
|
+
}, orgSubject);
|
|
145
172
|
registerPluginApiRoute(OUTREACH_API_ROUTES.doNotContactDomains, {
|
|
146
173
|
web: createOutreachDoNotContactDomainsRoute(deps)
|
|
147
174
|
}, 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 { 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 { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\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'\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\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.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","outreachOrgSubject","createOutreachCurateRoutes","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","textGenerator","generator","defaultOutreachLinkDomainDeps","hosts","loaded","list","listTrackingHosts","setUp","setUpTrackingHost","verify","verifyTrackingHost","remove","removeTrackingHost","consoleOrigin","registerOutreachRoutes","deps","sequences","enroll","curate","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","enrollPreview","preview","confirm","enrollmentsAction","curateDraft","draft","curateSave","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 { resolveMx } from 'node:dns/promises'\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 domain's MX (AGL-3326), from the console's own resolver.\n resolveMx: (domain) => resolveMx(domain),\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","resolveMx","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","domain","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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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { OutreachCampaignCredit, OutreachRecordEmailStamp } from '../runtime/runtime-deps';
|
|
18
|
+
import type { OutreachResolveMx } from '../storage/domain-intel-store';
|
|
18
19
|
import type { OutreachRouteGateDeps } from './route-gate';
|
|
19
20
|
/**
|
|
20
21
|
* What the settings, sequence and enrollment routes reach outside this
|
|
@@ -57,4 +58,9 @@ export interface OutreachRouteDeps {
|
|
|
57
58
|
* enroll route's `enrolled`; the runtime's twin credits the rest.
|
|
58
59
|
*/
|
|
59
60
|
creditCampaign: OutreachCampaignCredit['credit'];
|
|
61
|
+
/**
|
|
62
|
+
* The MX resolver the enroll routes classify a domain's mail gateway with
|
|
63
|
+
* (AGL-3326) — Node's `dns.promises.resolveMx` on the platform.
|
|
64
|
+
*/
|
|
65
|
+
resolveMx: OutreachResolveMx;
|
|
60
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/route-deps.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 type { OutreachCampaignCredit, OutreachRecordEmailStamp } from '../runtime/runtime-deps'\nimport type { OutreachRouteGateDeps } from './route-gate'\n\n/**\n * What the settings, sequence and enrollment routes reach outside this\n * plugin (AGL-2980). The platform's own are `register-routes.ts`; specs\n * build their own.\n */\n\n/**\n * The org activity target a sequence's rows are filed under: Outreach's own\n * namespaced resource (`pluginId:noun`), which the feed reads as \"Sequence\".\n * Core's target list names only core's resources, so a plugin's never joins\n * it.\n */\nexport const OUTREACH_SEQUENCE_ACTIVITY_TARGET = 'outreach:sequence'\n\n/** What an org activity line written by these routes points at. */\nexport type OutreachActivityTarget =\n | { type: 'org'; id: string }\n | { type: typeof OUTREACH_SEQUENCE_ACTIVITY_TARGET; id: string; name: string }\n\nexport interface OutreachRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n gate: OutreachRouteGateDeps\n now(): number\n /** A source of numbers in `[0, 1)` — the scheduler's jitter. */\n random(): number\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: OutreachActivityTarget,\n ): Promise<void>\n /**\n * Stamps a member's do-not-contact mark on the lead and the contact that\n * carry the address (AGL-3245); the runtime's twin is on its own deps.\n */\n stampRecordEmailState(stamp: OutreachRecordEmailStamp): Promise<void>\n /**\n * Credits an enrollment to the sequence's campaigns (AGL-3254) — the\n * enroll route's `enrolled`; the runtime's twin credits the rest.\n */\n creditCampaign: OutreachCampaignCredit['credit']\n}\n"],"names":["OUTREACH_SEQUENCE_ACTIVITY_TARGET"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/route-deps.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 type { OutreachCampaignCredit, OutreachRecordEmailStamp } from '../runtime/runtime-deps'\nimport type { OutreachResolveMx } from '../storage/domain-intel-store'\nimport type { OutreachRouteGateDeps } from './route-gate'\n\n/**\n * What the settings, sequence and enrollment routes reach outside this\n * plugin (AGL-2980). The platform's own are `register-routes.ts`; specs\n * build their own.\n */\n\n/**\n * The org activity target a sequence's rows are filed under: Outreach's own\n * namespaced resource (`pluginId:noun`), which the feed reads as \"Sequence\".\n * Core's target list names only core's resources, so a plugin's never joins\n * it.\n */\nexport const OUTREACH_SEQUENCE_ACTIVITY_TARGET = 'outreach:sequence'\n\n/** What an org activity line written by these routes points at. */\nexport type OutreachActivityTarget =\n | { type: 'org'; id: string }\n | { type: typeof OUTREACH_SEQUENCE_ACTIVITY_TARGET; id: string; name: string }\n\nexport interface OutreachRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n gate: OutreachRouteGateDeps\n now(): number\n /** A source of numbers in `[0, 1)` — the scheduler's jitter. */\n random(): number\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: OutreachActivityTarget,\n ): Promise<void>\n /**\n * Stamps a member's do-not-contact mark on the lead and the contact that\n * carry the address (AGL-3245); the runtime's twin is on its own deps.\n */\n stampRecordEmailState(stamp: OutreachRecordEmailStamp): Promise<void>\n /**\n * Credits an enrollment to the sequence's campaigns (AGL-3254) — the\n * enroll route's `enrolled`; the runtime's twin credits the rest.\n */\n creditCampaign: OutreachCampaignCredit['credit']\n /**\n * The MX resolver the enroll routes classify a domain's mail gateway with\n * (AGL-3326) — Node's `dns.promises.resolveMx` on the platform.\n */\n resolveMx: OutreachResolveMx\n}\n"],"names":["OUTREACH_SEQUENCE_ACTIVITY_TARGET"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD;;;;CAIC,GAED;;;;;CAKC,GACD,OAAO,MAAMA,oCAAoC,oBAAmB"}
|
|
@@ -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;
|