@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 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.\n *\n * Every write to an `outreach*` collection is made here: the Firestore rules\n * refuse all of them to clients, so a mailbox, a sequence, an enrollment and a\n * credential only ever change through a handler registered below, behind the\n * session, entitlement and permission checks that handler makes.\n *\n * `outreach/ping` exists so the wiring is proven rather than assumed, as the\n * CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the\n * `outreach` prefix, the generated server manifest loads this module, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler — after its\n * own gates, which already refuse a request while `release_outreach` is off\n * for the caller. A first real route that also had to be the first wiring\n * test would be two things to debug at once.\n *\n * A route that reads or writes org data resolves the caller the way the CRM's\n * org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from\n * `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`\n * and `outreach.use` before it touches a document.\n */\n\nimport {\n registerPluginApiRoute,\n registerPluginPermissions,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { OUTREACH_API_ROUTES } from './constants/api-routes'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n} from './constants/bundle-common'\nimport { registerOutreachMailboxRoutes } from './mailboxes/register-mailbox-routes'\nimport { registerOutreachRoutes } from './routes/register-routes'\n\n/**\n * No auth, no org, no data: it answers whether this server bundle was loaded\n * and its routes registered, which is a fact about the process rather than\n * about any workspace.\n */\nexport const outreachPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: OUTREACH_PLUGIN_ID })\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerOutreachConsoleApi(): void {\n // The server resolves plugin permission keys from this registry, so a\n // route's `outreach.use` check and the console's gate read the same tier\n // defaults.\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n registerPluginApiRoute(OUTREACH_API_ROUTES.ping, outreachPingHandler)\n // Connect a Google mailbox, its settings, pause, test and disconnect\n // (AGL-2978).\n registerOutreachMailboxRoutes()\n // Compliance settings, sequences, enrollment and enrollment actions\n // (AGL-2980).\n registerOutreachRoutes()\n // The one-click unsubscribe every email carries (AGL-2981): a recipient\n // link, so it answers whether or not Outreach is released to the\n // organization now. Its module loads on the first request.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.unsubscribe,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachUnsubscribeRoute()(request, context),\n },\n { recipientLink: true },\n )\n}\n\n// The transport the sending runtime reaches a connected mailbox through\n// (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,\n// send the engine's composed email through the one RFC 5322 writer, and read\n// a thread whole for the engine's reply and bounce classifier.\nexport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n type OpenedOutreachMailbox,\n} from './mailboxes/mailbox-transport'\nexport {\n GmailTransportError,\n isReconnectRequired,\n type GmailTransportErrorCode,\n} from './transport/gmail-errors'\nexport type {\n GmailClient,\n GmailFullThread,\n GmailMessageMetadata,\n GmailThread,\n} from './transport/gmail-client'\nexport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n LIST_UNSUBSCRIBE_ONE_CLICK,\n Rfc5322MessageError,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './transport/rfc5322'\nexport {\n sendComposedOutreachEmail,\n sendOutreachMessage,\n type OutreachSentMessage,\n type SendOutreachMessageOptions,\n} from './transport/send-message'\n\n// The organization's compliance settings and do-not-contact list\n// (AGL-2980), for the sending runtime: the footer is read before every send,\n// the list is checked before every send, and a reply that opts out, an\n// unsubscribe and a hard bounce each put the address on it.\nexport {\n outreachComplianceSettingsRef,\n readOutreachComplianceSettingsDoc,\n writeOutreachComplianceSettings,\n type WriteOutreachComplianceSettingsInput,\n type WriteOutreachComplianceSettingsResult,\n} from './storage/compliance-settings-store'\nexport {\n addOutreachDoNotContact,\n getOutreachDoNotContactEntry,\n isOutreachDoNotContact,\n lookupOutreachDoNotContact,\n outreachDoNotContactCollection,\n readOutreachDoNotContactEntry,\n type AddOutreachDoNotContactInput,\n type AddOutreachDoNotContactResult,\n} from './storage/do-not-contact-store'\n"],"names":["registerPluginApiRoute","registerPluginPermissions","OUTREACH_API_ROUTES","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","registerOutreachMailboxRoutes","registerOutreachRoutes","outreachPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","registerOutreachConsoleApi","ping","unsubscribe","web","request","context","platformOutreachUnsubscribeRoute","recipientLink","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","GmailTransportError","isReconnectRequired","buildRfc5322Message","encodeGmailRawMessage","LIST_UNSUBSCRIBE_ONE_CLICK","Rfc5322MessageError","sendComposedOutreachEmail","sendOutreachMessage","outreachComplianceSettingsRef","readOutreachComplianceSettingsDoc","writeOutreachComplianceSettings","addOutreachDoNotContact","getOutreachDoNotContactEntry","isOutreachDoNotContact","lookupOutreachDoNotContact","outreachDoNotContactCollection","readOutreachDoNotContactEntry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,SACEA,sBAAsB,EACtBC,yBAAyB,QAEpB,sBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SACEC,oBAAoB,EACpBC,kBAAkB,QACb,+BAA2B;AAClC,SAASC,6BAA6B,QAAQ,yCAAqC;AACnF,SAASC,sBAAsB,QAAQ,8BAA0B;AAEjE;;;;CAIC,GACD,OAAO,MAAMC,sBAAwC,CAACC,KAAKC;IACzD,IAAID,IAAIE,MAAM,KAAK,OAAO;QACxBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACnD;IACF;IACAL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEE,IAAI;QAAMC,QAAQZ;IAAmB;AAC9D,EAAC;AAED,8EAA8E,GAC9E,OAAO,SAASa;IACd,sEAAsE;IACtE,yEAAyE;IACzE,YAAY;IACZhB,0BAA0BE;IAC1BH,uBAAuBE,oBAAoBgB,IAAI,EAAEX;IACjD,qEAAqE;IACrE,cAAc;IACdF;IACA,oEAAoE;IACpE,cAAc;IACdC;IACA,wEAAwE;IACxE,iEAAiE;IACjE,2DAA2D;IAC3DN,uBACEE,oBAAoBiB,WAAW,EAC/B;QACEC,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGC,gCAAgC,GAAGF,SAASC;IAClG,GACA;QAAEE,eAAe;IAAK;AAE1B;AAEA,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,+DAA+D;AAC/D,SACEC,oCAAoC,EACpCC,yBAAyB,QAEpB,mCAA+B;AACtC,SACEC,mBAAmB,EACnBC,mBAAmB,QAEd,8BAA0B;AAOjC,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,0BAA0B,EAC1BC,mBAAmB,QAGd,yBAAqB;AAC5B,SACEC,yBAAyB,EACzBC,mBAAmB,QAGd,8BAA0B;AAEjC,iEAAiE;AACjE,6EAA6E;AAC7E,uEAAuE;AACvE,4DAA4D;AAC5D,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,+BAA+B,QAG1B,yCAAqC;AAC5C,SACEC,uBAAuB,EACvBC,4BAA4B,EAC5BC,sBAAsB,EACtBC,0BAA0B,EAC1BC,8BAA8B,EAC9BC,6BAA6B,QAGxB,oCAAgC"}
@@ -0,0 +1,44 @@
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 OutreachComplianceSettings, type OutreachComplianceSettingsDocument } from '../model/outreach.types';
18
+ /** `orgs/{orgId}/outreachSettings/compliance`. */
19
+ export declare function outreachComplianceSettingsRef(firestore: FirebaseFirestore.Firestore, orgId: string): FirebaseFirestore.DocumentReference;
20
+ /**
21
+ * The organization's compliance settings, defaults applied — an
22
+ * organization that never saved them reads as no legal name, no address,
23
+ * and the United States alone.
24
+ */
25
+ export declare function readOutreachComplianceSettingsDoc(firestore: FirebaseFirestore.Firestore, orgId: string): Promise<OutreachComplianceSettingsDocument>;
26
+ export interface WriteOutreachComplianceSettingsInput {
27
+ orgId: string;
28
+ /** Already validated: `validateOutreachComplianceSettings(...).settings`. */
29
+ settings: OutreachComplianceSettings;
30
+ byUid: string;
31
+ nowMs: number;
32
+ }
33
+ export interface WriteOutreachComplianceSettingsResult {
34
+ settings: OutreachComplianceSettingsDocument;
35
+ /** False when the save said what was already stored, and nothing was written. */
36
+ changed: boolean;
37
+ }
38
+ /**
39
+ * Stores the settings when they differ from what is stored, in a
40
+ * transaction, so two members saving at once each compare against the
41
+ * other's write rather than both believing they changed nothing — and the
42
+ * activity line a change earns is written once per actual change.
43
+ */
44
+ export declare function writeOutreachComplianceSettings(firestore: FirebaseFirestore.Firestore, input: WriteOutreachComplianceSettingsInput): Promise<WriteOutreachComplianceSettingsResult>;
@@ -0,0 +1,78 @@
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
+ * Where the organization's compliance settings are kept (AGL-2980):
18
+ * `orgs/{orgId}/outreachSettings/compliance`, one document.
19
+ *
20
+ * SERVER ONLY. The rules let an Outreach member read the document and refuse
21
+ * every client write, so the settings route is the one writer and this is
22
+ * the one place that knows the path. The sending runtime reads it through
23
+ * {@link readOutreachComplianceSettingsDoc} before every send, because the
24
+ * footer is written from it and must say what the organization says now.
25
+ *
26
+ * Firestore is a parameter throughout, as in the CRM's server modules: the
27
+ * callers hold one, and a module that reached for the Admin app itself
28
+ * would drag it into every spec that only wants the arithmetic.
29
+ */ import { outreachComplianceSettingsEqual, readOutreachComplianceSettings } from "../model/compliance-settings.js";
30
+ import { OUTREACH_COLLECTIONS, OUTREACH_COMPLIANCE_SETTINGS_ID } from "../model/outreach.types.js";
31
+ /** `orgs/{orgId}/outreachSettings/compliance`. */ export function outreachComplianceSettingsRef(firestore, orgId) {
32
+ return firestore.collection('orgs').doc(orgId).collection(OUTREACH_COLLECTIONS.settings).doc(OUTREACH_COMPLIANCE_SETTINGS_ID);
33
+ }
34
+ /**
35
+ * The organization's compliance settings, defaults applied — an
36
+ * organization that never saved them reads as no legal name, no address,
37
+ * and the United States alone.
38
+ */ export async function readOutreachComplianceSettingsDoc(firestore, orgId) {
39
+ const snapshot = await outreachComplianceSettingsRef(firestore, orgId).get();
40
+ return readOutreachComplianceSettings(snapshot.exists ? snapshot.data() : null);
41
+ }
42
+ /**
43
+ * Stores the settings when they differ from what is stored, in a
44
+ * transaction, so two members saving at once each compare against the
45
+ * other's write rather than both believing they changed nothing — and the
46
+ * activity line a change earns is written once per actual change.
47
+ */ export async function writeOutreachComplianceSettings(firestore, input) {
48
+ const ref = outreachComplianceSettingsRef(firestore, input.orgId);
49
+ return firestore.runTransaction(async (transaction)=>{
50
+ const snapshot = await transaction.get(ref);
51
+ const current = readOutreachComplianceSettings(snapshot.exists ? snapshot.data() : null);
52
+ // A document that was never saved is a change even when the save
53
+ // restates the defaults: the organization has now said them.
54
+ if (snapshot.exists && outreachComplianceSettingsEqual(current, input.settings)) {
55
+ return {
56
+ settings: current,
57
+ changed: false
58
+ };
59
+ }
60
+ const next = {
61
+ legalName: input.settings.legalName,
62
+ brandName: input.settings.brandName,
63
+ postalAddress: input.settings.postalAddress,
64
+ allowedCountries: [
65
+ ...input.settings.allowedCountries
66
+ ],
67
+ updatedAtMs: input.nowMs,
68
+ updatedByUid: input.byUid
69
+ };
70
+ transaction.set(ref, next);
71
+ return {
72
+ settings: next,
73
+ changed: true
74
+ };
75
+ });
76
+ }
77
+
78
+ //# sourceMappingURL=compliance-settings-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/compliance-settings-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Where the organization's compliance settings are kept (AGL-2980):\n * `orgs/{orgId}/outreachSettings/compliance`, one document.\n *\n * SERVER ONLY. The rules let an Outreach member read the document and refuse\n * every client write, so the settings route is the one writer and this is\n * the one place that knows the path. The sending runtime reads it through\n * {@link readOutreachComplianceSettingsDoc} before every send, because the\n * footer is written from it and must say what the organization says now.\n *\n * Firestore is a parameter throughout, as in the CRM's server modules: the\n * callers hold one, and a module that reached for the Admin app itself\n * would drag it into every spec that only wants the arithmetic.\n */\n\nimport {\n outreachComplianceSettingsEqual,\n readOutreachComplianceSettings,\n} from '../model/compliance-settings'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_COMPLIANCE_SETTINGS_ID,\n type OutreachComplianceSettings,\n type OutreachComplianceSettingsDocument,\n} from '../model/outreach.types'\n\n/** `orgs/{orgId}/outreachSettings/compliance`. */\nexport function outreachComplianceSettingsRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.settings)\n .doc(OUTREACH_COMPLIANCE_SETTINGS_ID)\n}\n\n/**\n * The organization's compliance settings, defaults applied — an\n * organization that never saved them reads as no legal name, no address,\n * and the United States alone.\n */\nexport async function readOutreachComplianceSettingsDoc(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<OutreachComplianceSettingsDocument> {\n const snapshot = await outreachComplianceSettingsRef(firestore, orgId).get()\n return readOutreachComplianceSettings(snapshot.exists ? snapshot.data() : null)\n}\n\nexport interface WriteOutreachComplianceSettingsInput {\n orgId: string\n /** Already validated: `validateOutreachComplianceSettings(...).settings`. */\n settings: OutreachComplianceSettings\n byUid: string\n nowMs: number\n}\n\nexport interface WriteOutreachComplianceSettingsResult {\n settings: OutreachComplianceSettingsDocument\n /** False when the save said what was already stored, and nothing was written. */\n changed: boolean\n}\n\n/**\n * Stores the settings when they differ from what is stored, in a\n * transaction, so two members saving at once each compare against the\n * other's write rather than both believing they changed nothing — and the\n * activity line a change earns is written once per actual change.\n */\nexport async function writeOutreachComplianceSettings(\n firestore: FirebaseFirestore.Firestore,\n input: WriteOutreachComplianceSettingsInput,\n): Promise<WriteOutreachComplianceSettingsResult> {\n const ref = outreachComplianceSettingsRef(firestore, input.orgId)\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readOutreachComplianceSettings(snapshot.exists ? snapshot.data() : null)\n // A document that was never saved is a change even when the save\n // restates the defaults: the organization has now said them.\n if (snapshot.exists && outreachComplianceSettingsEqual(current, input.settings)) {\n return { settings: current, changed: false }\n }\n const next: OutreachComplianceSettingsDocument = {\n legalName: input.settings.legalName,\n brandName: input.settings.brandName,\n postalAddress: input.settings.postalAddress,\n allowedCountries: [...input.settings.allowedCountries],\n updatedAtMs: input.nowMs,\n updatedByUid: input.byUid,\n }\n transaction.set(ref, next)\n return { settings: next, changed: true }\n })\n}\n"],"names":["outreachComplianceSettingsEqual","readOutreachComplianceSettings","OUTREACH_COLLECTIONS","OUTREACH_COMPLIANCE_SETTINGS_ID","outreachComplianceSettingsRef","firestore","orgId","collection","doc","settings","readOutreachComplianceSettingsDoc","snapshot","get","exists","data","writeOutreachComplianceSettings","input","ref","runTransaction","transaction","current","changed","next","legalName","brandName","postalAddress","allowedCountries","updatedAtMs","nowMs","updatedByUid","byUid","set"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC,GAED,SACEA,+BAA+B,EAC/BC,8BAA8B,QACzB,kCAA8B;AACrC,SACEC,oBAAoB,EACpBC,+BAA+B,QAG1B,6BAAyB;AAEhC,gDAAgD,GAChD,OAAO,SAASC,8BACdC,SAAsC,EACtCC,KAAa;IAEb,OAAOD,UACJE,UAAU,CAAC,QACXC,GAAG,CAACF,OACJC,UAAU,CAACL,qBAAqBO,QAAQ,EACxCD,GAAG,CAACL;AACT;AAEA;;;;CAIC,GACD,OAAO,eAAeO,kCACpBL,SAAsC,EACtCC,KAAa;IAEb,MAAMK,WAAW,MAAMP,8BAA8BC,WAAWC,OAAOM,GAAG;IAC1E,OAAOX,+BAA+BU,SAASE,MAAM,GAAGF,SAASG,IAAI,KAAK;AAC5E;AAgBA;;;;;CAKC,GACD,OAAO,eAAeC,gCACpBV,SAAsC,EACtCW,KAA2C;IAE3C,MAAMC,MAAMb,8BAA8BC,WAAWW,MAAMV,KAAK;IAChE,OAAOD,UAAUa,cAAc,CAAC,OAAOC;QACrC,MAAMR,WAAW,MAAMQ,YAAYP,GAAG,CAACK;QACvC,MAAMG,UAAUnB,+BAA+BU,SAASE,MAAM,GAAGF,SAASG,IAAI,KAAK;QACnF,iEAAiE;QACjE,6DAA6D;QAC7D,IAAIH,SAASE,MAAM,IAAIb,gCAAgCoB,SAASJ,MAAMP,QAAQ,GAAG;YAC/E,OAAO;gBAAEA,UAAUW;gBAASC,SAAS;YAAM;QAC7C;QACA,MAAMC,OAA2C;YAC/CC,WAAWP,MAAMP,QAAQ,CAACc,SAAS;YACnCC,WAAWR,MAAMP,QAAQ,CAACe,SAAS;YACnCC,eAAeT,MAAMP,QAAQ,CAACgB,aAAa;YAC3CC,kBAAkB;mBAAIV,MAAMP,QAAQ,CAACiB,gBAAgB;aAAC;YACtDC,aAAaX,MAAMY,KAAK;YACxBC,cAAcb,MAAMc,KAAK;QAC3B;QACAX,YAAYY,GAAG,CAACd,KAAKK;QACrB,OAAO;YAAEb,UAAUa;YAAMD,SAAS;QAAK;IACzC;AACF"}
@@ -0,0 +1,60 @@
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 OutreachDoNotContactEntry, type OutreachDoNotContactReason, type OutreachDoNotContactSource } from '../model/outreach.types';
18
+ /** The longest detail an entry keeps. */
19
+ export declare const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500;
20
+ /** `orgs/{orgId}/outreachDoNotContact`. */
21
+ export declare function outreachDoNotContactCollection(firestore: FirebaseFirestore.Firestore, orgId: string): FirebaseFirestore.CollectionReference;
22
+ /** A stored entry held to its shape, or `null` for a document that is not one. */
23
+ export declare function readOutreachDoNotContactEntry(key: string, data: Record<string, unknown> | undefined): OutreachDoNotContactEntry | null;
24
+ /**
25
+ * Whether each address is on the list, in one round trip: `true` or `false`
26
+ * per address, or `null` for an address that cannot be keyed and for every
27
+ * address when the read failed. Keyed by the address exactly as given.
28
+ */
29
+ export declare function lookupOutreachDoNotContact(firestore: FirebaseFirestore.Firestore, orgId: string, emails: readonly string[]): Promise<Map<string, boolean | null>>;
30
+ /** Whether one address is on the list; `null` when that could not be told. */
31
+ export declare function isOutreachDoNotContact(firestore: FirebaseFirestore.Firestore, orgId: string, email: string): Promise<boolean | null>;
32
+ /** The entry an address has, or `null` when it has none. */
33
+ export declare function getOutreachDoNotContactEntry(firestore: FirebaseFirestore.Firestore, orgId: string, email: string): Promise<OutreachDoNotContactEntry | null>;
34
+ export interface AddOutreachDoNotContactInput {
35
+ orgId: string;
36
+ email: string;
37
+ reason: OutreachDoNotContactReason;
38
+ source: OutreachDoNotContactSource;
39
+ /** The member adding it; omit or `null` for the runtime. */
40
+ addedByUid?: string | null;
41
+ nowMs: number;
42
+ enrollmentId?: string | null;
43
+ sequenceId?: string | null;
44
+ detail?: string | null;
45
+ }
46
+ export interface AddOutreachDoNotContactResult {
47
+ key: string;
48
+ /** False when the address was on the list already; its entry is unchanged. */
49
+ created: boolean;
50
+ /** The entry as it stands: the new one, or the one that was already there. */
51
+ entry: OutreachDoNotContactEntry;
52
+ }
53
+ /**
54
+ * Puts an address on the list — see the module note for why a second add
55
+ * changes nothing. Throws for a value that is not an address, a reason or a
56
+ * source the list does not know, and a member-sourced add with no member:
57
+ * a list that could hold an entry nobody can explain is not evidence of
58
+ * anything.
59
+ */
60
+ export declare function addOutreachDoNotContact(firestore: FirebaseFirestore.Firestore, input: AddOutreachDoNotContactInput): Promise<AddOutreachDoNotContactResult>;
@@ -0,0 +1,168 @@
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
+ * The organization's Outreach do-not-contact list (AGL-2980):
18
+ * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address.
19
+ *
20
+ * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and
21
+ * the API is shaped for both:
22
+ *
23
+ * - The console's routes: the enrollment gates read the list for every
24
+ * person being enrolled at once ({@link lookupOutreachDoNotContact}, one
25
+ * `getAll`), and "Mark do-not-contact" on an enrollment adds an entry with
26
+ * the member as its author.
27
+ * - The sending runtime: it re-reads the list for each person before each
28
+ * send ({@link isOutreachDoNotContact}), and adds an entry when a reply
29
+ * opts out, when the unsubscribe link is used and when an address bounces
30
+ * for good, with `source: 'runtime'` and no member.
31
+ *
32
+ * ## Reads answer `null` when they could not be made
33
+ *
34
+ * `null` is not `false`. The gates read a `null` lookup as "we couldn't
35
+ * check" and refuse the person, which is the only safe reading of a list
36
+ * whose whole job is to stop mail: a person who asked not to be emailed must
37
+ * not be emailed because a read timed out.
38
+ *
39
+ * ## An entry is written once
40
+ *
41
+ * The first reason an address was added is the record of why it is there;
42
+ * a later add for the same address — an unsubscribe after a member already
43
+ * marked it — finds it on the list and changes nothing. `create()` makes
44
+ * that atomic without a transaction: two adds racing for one address cannot
45
+ * both write.
46
+ */ import { outreachDoNotContactKey } from "../engine/do-not-contact.js";
47
+ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_CONTACT_SOURCES } from "../model/outreach.types.js";
48
+ /** The longest detail an entry keeps. */ export const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500;
49
+ /** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
50
+ /** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */ const ALREADY_EXISTS = 6;
51
+ /** `orgs/{orgId}/outreachDoNotContact`. */ export function outreachDoNotContactCollection(firestore, orgId) {
52
+ return firestore.collection('orgs').doc(orgId).collection(OUTREACH_COLLECTIONS.doNotContact);
53
+ }
54
+ /** A stored entry held to its shape, or `null` for a document that is not one. */ export function readOutreachDoNotContactEntry(key, data) {
55
+ if (!data) return null;
56
+ const reason = data['reason'];
57
+ const source = data['source'];
58
+ const text = (value)=>typeof value === 'string' && value ? value : null;
59
+ const addedAtMs = Number(data['addedAtMs']);
60
+ return {
61
+ key,
62
+ // An entry some other writer produced still keeps its address off the
63
+ // list's sends; only how it reads is defaulted.
64
+ reason: OUTREACH_DO_NOT_CONTACT_REASONS.includes(reason) ? reason : 'manual',
65
+ source: OUTREACH_DO_NOT_CONTACT_SOURCES.includes(source) ? source : 'member',
66
+ addedByUid: text(data['addedByUid']),
67
+ addedAtMs: Number.isFinite(addedAtMs) && addedAtMs > 0 ? addedAtMs : 0,
68
+ enrollmentId: text(data['enrollmentId']),
69
+ sequenceId: text(data['sequenceId']),
70
+ detail: text(data['detail'])
71
+ };
72
+ }
73
+ /**
74
+ * Whether each address is on the list, in one round trip: `true` or `false`
75
+ * per address, or `null` for an address that cannot be keyed and for every
76
+ * address when the read failed. Keyed by the address exactly as given.
77
+ */ export async function lookupOutreachDoNotContact(firestore, orgId, emails) {
78
+ const answers = new Map();
79
+ const keyed = [];
80
+ for (const email of emails){
81
+ const key = outreachDoNotContactKey(email);
82
+ if (key) keyed.push({
83
+ email,
84
+ key
85
+ });
86
+ else answers.set(email, null);
87
+ }
88
+ if (!keyed.length) return answers;
89
+ const list = outreachDoNotContactCollection(firestore, orgId);
90
+ try {
91
+ for(let start = 0; start < keyed.length; start += GET_ALL_CHUNK){
92
+ const chunk = keyed.slice(start, start + GET_ALL_CHUNK);
93
+ const snapshots = await firestore.getAll(...chunk.map((entry)=>list.doc(entry.key)));
94
+ chunk.forEach((entry, index)=>{
95
+ var _snapshots_index;
96
+ answers.set(entry.email, ((_snapshots_index = snapshots[index]) == null ? void 0 : _snapshots_index.exists) === true);
97
+ });
98
+ }
99
+ } catch (error) {
100
+ console.error('[outreach] do-not-contact lookup failed; reading as unchecked', error);
101
+ for (const entry of keyed)answers.set(entry.email, null);
102
+ }
103
+ return answers;
104
+ }
105
+ /** Whether one address is on the list; `null` when that could not be told. */ export async function isOutreachDoNotContact(firestore, orgId, email) {
106
+ var _get;
107
+ return (_get = (await lookupOutreachDoNotContact(firestore, orgId, [
108
+ email
109
+ ])).get(email)) != null ? _get : null;
110
+ }
111
+ /** The entry an address has, or `null` when it has none. */ export async function getOutreachDoNotContactEntry(firestore, orgId, email) {
112
+ const key = outreachDoNotContactKey(email);
113
+ if (!key) return null;
114
+ const snapshot = await outreachDoNotContactCollection(firestore, orgId).doc(key).get();
115
+ return snapshot.exists ? readOutreachDoNotContactEntry(key, snapshot.data()) : null;
116
+ }
117
+ /**
118
+ * Puts an address on the list — see the module note for why a second add
119
+ * changes nothing. Throws for a value that is not an address, a reason or a
120
+ * source the list does not know, and a member-sourced add with no member:
121
+ * a list that could hold an entry nobody can explain is not evidence of
122
+ * anything.
123
+ */ export async function addOutreachDoNotContact(firestore, input) {
124
+ var _input_detail;
125
+ const key = outreachDoNotContactKey(input.email);
126
+ if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value');
127
+ if (!OUTREACH_DO_NOT_CONTACT_REASONS.includes(input.reason)) {
128
+ throw new Error(`[outreach] "${String(input.reason)}" is not a do-not-contact reason`);
129
+ }
130
+ if (!OUTREACH_DO_NOT_CONTACT_SOURCES.includes(input.source)) {
131
+ throw new Error(`[outreach] "${String(input.source)}" is not a do-not-contact source`);
132
+ }
133
+ const addedByUid = typeof input.addedByUid === 'string' && input.addedByUid ? input.addedByUid : null;
134
+ if (input.source === 'member' && !addedByUid) {
135
+ throw new Error('[outreach] a member-added do-not-contact entry names the member');
136
+ }
137
+ const detail = String((_input_detail = input.detail) != null ? _input_detail : '').replace(/\s+/g, ' ').trim();
138
+ const entry = {
139
+ key,
140
+ reason: input.reason,
141
+ source: input.source,
142
+ addedByUid,
143
+ addedAtMs: input.nowMs,
144
+ enrollmentId: input.enrollmentId || null,
145
+ sequenceId: input.sequenceId || null,
146
+ detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null
147
+ };
148
+ const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key);
149
+ try {
150
+ await ref.create(entry);
151
+ return {
152
+ key,
153
+ created: true,
154
+ entry
155
+ };
156
+ } catch (error) {
157
+ var _readOutreachDoNotContactEntry;
158
+ if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
159
+ const existing = await ref.get();
160
+ return {
161
+ key,
162
+ created: false,
163
+ entry: (_readOutreachDoNotContactEntry = readOutreachDoNotContactEntry(key, existing.data())) != null ? _readOutreachDoNotContactEntry : entry
164
+ };
165
+ }
166
+ }
167
+
168
+ //# sourceMappingURL=do-not-contact-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/do-not-contact-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The organization's Outreach do-not-contact list (AGL-2980):\n * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address.\n *\n * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and\n * the API is shaped for both:\n *\n * - The console's routes: the enrollment gates read the list for every\n * person being enrolled at once ({@link lookupOutreachDoNotContact}, one\n * `getAll`), and \"Mark do-not-contact\" on an enrollment adds an entry with\n * the member as its author.\n * - The sending runtime: it re-reads the list for each person before each\n * send ({@link isOutreachDoNotContact}), and adds an entry when a reply\n * opts out, when the unsubscribe link is used and when an address bounces\n * for good, with `source: 'runtime'` and no member.\n *\n * ## Reads answer `null` when they could not be made\n *\n * `null` is not `false`. The gates read a `null` lookup as \"we couldn't\n * check\" and refuse the person, which is the only safe reading of a list\n * whose whole job is to stop mail: a person who asked not to be emailed must\n * not be emailed because a read timed out.\n *\n * ## An entry is written once\n *\n * The first reason an address was added is the record of why it is there;\n * a later add for the same address — an unsubscribe after a member already\n * marked it — finds it on the list and changes nothing. `create()` makes\n * that atomic without a transaction: two adds racing for one address cannot\n * both write.\n */\n\nimport { outreachDoNotContactKey } from '../engine/do-not-contact'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n OUTREACH_DO_NOT_CONTACT_SOURCES,\n type OutreachDoNotContactEntry,\n type OutreachDoNotContactReason,\n type OutreachDoNotContactSource,\n} from '../model/outreach.types'\n\n/** The longest detail an entry keeps. */\nexport const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** `orgs/{orgId}/outreachDoNotContact`. */\nexport function outreachDoNotContactCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContact)\n}\n\n/** A stored entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactEntry(\n key: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactEntry | null {\n if (!data) return null\n const reason = data['reason']\n const source = data['source']\n const text = (value: unknown): string | null =>\n typeof value === 'string' && value ? value : null\n const addedAtMs = Number(data['addedAtMs'])\n return {\n key,\n // An entry some other writer produced still keeps its address off the\n // list's sends; only how it reads is defaulted.\n reason: (OUTREACH_DO_NOT_CONTACT_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachDoNotContactReason)\n : 'manual',\n source: (OUTREACH_DO_NOT_CONTACT_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachDoNotContactSource)\n : 'member',\n addedByUid: text(data['addedByUid']),\n addedAtMs: Number.isFinite(addedAtMs) && addedAtMs > 0 ? addedAtMs : 0,\n enrollmentId: text(data['enrollmentId']),\n sequenceId: text(data['sequenceId']),\n detail: text(data['detail']),\n }\n}\n\n/**\n * Whether each address is on the list, in one round trip: `true` or `false`\n * per address, or `null` for an address that cannot be keyed and for every\n * address when the read failed. Keyed by the address exactly as given.\n */\nexport async function lookupOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const keyed: Array<{ email: string; key: string }> = []\n for (const email of emails) {\n const key = outreachDoNotContactKey(email)\n if (key) keyed.push({ email, key })\n else answers.set(email, null)\n }\n if (!keyed.length) return answers\n const list = outreachDoNotContactCollection(firestore, orgId)\n try {\n for (let start = 0; start < keyed.length; start += GET_ALL_CHUNK) {\n const chunk = keyed.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((entry) => list.doc(entry.key)))\n chunk.forEach((entry, index) => {\n answers.set(entry.email, snapshots[index]?.exists === true)\n })\n }\n } catch (error) {\n console.error('[outreach] do-not-contact lookup failed; reading as unchecked', error)\n for (const entry of keyed) answers.set(entry.email, null)\n }\n return answers\n}\n\n/** Whether one address is on the list; `null` when that could not be told. */\nexport async function isOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<boolean | null> {\n return (await lookupOutreachDoNotContact(firestore, orgId, [email])).get(email) ?? null\n}\n\n/** The entry an address has, or `null` when it has none. */\nexport async function getOutreachDoNotContactEntry(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<OutreachDoNotContactEntry | null> {\n const key = outreachDoNotContactKey(email)\n if (!key) return null\n const snapshot = await outreachDoNotContactCollection(firestore, orgId).doc(key).get()\n return snapshot.exists ? readOutreachDoNotContactEntry(key, snapshot.data()) : null\n}\n\nexport interface AddOutreachDoNotContactInput {\n orgId: string\n email: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member adding it; omit or `null` for the runtime. */\n addedByUid?: string | null\n nowMs: number\n enrollmentId?: string | null\n sequenceId?: string | null\n detail?: string | null\n}\n\nexport interface AddOutreachDoNotContactResult {\n key: string\n /** False when the address was on the list already; its entry is unchanged. */\n created: boolean\n /** The entry as it stands: the new one, or the one that was already there. */\n entry: OutreachDoNotContactEntry\n}\n\n/**\n * Puts an address on the list — see the module note for why a second add\n * changes nothing. Throws for a value that is not an address, a reason or a\n * source the list does not know, and a member-sourced add with no member:\n * a list that could hold an entry nobody can explain is not evidence of\n * anything.\n */\nexport async function addOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactInput,\n): Promise<AddOutreachDoNotContactResult> {\n const key = outreachDoNotContactKey(input.email)\n if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value')\n if (!(OUTREACH_DO_NOT_CONTACT_REASONS as readonly string[]).includes(input.reason)) {\n throw new Error(`[outreach] \"${String(input.reason)}\" is not a do-not-contact reason`)\n }\n if (!(OUTREACH_DO_NOT_CONTACT_SOURCES as readonly string[]).includes(input.source)) {\n throw new Error(`[outreach] \"${String(input.source)}\" is not a do-not-contact source`)\n }\n const addedByUid = typeof input.addedByUid === 'string' && input.addedByUid ? input.addedByUid : null\n if (input.source === 'member' && !addedByUid) {\n throw new Error('[outreach] a member-added do-not-contact entry names the member')\n }\n const detail = String(input.detail ?? '').replace(/\\s+/g, ' ').trim()\n const entry: OutreachDoNotContactEntry = {\n key,\n reason: input.reason,\n source: input.source,\n addedByUid,\n addedAtMs: input.nowMs,\n enrollmentId: input.enrollmentId || null,\n sequenceId: input.sequenceId || null,\n detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null,\n }\n const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key)\n try {\n await ref.create(entry)\n return { key, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n key,\n created: false,\n entry: readOutreachDoNotContactEntry(key, existing.data()) ?? entry,\n }\n }\n}\n"],"names":["outreachDoNotContactKey","OUTREACH_COLLECTIONS","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_DETAIL_MAX","GET_ALL_CHUNK","ALREADY_EXISTS","outreachDoNotContactCollection","firestore","orgId","collection","doc","doNotContact","readOutreachDoNotContactEntry","key","data","reason","source","text","value","addedAtMs","Number","includes","addedByUid","isFinite","enrollmentId","sequenceId","detail","lookupOutreachDoNotContact","emails","answers","Map","keyed","email","push","set","length","list","start","chunk","slice","snapshots","getAll","map","entry","forEach","index","exists","error","console","isOutreachDoNotContact","get","getOutreachDoNotContactEntry","snapshot","addOutreachDoNotContact","input","Error","String","replace","trim","nowMs","ref","create","created","code","existing"],"mappings":"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"}
@@ -0,0 +1,33 @@
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
+ * Where the org-scoped Outreach documents live, as the server reaches them
19
+ * (AGL-2980). Server only — the references are the Admin SDK's. Each
20
+ * document reads back into its model shape through `model/stored-records`,
21
+ * re-exported here for the routes.
22
+ */
23
+ import { type OutreachOrgCollection } from '../model/outreach.types';
24
+ export { readStoredOutreachEnrollment, readStoredOutreachMailbox, readStoredOutreachSequence, } from '../model/stored-records';
25
+ /** `orgs/{orgId}/<collection>` for one of Outreach's org-scoped collections. */
26
+ export declare function outreachOrgCollection(firestore: FirebaseFirestore.Firestore, orgId: string, collection: OutreachOrgCollection): FirebaseFirestore.CollectionReference;
27
+ /**
28
+ * The id of one person's enrollment in one sequence. Deterministic, so a
29
+ * person is enrolled in a sequence once, ever: a second enroll finds the
30
+ * first document where a random id would have made a second, and a second
31
+ * run through the same emails is exactly what must not happen.
32
+ */
33
+ export declare function outreachEnrollmentId(sequenceId: string, contactId: string): string;
@@ -0,0 +1,35 @@
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
+ * Where the org-scoped Outreach documents live, as the server reaches them
18
+ * (AGL-2980). Server only — the references are the Admin SDK's. Each
19
+ * document reads back into its model shape through `model/stored-records`,
20
+ * re-exported here for the routes.
21
+ */ import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
22
+ export { readStoredOutreachEnrollment, readStoredOutreachMailbox, readStoredOutreachSequence } from "../model/stored-records.js";
23
+ /** `orgs/{orgId}/<collection>` for one of Outreach's org-scoped collections. */ export function outreachOrgCollection(firestore, orgId, collection) {
24
+ return firestore.collection('orgs').doc(orgId).collection(OUTREACH_COLLECTIONS[collection]);
25
+ }
26
+ /**
27
+ * The id of one person's enrollment in one sequence. Deterministic, so a
28
+ * person is enrolled in a sequence once, ever: a second enroll finds the
29
+ * first document where a random id would have made a second, and a second
30
+ * run through the same emails is exactly what must not happen.
31
+ */ export function outreachEnrollmentId(sequenceId, contactId) {
32
+ return `${sequenceId}_${contactId}`;
33
+ }
34
+
35
+ //# sourceMappingURL=outreach-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/outreach-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Where the org-scoped Outreach documents live, as the server reaches them\n * (AGL-2980). Server only — the references are the Admin SDK's. Each\n * document reads back into its model shape through `model/stored-records`,\n * re-exported here for the routes.\n */\n\nimport { OUTREACH_COLLECTIONS, type OutreachOrgCollection } from '../model/outreach.types'\n\nexport {\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../model/stored-records'\n\n/** `orgs/{orgId}/<collection>` for one of Outreach's org-scoped collections. */\nexport function outreachOrgCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n collection: OutreachOrgCollection,\n): FirebaseFirestore.CollectionReference {\n return firestore.collection('orgs').doc(orgId).collection(OUTREACH_COLLECTIONS[collection])\n}\n\n/**\n * The id of one person's enrollment in one sequence. Deterministic, so a\n * person is enrolled in a sequence once, ever: a second enroll finds the\n * first document where a random id would have made a second, and a second\n * run through the same emails is exactly what must not happen.\n */\nexport function outreachEnrollmentId(sequenceId: string, contactId: string): string {\n return `${sequenceId}_${contactId}`\n}\n"],"names":["OUTREACH_COLLECTIONS","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","outreachOrgCollection","firestore","orgId","collection","doc","outreachEnrollmentId","sequenceId","contactId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC,GAED,SAASA,oBAAoB,QAAoC,6BAAyB;AAE1F,SACEC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,QACrB,6BAAyB;AAEhC,8EAA8E,GAC9E,OAAO,SAASC,sBACdC,SAAsC,EACtCC,KAAa,EACbC,UAAiC;IAEjC,OAAOF,UAAUE,UAAU,CAAC,QAAQC,GAAG,CAACF,OAAOC,UAAU,CAACP,oBAAoB,CAACO,WAAW;AAC5F;AAEA;;;;;CAKC,GACD,OAAO,SAASE,qBAAqBC,UAAkB,EAAEC,SAAiB;IACxE,OAAO,GAAGD,WAAW,CAAC,EAAEC,WAAW;AACrC"}
@@ -0,0 +1,48 @@
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 { PluginEgressHostDeclaration, PluginEgressUseDeclaration, PluginSubprocessorsAnswer } from '@aglyn/aglyn/plugin-manager/plugin-subprocessors';
18
+ /**
19
+ * The hosts this plugin's code names (AGL-2978), declared here rather than in
20
+ * the console's subprocessor inventory. Named under `subprocessors` in
21
+ * `plugins.config.json`; the manifest generator calls
22
+ * {@link outreachSubprocessors} and the inventory folds the answer in.
23
+ *
24
+ * A sequence sends a rep's one-to-one mail from the rep's OWN Google mailbox,
25
+ * through the Gmail API, under an OAuth grant the rep gives from their own
26
+ * Google account. `not-a-subprocessor` on the SECOND admissible reason: the
27
+ * mailbox provider is the customer's, engaged by the customer for their own
28
+ * mail, and a sequence acts in it at the rep's direction — Aglyn selects no
29
+ * mail vendor for this, any more than it selects the video host an author
30
+ * embeds. ⚑ A classification for legal to confirm when Sequences leaves staff
31
+ * preview; the consent screen is Internal to the aglyn.com Workspace until
32
+ * then.
33
+ *
34
+ * Google's token endpoint is already declared by the inventory, for the
35
+ * platform's own service-account exchange, so this plugin adds its use of it
36
+ * rather than a second declaration.
37
+ *
38
+ * Each host is read off the constant the transport calls, so a moved
39
+ * endpoint moves its declaration with it.
40
+ */
41
+ /** The Gmail REST API: the rep's own mailbox. */
42
+ export declare const OUTREACH_GMAIL_HOST: PluginEgressHostDeclaration;
43
+ /** Google's consent address, which only the rep's browser opens. */
44
+ export declare const OUTREACH_GOOGLE_CONSENT_HOST: PluginEgressHostDeclaration;
45
+ /** Google's token and revocation endpoint, declared by the inventory. */
46
+ export declare const OUTREACH_GOOGLE_TOKEN_USE: PluginEgressUseDeclaration;
47
+ /** The plugin's `subprocessors` entry: no recipient of its own, two hosts and one use. */
48
+ export declare function outreachSubprocessors(): PluginSubprocessorsAnswer;
@@ -0,0 +1,70 @@
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 { GMAIL_API_BASE } from "./transport/gmail-client.js";
17
+ import { GOOGLE_OAUTH_ENDPOINTS } from "./transport/google-oauth.js";
18
+ /**
19
+ * The hosts this plugin's code names (AGL-2978), declared here rather than in
20
+ * the console's subprocessor inventory. Named under `subprocessors` in
21
+ * `plugins.config.json`; the manifest generator calls
22
+ * {@link outreachSubprocessors} and the inventory folds the answer in.
23
+ *
24
+ * A sequence sends a rep's one-to-one mail from the rep's OWN Google mailbox,
25
+ * through the Gmail API, under an OAuth grant the rep gives from their own
26
+ * Google account. `not-a-subprocessor` on the SECOND admissible reason: the
27
+ * mailbox provider is the customer's, engaged by the customer for their own
28
+ * mail, and a sequence acts in it at the rep's direction — Aglyn selects no
29
+ * mail vendor for this, any more than it selects the video host an author
30
+ * embeds. ⚑ A classification for legal to confirm when Sequences leaves staff
31
+ * preview; the consent screen is Internal to the aglyn.com Workspace until
32
+ * then.
33
+ *
34
+ * Google's token endpoint is already declared by the inventory, for the
35
+ * platform's own service-account exchange, so this plugin adds its use of it
36
+ * rather than a second declaration.
37
+ *
38
+ * Each host is read off the constant the transport calls, so a moved
39
+ * endpoint moves its declaration with it.
40
+ */ /** The Gmail REST API: the rep's own mailbox. */ export const OUTREACH_GMAIL_HOST = {
41
+ host: new URL(GMAIL_API_BASE).host,
42
+ disposition: 'not-a-subprocessor',
43
+ reason: "Customer-chosen destination. The Gmail REST API of the rep's own Google Workspace mailbox, which the rep connects in Sequences → Mailboxes (`libs/plugins/outreach/src/lib/transport/gmail-client.ts`): the account's profile and verified send-as addresses at connect, a plain-text test the rep sends to themselves, and — for the sending runtime (AGL-2981) — the sequence messages it sends, and the reads and searches of that same mailbox that find what came back: replies, out-of-office answers, bounces and unsubscribe requests. The provider is the one the customer runs its mail on; nothing is sent to a mailbox the rep did not connect.",
44
+ dataReceived: "The rep's own OAuth access token, and the mail the rep sends from their own mailbox — each recipient's address, the subject and the plain-text body. The runtime's searches carry the addresses of the people the rep is writing to, the rep's `+unsubscribe` address and the Message-IDs of mail it sent. What the runtime reads back is the rep's own mail: the ids of newly received messages, and whole messages — headers and plain-text or HTML bodies, delivery reports included — of the rep's sequence threads and of the replies, bounces and unsubscribe requests its searches find. No other customer record, and nothing about a site visitor."
45
+ };
46
+ /** Google's consent address, which only the rep's browser opens. */ export const OUTREACH_GOOGLE_CONSENT_HOST = {
47
+ host: new URL(GOOGLE_OAUTH_ENDPOINTS.authorize).host,
48
+ disposition: 'no-request',
49
+ reason: "Google's OAuth consent address, built by `buildGoogleAuthorizationUrl` in `libs/plugins/outreach/src/lib/transport/google-oauth.ts` and handed to the rep's own browser, which opens it to grant Sequences access to the rep's own mailbox. No server of ours requests it; the browser's visit is between the rep and their Google account.",
50
+ dataReceived: "Nothing from our servers. The rep's browser carries the OAuth client id, the requested scopes, a signed state, a PKCE challenge and a login hint — the rep's own sign-in address."
51
+ };
52
+ /** Google's token and revocation endpoint, declared by the inventory. */ export const OUTREACH_GOOGLE_TOKEN_USE = {
53
+ host: new URL(GOOGLE_OAUTH_ENDPOINTS.token).host,
54
+ reason: "Since AGL-2978 also the Sequences OAuth token endpoint for a rep's own Google mailbox grant — the code exchange at connect, the access-token refresh before each Gmail call, and the revocation on disconnect, org erasure or account erasure — which is the rep's own account at the rep's own provider, the same footing as `gmail.googleapis.com`.",
55
+ dataReceived: "For Sequences: the deployment's OAuth client credentials and, for the rep's own grant, the authorization code, PKCE verifier, refresh token and access token Google itself issued — credentials, never message content. ⚑ Legal to confirm the Annex III cell needs no change for the Sequences use."
56
+ };
57
+ /** The plugin's `subprocessors` entry: no recipient of its own, two hosts and one use. */ export function outreachSubprocessors() {
58
+ return {
59
+ subprocessors: [],
60
+ hosts: [
61
+ OUTREACH_GMAIL_HOST,
62
+ OUTREACH_GOOGLE_CONSENT_HOST
63
+ ],
64
+ uses: [
65
+ OUTREACH_GOOGLE_TOKEN_USE
66
+ ]
67
+ };
68
+ }
69
+
70
+ //# sourceMappingURL=subprocessors.js.map