@aglyn/plugins-outreach 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +93 -0
- package/package.json +52 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/compliance-section.d.ts +20 -0
- package/src/lib/components/compliance-section.js +291 -0
- package/src/lib/components/compliance-section.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +36 -0
- package/src/lib/components/enroll-dialog.js +656 -0
- package/src/lib/components/enroll-dialog.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +28 -0
- package/src/lib/components/enrollments-table.js +375 -0
- package/src/lib/components/enrollments-table.js.map +1 -0
- package/src/lib/components/mailbox-card.d.ts +46 -0
- package/src/lib/components/mailbox-card.js +517 -0
- package/src/lib/components/mailbox-card.js.map +1 -0
- package/src/lib/components/mailboxes-section.d.ts +24 -0
- package/src/lib/components/mailboxes-section.js +258 -0
- package/src/lib/components/mailboxes-section.js.map +1 -0
- package/src/lib/components/outreach-console-page.d.ts +17 -0
- package/src/lib/components/outreach-console-page.js +81 -0
- package/src/lib/components/outreach-console-page.js.map +1 -0
- package/src/lib/components/outreach-console-sections.d.ts +43 -0
- package/src/lib/components/outreach-console-sections.js +55 -0
- package/src/lib/components/outreach-console-sections.js.map +1 -0
- package/src/lib/components/outreach-ui.d.ts +51 -0
- package/src/lib/components/outreach-ui.js +171 -0
- package/src/lib/components/outreach-ui.js.map +1 -0
- package/src/lib/components/send-window-fields.d.ts +25 -0
- package/src/lib/components/send-window-fields.js +178 -0
- package/src/lib/components/send-window-fields.js.map +1 -0
- package/src/lib/components/sequence-detail.d.ts +54 -0
- package/src/lib/components/sequence-detail.js +435 -0
- package/src/lib/components/sequence-detail.js.map +1 -0
- package/src/lib/components/sequence-editor.d.ts +37 -0
- package/src/lib/components/sequence-editor.js +485 -0
- package/src/lib/components/sequence-editor.js.map +1 -0
- package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
- package/src/lib/components/sequence-mailbox-picker.js +137 -0
- package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
- package/src/lib/components/sequence-preview.d.ts +27 -0
- package/src/lib/components/sequence-preview.js +146 -0
- package/src/lib/components/sequence-preview.js.map +1 -0
- package/src/lib/components/sequence-step-card.d.ts +31 -0
- package/src/lib/components/sequence-step-card.js +326 -0
- package/src/lib/components/sequence-step-card.js.map +1 -0
- package/src/lib/components/sequences-section.d.ts +40 -0
- package/src/lib/components/sequences-section.js +310 -0
- package/src/lib/components/sequences-section.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +38 -0
- package/src/lib/components/use-outreach-api.js +139 -0
- package/src/lib/components/use-outreach-api.js.map +1 -0
- package/src/lib/components/use-outreach-crm.d.ts +50 -0
- package/src/lib/components/use-outreach-crm.js +200 -0
- package/src/lib/components/use-outreach-crm.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +55 -0
- package/src/lib/components/use-outreach-data.js +199 -0
- package/src/lib/components/use-outreach-data.js.map +1 -0
- package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
- package/src/lib/components/use-outreach-mailbox-api.js +101 -0
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
- package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
- package/src/lib/components/use-outreach-mailboxes.js +68 -0
- package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
- package/src/lib/components/use-outreach-settings.d.ts +17 -0
- package/src/lib/components/use-outreach-settings.js +64 -0
- package/src/lib/components/use-outreach-settings.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +68 -0
- package/src/lib/constants/api-routes.js +57 -0
- package/src/lib/constants/api-routes.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +46 -0
- package/src/lib/constants/bundle-common.js +55 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/runtime-jobs.d.ts +24 -0
- package/src/lib/constants/runtime-jobs.js +24 -0
- package/src/lib/constants/runtime-jobs.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +99 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/engine/compose.d.ts +86 -0
- package/src/lib/engine/compose.js +212 -0
- package/src/lib/engine/compose.js.map +1 -0
- package/src/lib/engine/delivery-status.d.ts +53 -0
- package/src/lib/engine/delivery-status.js +184 -0
- package/src/lib/engine/delivery-status.js.map +1 -0
- package/src/lib/engine/do-not-contact.d.ts +18 -0
- package/src/lib/engine/do-not-contact.js +34 -0
- package/src/lib/engine/do-not-contact.js.map +1 -0
- package/src/lib/engine/enrollment-state.d.ts +156 -0
- package/src/lib/engine/enrollment-state.js +303 -0
- package/src/lib/engine/enrollment-state.js.map +1 -0
- package/src/lib/engine/gates.d.ts +125 -0
- package/src/lib/engine/gates.js +232 -0
- package/src/lib/engine/gates.js.map +1 -0
- package/src/lib/engine/index.d.ts +28 -0
- package/src/lib/engine/index.js +33 -0
- package/src/lib/engine/index.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +49 -0
- package/src/lib/engine/mailbox-health.js +83 -0
- package/src/lib/engine/mailbox-health.js.map +1 -0
- package/src/lib/engine/opt-out-intent.d.ts +41 -0
- package/src/lib/engine/opt-out-intent.js +220 -0
- package/src/lib/engine/opt-out-intent.js.map +1 -0
- package/src/lib/engine/recipient-country.d.ts +41 -0
- package/src/lib/engine/recipient-country.js +171 -0
- package/src/lib/engine/recipient-country.js.map +1 -0
- package/src/lib/engine/schedule.d.ts +70 -0
- package/src/lib/engine/schedule.js +115 -0
- package/src/lib/engine/schedule.js.map +1 -0
- package/src/lib/engine/sending-capacity.d.ts +96 -0
- package/src/lib/engine/sending-capacity.js +172 -0
- package/src/lib/engine/sending-capacity.js.map +1 -0
- package/src/lib/engine/sequence-validation.d.ts +90 -0
- package/src/lib/engine/sequence-validation.js +291 -0
- package/src/lib/engine/sequence-validation.js.map +1 -0
- package/src/lib/engine/thread-classification.d.ts +78 -0
- package/src/lib/engine/thread-classification.js +224 -0
- package/src/lib/engine/thread-classification.js.map +1 -0
- package/src/lib/engine/thread-message.d.ts +80 -0
- package/src/lib/engine/thread-message.js +143 -0
- package/src/lib/engine/thread-message.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +81 -0
- package/src/lib/enrollment/enroll-people.js +250 -0
- package/src/lib/enrollment/enroll-people.js.map +1 -0
- package/src/lib/enrollment/gate-lookups.d.ts +32 -0
- package/src/lib/enrollment/gate-lookups.js +154 -0
- package/src/lib/enrollment/gate-lookups.js.map +1 -0
- package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
- package/src/lib/mailboxes/mailbox-api.js +66 -0
- package/src/lib/mailboxes/mailbox-api.js.map +1 -0
- package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
- package/src/lib/mailboxes/mailbox-credentials.js +102 -0
- package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
- package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
- package/src/lib/mailboxes/mailbox-erasure.js +148 -0
- package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
- package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
- package/src/lib/mailboxes/mailbox-gate.js +98 -0
- package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
- package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
- package/src/lib/mailboxes/mailbox-revoke.js +41 -0
- package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
- package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
- package/src/lib/mailboxes/mailbox-routes.js +605 -0
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
- package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
- package/src/lib/mailboxes/mailbox-settings.js +212 -0
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
- package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
- package/src/lib/mailboxes/mailbox-transport.js +94 -0
- package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
- package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
- package/src/lib/mailboxes/oauth-redirect.js +63 -0
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
- package/src/lib/mailboxes/oauth-state.d.ts +152 -0
- package/src/lib/mailboxes/oauth-state.js +212 -0
- package/src/lib/mailboxes/oauth-state.js.map +1 -0
- package/src/lib/mailboxes/outreach-config.d.ts +64 -0
- package/src/lib/mailboxes/outreach-config.js +77 -0
- package/src/lib/mailboxes/outreach-config.js.map +1 -0
- package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
- package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
- package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
- package/src/lib/model/compliance-settings.d.ts +78 -0
- package/src/lib/model/compliance-settings.js +155 -0
- package/src/lib/model/compliance-settings.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +210 -0
- package/src/lib/model/outreach-api.js +28 -0
- package/src/lib/model/outreach-api.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +563 -0
- package/src/lib/model/outreach.types.js +225 -0
- package/src/lib/model/outreach.types.js.map +1 -0
- package/src/lib/model/sequence-draft.d.ts +84 -0
- package/src/lib/model/sequence-draft.js +239 -0
- package/src/lib/model/sequence-draft.js.map +1 -0
- package/src/lib/model/step-preview.d.ts +45 -0
- package/src/lib/model/step-preview.js +98 -0
- package/src/lib/model/step-preview.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +23 -0
- package/src/lib/model/stored-records.js +84 -0
- package/src/lib/model/stored-records.js.map +1 -0
- package/src/lib/plugin.d.ts +46 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +64 -0
- package/src/lib/routes/enroll-routes.js +350 -0
- package/src/lib/routes/enroll-routes.js.map +1 -0
- package/src/lib/routes/enrollment-routes.d.ts +19 -0
- package/src/lib/routes/enrollment-routes.js +171 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -0
- package/src/lib/routes/platform-deps.d.ts +51 -0
- package/src/lib/routes/platform-deps.js +66 -0
- package/src/lib/routes/platform-deps.js.map +1 -0
- package/src/lib/routes/preview-routes.d.ts +33 -0
- package/src/lib/routes/preview-routes.js +117 -0
- package/src/lib/routes/preview-routes.js.map +1 -0
- package/src/lib/routes/register-routes.d.ts +22 -0
- package/src/lib/routes/register-routes.js +92 -0
- package/src/lib/routes/register-routes.js.map +1 -0
- package/src/lib/routes/route-deps.d.ts +49 -0
- package/src/lib/routes/route-deps.js +27 -0
- package/src/lib/routes/route-deps.js.map +1 -0
- package/src/lib/routes/route-gate.d.ts +94 -0
- package/src/lib/routes/route-gate.js +87 -0
- package/src/lib/routes/route-gate.js.map +1 -0
- package/src/lib/routes/route-http.d.ts +32 -0
- package/src/lib/routes/route-http.js +62 -0
- package/src/lib/routes/route-http.js.map +1 -0
- package/src/lib/routes/sequence-routes.d.ts +32 -0
- package/src/lib/routes/sequence-routes.js +359 -0
- package/src/lib/routes/sequence-routes.js.map +1 -0
- package/src/lib/routes/settings-routes.d.ts +30 -0
- package/src/lib/routes/settings-routes.js +73 -0
- package/src/lib/routes/settings-routes.js.map +1 -0
- package/src/lib/runtime/enrollment-events.d.ts +86 -0
- package/src/lib/runtime/enrollment-events.js +141 -0
- package/src/lib/runtime/enrollment-events.js.map +1 -0
- package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
- package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
- package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
- package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
- package/src/lib/runtime/mailbox-health-store.js +168 -0
- package/src/lib/runtime/mailbox-health-store.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +39 -0
- package/src/lib/runtime/person-erasure.js +57 -0
- package/src/lib/runtime/person-erasure.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
- package/src/lib/runtime/platform-runtime-deps.js +98 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
- package/src/lib/runtime/runtime-deps.d.ts +77 -0
- package/src/lib/runtime/runtime-deps.js +18 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -0
- package/src/lib/runtime/send-job.d.ts +42 -0
- package/src/lib/runtime/send-job.js +780 -0
- package/src/lib/runtime/send-job.js.map +1 -0
- package/src/lib/runtime/sync-job.d.ts +41 -0
- package/src/lib/runtime/sync-job.js +480 -0
- package/src/lib/runtime/sync-job.js.map +1 -0
- package/src/lib/runtime/timeline.d.ts +67 -0
- package/src/lib/runtime/timeline.js +93 -0
- package/src/lib/runtime/timeline.js.map +1 -0
- package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
- package/src/lib/runtime/unsubscribe-link.js +121 -0
- package/src/lib/runtime/unsubscribe-link.js.map +1 -0
- package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
- package/src/lib/runtime/unsubscribe-route.js +103 -0
- package/src/lib/runtime/unsubscribe-route.js.map +1 -0
- package/src/lib/server.d.ts +53 -0
- package/src/lib/server.js +94 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/storage/compliance-settings-store.d.ts +44 -0
- package/src/lib/storage/compliance-settings-store.js +78 -0
- package/src/lib/storage/compliance-settings-store.js.map +1 -0
- package/src/lib/storage/do-not-contact-store.d.ts +60 -0
- package/src/lib/storage/do-not-contact-store.js +168 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -0
- package/src/lib/storage/outreach-records.d.ts +33 -0
- package/src/lib/storage/outreach-records.js +35 -0
- package/src/lib/storage/outreach-records.js.map +1 -0
- package/src/lib/subprocessors.d.ts +48 -0
- package/src/lib/subprocessors.js +70 -0
- package/src/lib/subprocessors.js.map +1 -0
- package/src/lib/transport/gmail-client.d.ts +145 -0
- package/src/lib/transport/gmail-client.js +229 -0
- package/src/lib/transport/gmail-client.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +84 -0
- package/src/lib/transport/gmail-errors.js +138 -0
- package/src/lib/transport/gmail-errors.js.map +1 -0
- package/src/lib/transport/google-oauth.d.ts +138 -0
- package/src/lib/transport/google-oauth.js +247 -0
- package/src/lib/transport/google-oauth.js.map +1 -0
- package/src/lib/transport/http.d.ts +53 -0
- package/src/lib/transport/http.js +79 -0
- package/src/lib/transport/http.js.map +1 -0
- package/src/lib/transport/rfc5322.d.ts +116 -0
- package/src/lib/transport/rfc5322.js +330 -0
- package/src/lib/transport/rfc5322.js.map +1 -0
- package/src/lib/transport/send-message.d.ts +69 -0
- package/src/lib/transport/send-message.js +77 -0
- package/src/lib/transport/send-message.js.map +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
*/ import { OUTREACH_PLUGIN_ID } from "../constants/bundle-common.js";
|
|
17
|
+
/**
|
|
18
|
+
* OUTREACH ON THE CONTACT'S TIMELINE (AGL-2981).
|
|
19
|
+
*
|
|
20
|
+
* Every email a sequence sends, every reply it reads, and every task it
|
|
21
|
+
* asks of the rep goes onto the contact's record through the core's
|
|
22
|
+
* record-timeline seam — whichever plugin keeps the workspace's records —
|
|
23
|
+
* never by writing that plugin's documents. An email is filed once per
|
|
24
|
+
* `Message-ID`, under the id the capture address would file a forwarded copy
|
|
25
|
+
* by; a task once per the key given here.
|
|
26
|
+
*
|
|
27
|
+
* The timeline is bookkeeping beside the act: an email has already left
|
|
28
|
+
* when it is filed, so a writer that refuses or fails is logged and the
|
|
29
|
+
* send stands. A workspace with no record system files nothing.
|
|
30
|
+
*/ /** The record system's word for a step's task kind. */ export const OUTREACH_TASK_KIND_TO_RECORD = {
|
|
31
|
+
call: 'call',
|
|
32
|
+
linkedin: 'todo',
|
|
33
|
+
todo: 'todo'
|
|
34
|
+
};
|
|
35
|
+
/** Files one email on a contact; never throws. */ export async function fileOutreachEmail(deps, input) {
|
|
36
|
+
const writer = deps.timeline();
|
|
37
|
+
if (!writer) return null;
|
|
38
|
+
try {
|
|
39
|
+
var _input_byName, _input_inReplyTo;
|
|
40
|
+
const written = await writer.logActivity({
|
|
41
|
+
orgId: input.orgId,
|
|
42
|
+
hostId: input.hostId,
|
|
43
|
+
link: {
|
|
44
|
+
contactId: input.contactId
|
|
45
|
+
},
|
|
46
|
+
sourcePluginId: OUTREACH_PLUGIN_ID,
|
|
47
|
+
kind: 'email',
|
|
48
|
+
atMs: input.atMs,
|
|
49
|
+
body: input.body,
|
|
50
|
+
byUid: input.byUid,
|
|
51
|
+
byName: (_input_byName = input.byName) != null ? _input_byName : null,
|
|
52
|
+
email: {
|
|
53
|
+
direction: input.direction,
|
|
54
|
+
subject: input.subject,
|
|
55
|
+
from: input.from,
|
|
56
|
+
to: input.to,
|
|
57
|
+
messageId: input.messageId,
|
|
58
|
+
inReplyTo: (_input_inReplyTo = input.inReplyTo) != null ? _input_inReplyTo : null
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (written.ok === false) console.warn(`[outreach] the record system did not file an email: ${written.error}`);
|
|
62
|
+
return written;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('[outreach] filing an email on the record failed', error);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Files one task on a contact. Answers the record system's answer, `null`
|
|
70
|
+
* when the workspace keeps no records; a writer that THROWS is the caller's,
|
|
71
|
+
* which a step waits out rather than completing without its task.
|
|
72
|
+
*/ export async function fileOutreachTask(deps, input) {
|
|
73
|
+
var _input_notes;
|
|
74
|
+
const writer = deps.timeline();
|
|
75
|
+
if (!writer) return null;
|
|
76
|
+
return writer.createTask({
|
|
77
|
+
orgId: input.orgId,
|
|
78
|
+
hostId: input.hostId,
|
|
79
|
+
link: {
|
|
80
|
+
contactId: input.contactId
|
|
81
|
+
},
|
|
82
|
+
sourcePluginId: OUTREACH_PLUGIN_ID,
|
|
83
|
+
dedupeKey: input.dedupeKey,
|
|
84
|
+
title: input.title,
|
|
85
|
+
notes: (_input_notes = input.notes) != null ? _input_notes : null,
|
|
86
|
+
kind: input.kind,
|
|
87
|
+
dueAtMs: input.dueAtMs,
|
|
88
|
+
assigneeUid: input.assigneeUid,
|
|
89
|
+
createdByUid: ''
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/timeline.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 {\n PluginRecordTaskKind,\n PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport type { OutreachTaskKind } from '../model/outreach.types'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * OUTREACH ON THE CONTACT'S TIMELINE (AGL-2981).\n *\n * Every email a sequence sends, every reply it reads, and every task it\n * asks of the rep goes onto the contact's record through the core's\n * record-timeline seam — whichever plugin keeps the workspace's records —\n * never by writing that plugin's documents. An email is filed once per\n * `Message-ID`, under the id the capture address would file a forwarded copy\n * by; a task once per the key given here.\n *\n * The timeline is bookkeeping beside the act: an email has already left\n * when it is filed, so a writer that refuses or fails is logged and the\n * send stands. A workspace with no record system files nothing.\n */\n\n/** The record system's word for a step's task kind. */\nexport const OUTREACH_TASK_KIND_TO_RECORD: Record<OutreachTaskKind, PluginRecordTaskKind> = {\n call: 'call',\n linkedin: 'todo',\n todo: 'todo',\n}\n\n/** Files one email on a contact; never throws. */\nexport async function fileOutreachEmail(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n contactId: string\n direction: 'outbound' | 'inbound'\n subject: string\n from: string\n to: string | null\n messageId: string\n inReplyTo?: string | null\n body: string\n atMs: number\n byUid: string\n byName?: string | null\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n try {\n const written = await writer.logActivity({\n orgId: input.orgId,\n hostId: input.hostId,\n link: { contactId: input.contactId },\n sourcePluginId: OUTREACH_PLUGIN_ID,\n kind: 'email',\n atMs: input.atMs,\n body: input.body,\n byUid: input.byUid,\n byName: input.byName ?? null,\n email: {\n direction: input.direction,\n subject: input.subject,\n from: input.from,\n to: input.to,\n messageId: input.messageId,\n inReplyTo: input.inReplyTo ?? null,\n },\n })\n if (written.ok === false) console.warn(`[outreach] the record system did not file an email: ${written.error}`)\n return written\n } catch (error) {\n console.error('[outreach] filing an email on the record failed', error)\n return null\n }\n}\n\n/**\n * Files one task on a contact. Answers the record system's answer, `null`\n * when the workspace keeps no records; a writer that THROWS is the caller's,\n * which a step waits out rather than completing without its task.\n */\nexport async function fileOutreachTask(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n contactId: string\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n dueAtMs: number\n assigneeUid: string\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n return writer.createTask({\n orgId: input.orgId,\n hostId: input.hostId,\n link: { contactId: input.contactId },\n sourcePluginId: OUTREACH_PLUGIN_ID,\n dedupeKey: input.dedupeKey,\n title: input.title,\n notes: input.notes ?? null,\n kind: input.kind,\n dueAtMs: input.dueAtMs,\n assigneeUid: input.assigneeUid,\n createdByUid: '',\n })\n}\n"],"names":["OUTREACH_PLUGIN_ID","OUTREACH_TASK_KIND_TO_RECORD","call","linkedin","todo","fileOutreachEmail","deps","input","writer","timeline","written","logActivity","orgId","hostId","link","contactId","sourcePluginId","kind","atMs","body","byUid","byName","email","direction","subject","from","to","messageId","inReplyTo","ok","console","warn","error","fileOutreachTask","createTask","dedupeKey","title","notes","dueAtMs","assigneeUid","createdByUid"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD,SAASA,kBAAkB,QAAQ,gCAA4B;AAI/D;;;;;;;;;;;;;CAaC,GAED,qDAAqD,GACrD,OAAO,MAAMC,+BAA+E;IAC1FC,MAAM;IACNC,UAAU;IACVC,MAAM;AACR,EAAC;AAED,gDAAgD,GAChD,OAAO,eAAeC,kBACpBC,IAA2C,EAC3CC,KAcC;IAED,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,IAAI;YAUQD,eAOKA;QAhBf,MAAMG,UAAU,MAAMF,OAAOG,WAAW,CAAC;YACvCC,OAAOL,MAAMK,KAAK;YAClBC,QAAQN,MAAMM,MAAM;YACpBC,MAAM;gBAAEC,WAAWR,MAAMQ,SAAS;YAAC;YACnCC,gBAAgBhB;YAChBiB,MAAM;YACNC,MAAMX,MAAMW,IAAI;YAChBC,MAAMZ,MAAMY,IAAI;YAChBC,OAAOb,MAAMa,KAAK;YAClBC,MAAM,GAAEd,gBAAAA,MAAMc,MAAM,YAAZd,gBAAgB;YACxBe,OAAO;gBACLC,WAAWhB,MAAMgB,SAAS;gBAC1BC,SAASjB,MAAMiB,OAAO;gBACtBC,MAAMlB,MAAMkB,IAAI;gBAChBC,IAAInB,MAAMmB,EAAE;gBACZC,WAAWpB,MAAMoB,SAAS;gBAC1BC,SAAS,GAAErB,mBAAAA,MAAMqB,SAAS,YAAfrB,mBAAmB;YAChC;QACF;QACA,IAAIG,QAAQmB,EAAE,KAAK,OAAOC,QAAQC,IAAI,CAAC,CAAC,oDAAoD,EAAErB,QAAQsB,KAAK,EAAE;QAC7G,OAAOtB;IACT,EAAE,OAAOsB,OAAO;QACdF,QAAQE,KAAK,CAAC,mDAAmDA;QACjE,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iBACpB3B,IAA2C,EAC3CC,KAUC;QAWQA;IATT,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,OAAOA,OAAO0B,UAAU,CAAC;QACvBtB,OAAOL,MAAMK,KAAK;QAClBC,QAAQN,MAAMM,MAAM;QACpBC,MAAM;YAAEC,WAAWR,MAAMQ,SAAS;QAAC;QACnCC,gBAAgBhB;QAChBmC,WAAW5B,MAAM4B,SAAS;QAC1BC,OAAO7B,MAAM6B,KAAK;QAClBC,KAAK,GAAE9B,eAAAA,MAAM8B,KAAK,YAAX9B,eAAe;QACtBU,MAAMV,MAAMU,IAAI;QAChBqB,SAAS/B,MAAM+B,OAAO;QACtBC,aAAahC,MAAMgC,WAAW;QAC9BC,cAAc;IAChB;AACF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* THE WAY OUT EVERY OUTREACH EMAIL CARRIES (AGL-2981).
|
|
19
|
+
*
|
|
20
|
+
* Two, in the `List-Unsubscribe` header, beside the footer's "reply no":
|
|
21
|
+
*
|
|
22
|
+
* - a signed HTTPS link on the console, `/api/outreach/unsubscribe?t=…`,
|
|
23
|
+
* which a mailbox provider POSTs with no person present (RFC 8058) and a
|
|
24
|
+
* mail client opens for a person;
|
|
25
|
+
* - a `mailto:` at the rep's own mailbox, `<name>+unsubscribe@<domain>`,
|
|
26
|
+
* which the sync reads as an opt-out from whoever writes to it.
|
|
27
|
+
*
|
|
28
|
+
* ## The token
|
|
29
|
+
*
|
|
30
|
+
* `<payload>.<signature>`: the payload is base64url JSON naming the
|
|
31
|
+
* organization and the enrollment, and the signature is the platform's
|
|
32
|
+
* signed-link HMAC under Outreach's own purpose, so no other link verifies
|
|
33
|
+
* as this one and this one verifies as no other. It carries no address —
|
|
34
|
+
* the enrollment names the person, and a URL that lands in logs should not.
|
|
35
|
+
*
|
|
36
|
+
* It does not expire. CAN-SPAM holds an opt-out open for thirty days after
|
|
37
|
+
* the send, and a link that stops working on day thirty-one helps nobody:
|
|
38
|
+
* the only thing that retires one is rotating the signing secret
|
|
39
|
+
* (`EMAIL_UNSUBSCRIBE_SECRET`, else `CRON_SECRET`).
|
|
40
|
+
*/
|
|
41
|
+
/** The purpose Outreach's links are signed under. */
|
|
42
|
+
export declare const OUTREACH_UNSUBSCRIBE_PURPOSE = "outreach-unsubscribe";
|
|
43
|
+
/** The link's path on the console. */
|
|
44
|
+
export declare const OUTREACH_UNSUBSCRIBE_PATH: string;
|
|
45
|
+
/** The subaddress a mailbox's `mailto:` unsubscribe is written to. */
|
|
46
|
+
export declare const OUTREACH_UNSUBSCRIBE_SUBADDRESS = "unsubscribe";
|
|
47
|
+
/** What a token names. */
|
|
48
|
+
export interface OutreachUnsubscribeTarget {
|
|
49
|
+
orgId: string;
|
|
50
|
+
enrollmentId: string;
|
|
51
|
+
}
|
|
52
|
+
/** A token for one enrollment, or `null` when there is no secret to sign it with. */
|
|
53
|
+
export declare function mintOutreachUnsubscribeToken(target: OutreachUnsubscribeTarget, secret?: string): string | null;
|
|
54
|
+
/** What a token names, or `null` for one that is malformed or not ours. */
|
|
55
|
+
export declare function readOutreachUnsubscribeToken(token: unknown, secret?: string): OutreachUnsubscribeTarget | null;
|
|
56
|
+
/**
|
|
57
|
+
* The HTTPS link for one enrollment on the console at `origin`, or `null`
|
|
58
|
+
* when the origin is not HTTPS or there is no secret — no link rather than
|
|
59
|
+
* one that points at nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function outreachUnsubscribeUrl(input: {
|
|
62
|
+
origin: string | null | undefined;
|
|
63
|
+
target: OutreachUnsubscribeTarget;
|
|
64
|
+
secret?: string;
|
|
65
|
+
}): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The `mailto:` unsubscribe at a mailbox: its account address with the
|
|
68
|
+
* `+unsubscribe` subaddress, which Google Workspace delivers to the same
|
|
69
|
+
* mailbox and the sync reads apart from the rep's other mail. `null` for an
|
|
70
|
+
* address that is not one.
|
|
71
|
+
*/
|
|
72
|
+
export declare function outreachUnsubscribeMailbox(accountEmail: string): {
|
|
73
|
+
address: string;
|
|
74
|
+
uri: string;
|
|
75
|
+
} | null;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
18
|
+
import { signedLinkSignature, signedLinkSignatureMatches } from "@aglyn/tenant-data-admin/server/email-unsubscribe-link";
|
|
19
|
+
import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
20
|
+
/**
|
|
21
|
+
* THE WAY OUT EVERY OUTREACH EMAIL CARRIES (AGL-2981).
|
|
22
|
+
*
|
|
23
|
+
* Two, in the `List-Unsubscribe` header, beside the footer's "reply no":
|
|
24
|
+
*
|
|
25
|
+
* - a signed HTTPS link on the console, `/api/outreach/unsubscribe?t=…`,
|
|
26
|
+
* which a mailbox provider POSTs with no person present (RFC 8058) and a
|
|
27
|
+
* mail client opens for a person;
|
|
28
|
+
* - a `mailto:` at the rep's own mailbox, `<name>+unsubscribe@<domain>`,
|
|
29
|
+
* which the sync reads as an opt-out from whoever writes to it.
|
|
30
|
+
*
|
|
31
|
+
* ## The token
|
|
32
|
+
*
|
|
33
|
+
* `<payload>.<signature>`: the payload is base64url JSON naming the
|
|
34
|
+
* organization and the enrollment, and the signature is the platform's
|
|
35
|
+
* signed-link HMAC under Outreach's own purpose, so no other link verifies
|
|
36
|
+
* as this one and this one verifies as no other. It carries no address —
|
|
37
|
+
* the enrollment names the person, and a URL that lands in logs should not.
|
|
38
|
+
*
|
|
39
|
+
* It does not expire. CAN-SPAM holds an opt-out open for thirty days after
|
|
40
|
+
* the send, and a link that stops working on day thirty-one helps nobody:
|
|
41
|
+
* the only thing that retires one is rotating the signing secret
|
|
42
|
+
* (`EMAIL_UNSUBSCRIBE_SECRET`, else `CRON_SECRET`).
|
|
43
|
+
*/ /** The purpose Outreach's links are signed under. */ export const OUTREACH_UNSUBSCRIBE_PURPOSE = 'outreach-unsubscribe';
|
|
44
|
+
/** The link's path on the console. */ export const OUTREACH_UNSUBSCRIBE_PATH = `/api/${OUTREACH_API_ROUTES.unsubscribe}`;
|
|
45
|
+
/** The subaddress a mailbox's `mailto:` unsubscribe is written to. */ export const OUTREACH_UNSUBSCRIBE_SUBADDRESS = 'unsubscribe';
|
|
46
|
+
/** A document id as a token may carry one. */ const DOCUMENT_ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
47
|
+
const base64url = (text)=>Buffer.from(text, 'utf8').toString('base64url');
|
|
48
|
+
/** A token for one enrollment, or `null` when there is no secret to sign it with. */ export function mintOutreachUnsubscribeToken(target, secret) {
|
|
49
|
+
if (!DOCUMENT_ID.test(target.orgId) || !DOCUMENT_ID.test(target.enrollmentId)) return null;
|
|
50
|
+
const payload = base64url(JSON.stringify({
|
|
51
|
+
v: 1,
|
|
52
|
+
o: target.orgId,
|
|
53
|
+
e: target.enrollmentId
|
|
54
|
+
}));
|
|
55
|
+
const signature = secret === undefined ? signedLinkSignature(OUTREACH_UNSUBSCRIBE_PURPOSE, payload) : signedLinkSignature(OUTREACH_UNSUBSCRIBE_PURPOSE, payload, secret);
|
|
56
|
+
return signature ? `${payload}.${signature}` : null;
|
|
57
|
+
}
|
|
58
|
+
/** What a token names, or `null` for one that is malformed or not ours. */ export function readOutreachUnsubscribeToken(token, secret) {
|
|
59
|
+
const raw = typeof token === 'string' ? token.trim() : '';
|
|
60
|
+
const dot = raw.indexOf('.');
|
|
61
|
+
if (dot <= 0 || raw.length > 1024) return null;
|
|
62
|
+
const payload = raw.slice(0, dot);
|
|
63
|
+
const signature = raw.slice(dot + 1);
|
|
64
|
+
if (!signedLinkSignatureMatches(_extends({
|
|
65
|
+
purpose: OUTREACH_UNSUBSCRIBE_PURPOSE,
|
|
66
|
+
payload,
|
|
67
|
+
signature
|
|
68
|
+
}, secret === undefined ? {} : {
|
|
69
|
+
secret
|
|
70
|
+
}))) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
var _body_o, _body_e;
|
|
75
|
+
const body = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
76
|
+
const orgId = String((_body_o = body['o']) != null ? _body_o : '');
|
|
77
|
+
const enrollmentId = String((_body_e = body['e']) != null ? _body_e : '');
|
|
78
|
+
if (body['v'] !== 1 || !DOCUMENT_ID.test(orgId) || !DOCUMENT_ID.test(enrollmentId)) return null;
|
|
79
|
+
return {
|
|
80
|
+
orgId,
|
|
81
|
+
enrollmentId
|
|
82
|
+
};
|
|
83
|
+
} catch (unused) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The HTTPS link for one enrollment on the console at `origin`, or `null`
|
|
89
|
+
* when the origin is not HTTPS or there is no secret — no link rather than
|
|
90
|
+
* one that points at nothing.
|
|
91
|
+
*/ export function outreachUnsubscribeUrl(input) {
|
|
92
|
+
let origin;
|
|
93
|
+
try {
|
|
94
|
+
var _input_origin;
|
|
95
|
+
origin = new URL(String((_input_origin = input.origin) != null ? _input_origin : ''));
|
|
96
|
+
} catch (unused) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (origin.protocol !== 'https:') return null;
|
|
100
|
+
const token = mintOutreachUnsubscribeToken(input.target, input.secret);
|
|
101
|
+
return token ? `${origin.origin}${OUTREACH_UNSUBSCRIBE_PATH}?t=${token}` : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The `mailto:` unsubscribe at a mailbox: its account address with the
|
|
105
|
+
* `+unsubscribe` subaddress, which Google Workspace delivers to the same
|
|
106
|
+
* mailbox and the sync reads apart from the rep's other mail. `null` for an
|
|
107
|
+
* address that is not one.
|
|
108
|
+
*/ export function outreachUnsubscribeMailbox(accountEmail) {
|
|
109
|
+
const email = normalizeContactEmail(accountEmail);
|
|
110
|
+
if (!email) return null;
|
|
111
|
+
const at = email.lastIndexOf('@');
|
|
112
|
+
const local = email.slice(0, at).split('+')[0];
|
|
113
|
+
if (!local) return null;
|
|
114
|
+
const address = `${local}+${OUTREACH_UNSUBSCRIBE_SUBADDRESS}@${email.slice(at + 1)}`;
|
|
115
|
+
return {
|
|
116
|
+
address,
|
|
117
|
+
uri: `mailto:${address}?subject=unsubscribe`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=unsubscribe-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/unsubscribe-link.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n signedLinkSignature,\n signedLinkSignatureMatches,\n} from '@aglyn/tenant-data-admin/server/email-unsubscribe-link'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\n\n/**\n * THE WAY OUT EVERY OUTREACH EMAIL CARRIES (AGL-2981).\n *\n * Two, in the `List-Unsubscribe` header, beside the footer's \"reply no\":\n *\n * - a signed HTTPS link on the console, `/api/outreach/unsubscribe?t=…`,\n * which a mailbox provider POSTs with no person present (RFC 8058) and a\n * mail client opens for a person;\n * - a `mailto:` at the rep's own mailbox, `<name>+unsubscribe@<domain>`,\n * which the sync reads as an opt-out from whoever writes to it.\n *\n * ## The token\n *\n * `<payload>.<signature>`: the payload is base64url JSON naming the\n * organization and the enrollment, and the signature is the platform's\n * signed-link HMAC under Outreach's own purpose, so no other link verifies\n * as this one and this one verifies as no other. It carries no address —\n * the enrollment names the person, and a URL that lands in logs should not.\n *\n * It does not expire. CAN-SPAM holds an opt-out open for thirty days after\n * the send, and a link that stops working on day thirty-one helps nobody:\n * the only thing that retires one is rotating the signing secret\n * (`EMAIL_UNSUBSCRIBE_SECRET`, else `CRON_SECRET`).\n */\n\n/** The purpose Outreach's links are signed under. */\nexport const OUTREACH_UNSUBSCRIBE_PURPOSE = 'outreach-unsubscribe'\n\n/** The link's path on the console. */\nexport const OUTREACH_UNSUBSCRIBE_PATH = `/api/${OUTREACH_API_ROUTES.unsubscribe}`\n\n/** The subaddress a mailbox's `mailto:` unsubscribe is written to. */\nexport const OUTREACH_UNSUBSCRIBE_SUBADDRESS = 'unsubscribe'\n\n/** What a token names. */\nexport interface OutreachUnsubscribeTarget {\n orgId: string\n enrollmentId: string\n}\n\n/** A document id as a token may carry one. */\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,160}$/\n\nconst base64url = (text: string): string => Buffer.from(text, 'utf8').toString('base64url')\n\n/** A token for one enrollment, or `null` when there is no secret to sign it with. */\nexport function mintOutreachUnsubscribeToken(\n target: OutreachUnsubscribeTarget,\n secret?: string,\n): string | null {\n if (!DOCUMENT_ID.test(target.orgId) || !DOCUMENT_ID.test(target.enrollmentId)) return null\n const payload = base64url(JSON.stringify({ v: 1, o: target.orgId, e: target.enrollmentId }))\n const signature =\n secret === undefined\n ? signedLinkSignature(OUTREACH_UNSUBSCRIBE_PURPOSE, payload)\n : signedLinkSignature(OUTREACH_UNSUBSCRIBE_PURPOSE, payload, secret)\n return signature ? `${payload}.${signature}` : null\n}\n\n/** What a token names, or `null` for one that is malformed or not ours. */\nexport function readOutreachUnsubscribeToken(\n token: unknown,\n secret?: string,\n): OutreachUnsubscribeTarget | null {\n const raw = typeof token === 'string' ? token.trim() : ''\n const dot = raw.indexOf('.')\n if (dot <= 0 || raw.length > 1024) return null\n const payload = raw.slice(0, dot)\n const signature = raw.slice(dot + 1)\n if (\n !signedLinkSignatureMatches({\n purpose: OUTREACH_UNSUBSCRIBE_PURPOSE,\n payload,\n signature,\n ...(secret === undefined ? {} : { secret }),\n })\n ) {\n return null\n }\n try {\n const body = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>\n const orgId = String(body['o'] ?? '')\n const enrollmentId = String(body['e'] ?? '')\n if (body['v'] !== 1 || !DOCUMENT_ID.test(orgId) || !DOCUMENT_ID.test(enrollmentId)) return null\n return { orgId, enrollmentId }\n } catch {\n return null\n }\n}\n\n/**\n * The HTTPS link for one enrollment on the console at `origin`, or `null`\n * when the origin is not HTTPS or there is no secret — no link rather than\n * one that points at nothing.\n */\nexport function outreachUnsubscribeUrl(input: {\n origin: string | null | undefined\n target: OutreachUnsubscribeTarget\n secret?: string\n}): string | null {\n let origin: URL\n try {\n origin = new URL(String(input.origin ?? ''))\n } catch {\n return null\n }\n if (origin.protocol !== 'https:') return null\n const token = mintOutreachUnsubscribeToken(input.target, input.secret)\n return token ? `${origin.origin}${OUTREACH_UNSUBSCRIBE_PATH}?t=${token}` : null\n}\n\n/**\n * The `mailto:` unsubscribe at a mailbox: its account address with the\n * `+unsubscribe` subaddress, which Google Workspace delivers to the same\n * mailbox and the sync reads apart from the rep's other mail. `null` for an\n * address that is not one.\n */\nexport function outreachUnsubscribeMailbox(accountEmail: string): { address: string; uri: string } | null {\n const email = normalizeContactEmail(accountEmail)\n if (!email) return null\n const at = email.lastIndexOf('@')\n const local = email.slice(0, at).split('+')[0]\n if (!local) return null\n const address = `${local}+${OUTREACH_UNSUBSCRIBE_SUBADDRESS}@${email.slice(at + 1)}`\n return { address, uri: `mailto:${address}?subject=unsubscribe` }\n}\n"],"names":["normalizeContactEmail","signedLinkSignature","signedLinkSignatureMatches","OUTREACH_API_ROUTES","OUTREACH_UNSUBSCRIBE_PURPOSE","OUTREACH_UNSUBSCRIBE_PATH","unsubscribe","OUTREACH_UNSUBSCRIBE_SUBADDRESS","DOCUMENT_ID","base64url","text","Buffer","from","toString","mintOutreachUnsubscribeToken","target","secret","test","orgId","enrollmentId","payload","JSON","stringify","v","o","e","signature","undefined","readOutreachUnsubscribeToken","token","raw","trim","dot","indexOf","length","slice","purpose","body","parse","String","outreachUnsubscribeUrl","input","origin","URL","protocol","outreachUnsubscribeMailbox","accountEmail","email","at","lastIndexOf","local","split","address","uri"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SACEC,mBAAmB,EACnBC,0BAA0B,QACrB,yDAAwD;AAC/D,SAASC,mBAAmB,QAAQ,6BAAyB;AAE7D;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,mDAAmD,GACnD,OAAO,MAAMC,+BAA+B,uBAAsB;AAElE,oCAAoC,GACpC,OAAO,MAAMC,4BAA4B,CAAC,KAAK,EAAEF,oBAAoBG,WAAW,EAAE,CAAA;AAElF,oEAAoE,GACpE,OAAO,MAAMC,kCAAkC,cAAa;AAQ5D,4CAA4C,GAC5C,MAAMC,cAAc;AAEpB,MAAMC,YAAY,CAACC,OAAyBC,OAAOC,IAAI,CAACF,MAAM,QAAQG,QAAQ,CAAC;AAE/E,mFAAmF,GACnF,OAAO,SAASC,6BACdC,MAAiC,EACjCC,MAAe;IAEf,IAAI,CAACR,YAAYS,IAAI,CAACF,OAAOG,KAAK,KAAK,CAACV,YAAYS,IAAI,CAACF,OAAOI,YAAY,GAAG,OAAO;IACtF,MAAMC,UAAUX,UAAUY,KAAKC,SAAS,CAAC;QAAEC,GAAG;QAAGC,GAAGT,OAAOG,KAAK;QAAEO,GAAGV,OAAOI,YAAY;IAAC;IACzF,MAAMO,YACJV,WAAWW,YACP1B,oBAAoBG,8BAA8BgB,WAClDnB,oBAAoBG,8BAA8BgB,SAASJ;IACjE,OAAOU,YAAY,GAAGN,QAAQ,CAAC,EAAEM,WAAW,GAAG;AACjD;AAEA,yEAAyE,GACzE,OAAO,SAASE,6BACdC,KAAc,EACdb,MAAe;IAEf,MAAMc,MAAM,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACvD,MAAMC,MAAMF,IAAIG,OAAO,CAAC;IACxB,IAAID,OAAO,KAAKF,IAAII,MAAM,GAAG,MAAM,OAAO;IAC1C,MAAMd,UAAUU,IAAIK,KAAK,CAAC,GAAGH;IAC7B,MAAMN,YAAYI,IAAIK,KAAK,CAACH,MAAM;IAClC,IACE,CAAC9B,2BAA2B;QAC1BkC,SAAShC;QACTgB;QACAM;OACIV,WAAWW,YAAY,CAAC,IAAI;QAAEX;IAAO,KAE3C;QACA,OAAO;IACT;IACA,IAAI;YAEmBqB,SACOA;QAF5B,MAAMA,OAAOhB,KAAKiB,KAAK,CAAC3B,OAAOC,IAAI,CAACQ,SAAS,aAAaP,QAAQ,CAAC;QACnE,MAAMK,QAAQqB,QAAOF,UAAAA,IAAI,CAAC,IAAI,YAATA,UAAa;QAClC,MAAMlB,eAAeoB,QAAOF,UAAAA,IAAI,CAAC,IAAI,YAATA,UAAa;QACzC,IAAIA,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC7B,YAAYS,IAAI,CAACC,UAAU,CAACV,YAAYS,IAAI,CAACE,eAAe,OAAO;QAC3F,OAAO;YAAED;YAAOC;QAAa;IAC/B,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASqB,uBAAuBC,KAItC;IACC,IAAIC;IACJ,IAAI;YACsBD;QAAxBC,SAAS,IAAIC,IAAIJ,QAAOE,gBAAAA,MAAMC,MAAM,YAAZD,gBAAgB;IAC1C,EAAE,eAAM;QACN,OAAO;IACT;IACA,IAAIC,OAAOE,QAAQ,KAAK,UAAU,OAAO;IACzC,MAAMf,QAAQf,6BAA6B2B,MAAM1B,MAAM,EAAE0B,MAAMzB,MAAM;IACrE,OAAOa,QAAQ,GAAGa,OAAOA,MAAM,GAAGrC,0BAA0B,GAAG,EAAEwB,OAAO,GAAG;AAC7E;AAEA;;;;;CAKC,GACD,OAAO,SAASgB,2BAA2BC,YAAoB;IAC7D,MAAMC,QAAQ/C,sBAAsB8C;IACpC,IAAI,CAACC,OAAO,OAAO;IACnB,MAAMC,KAAKD,MAAME,WAAW,CAAC;IAC7B,MAAMC,QAAQH,MAAMZ,KAAK,CAAC,GAAGa,IAAIG,KAAK,CAAC,IAAI,CAAC,EAAE;IAC9C,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,UAAU,GAAGF,MAAM,CAAC,EAAE3C,gCAAgC,CAAC,EAAEwC,MAAMZ,KAAK,CAACa,KAAK,IAAI;IACpF,OAAO;QAAEI;QAASC,KAAK,CAAC,OAAO,EAAED,QAAQ,oBAAoB,CAAC;IAAC;AACjE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { OutreachRuntimeDeps } from './runtime-deps';
|
|
19
|
+
/** The plain page both methods answer with. */
|
|
20
|
+
export declare function outreachUnsubscribePage(input: {
|
|
21
|
+
title: string;
|
|
22
|
+
body: string;
|
|
23
|
+
}, status?: number): Response;
|
|
24
|
+
export declare function createOutreachUnsubscribeRoute(deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'optOutOfSalesTopic'>): PluginWebApiHandler;
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
*/ import { readOutreachComplianceSettingsDoc } from "../storage/compliance-settings-store.js";
|
|
17
|
+
import { outreachOrgCollection, readStoredOutreachEnrollment } from "../storage/outreach-records.js";
|
|
18
|
+
import { recordOutreachOptOut } from "./enrollment-events.js";
|
|
19
|
+
import { readOutreachUnsubscribeToken } from "./unsubscribe-link.js";
|
|
20
|
+
/**
|
|
21
|
+
* ONE-CLICK UNSUBSCRIBE (AGL-2981): `GET` and `POST /api/outreach/unsubscribe?t=…`.
|
|
22
|
+
*
|
|
23
|
+
* The link in every Outreach email's `List-Unsubscribe` header. A mailbox
|
|
24
|
+
* provider POSTs it with `List-Unsubscribe=One-Click` and no person present
|
|
25
|
+
* (RFC 8058); a mail client that opens it for a person sends a GET. BOTH
|
|
26
|
+
* act, and both answer a plain page saying it is done.
|
|
27
|
+
*
|
|
28
|
+
* That GET acts is deliberate, and the opposite of the platform's newsletter
|
|
29
|
+
* links. A person who used their mail client's Unsubscribe command has asked
|
|
30
|
+
* already, and a second button would be a way out with a hurdle in it. The
|
|
31
|
+
* one caller that could fetch the link without asking — a link scanner — can
|
|
32
|
+
* only make a cold sequence stop writing to somebody, which is the direction
|
|
33
|
+
* cold email should err in.
|
|
34
|
+
*
|
|
35
|
+
* No sign-in: the signature in the token is the authority, and a token that
|
|
36
|
+
* does not verify changes nothing. It is registered as a recipient link, so
|
|
37
|
+
* it answers whether or not Outreach is released to the organization now —
|
|
38
|
+
* an opt-out outlives a paused rollout. Each act is idempotent: the link
|
|
39
|
+
* used twice changes nothing the first use did not.
|
|
40
|
+
*
|
|
41
|
+
* What it records is what a reply saying "stop" records: the organization's
|
|
42
|
+
* do-not-contact list, the site's `sales` topic, and every open enrollment
|
|
43
|
+
* of the address stopped. An enrollment that no longer exists belongs to a
|
|
44
|
+
* person or a workspace already erased, whose every site already refuses
|
|
45
|
+
* them; the page says the same thing either way, and reveals nothing.
|
|
46
|
+
*/ const PAGE_HEADERS = {
|
|
47
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
48
|
+
'Cache-Control': 'no-store',
|
|
49
|
+
'X-Robots-Tag': 'noindex',
|
|
50
|
+
'Referrer-Policy': 'no-referrer'
|
|
51
|
+
};
|
|
52
|
+
const escapeHtml = (value)=>value.replace(/[&<>"']/g, (character)=>`&#${character.charCodeAt(0)};`);
|
|
53
|
+
/** The plain page both methods answer with. */ export function outreachUnsubscribePage(input, status = 200) {
|
|
54
|
+
const html = '<!doctype html><html lang="en"><head><meta charset="utf-8">' + '<meta name="viewport" content="width=device-width, initial-scale=1">' + // The reader's own colors, light or dark: the page has no theme to take.
|
|
55
|
+
'<meta name="color-scheme" content="light dark">' + `<title>${escapeHtml(input.title)}</title>` + '<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5}h1{font-size:1.5rem}</style>' + `</head><body><main><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.body)}</p></main></body></html>`;
|
|
56
|
+
return new Response(html, {
|
|
57
|
+
status,
|
|
58
|
+
headers: PAGE_HEADERS
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function createOutreachUnsubscribeRoute(deps) {
|
|
62
|
+
return async (request)=>{
|
|
63
|
+
if (request.method !== 'GET' && request.method !== 'POST') {
|
|
64
|
+
return new Response('Method not allowed', {
|
|
65
|
+
status: 405,
|
|
66
|
+
headers: {
|
|
67
|
+
Allow: 'GET, POST'
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const target = readOutreachUnsubscribeToken(new URL(request.url).searchParams.get('t'));
|
|
72
|
+
if (!target) {
|
|
73
|
+
return outreachUnsubscribePage({
|
|
74
|
+
title: 'This link doesn’t work',
|
|
75
|
+
body: 'This unsubscribe link isn’t valid. To stop these emails, reply to one of them with “no”.'
|
|
76
|
+
}, 400);
|
|
77
|
+
}
|
|
78
|
+
const firestore = deps.firestore();
|
|
79
|
+
const snapshot = await outreachOrgCollection(firestore, target.orgId, 'enrollments').doc(target.enrollmentId).get();
|
|
80
|
+
const enrollment = readStoredOutreachEnrollment(target.enrollmentId, snapshot.exists ? snapshot.data() : undefined);
|
|
81
|
+
if (enrollment) {
|
|
82
|
+
await recordOutreachOptOut(deps, {
|
|
83
|
+
orgId: target.orgId,
|
|
84
|
+
email: enrollment.email,
|
|
85
|
+
source: 'unsubscribe',
|
|
86
|
+
enrollment,
|
|
87
|
+
hostIds: [
|
|
88
|
+
enrollment.hostId
|
|
89
|
+
],
|
|
90
|
+
detail: request.method === 'POST' ? 'One-click unsubscribe.' : 'Used the unsubscribe link.',
|
|
91
|
+
nowMs: deps.now()
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const settings = await readOutreachComplianceSettingsDoc(firestore, target.orgId).catch(()=>null);
|
|
95
|
+
const sender = (settings == null ? void 0 : settings.brandName) || (settings == null ? void 0 : settings.legalName) || 'The sender';
|
|
96
|
+
return outreachUnsubscribePage({
|
|
97
|
+
title: 'You’re unsubscribed',
|
|
98
|
+
body: `${sender} won’t send you any more of these emails.`
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=unsubscribe-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/unsubscribe-route.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 { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { recordOutreachOptOut } from './enrollment-events'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { readOutreachUnsubscribeToken } from './unsubscribe-link'\n\n/**\n * ONE-CLICK UNSUBSCRIBE (AGL-2981): `GET` and `POST /api/outreach/unsubscribe?t=…`.\n *\n * The link in every Outreach email's `List-Unsubscribe` header. A mailbox\n * provider POSTs it with `List-Unsubscribe=One-Click` and no person present\n * (RFC 8058); a mail client that opens it for a person sends a GET. BOTH\n * act, and both answer a plain page saying it is done.\n *\n * That GET acts is deliberate, and the opposite of the platform's newsletter\n * links. A person who used their mail client's Unsubscribe command has asked\n * already, and a second button would be a way out with a hurdle in it. The\n * one caller that could fetch the link without asking — a link scanner — can\n * only make a cold sequence stop writing to somebody, which is the direction\n * cold email should err in.\n *\n * No sign-in: the signature in the token is the authority, and a token that\n * does not verify changes nothing. It is registered as a recipient link, so\n * it answers whether or not Outreach is released to the organization now —\n * an opt-out outlives a paused rollout. Each act is idempotent: the link\n * used twice changes nothing the first use did not.\n *\n * What it records is what a reply saying \"stop\" records: the organization's\n * do-not-contact list, the site's `sales` topic, and every open enrollment\n * of the address stopped. An enrollment that no longer exists belongs to a\n * person or a workspace already erased, whose every site already refuses\n * them; the page says the same thing either way, and reveals nothing.\n */\n\nconst PAGE_HEADERS = {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n 'X-Robots-Tag': 'noindex',\n 'Referrer-Policy': 'no-referrer',\n}\n\nconst escapeHtml = (value: string) =>\n value.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`)\n\n/** The plain page both methods answer with. */\nexport function outreachUnsubscribePage(input: { title: string; body: string }, status = 200): Response {\n const html =\n '<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n // The reader's own colors, light or dark: the page has no theme to take.\n '<meta name=\"color-scheme\" content=\"light dark\">' +\n `<title>${escapeHtml(input.title)}</title>` +\n '<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5}h1{font-size:1.5rem}</style>' +\n `</head><body><main><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.body)}</p></main></body></html>`\n return new Response(html, { status, headers: PAGE_HEADERS })\n}\n\nexport function createOutreachUnsubscribeRoute(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'optOutOfSalesTopic'>,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'GET' && request.method !== 'POST') {\n return new Response('Method not allowed', { status: 405, headers: { Allow: 'GET, POST' } })\n }\n const target = readOutreachUnsubscribeToken(new URL(request.url).searchParams.get('t'))\n if (!target) {\n return outreachUnsubscribePage(\n {\n title: 'This link doesn’t work',\n body: 'This unsubscribe link isn’t valid. To stop these emails, reply to one of them with “no”.',\n },\n 400,\n )\n }\n const firestore = deps.firestore()\n const snapshot = await outreachOrgCollection(firestore, target.orgId, 'enrollments').doc(target.enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(target.enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (enrollment) {\n await recordOutreachOptOut(deps, {\n orgId: target.orgId,\n email: enrollment.email,\n source: 'unsubscribe',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: request.method === 'POST' ? 'One-click unsubscribe.' : 'Used the unsubscribe link.',\n nowMs: deps.now(),\n })\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, target.orgId).catch(() => null)\n const sender = settings?.brandName || settings?.legalName || 'The sender'\n return outreachUnsubscribePage({\n title: 'You’re unsubscribed',\n body: `${sender} won’t send you any more of these emails.`,\n })\n }\n}\n"],"names":["readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","recordOutreachOptOut","readOutreachUnsubscribeToken","PAGE_HEADERS","escapeHtml","value","replace","character","charCodeAt","outreachUnsubscribePage","input","status","html","title","body","Response","headers","createOutreachUnsubscribeRoute","deps","request","method","Allow","target","URL","url","searchParams","get","firestore","snapshot","orgId","doc","enrollmentId","enrollment","exists","data","undefined","email","source","hostIds","hostId","detail","nowMs","now","settings","catch","sender","brandName","legalName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,iCAAiC,QAAQ,0CAAsC;AACxF,SAASC,qBAAqB,EAAEC,4BAA4B,QAAQ,iCAA6B;AACjG,SAASC,oBAAoB,QAAQ,yBAAqB;AAE1D,SAASC,4BAA4B,QAAQ,wBAAoB;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,MAAMC,eAAe;IACnB,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,mBAAmB;AACrB;AAEA,MAAMC,aAAa,CAACC,QAClBA,MAAMC,OAAO,CAAC,YAAY,CAACC,YAAc,CAAC,EAAE,EAAEA,UAAUC,UAAU,CAAC,GAAG,CAAC,CAAC;AAE1E,6CAA6C,GAC7C,OAAO,SAASC,wBAAwBC,KAAsC,EAAEC,SAAS,GAAG;IAC1F,MAAMC,OACJ,gEACA,yEACA,yEAAyE;IACzE,oDACA,CAAC,OAAO,EAAER,WAAWM,MAAMG,KAAK,EAAE,QAAQ,CAAC,GAC3C,8IACA,CAAC,uBAAuB,EAAET,WAAWM,MAAMG,KAAK,EAAE,QAAQ,EAAET,WAAWM,MAAMI,IAAI,EAAE,yBAAyB,CAAC;IAC/G,OAAO,IAAIC,SAASH,MAAM;QAAED;QAAQK,SAASb;IAAa;AAC5D;AAEA,OAAO,SAASc,+BACdC,IAA2E;IAE3E,OAAO,OAAOC;QACZ,IAAIA,QAAQC,MAAM,KAAK,SAASD,QAAQC,MAAM,KAAK,QAAQ;YACzD,OAAO,IAAIL,SAAS,sBAAsB;gBAAEJ,QAAQ;gBAAKK,SAAS;oBAAEK,OAAO;gBAAY;YAAE;QAC3F;QACA,MAAMC,SAASpB,6BAA6B,IAAIqB,IAAIJ,QAAQK,GAAG,EAAEC,YAAY,CAACC,GAAG,CAAC;QAClF,IAAI,CAACJ,QAAQ;YACX,OAAOb,wBACL;gBACEI,OAAO;gBACPC,MAAM;YACR,GACA;QAEJ;QACA,MAAMa,YAAYT,KAAKS,SAAS;QAChC,MAAMC,WAAW,MAAM7B,sBAAsB4B,WAAWL,OAAOO,KAAK,EAAE,eAAeC,GAAG,CAACR,OAAOS,YAAY,EAAEL,GAAG;QACjH,MAAMM,aAAahC,6BAA6BsB,OAAOS,YAAY,EAAEH,SAASK,MAAM,GAAGL,SAASM,IAAI,KAAKC;QACzG,IAAIH,YAAY;YACd,MAAM/B,qBAAqBiB,MAAM;gBAC/BW,OAAOP,OAAOO,KAAK;gBACnBO,OAAOJ,WAAWI,KAAK;gBACvBC,QAAQ;gBACRL;gBACAM,SAAS;oBAACN,WAAWO,MAAM;iBAAC;gBAC5BC,QAAQrB,QAAQC,MAAM,KAAK,SAAS,2BAA2B;gBAC/DqB,OAAOvB,KAAKwB,GAAG;YACjB;QACF;QACA,MAAMC,WAAW,MAAM7C,kCAAkC6B,WAAWL,OAAOO,KAAK,EAAEe,KAAK,CAAC,IAAM;QAC9F,MAAMC,SAASF,CAAAA,4BAAAA,SAAUG,SAAS,MAAIH,4BAAAA,SAAUI,SAAS,KAAI;QAC7D,OAAOtC,wBAAwB;YAC7BI,OAAO;YACPC,MAAM,GAAG+B,OAAO,yCAAyC,CAAC;QAC5D;IACF;AACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.
|
|
19
|
+
*
|
|
20
|
+
* Every write to an `outreach*` collection is made here: the Firestore rules
|
|
21
|
+
* refuse all of them to clients, so a mailbox, a sequence, an enrollment and a
|
|
22
|
+
* credential only ever change through a handler registered below, behind the
|
|
23
|
+
* session, entitlement and permission checks that handler makes.
|
|
24
|
+
*
|
|
25
|
+
* `outreach/ping` exists so the wiring is proven rather than assumed, as the
|
|
26
|
+
* CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the
|
|
27
|
+
* `outreach` prefix, the generated server manifest loads this module, and the
|
|
28
|
+
* console's `/api/[...pluginApi]` dispatcher reaches the handler — after its
|
|
29
|
+
* own gates, which already refuse a request while `release_outreach` is off
|
|
30
|
+
* for the caller. A first real route that also had to be the first wiring
|
|
31
|
+
* test would be two things to debug at once.
|
|
32
|
+
*
|
|
33
|
+
* A route that reads or writes org data resolves the caller the way the CRM's
|
|
34
|
+
* org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from
|
|
35
|
+
* `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`
|
|
36
|
+
* and `outreach.use` before it touches a document.
|
|
37
|
+
*/
|
|
38
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
39
|
+
/**
|
|
40
|
+
* No auth, no org, no data: it answers whether this server bundle was loaded
|
|
41
|
+
* and its routes registered, which is a fact about the process rather than
|
|
42
|
+
* about any workspace.
|
|
43
|
+
*/
|
|
44
|
+
export declare const outreachPingHandler: PluginApiHandler;
|
|
45
|
+
/** Console API registration, named in `plugins.config.json` as `consoleApi`. */
|
|
46
|
+
export declare function registerOutreachConsoleApi(): void;
|
|
47
|
+
export { markOutreachMailboxReconnectRequired, openOutreachMailboxClient, type OpenedOutreachMailbox, } from './mailboxes/mailbox-transport';
|
|
48
|
+
export { GmailTransportError, isReconnectRequired, type GmailTransportErrorCode, } from './transport/gmail-errors';
|
|
49
|
+
export type { GmailClient, GmailFullThread, GmailMessageMetadata, GmailThread, } from './transport/gmail-client';
|
|
50
|
+
export { buildRfc5322Message, encodeGmailRawMessage, LIST_UNSUBSCRIBE_ONE_CLICK, Rfc5322MessageError, type OutreachComposedMessage, type OutreachMailAddress, } from './transport/rfc5322';
|
|
51
|
+
export { sendComposedOutreachEmail, sendOutreachMessage, type OutreachSentMessage, type SendOutreachMessageOptions, } from './transport/send-message';
|
|
52
|
+
export { outreachComplianceSettingsRef, readOutreachComplianceSettingsDoc, writeOutreachComplianceSettings, type WriteOutreachComplianceSettingsInput, type WriteOutreachComplianceSettingsResult, } from './storage/compliance-settings-store';
|
|
53
|
+
export { addOutreachDoNotContact, getOutreachDoNotContactEntry, isOutreachDoNotContact, lookupOutreachDoNotContact, outreachDoNotContactCollection, readOutreachDoNotContactEntry, type AddOutreachDoNotContactInput, type AddOutreachDoNotContactResult, } from './storage/do-not-contact-store';
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
* Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.
|
|
18
|
+
*
|
|
19
|
+
* Every write to an `outreach*` collection is made here: the Firestore rules
|
|
20
|
+
* refuse all of them to clients, so a mailbox, a sequence, an enrollment and a
|
|
21
|
+
* credential only ever change through a handler registered below, behind the
|
|
22
|
+
* session, entitlement and permission checks that handler makes.
|
|
23
|
+
*
|
|
24
|
+
* `outreach/ping` exists so the wiring is proven rather than assumed, as the
|
|
25
|
+
* CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the
|
|
26
|
+
* `outreach` prefix, the generated server manifest loads this module, and the
|
|
27
|
+
* console's `/api/[...pluginApi]` dispatcher reaches the handler — after its
|
|
28
|
+
* own gates, which already refuse a request while `release_outreach` is off
|
|
29
|
+
* for the caller. A first real route that also had to be the first wiring
|
|
30
|
+
* test would be two things to debug at once.
|
|
31
|
+
*
|
|
32
|
+
* A route that reads or writes org data resolves the caller the way the CRM's
|
|
33
|
+
* org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from
|
|
34
|
+
* `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`
|
|
35
|
+
* and `outreach.use` before it touches a document.
|
|
36
|
+
*/ import { registerPluginApiRoute, registerPluginPermissions } from "@aglyn/aglyn/server";
|
|
37
|
+
import { OUTREACH_API_ROUTES } from "./constants/api-routes.js";
|
|
38
|
+
import { OUTREACH_PERMISSIONS, OUTREACH_PLUGIN_ID } from "./constants/bundle-common.js";
|
|
39
|
+
import { registerOutreachMailboxRoutes } from "./mailboxes/register-mailbox-routes.js";
|
|
40
|
+
import { registerOutreachRoutes } from "./routes/register-routes.js";
|
|
41
|
+
/**
|
|
42
|
+
* No auth, no org, no data: it answers whether this server bundle was loaded
|
|
43
|
+
* and its routes registered, which is a fact about the process rather than
|
|
44
|
+
* about any workspace.
|
|
45
|
+
*/ export const outreachPingHandler = (req, res)=>{
|
|
46
|
+
if (req.method !== 'GET') {
|
|
47
|
+
res.setHeader('Allow', 'GET');
|
|
48
|
+
res.status(405).json({
|
|
49
|
+
error: 'Method not allowed'
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.status(200).json({
|
|
54
|
+
ok: true,
|
|
55
|
+
plugin: OUTREACH_PLUGIN_ID
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
/** Console API registration, named in `plugins.config.json` as `consoleApi`. */ export function registerOutreachConsoleApi() {
|
|
59
|
+
// The server resolves plugin permission keys from this registry, so a
|
|
60
|
+
// route's `outreach.use` check and the console's gate read the same tier
|
|
61
|
+
// defaults.
|
|
62
|
+
registerPluginPermissions(OUTREACH_PERMISSIONS);
|
|
63
|
+
registerPluginApiRoute(OUTREACH_API_ROUTES.ping, outreachPingHandler);
|
|
64
|
+
// Connect a Google mailbox, its settings, pause, test and disconnect
|
|
65
|
+
// (AGL-2978).
|
|
66
|
+
registerOutreachMailboxRoutes();
|
|
67
|
+
// Compliance settings, sequences, enrollment and enrollment actions
|
|
68
|
+
// (AGL-2980).
|
|
69
|
+
registerOutreachRoutes();
|
|
70
|
+
// The one-click unsubscribe every email carries (AGL-2981): a recipient
|
|
71
|
+
// link, so it answers whether or not Outreach is released to the
|
|
72
|
+
// organization now. Its module loads on the first request.
|
|
73
|
+
registerPluginApiRoute(OUTREACH_API_ROUTES.unsubscribe, {
|
|
74
|
+
web: async (request, context)=>(await import("./runtime/platform-runtime-deps.js")).platformOutreachUnsubscribeRoute()(request, context)
|
|
75
|
+
}, {
|
|
76
|
+
recipientLink: true
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// The transport the sending runtime reaches a connected mailbox through
|
|
80
|
+
// (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,
|
|
81
|
+
// send the engine's composed email through the one RFC 5322 writer, and read
|
|
82
|
+
// a thread whole for the engine's reply and bounce classifier.
|
|
83
|
+
export { markOutreachMailboxReconnectRequired, openOutreachMailboxClient } from "./mailboxes/mailbox-transport.js";
|
|
84
|
+
export { GmailTransportError, isReconnectRequired } from "./transport/gmail-errors.js";
|
|
85
|
+
export { buildRfc5322Message, encodeGmailRawMessage, LIST_UNSUBSCRIBE_ONE_CLICK, Rfc5322MessageError } from "./transport/rfc5322.js";
|
|
86
|
+
export { sendComposedOutreachEmail, sendOutreachMessage } from "./transport/send-message.js";
|
|
87
|
+
// The organization's compliance settings and do-not-contact list
|
|
88
|
+
// (AGL-2980), for the sending runtime: the footer is read before every send,
|
|
89
|
+
// the list is checked before every send, and a reply that opts out, an
|
|
90
|
+
// unsubscribe and a hard bounce each put the address on it.
|
|
91
|
+
export { outreachComplianceSettingsRef, readOutreachComplianceSettingsDoc, writeOutreachComplianceSettings } from "./storage/compliance-settings-store.js";
|
|
92
|
+
export { addOutreachDoNotContact, getOutreachDoNotContactEntry, isOutreachDoNotContact, lookupOutreachDoNotContact, outreachDoNotContactCollection, readOutreachDoNotContactEntry } from "./storage/do-not-contact-store.js";
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=server.js.map
|