@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.
Files changed (282) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +93 -0
  3. package/package.json +52 -0
  4. package/src/index.d.ts +22 -0
  5. package/src/index.js +29 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/compliance-section.d.ts +20 -0
  8. package/src/lib/components/compliance-section.js +291 -0
  9. package/src/lib/components/compliance-section.js.map +1 -0
  10. package/src/lib/components/enroll-dialog.d.ts +36 -0
  11. package/src/lib/components/enroll-dialog.js +656 -0
  12. package/src/lib/components/enroll-dialog.js.map +1 -0
  13. package/src/lib/components/enrollments-table.d.ts +28 -0
  14. package/src/lib/components/enrollments-table.js +375 -0
  15. package/src/lib/components/enrollments-table.js.map +1 -0
  16. package/src/lib/components/mailbox-card.d.ts +46 -0
  17. package/src/lib/components/mailbox-card.js +517 -0
  18. package/src/lib/components/mailbox-card.js.map +1 -0
  19. package/src/lib/components/mailboxes-section.d.ts +24 -0
  20. package/src/lib/components/mailboxes-section.js +258 -0
  21. package/src/lib/components/mailboxes-section.js.map +1 -0
  22. package/src/lib/components/outreach-console-page.d.ts +17 -0
  23. package/src/lib/components/outreach-console-page.js +81 -0
  24. package/src/lib/components/outreach-console-page.js.map +1 -0
  25. package/src/lib/components/outreach-console-sections.d.ts +43 -0
  26. package/src/lib/components/outreach-console-sections.js +55 -0
  27. package/src/lib/components/outreach-console-sections.js.map +1 -0
  28. package/src/lib/components/outreach-ui.d.ts +51 -0
  29. package/src/lib/components/outreach-ui.js +171 -0
  30. package/src/lib/components/outreach-ui.js.map +1 -0
  31. package/src/lib/components/send-window-fields.d.ts +25 -0
  32. package/src/lib/components/send-window-fields.js +178 -0
  33. package/src/lib/components/send-window-fields.js.map +1 -0
  34. package/src/lib/components/sequence-detail.d.ts +54 -0
  35. package/src/lib/components/sequence-detail.js +435 -0
  36. package/src/lib/components/sequence-detail.js.map +1 -0
  37. package/src/lib/components/sequence-editor.d.ts +37 -0
  38. package/src/lib/components/sequence-editor.js +485 -0
  39. package/src/lib/components/sequence-editor.js.map +1 -0
  40. package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
  41. package/src/lib/components/sequence-mailbox-picker.js +137 -0
  42. package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
  43. package/src/lib/components/sequence-preview.d.ts +27 -0
  44. package/src/lib/components/sequence-preview.js +146 -0
  45. package/src/lib/components/sequence-preview.js.map +1 -0
  46. package/src/lib/components/sequence-step-card.d.ts +31 -0
  47. package/src/lib/components/sequence-step-card.js +326 -0
  48. package/src/lib/components/sequence-step-card.js.map +1 -0
  49. package/src/lib/components/sequences-section.d.ts +40 -0
  50. package/src/lib/components/sequences-section.js +310 -0
  51. package/src/lib/components/sequences-section.js.map +1 -0
  52. package/src/lib/components/use-outreach-api.d.ts +38 -0
  53. package/src/lib/components/use-outreach-api.js +139 -0
  54. package/src/lib/components/use-outreach-api.js.map +1 -0
  55. package/src/lib/components/use-outreach-crm.d.ts +50 -0
  56. package/src/lib/components/use-outreach-crm.js +200 -0
  57. package/src/lib/components/use-outreach-crm.js.map +1 -0
  58. package/src/lib/components/use-outreach-data.d.ts +55 -0
  59. package/src/lib/components/use-outreach-data.js +199 -0
  60. package/src/lib/components/use-outreach-data.js.map +1 -0
  61. package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
  62. package/src/lib/components/use-outreach-mailbox-api.js +101 -0
  63. package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
  64. package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
  65. package/src/lib/components/use-outreach-mailboxes.js +68 -0
  66. package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
  67. package/src/lib/components/use-outreach-settings.d.ts +17 -0
  68. package/src/lib/components/use-outreach-settings.js +64 -0
  69. package/src/lib/components/use-outreach-settings.js.map +1 -0
  70. package/src/lib/constants/api-routes.d.ts +68 -0
  71. package/src/lib/constants/api-routes.js +57 -0
  72. package/src/lib/constants/api-routes.js.map +1 -0
  73. package/src/lib/constants/bundle-common.d.ts +46 -0
  74. package/src/lib/constants/bundle-common.js +55 -0
  75. package/src/lib/constants/bundle-common.js.map +1 -0
  76. package/src/lib/constants/runtime-jobs.d.ts +24 -0
  77. package/src/lib/constants/runtime-jobs.js +24 -0
  78. package/src/lib/constants/runtime-jobs.js.map +1 -0
  79. package/src/lib/declarations.console-server.d.ts +38 -0
  80. package/src/lib/declarations.console-server.js +99 -0
  81. package/src/lib/declarations.console-server.js.map +1 -0
  82. package/src/lib/engine/compose.d.ts +86 -0
  83. package/src/lib/engine/compose.js +212 -0
  84. package/src/lib/engine/compose.js.map +1 -0
  85. package/src/lib/engine/delivery-status.d.ts +53 -0
  86. package/src/lib/engine/delivery-status.js +184 -0
  87. package/src/lib/engine/delivery-status.js.map +1 -0
  88. package/src/lib/engine/do-not-contact.d.ts +18 -0
  89. package/src/lib/engine/do-not-contact.js +34 -0
  90. package/src/lib/engine/do-not-contact.js.map +1 -0
  91. package/src/lib/engine/enrollment-state.d.ts +156 -0
  92. package/src/lib/engine/enrollment-state.js +303 -0
  93. package/src/lib/engine/enrollment-state.js.map +1 -0
  94. package/src/lib/engine/gates.d.ts +125 -0
  95. package/src/lib/engine/gates.js +232 -0
  96. package/src/lib/engine/gates.js.map +1 -0
  97. package/src/lib/engine/index.d.ts +28 -0
  98. package/src/lib/engine/index.js +33 -0
  99. package/src/lib/engine/index.js.map +1 -0
  100. package/src/lib/engine/mailbox-health.d.ts +49 -0
  101. package/src/lib/engine/mailbox-health.js +83 -0
  102. package/src/lib/engine/mailbox-health.js.map +1 -0
  103. package/src/lib/engine/opt-out-intent.d.ts +41 -0
  104. package/src/lib/engine/opt-out-intent.js +220 -0
  105. package/src/lib/engine/opt-out-intent.js.map +1 -0
  106. package/src/lib/engine/recipient-country.d.ts +41 -0
  107. package/src/lib/engine/recipient-country.js +171 -0
  108. package/src/lib/engine/recipient-country.js.map +1 -0
  109. package/src/lib/engine/schedule.d.ts +70 -0
  110. package/src/lib/engine/schedule.js +115 -0
  111. package/src/lib/engine/schedule.js.map +1 -0
  112. package/src/lib/engine/sending-capacity.d.ts +96 -0
  113. package/src/lib/engine/sending-capacity.js +172 -0
  114. package/src/lib/engine/sending-capacity.js.map +1 -0
  115. package/src/lib/engine/sequence-validation.d.ts +90 -0
  116. package/src/lib/engine/sequence-validation.js +291 -0
  117. package/src/lib/engine/sequence-validation.js.map +1 -0
  118. package/src/lib/engine/thread-classification.d.ts +78 -0
  119. package/src/lib/engine/thread-classification.js +224 -0
  120. package/src/lib/engine/thread-classification.js.map +1 -0
  121. package/src/lib/engine/thread-message.d.ts +80 -0
  122. package/src/lib/engine/thread-message.js +143 -0
  123. package/src/lib/engine/thread-message.js.map +1 -0
  124. package/src/lib/enrollment/enroll-people.d.ts +81 -0
  125. package/src/lib/enrollment/enroll-people.js +250 -0
  126. package/src/lib/enrollment/enroll-people.js.map +1 -0
  127. package/src/lib/enrollment/gate-lookups.d.ts +32 -0
  128. package/src/lib/enrollment/gate-lookups.js +154 -0
  129. package/src/lib/enrollment/gate-lookups.js.map +1 -0
  130. package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
  131. package/src/lib/mailboxes/mailbox-api.js +66 -0
  132. package/src/lib/mailboxes/mailbox-api.js.map +1 -0
  133. package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
  134. package/src/lib/mailboxes/mailbox-credentials.js +102 -0
  135. package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
  136. package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
  137. package/src/lib/mailboxes/mailbox-erasure.js +148 -0
  138. package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
  139. package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
  140. package/src/lib/mailboxes/mailbox-gate.js +98 -0
  141. package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
  142. package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
  143. package/src/lib/mailboxes/mailbox-revoke.js +41 -0
  144. package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
  145. package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
  146. package/src/lib/mailboxes/mailbox-routes.js +605 -0
  147. package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
  148. package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
  149. package/src/lib/mailboxes/mailbox-settings.js +212 -0
  150. package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
  151. package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
  152. package/src/lib/mailboxes/mailbox-transport.js +94 -0
  153. package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
  154. package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
  155. package/src/lib/mailboxes/oauth-redirect.js +63 -0
  156. package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
  157. package/src/lib/mailboxes/oauth-state.d.ts +152 -0
  158. package/src/lib/mailboxes/oauth-state.js +212 -0
  159. package/src/lib/mailboxes/oauth-state.js.map +1 -0
  160. package/src/lib/mailboxes/outreach-config.d.ts +64 -0
  161. package/src/lib/mailboxes/outreach-config.js +77 -0
  162. package/src/lib/mailboxes/outreach-config.js.map +1 -0
  163. package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
  164. package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
  165. package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
  166. package/src/lib/model/compliance-settings.d.ts +78 -0
  167. package/src/lib/model/compliance-settings.js +155 -0
  168. package/src/lib/model/compliance-settings.js.map +1 -0
  169. package/src/lib/model/outreach-api.d.ts +210 -0
  170. package/src/lib/model/outreach-api.js +28 -0
  171. package/src/lib/model/outreach-api.js.map +1 -0
  172. package/src/lib/model/outreach.types.d.ts +563 -0
  173. package/src/lib/model/outreach.types.js +225 -0
  174. package/src/lib/model/outreach.types.js.map +1 -0
  175. package/src/lib/model/sequence-draft.d.ts +84 -0
  176. package/src/lib/model/sequence-draft.js +239 -0
  177. package/src/lib/model/sequence-draft.js.map +1 -0
  178. package/src/lib/model/step-preview.d.ts +45 -0
  179. package/src/lib/model/step-preview.js +98 -0
  180. package/src/lib/model/step-preview.js.map +1 -0
  181. package/src/lib/model/stored-records.d.ts +23 -0
  182. package/src/lib/model/stored-records.js +84 -0
  183. package/src/lib/model/stored-records.js.map +1 -0
  184. package/src/lib/plugin.d.ts +46 -0
  185. package/src/lib/plugin.js +86 -0
  186. package/src/lib/plugin.js.map +1 -0
  187. package/src/lib/routes/enroll-routes.d.ts +64 -0
  188. package/src/lib/routes/enroll-routes.js +350 -0
  189. package/src/lib/routes/enroll-routes.js.map +1 -0
  190. package/src/lib/routes/enrollment-routes.d.ts +19 -0
  191. package/src/lib/routes/enrollment-routes.js +171 -0
  192. package/src/lib/routes/enrollment-routes.js.map +1 -0
  193. package/src/lib/routes/platform-deps.d.ts +51 -0
  194. package/src/lib/routes/platform-deps.js +66 -0
  195. package/src/lib/routes/platform-deps.js.map +1 -0
  196. package/src/lib/routes/preview-routes.d.ts +33 -0
  197. package/src/lib/routes/preview-routes.js +117 -0
  198. package/src/lib/routes/preview-routes.js.map +1 -0
  199. package/src/lib/routes/register-routes.d.ts +22 -0
  200. package/src/lib/routes/register-routes.js +92 -0
  201. package/src/lib/routes/register-routes.js.map +1 -0
  202. package/src/lib/routes/route-deps.d.ts +49 -0
  203. package/src/lib/routes/route-deps.js +27 -0
  204. package/src/lib/routes/route-deps.js.map +1 -0
  205. package/src/lib/routes/route-gate.d.ts +94 -0
  206. package/src/lib/routes/route-gate.js +87 -0
  207. package/src/lib/routes/route-gate.js.map +1 -0
  208. package/src/lib/routes/route-http.d.ts +32 -0
  209. package/src/lib/routes/route-http.js +62 -0
  210. package/src/lib/routes/route-http.js.map +1 -0
  211. package/src/lib/routes/sequence-routes.d.ts +32 -0
  212. package/src/lib/routes/sequence-routes.js +359 -0
  213. package/src/lib/routes/sequence-routes.js.map +1 -0
  214. package/src/lib/routes/settings-routes.d.ts +30 -0
  215. package/src/lib/routes/settings-routes.js +73 -0
  216. package/src/lib/routes/settings-routes.js.map +1 -0
  217. package/src/lib/runtime/enrollment-events.d.ts +86 -0
  218. package/src/lib/runtime/enrollment-events.js +141 -0
  219. package/src/lib/runtime/enrollment-events.js.map +1 -0
  220. package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
  221. package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
  222. package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
  223. package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
  224. package/src/lib/runtime/mailbox-health-store.js +168 -0
  225. package/src/lib/runtime/mailbox-health-store.js.map +1 -0
  226. package/src/lib/runtime/person-erasure.d.ts +39 -0
  227. package/src/lib/runtime/person-erasure.js +57 -0
  228. package/src/lib/runtime/person-erasure.js.map +1 -0
  229. package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
  230. package/src/lib/runtime/platform-runtime-deps.js +98 -0
  231. package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
  232. package/src/lib/runtime/runtime-deps.d.ts +77 -0
  233. package/src/lib/runtime/runtime-deps.js +18 -0
  234. package/src/lib/runtime/runtime-deps.js.map +1 -0
  235. package/src/lib/runtime/send-job.d.ts +42 -0
  236. package/src/lib/runtime/send-job.js +780 -0
  237. package/src/lib/runtime/send-job.js.map +1 -0
  238. package/src/lib/runtime/sync-job.d.ts +41 -0
  239. package/src/lib/runtime/sync-job.js +480 -0
  240. package/src/lib/runtime/sync-job.js.map +1 -0
  241. package/src/lib/runtime/timeline.d.ts +67 -0
  242. package/src/lib/runtime/timeline.js +93 -0
  243. package/src/lib/runtime/timeline.js.map +1 -0
  244. package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
  245. package/src/lib/runtime/unsubscribe-link.js +121 -0
  246. package/src/lib/runtime/unsubscribe-link.js.map +1 -0
  247. package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
  248. package/src/lib/runtime/unsubscribe-route.js +103 -0
  249. package/src/lib/runtime/unsubscribe-route.js.map +1 -0
  250. package/src/lib/server.d.ts +53 -0
  251. package/src/lib/server.js +94 -0
  252. package/src/lib/server.js.map +1 -0
  253. package/src/lib/storage/compliance-settings-store.d.ts +44 -0
  254. package/src/lib/storage/compliance-settings-store.js +78 -0
  255. package/src/lib/storage/compliance-settings-store.js.map +1 -0
  256. package/src/lib/storage/do-not-contact-store.d.ts +60 -0
  257. package/src/lib/storage/do-not-contact-store.js +168 -0
  258. package/src/lib/storage/do-not-contact-store.js.map +1 -0
  259. package/src/lib/storage/outreach-records.d.ts +33 -0
  260. package/src/lib/storage/outreach-records.js +35 -0
  261. package/src/lib/storage/outreach-records.js.map +1 -0
  262. package/src/lib/subprocessors.d.ts +48 -0
  263. package/src/lib/subprocessors.js +70 -0
  264. package/src/lib/subprocessors.js.map +1 -0
  265. package/src/lib/transport/gmail-client.d.ts +145 -0
  266. package/src/lib/transport/gmail-client.js +229 -0
  267. package/src/lib/transport/gmail-client.js.map +1 -0
  268. package/src/lib/transport/gmail-errors.d.ts +84 -0
  269. package/src/lib/transport/gmail-errors.js +138 -0
  270. package/src/lib/transport/gmail-errors.js.map +1 -0
  271. package/src/lib/transport/google-oauth.d.ts +138 -0
  272. package/src/lib/transport/google-oauth.js +247 -0
  273. package/src/lib/transport/google-oauth.js.map +1 -0
  274. package/src/lib/transport/http.d.ts +53 -0
  275. package/src/lib/transport/http.js +79 -0
  276. package/src/lib/transport/http.js.map +1 -0
  277. package/src/lib/transport/rfc5322.d.ts +116 -0
  278. package/src/lib/transport/rfc5322.js +330 -0
  279. package/src/lib/transport/rfc5322.js.map +1 -0
  280. package/src/lib/transport/send-message.d.ts +69 -0
  281. package/src/lib/transport/send-message.js +77 -0
  282. 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