@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/subprocessors.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 PluginEgressHostDeclaration,\n PluginEgressUseDeclaration,\n PluginSubprocessorsAnswer,\n} from '@aglyn/aglyn/plugin-manager/plugin-subprocessors'\nimport { GMAIL_API_BASE } from './transport/gmail-client'\nimport { GOOGLE_OAUTH_ENDPOINTS } from './transport/google-oauth'\n\n/**\n * The hosts this plugin's code names (AGL-2978), declared here rather than in\n * the console's subprocessor inventory. Named under `subprocessors` in\n * `plugins.config.json`; the manifest generator calls\n * {@link outreachSubprocessors} and the inventory folds the answer in.\n *\n * A sequence sends a rep's one-to-one mail from the rep's OWN Google mailbox,\n * through the Gmail API, under an OAuth grant the rep gives from their own\n * Google account. `not-a-subprocessor` on the SECOND admissible reason: the\n * mailbox provider is the customer's, engaged by the customer for their own\n * mail, and a sequence acts in it at the rep's direction — Aglyn selects no\n * mail vendor for this, any more than it selects the video host an author\n * embeds. ⚑ A classification for legal to confirm when Sequences leaves staff\n * preview; the consent screen is Internal to the aglyn.com Workspace until\n * then.\n *\n * Google's token endpoint is already declared by the inventory, for the\n * platform's own service-account exchange, so this plugin adds its use of it\n * rather than a second declaration.\n *\n * Each host is read off the constant the transport calls, so a moved\n * endpoint moves its declaration with it.\n */\n\n/** The Gmail REST API: the rep's own mailbox. */\nexport const OUTREACH_GMAIL_HOST: PluginEgressHostDeclaration = {\n host: new URL(GMAIL_API_BASE).host,\n disposition: 'not-a-subprocessor',\n reason:\n \"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.\",\n dataReceived:\n \"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.\",\n}\n\n/** Google's consent address, which only the rep's browser opens. */\nexport const OUTREACH_GOOGLE_CONSENT_HOST: PluginEgressHostDeclaration = {\n host: new URL(GOOGLE_OAUTH_ENDPOINTS.authorize).host,\n disposition: 'no-request',\n reason:\n \"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.\",\n dataReceived:\n \"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.\",\n}\n\n/** Google's token and revocation endpoint, declared by the inventory. */\nexport const OUTREACH_GOOGLE_TOKEN_USE: PluginEgressUseDeclaration = {\n host: new URL(GOOGLE_OAUTH_ENDPOINTS.token).host,\n reason:\n \"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`.\",\n dataReceived:\n \"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.\",\n}\n\n/** The plugin's `subprocessors` entry: no recipient of its own, two hosts and one use. */\nexport function outreachSubprocessors(): PluginSubprocessorsAnswer {\n return {\n subprocessors: [],\n hosts: [OUTREACH_GMAIL_HOST, OUTREACH_GOOGLE_CONSENT_HOST],\n uses: [OUTREACH_GOOGLE_TOKEN_USE],\n }\n}\n"],"names":["GMAIL_API_BASE","GOOGLE_OAUTH_ENDPOINTS","OUTREACH_GMAIL_HOST","host","URL","disposition","reason","dataReceived","OUTREACH_GOOGLE_CONSENT_HOST","authorize","OUTREACH_GOOGLE_TOKEN_USE","token","outreachSubprocessors","subprocessors","hosts","uses"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD,SAASA,cAAc,QAAQ,8BAA0B;AACzD,SAASC,sBAAsB,QAAQ,8BAA0B;AAEjE;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,+CAA+C,GAC/C,OAAO,MAAMC,sBAAmD;IAC9DC,MAAM,IAAIC,IAAIJ,gBAAgBG,IAAI;IAClCE,aAAa;IACbC,QACE;IACFC,cACE;AACJ,EAAC;AAED,kEAAkE,GAClE,OAAO,MAAMC,+BAA4D;IACvEL,MAAM,IAAIC,IAAIH,uBAAuBQ,SAAS,EAAEN,IAAI;IACpDE,aAAa;IACbC,QACE;IACFC,cACE;AACJ,EAAC;AAED,uEAAuE,GACvE,OAAO,MAAMG,4BAAwD;IACnEP,MAAM,IAAIC,IAAIH,uBAAuBU,KAAK,EAAER,IAAI;IAChDG,QACE;IACFC,cACE;AACJ,EAAC;AAED,wFAAwF,GACxF,OAAO,SAASK;IACd,OAAO;QACLC,eAAe,EAAE;QACjBC,OAAO;YAACZ;YAAqBM;SAA6B;QAC1DO,MAAM;YAACL;SAA0B;IACnC;AACF"}
@@ -0,0 +1,145 @@
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 { GmailApiMessage } from '../engine/thread-message';
18
+ import { type TransportDeps } from './http';
19
+ /**
20
+ * THE GMAIL REST TRANSPORT (AGL-2978): one connected mailbox, as the calls
21
+ * Outreach makes against it.
22
+ *
23
+ * Built per mailbox, per invocation, from the mailbox's refresh token. The
24
+ * access token is minted on first use and reused until a minute before it
25
+ * expires, so one cron pass over one mailbox — a send, a thread read, a
26
+ * bounce search — refreshes once. Concurrent callers share the one refresh
27
+ * in flight.
28
+ *
29
+ * A 401 from Gmail drops the cached token and retries the call once with a
30
+ * fresh one: an access token can be refused before its stated expiry (a
31
+ * password change, an admin action), and that is not yet a dead grant. If
32
+ * the REFRESH is refused, it is — `invalid_grant`, `reconnectRequired`.
33
+ *
34
+ * Every failure is a `GmailTransportError`; see `gmail-errors.ts` for how a
35
+ * caller decides between reconnecting, retrying later and giving up.
36
+ */
37
+ export declare const GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
38
+ export interface GmailClientOptions extends TransportDeps {
39
+ clientId: string;
40
+ clientSecret: string;
41
+ refreshToken: string;
42
+ }
43
+ /** `users.getProfile`. */
44
+ export interface GmailProfile {
45
+ emailAddress: string;
46
+ messagesTotal: number;
47
+ threadsTotal: number;
48
+ historyId: string;
49
+ }
50
+ /** One entry of `users.settings.sendAs.list`. */
51
+ export interface GmailSendAs {
52
+ sendAsEmail: string;
53
+ displayName: string;
54
+ replyToAddress: string | null;
55
+ isPrimary: boolean;
56
+ isDefault: boolean;
57
+ treatAsAlias: boolean;
58
+ /**
59
+ * `accepted` once Gmail verified the address; absent on the primary
60
+ * address, which needs no verification.
61
+ */
62
+ verificationStatus: string | null;
63
+ }
64
+ export interface GmailMessageHeader {
65
+ name: string;
66
+ value: string;
67
+ }
68
+ /** A message in `format=metadata`: ids, labels, snippet and the headers asked for. */
69
+ export interface GmailMessageMetadata {
70
+ id: string;
71
+ threadId: string;
72
+ labelIds: string[];
73
+ snippet: string;
74
+ internalDateMs: number | null;
75
+ headers: GmailMessageHeader[];
76
+ }
77
+ export interface GmailThread {
78
+ id: string;
79
+ historyId: string | null;
80
+ messages: GmailMessageMetadata[];
81
+ }
82
+ /**
83
+ * A thread in `format=full`: every message as Gmail returns it — headers,
84
+ * MIME tree and base64url bodies — which is what the engine's
85
+ * `outreachThreadMessageFromGmail` reads replies and bounces from.
86
+ */
87
+ export interface GmailFullThread {
88
+ id: string;
89
+ historyId: string | null;
90
+ messages: GmailApiMessage[];
91
+ }
92
+ export interface GmailMessageList {
93
+ messages: Array<{
94
+ id: string;
95
+ threadId: string;
96
+ }>;
97
+ nextPageToken: string | null;
98
+ resultSizeEstimate: number;
99
+ }
100
+ export interface GmailSentMessage {
101
+ id: string;
102
+ threadId: string;
103
+ labelIds: string[];
104
+ }
105
+ export interface GmailClient {
106
+ /** A live access token, minted or reused. */
107
+ getAccessToken(): Promise<string>;
108
+ getProfile(): Promise<GmailProfile>;
109
+ listSendAs(): Promise<GmailSendAs[]>;
110
+ /**
111
+ * `messages.send`: `raw` is the base64url RFC 5322 message
112
+ * (`encodeGmailRawMessage`), and `threadId` files a follow-up into the
113
+ * thread its first step started.
114
+ */
115
+ sendMessage(input: {
116
+ raw: string;
117
+ threadId?: string | null;
118
+ }): Promise<GmailSentMessage>;
119
+ /** `threads.get` with `format=metadata` and only the headers named. */
120
+ getThread(threadId: string, options?: {
121
+ metadataHeaders?: readonly string[];
122
+ }): Promise<GmailThread>;
123
+ /**
124
+ * `threads.get` with `format=full`: every message whole, unflattened, for
125
+ * the engine's classifier — a reply's text, a bounce's delivery report.
126
+ */
127
+ getFullThread(threadId: string): Promise<GmailFullThread>;
128
+ /** `messages.get` with `format=metadata` and only the headers named. */
129
+ getMessage(messageId: string, options?: {
130
+ metadataHeaders?: readonly string[];
131
+ }): Promise<GmailMessageMetadata>;
132
+ /** `messages.get` with `format=full` — a message a search found, read whole. */
133
+ getFullMessage(messageId: string): Promise<GmailApiMessage>;
134
+ /** `messages.list` with a Gmail search `q`. */
135
+ listMessages(options: {
136
+ q: string;
137
+ maxResults?: number;
138
+ pageToken?: string | null;
139
+ includeSpamTrash?: boolean;
140
+ }): Promise<GmailMessageList>;
141
+ /** Revokes the grant at Google. `already-invalid` when it was dead. */
142
+ revoke(): Promise<'revoked' | 'already-invalid'>;
143
+ }
144
+ /** A Gmail client for one mailbox. See the module comment. */
145
+ export declare function createGmailClient(options: GmailClientOptions): GmailClient;
@@ -0,0 +1,229 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { GmailTransportError, gmailApiError } from "./gmail-errors.js";
18
+ import { refreshGoogleAccessToken, revokeGoogleToken } from "./google-oauth.js";
19
+ import { fetchWithBackoff, retryAfterMs } from "./http.js";
20
+ /**
21
+ * THE GMAIL REST TRANSPORT (AGL-2978): one connected mailbox, as the calls
22
+ * Outreach makes against it.
23
+ *
24
+ * Built per mailbox, per invocation, from the mailbox's refresh token. The
25
+ * access token is minted on first use and reused until a minute before it
26
+ * expires, so one cron pass over one mailbox — a send, a thread read, a
27
+ * bounce search — refreshes once. Concurrent callers share the one refresh
28
+ * in flight.
29
+ *
30
+ * A 401 from Gmail drops the cached token and retries the call once with a
31
+ * fresh one: an access token can be refused before its stated expiry (a
32
+ * password change, an admin action), and that is not yet a dead grant. If
33
+ * the REFRESH is refused, it is — `invalid_grant`, `reconnectRequired`.
34
+ *
35
+ * Every failure is a `GmailTransportError`; see `gmail-errors.ts` for how a
36
+ * caller decides between reconnecting, retrying later and giving up.
37
+ */ export const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me';
38
+ /** Refresh this long before the stated expiry, so a slow call cannot cross it. */ const EXPIRY_MARGIN_MS = 60000;
39
+ const text = (value)=>typeof value === 'string' ? value : '';
40
+ function readMessage(value) {
41
+ var _record_payload;
42
+ const record = value != null ? value : {};
43
+ const payload = (_record_payload = record['payload']) != null ? _record_payload : {};
44
+ const headers = Array.isArray(payload.headers) ? payload.headers : [];
45
+ const internalDate = Number(record['internalDate']);
46
+ return {
47
+ id: text(record['id']),
48
+ threadId: text(record['threadId']),
49
+ labelIds: Array.isArray(record['labelIds']) ? record['labelIds'].map(String) : [],
50
+ snippet: text(record['snippet']),
51
+ internalDateMs: Number.isFinite(internalDate) ? internalDate : null,
52
+ headers: headers.map((header)=>header).filter((header)=>typeof header.name === 'string').map((header)=>({
53
+ name: String(header.name),
54
+ value: text(header.value)
55
+ }))
56
+ };
57
+ }
58
+ function readSendAs(value) {
59
+ const record = value != null ? value : {};
60
+ return {
61
+ sendAsEmail: text(record['sendAsEmail']).trim().toLowerCase(),
62
+ displayName: text(record['displayName']),
63
+ replyToAddress: text(record['replyToAddress']) || null,
64
+ isPrimary: record['isPrimary'] === true,
65
+ isDefault: record['isDefault'] === true,
66
+ treatAsAlias: record['treatAsAlias'] === true,
67
+ verificationStatus: text(record['verificationStatus']) || null
68
+ };
69
+ }
70
+ /**
71
+ * A `format=full` message, as Gmail sent it. Only the id is checked here: the
72
+ * engine's reader decodes the rest defensively and never throws.
73
+ */ function readFullMessage(value) {
74
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
75
+ throw new GmailTransportError('unexpected', 'Gmail answered with a message that is not an object.');
76
+ }
77
+ const record = value;
78
+ return _extends({}, record, {
79
+ id: text(record['id'])
80
+ });
81
+ }
82
+ function metadataQuery(headers) {
83
+ const query = new URLSearchParams({
84
+ format: 'metadata'
85
+ });
86
+ for (const header of headers != null ? headers : [])query.append('metadataHeaders', header);
87
+ return query.toString();
88
+ }
89
+ /** A Gmail client for one mailbox. See the module comment. */ export function createGmailClient(options) {
90
+ var _options_now;
91
+ const deps = options;
92
+ const now = (_options_now = options.now) != null ? _options_now : Date.now;
93
+ let cached = null;
94
+ let refreshing = null;
95
+ const mint = ()=>{
96
+ refreshing != null ? refreshing : refreshing = refreshGoogleAccessToken({
97
+ clientId: options.clientId,
98
+ clientSecret: options.clientSecret,
99
+ refreshToken: options.refreshToken
100
+ }, deps).then((minted)=>{
101
+ cached = {
102
+ token: minted.accessToken,
103
+ expiresAtMs: now() + minted.expiresInSeconds * 1000
104
+ };
105
+ return minted.accessToken;
106
+ }).finally(()=>{
107
+ refreshing = null;
108
+ });
109
+ return refreshing;
110
+ };
111
+ const getAccessToken = async ()=>{
112
+ if (cached && cached.expiresAtMs - EXPIRY_MARGIN_MS > now()) return cached.token;
113
+ return mint();
114
+ };
115
+ /** One Gmail API call: authorized, retried per policy, 401 once refreshed. */ const call = async (path, init = {})=>{
116
+ for(let tries = 0;; tries += 1){
117
+ const token = await getAccessToken();
118
+ const response = await fetchWithBackoff(`${GMAIL_API_BASE}${path}`, _extends({}, init, {
119
+ headers: _extends({
120
+ Accept: 'application/json'
121
+ }, init.body ? {
122
+ 'Content-Type': 'application/json'
123
+ } : {}, init.headers, {
124
+ Authorization: `Bearer ${token}`
125
+ })
126
+ }), deps);
127
+ if (response.status === 401 && tries === 0) {
128
+ cached = null;
129
+ continue;
130
+ }
131
+ const body = await response.json().catch(()=>null);
132
+ if (response.ok) {
133
+ if (body === null) {
134
+ throw new GmailTransportError('unexpected', 'Gmail answered with a body that is not JSON.', {
135
+ status: response.status
136
+ });
137
+ }
138
+ return body;
139
+ }
140
+ throw gmailApiError(response.status, body, {
141
+ retryAfterMs: retryAfterMs(response, now())
142
+ });
143
+ }
144
+ };
145
+ return {
146
+ getAccessToken,
147
+ async getProfile () {
148
+ const body = await call('/profile');
149
+ return {
150
+ emailAddress: text(body['emailAddress']).trim().toLowerCase(),
151
+ messagesTotal: Number(body['messagesTotal']) || 0,
152
+ threadsTotal: Number(body['threadsTotal']) || 0,
153
+ historyId: text(body['historyId'])
154
+ };
155
+ },
156
+ async listSendAs () {
157
+ const body = await call('/settings/sendAs');
158
+ return (Array.isArray(body.sendAs) ? body.sendAs : []).map(readSendAs);
159
+ },
160
+ async sendMessage (input) {
161
+ if (!(input == null ? void 0 : input.raw)) {
162
+ throw new GmailTransportError('invalid_request', 'A message to send needs its raw content.');
163
+ }
164
+ const body = await call('/messages/send', {
165
+ method: 'POST',
166
+ body: JSON.stringify(_extends({
167
+ raw: input.raw
168
+ }, input.threadId ? {
169
+ threadId: input.threadId
170
+ } : {}))
171
+ });
172
+ return {
173
+ id: text(body['id']),
174
+ threadId: text(body['threadId']),
175
+ labelIds: Array.isArray(body['labelIds']) ? body['labelIds'].map(String) : []
176
+ };
177
+ },
178
+ async getThread (threadId, threadOptions) {
179
+ const body = await call(`/threads/${encodeURIComponent(threadId)}?${metadataQuery(threadOptions == null ? void 0 : threadOptions.metadataHeaders)}`);
180
+ return {
181
+ id: text(body['id']),
182
+ historyId: text(body['historyId']) || null,
183
+ messages: (Array.isArray(body['messages']) ? body['messages'] : []).map(readMessage)
184
+ };
185
+ },
186
+ async getFullThread (threadId) {
187
+ const body = await call(`/threads/${encodeURIComponent(threadId)}?format=full`);
188
+ return {
189
+ id: text(body['id']),
190
+ historyId: text(body['historyId']) || null,
191
+ // An entry that is not a message object is not one to classify.
192
+ messages: (Array.isArray(body['messages']) ? body['messages'] : []).filter((entry)=>Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry)).map(readFullMessage)
193
+ };
194
+ },
195
+ async getMessage (messageId, messageOptions) {
196
+ const body = await call(`/messages/${encodeURIComponent(messageId)}?${metadataQuery(messageOptions == null ? void 0 : messageOptions.metadataHeaders)}`);
197
+ return readMessage(body);
198
+ },
199
+ async getFullMessage (messageId) {
200
+ return readFullMessage(await call(`/messages/${encodeURIComponent(messageId)}?format=full`));
201
+ },
202
+ async listMessages (listOptions) {
203
+ const query = new URLSearchParams({
204
+ q: listOptions.q
205
+ });
206
+ if (listOptions.maxResults) query.set('maxResults', String(listOptions.maxResults));
207
+ if (listOptions.pageToken) query.set('pageToken', listOptions.pageToken);
208
+ if (listOptions.includeSpamTrash) query.set('includeSpamTrash', 'true');
209
+ const body = await call(`/messages?${query.toString()}`);
210
+ const messages = Array.isArray(body['messages']) ? body['messages'] : [];
211
+ return {
212
+ messages: messages.map((entry)=>{
213
+ const record = entry != null ? entry : {};
214
+ return {
215
+ id: text(record['id']),
216
+ threadId: text(record['threadId'])
217
+ };
218
+ }),
219
+ nextPageToken: text(body['nextPageToken']) || null,
220
+ resultSizeEstimate: Number(body['resultSizeEstimate']) || 0
221
+ };
222
+ },
223
+ revoke () {
224
+ return revokeGoogleToken(options.refreshToken, deps);
225
+ }
226
+ };
227
+ }
228
+
229
+ //# sourceMappingURL=gmail-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/gmail-client.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 { GmailApiMessage } from '../engine/thread-message'\nimport { GmailTransportError, gmailApiError } from './gmail-errors'\nimport {\n refreshGoogleAccessToken,\n revokeGoogleToken,\n} from './google-oauth'\nimport { fetchWithBackoff, retryAfterMs, type TransportDeps } from './http'\n\n/**\n * THE GMAIL REST TRANSPORT (AGL-2978): one connected mailbox, as the calls\n * Outreach makes against it.\n *\n * Built per mailbox, per invocation, from the mailbox's refresh token. The\n * access token is minted on first use and reused until a minute before it\n * expires, so one cron pass over one mailbox — a send, a thread read, a\n * bounce search — refreshes once. Concurrent callers share the one refresh\n * in flight.\n *\n * A 401 from Gmail drops the cached token and retries the call once with a\n * fresh one: an access token can be refused before its stated expiry (a\n * password change, an admin action), and that is not yet a dead grant. If\n * the REFRESH is refused, it is — `invalid_grant`, `reconnectRequired`.\n *\n * Every failure is a `GmailTransportError`; see `gmail-errors.ts` for how a\n * caller decides between reconnecting, retrying later and giving up.\n */\n\nexport const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'\n\nexport interface GmailClientOptions extends TransportDeps {\n clientId: string\n clientSecret: string\n refreshToken: string\n}\n\n/** `users.getProfile`. */\nexport interface GmailProfile {\n emailAddress: string\n messagesTotal: number\n threadsTotal: number\n historyId: string\n}\n\n/** One entry of `users.settings.sendAs.list`. */\nexport interface GmailSendAs {\n sendAsEmail: string\n displayName: string\n replyToAddress: string | null\n isPrimary: boolean\n isDefault: boolean\n treatAsAlias: boolean\n /**\n * `accepted` once Gmail verified the address; absent on the primary\n * address, which needs no verification.\n */\n verificationStatus: string | null\n}\n\nexport interface GmailMessageHeader {\n name: string\n value: string\n}\n\n/** A message in `format=metadata`: ids, labels, snippet and the headers asked for. */\nexport interface GmailMessageMetadata {\n id: string\n threadId: string\n labelIds: string[]\n snippet: string\n internalDateMs: number | null\n headers: GmailMessageHeader[]\n}\n\nexport interface GmailThread {\n id: string\n historyId: string | null\n messages: GmailMessageMetadata[]\n}\n\n/**\n * A thread in `format=full`: every message as Gmail returns it — headers,\n * MIME tree and base64url bodies — which is what the engine's\n * `outreachThreadMessageFromGmail` reads replies and bounces from.\n */\nexport interface GmailFullThread {\n id: string\n historyId: string | null\n messages: GmailApiMessage[]\n}\n\nexport interface GmailMessageList {\n messages: Array<{ id: string; threadId: string }>\n nextPageToken: string | null\n resultSizeEstimate: number\n}\n\nexport interface GmailSentMessage {\n id: string\n threadId: string\n labelIds: string[]\n}\n\nexport interface GmailClient {\n /** A live access token, minted or reused. */\n getAccessToken(): Promise<string>\n getProfile(): Promise<GmailProfile>\n listSendAs(): Promise<GmailSendAs[]>\n /**\n * `messages.send`: `raw` is the base64url RFC 5322 message\n * (`encodeGmailRawMessage`), and `threadId` files a follow-up into the\n * thread its first step started.\n */\n sendMessage(input: { raw: string; threadId?: string | null }): Promise<GmailSentMessage>\n /** `threads.get` with `format=metadata` and only the headers named. */\n getThread(threadId: string, options?: { metadataHeaders?: readonly string[] }): Promise<GmailThread>\n /**\n * `threads.get` with `format=full`: every message whole, unflattened, for\n * the engine's classifier — a reply's text, a bounce's delivery report.\n */\n getFullThread(threadId: string): Promise<GmailFullThread>\n /** `messages.get` with `format=metadata` and only the headers named. */\n getMessage(messageId: string, options?: { metadataHeaders?: readonly string[] }): Promise<GmailMessageMetadata>\n /** `messages.get` with `format=full` — a message a search found, read whole. */\n getFullMessage(messageId: string): Promise<GmailApiMessage>\n /** `messages.list` with a Gmail search `q`. */\n listMessages(options: {\n q: string\n maxResults?: number\n pageToken?: string | null\n includeSpamTrash?: boolean\n }): Promise<GmailMessageList>\n /** Revokes the grant at Google. `already-invalid` when it was dead. */\n revoke(): Promise<'revoked' | 'already-invalid'>\n}\n\n/** Refresh this long before the stated expiry, so a slow call cannot cross it. */\nconst EXPIRY_MARGIN_MS = 60_000\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\nfunction readMessage(value: unknown): GmailMessageMetadata {\n const record = (value ?? {}) as Record<string, unknown>\n const payload = (record['payload'] ?? {}) as { headers?: unknown }\n const headers = Array.isArray(payload.headers) ? payload.headers : []\n const internalDate = Number(record['internalDate'])\n return {\n id: text(record['id']),\n threadId: text(record['threadId']),\n labelIds: Array.isArray(record['labelIds']) ? record['labelIds'].map(String) : [],\n snippet: text(record['snippet']),\n internalDateMs: Number.isFinite(internalDate) ? internalDate : null,\n headers: headers\n .map((header) => header as { name?: unknown; value?: unknown })\n .filter((header) => typeof header.name === 'string')\n .map((header) => ({ name: String(header.name), value: text(header.value) })),\n }\n}\n\nfunction readSendAs(value: unknown): GmailSendAs {\n const record = (value ?? {}) as Record<string, unknown>\n return {\n sendAsEmail: text(record['sendAsEmail']).trim().toLowerCase(),\n displayName: text(record['displayName']),\n replyToAddress: text(record['replyToAddress']) || null,\n isPrimary: record['isPrimary'] === true,\n isDefault: record['isDefault'] === true,\n treatAsAlias: record['treatAsAlias'] === true,\n verificationStatus: text(record['verificationStatus']) || null,\n }\n}\n\n/**\n * A `format=full` message, as Gmail sent it. Only the id is checked here: the\n * engine's reader decodes the rest defensively and never throws.\n */\nfunction readFullMessage(value: unknown): GmailApiMessage {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new GmailTransportError('unexpected', 'Gmail answered with a message that is not an object.')\n }\n const record = value as Record<string, unknown>\n return { ...(record as unknown as GmailApiMessage), id: text(record['id']) }\n}\n\nfunction metadataQuery(headers: readonly string[] | undefined): string {\n const query = new URLSearchParams({ format: 'metadata' })\n for (const header of headers ?? []) query.append('metadataHeaders', header)\n return query.toString()\n}\n\n/** A Gmail client for one mailbox. See the module comment. */\nexport function createGmailClient(options: GmailClientOptions): GmailClient {\n const deps: TransportDeps = options\n const now = options.now ?? Date.now\n let cached: { token: string; expiresAtMs: number } | null = null\n let refreshing: Promise<string> | null = null\n\n const mint = (): Promise<string> => {\n refreshing ??= refreshGoogleAccessToken(\n {\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n refreshToken: options.refreshToken,\n },\n deps,\n )\n .then((minted) => {\n cached = {\n token: minted.accessToken,\n expiresAtMs: now() + minted.expiresInSeconds * 1000,\n }\n return minted.accessToken\n })\n .finally(() => {\n refreshing = null\n })\n return refreshing\n }\n\n const getAccessToken = async (): Promise<string> => {\n if (cached && cached.expiresAtMs - EXPIRY_MARGIN_MS > now()) return cached.token\n return mint()\n }\n\n /** One Gmail API call: authorized, retried per policy, 401 once refreshed. */\n const call = async <T>(path: string, init: RequestInit = {}): Promise<T> => {\n for (let tries = 0; ; tries += 1) {\n const token = await getAccessToken()\n const response = await fetchWithBackoff(\n `${GMAIL_API_BASE}${path}`,\n {\n ...init,\n headers: {\n Accept: 'application/json',\n ...(init.body ? { 'Content-Type': 'application/json' } : {}),\n ...(init.headers as Record<string, string> | undefined),\n Authorization: `Bearer ${token}`,\n },\n },\n deps,\n )\n if (response.status === 401 && tries === 0) {\n cached = null\n continue\n }\n const body = await response.json().catch(() => null)\n if (response.ok) {\n if (body === null) {\n throw new GmailTransportError('unexpected', 'Gmail answered with a body that is not JSON.', {\n status: response.status,\n })\n }\n return body as T\n }\n throw gmailApiError(response.status, body, { retryAfterMs: retryAfterMs(response, now()) })\n }\n }\n\n return {\n getAccessToken,\n\n async getProfile() {\n const body = await call<Record<string, unknown>>('/profile')\n return {\n emailAddress: text(body['emailAddress']).trim().toLowerCase(),\n messagesTotal: Number(body['messagesTotal']) || 0,\n threadsTotal: Number(body['threadsTotal']) || 0,\n historyId: text(body['historyId']),\n }\n },\n\n async listSendAs() {\n const body = await call<{ sendAs?: unknown }>('/settings/sendAs')\n return (Array.isArray(body.sendAs) ? body.sendAs : []).map(readSendAs)\n },\n\n async sendMessage(input) {\n if (!input?.raw) {\n throw new GmailTransportError('invalid_request', 'A message to send needs its raw content.')\n }\n const body = await call<Record<string, unknown>>('/messages/send', {\n method: 'POST',\n body: JSON.stringify({ raw: input.raw, ...(input.threadId ? { threadId: input.threadId } : {}) }),\n })\n return {\n id: text(body['id']),\n threadId: text(body['threadId']),\n labelIds: Array.isArray(body['labelIds']) ? body['labelIds'].map(String) : [],\n }\n },\n\n async getThread(threadId, threadOptions) {\n const body = await call<Record<string, unknown>>(\n `/threads/${encodeURIComponent(threadId)}?${metadataQuery(threadOptions?.metadataHeaders)}`,\n )\n return {\n id: text(body['id']),\n historyId: text(body['historyId']) || null,\n messages: (Array.isArray(body['messages']) ? body['messages'] : []).map(readMessage),\n }\n },\n\n async getFullThread(threadId) {\n const body = await call<Record<string, unknown>>(\n `/threads/${encodeURIComponent(threadId)}?format=full`,\n )\n return {\n id: text(body['id']),\n historyId: text(body['historyId']) || null,\n // An entry that is not a message object is not one to classify.\n messages: (Array.isArray(body['messages']) ? body['messages'] : [])\n .filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))\n .map(readFullMessage),\n }\n },\n\n async getMessage(messageId, messageOptions) {\n const body = await call<unknown>(\n `/messages/${encodeURIComponent(messageId)}?${metadataQuery(messageOptions?.metadataHeaders)}`,\n )\n return readMessage(body)\n },\n\n async getFullMessage(messageId) {\n return readFullMessage(await call<unknown>(`/messages/${encodeURIComponent(messageId)}?format=full`))\n },\n\n async listMessages(listOptions) {\n const query = new URLSearchParams({ q: listOptions.q })\n if (listOptions.maxResults) query.set('maxResults', String(listOptions.maxResults))\n if (listOptions.pageToken) query.set('pageToken', listOptions.pageToken)\n if (listOptions.includeSpamTrash) query.set('includeSpamTrash', 'true')\n const body = await call<Record<string, unknown>>(`/messages?${query.toString()}`)\n const messages = Array.isArray(body['messages']) ? body['messages'] : []\n return {\n messages: messages.map((entry) => {\n const record = (entry ?? {}) as Record<string, unknown>\n return { id: text(record['id']), threadId: text(record['threadId']) }\n }),\n nextPageToken: text(body['nextPageToken']) || null,\n resultSizeEstimate: Number(body['resultSizeEstimate']) || 0,\n }\n },\n\n revoke() {\n return revokeGoogleToken(options.refreshToken, deps)\n },\n }\n}\n"],"names":["GmailTransportError","gmailApiError","refreshGoogleAccessToken","revokeGoogleToken","fetchWithBackoff","retryAfterMs","GMAIL_API_BASE","EXPIRY_MARGIN_MS","text","value","readMessage","record","payload","headers","Array","isArray","internalDate","Number","id","threadId","labelIds","map","String","snippet","internalDateMs","isFinite","header","filter","name","readSendAs","sendAsEmail","trim","toLowerCase","displayName","replyToAddress","isPrimary","isDefault","treatAsAlias","verificationStatus","readFullMessage","metadataQuery","query","URLSearchParams","format","append","toString","createGmailClient","options","deps","now","Date","cached","refreshing","mint","clientId","clientSecret","refreshToken","then","minted","token","accessToken","expiresAtMs","expiresInSeconds","finally","getAccessToken","call","path","init","tries","response","Accept","body","Authorization","status","json","catch","ok","getProfile","emailAddress","messagesTotal","threadsTotal","historyId","listSendAs","sendAs","sendMessage","input","raw","method","JSON","stringify","getThread","threadOptions","encodeURIComponent","metadataHeaders","messages","getFullThread","entry","Boolean","getMessage","messageId","messageOptions","getFullMessage","listMessages","listOptions","q","maxResults","set","pageToken","includeSpamTrash","nextPageToken","resultSizeEstimate","revoke"],"mappings":"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"}
@@ -0,0 +1,84 @@
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
+ * WHAT A GOOGLE CALL CAN FAIL WITH, AS OUTREACH ACTS ON IT (AGL-2978).
19
+ *
20
+ * Google answers in two dialects — the OAuth token endpoint's
21
+ * `{ error: 'invalid_grant' }` and the Gmail API's
22
+ * `{ error: { code, status, errors: [{ reason }] } }` — and a caller should
23
+ * not have to speak either. Every failure the transport raises is one
24
+ * {@link GmailTransportError} carrying the three decisions a caller makes:
25
+ *
26
+ * - `reconnectRequired` — the grant itself is gone (revoked, expired, a
27
+ * password change, a scope removed). Nothing will work until the rep
28
+ * connects again, so the mailbox goes to `reconnect_required`.
29
+ * - `retryable` — the request may succeed later as it stands (a rate limit,
30
+ * an outage, a dropped connection). `retryAfterMs` says when, if Google
31
+ * said.
32
+ * - neither — the request itself is wrong, or the deployment is (a bad
33
+ * client id or secret), and retrying it changes nothing.
34
+ */
35
+ export type GmailTransportErrorCode =
36
+ /** The refresh token was refused: revoked, expired or otherwise dead. */
37
+ 'invalid_grant'
38
+ /** The grant is missing a scope the call needs. */
39
+ | 'insufficient_scope'
40
+ /** The OAuth client id or secret was refused — a deployment problem. */
41
+ | 'client_misconfigured'
42
+ /** Rate-limited, still after backing off. */
43
+ | 'rate_limited'
44
+ /** The account's sending or API quota for the day is spent. */
45
+ | 'quota_exceeded'
46
+ /** Google answered 5xx, still after backing off. */
47
+ | 'unavailable'
48
+ /** The request never got an answer: a network failure or a timeout. */
49
+ | 'network'
50
+ /** The access token was refused even after a fresh one was minted. */
51
+ | 'unauthorized'
52
+ /** The account has no Gmail service (a Workspace user without Gmail). */
53
+ | 'mail_service_unavailable' | 'not_found'
54
+ /** Google refused the request as malformed. */
55
+ | 'invalid_request' | 'forbidden'
56
+ /** An answer the transport could not read. */
57
+ | 'unexpected';
58
+ export interface GmailTransportErrorInit {
59
+ status?: number | null;
60
+ retryAfterMs?: number | null;
61
+ /** The provider's own reason string, kept for logs and health rows. */
62
+ providerReason?: string | null;
63
+ }
64
+ export declare class GmailTransportError extends Error {
65
+ readonly code: GmailTransportErrorCode;
66
+ readonly status: number | null;
67
+ readonly retryAfterMs: number | null;
68
+ readonly providerReason: string | null;
69
+ constructor(code: GmailTransportErrorCode, message: string, init?: GmailTransportErrorInit);
70
+ /** The grant is gone; the mailbox needs connecting again. */
71
+ get reconnectRequired(): boolean;
72
+ /** The same request may succeed later. */
73
+ get retryable(): boolean;
74
+ }
75
+ /** Whether a thrown value says the mailbox's grant is gone. */
76
+ export declare function isReconnectRequired(error: unknown): boolean;
77
+ /** The OAuth token endpoint's error body: `{ error, error_description }`. */
78
+ export declare function tokenEndpointError(status: number, body: unknown): GmailTransportError;
79
+ /** Whether a Gmail API 403 is a rate limit, which backs off like a 429. */
80
+ export declare function isRateLimitBody(body: unknown): boolean;
81
+ /** A Gmail API error response, mapped. */
82
+ export declare function gmailApiError(status: number, body: unknown, init?: {
83
+ retryAfterMs?: number | null;
84
+ }): GmailTransportError;
@@ -0,0 +1,138 @@
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
+ * WHAT A GOOGLE CALL CAN FAIL WITH, AS OUTREACH ACTS ON IT (AGL-2978).
18
+ *
19
+ * Google answers in two dialects — the OAuth token endpoint's
20
+ * `{ error: 'invalid_grant' }` and the Gmail API's
21
+ * `{ error: { code, status, errors: [{ reason }] } }` — and a caller should
22
+ * not have to speak either. Every failure the transport raises is one
23
+ * {@link GmailTransportError} carrying the three decisions a caller makes:
24
+ *
25
+ * - `reconnectRequired` — the grant itself is gone (revoked, expired, a
26
+ * password change, a scope removed). Nothing will work until the rep
27
+ * connects again, so the mailbox goes to `reconnect_required`.
28
+ * - `retryable` — the request may succeed later as it stands (a rate limit,
29
+ * an outage, a dropped connection). `retryAfterMs` says when, if Google
30
+ * said.
31
+ * - neither — the request itself is wrong, or the deployment is (a bad
32
+ * client id or secret), and retrying it changes nothing.
33
+ */ const RECONNECT = new Set([
34
+ 'invalid_grant',
35
+ 'insufficient_scope'
36
+ ]);
37
+ const RETRYABLE = new Set([
38
+ 'rate_limited',
39
+ 'unavailable',
40
+ 'network'
41
+ ]);
42
+ export class GmailTransportError extends Error {
43
+ /** The grant is gone; the mailbox needs connecting again. */ get reconnectRequired() {
44
+ return RECONNECT.has(this.code);
45
+ }
46
+ /** The same request may succeed later. */ get retryable() {
47
+ return RETRYABLE.has(this.code);
48
+ }
49
+ constructor(code, message, init = {}){
50
+ var _init_status, _init_retryAfterMs, _init_providerReason;
51
+ super(message);
52
+ this.name = 'GmailTransportError';
53
+ this.code = code;
54
+ this.status = (_init_status = init.status) != null ? _init_status : null;
55
+ this.retryAfterMs = (_init_retryAfterMs = init.retryAfterMs) != null ? _init_retryAfterMs : null;
56
+ this.providerReason = (_init_providerReason = init.providerReason) != null ? _init_providerReason : null;
57
+ }
58
+ }
59
+ /** Whether a thrown value says the mailbox's grant is gone. */ export function isReconnectRequired(error) {
60
+ return error instanceof GmailTransportError && error.reconnectRequired;
61
+ }
62
+ /** The OAuth token endpoint's error body: `{ error, error_description }`. */ export function tokenEndpointError(status, body) {
63
+ const record = body != null ? body : {};
64
+ const reason = typeof record.error === 'string' ? record.error : null;
65
+ const described = typeof record.error_description === 'string' ? record.error_description : '';
66
+ if (reason === 'invalid_grant') {
67
+ return new GmailTransportError('invalid_grant', 'Google refused the mailbox grant. The mailbox must be connected again.', {
68
+ status,
69
+ providerReason: reason
70
+ });
71
+ }
72
+ if (reason === 'invalid_client' || reason === 'unauthorized_client') {
73
+ return new GmailTransportError('client_misconfigured', 'Google refused this deployment’s OAuth client. Check its client id and secret.', {
74
+ status,
75
+ providerReason: reason
76
+ });
77
+ }
78
+ if (reason === 'invalid_scope') {
79
+ return new GmailTransportError('insufficient_scope', 'Google refused a requested scope.', {
80
+ status,
81
+ providerReason: reason
82
+ });
83
+ }
84
+ return new GmailTransportError(status >= 500 ? 'unavailable' : 'invalid_request', `Google’s token endpoint refused the request${described ? `: ${described.slice(0, 200)}` : '.'}`, {
85
+ status,
86
+ providerReason: reason
87
+ });
88
+ }
89
+ /** Every reason string a Gmail API error body carries. */ function apiReasons(body) {
90
+ var _ref, _ref1;
91
+ const error = body == null ? void 0 : body.error;
92
+ return [
93
+ ...((_ref = error == null ? void 0 : error.errors) != null ? _ref : []).map((entry)=>entry == null ? void 0 : entry.reason),
94
+ ...((_ref1 = error == null ? void 0 : error.details) != null ? _ref1 : []).map((entry)=>entry == null ? void 0 : entry.reason),
95
+ error == null ? void 0 : error.status
96
+ ].filter((reason)=>typeof reason === 'string' && reason.length > 0);
97
+ }
98
+ /** Whether a Gmail API 403 is a rate limit, which backs off like a 429. */ export function isRateLimitBody(body) {
99
+ return apiReasons(body).some((reason)=>[
100
+ 'rateLimitExceeded',
101
+ 'userRateLimitExceeded',
102
+ 'RESOURCE_EXHAUSTED'
103
+ ].includes(reason));
104
+ }
105
+ /** A Gmail API error response, mapped. */ export function gmailApiError(status, body, init = {}) {
106
+ var _reasons_;
107
+ var _body_error;
108
+ const reasons = apiReasons(body);
109
+ const providerReason = (_reasons_ = reasons[0]) != null ? _reasons_ : null;
110
+ const has = (...names)=>reasons.some((reason)=>names.includes(reason));
111
+ const message = body == null ? void 0 : (_body_error = body.error) == null ? void 0 : _body_error.message;
112
+ const detail = typeof message === 'string' && message ? `: ${message.slice(0, 200)}` : '.';
113
+ const make = (code, text)=>new GmailTransportError(code, text, {
114
+ status,
115
+ providerReason,
116
+ retryAfterMs: init.retryAfterMs
117
+ });
118
+ if (status === 429 || status === 403 && isRateLimitBody(body)) {
119
+ return make('rate_limited', 'Gmail is rate-limiting this mailbox. Try again later.');
120
+ }
121
+ if (has('dailyLimitExceeded', 'quotaExceeded')) {
122
+ return make('quota_exceeded', 'This mailbox has spent its Gmail quota for the day.');
123
+ }
124
+ if (has('insufficientPermissions', 'ACCESS_TOKEN_SCOPE_INSUFFICIENT')) {
125
+ return make('insufficient_scope', 'The mailbox grant is missing a permission Sequences needs.');
126
+ }
127
+ if (status >= 500) return make('unavailable', `Gmail is unavailable${detail}`);
128
+ if (status === 401) return make('unauthorized', 'Gmail refused the mailbox’s access token.');
129
+ if (status === 400 && has('failedPrecondition') && /mail service not enabled/i.test(String(message))) {
130
+ return make('mail_service_unavailable', 'This Google account has no Gmail service.');
131
+ }
132
+ if (status === 404) return make('not_found', `Gmail found nothing there${detail}`);
133
+ if (status === 400) return make('invalid_request', `Gmail refused the request${detail}`);
134
+ if (status === 403) return make('forbidden', `Gmail refused the request${detail}`);
135
+ return make('unexpected', `Gmail answered ${status}${detail}`);
136
+ }
137
+
138
+ //# sourceMappingURL=gmail-errors.js.map