@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,94 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { SecretBoxError } from "@aglyn/shared-util-tools/secret-box";
18
+ import { createGmailClient } from "../transport/gmail-client.js";
19
+ import { mailboxCredentialsRef, mailboxRef, openMailboxRefreshToken, readMailboxCredentials, sealMailboxRefreshToken } from "./mailbox-credentials.js";
20
+ import { readOutreachGoogleConfig } from "./outreach-config.js";
21
+ export async function openOutreachMailboxClient(firestore, input, deps = {}) {
22
+ var _deps_readConfig;
23
+ const config = ((_deps_readConfig = deps.readConfig) != null ? _deps_readConfig : readOutreachGoogleConfig)();
24
+ if (!config.configured) return {
25
+ ok: false,
26
+ reason: 'not-configured'
27
+ };
28
+ const snapshot = await mailboxCredentialsRef(firestore, input.mailboxId).get();
29
+ const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null);
30
+ if (!credential || credential.mailboxId !== input.mailboxId) {
31
+ return {
32
+ ok: false,
33
+ reason: 'no-credential'
34
+ };
35
+ }
36
+ let opened;
37
+ try {
38
+ opened = openMailboxRefreshToken(credential, config.config.keyring);
39
+ } catch (error) {
40
+ if (error instanceof SecretBoxError) return {
41
+ ok: false,
42
+ reason: 'sealed-token-unreadable'
43
+ };
44
+ throw error;
45
+ }
46
+ if (opened.needsReseal) {
47
+ await resealMailboxRefreshToken(firestore, credential, opened.refreshToken, config.config.keyring, deps.now);
48
+ }
49
+ const client = createGmailClient(_extends({}, deps, {
50
+ clientId: config.config.clientId,
51
+ clientSecret: config.config.clientSecret,
52
+ refreshToken: opened.refreshToken
53
+ }));
54
+ return {
55
+ ok: true,
56
+ client,
57
+ credential
58
+ };
59
+ }
60
+ /**
61
+ * Seals a token again under the current key, only if the stored value is
62
+ * still the one that was opened: a reconnect that landed in between wrote a
63
+ * newer grant, and that one wins. Best-effort — a failure leaves the old
64
+ * seal, which still opens.
65
+ */ async function resealMailboxRefreshToken(firestore, credential, refreshToken, keyring, now) {
66
+ const ref = mailboxCredentialsRef(firestore, credential.mailboxId);
67
+ try {
68
+ await firestore.runTransaction(async (tx)=>{
69
+ const current = await tx.get(ref);
70
+ if (current.get('sealedRefreshToken') !== credential.sealedRefreshToken) return;
71
+ tx.update(ref, _extends({}, sealMailboxRefreshToken(refreshToken, credential.mailboxId, keyring), {
72
+ updatedAtMs: (now != null ? now : Date.now)()
73
+ }));
74
+ });
75
+ } catch (error) {
76
+ console.error('[outreach] resealing a mailbox token failed', error);
77
+ }
78
+ }
79
+ /**
80
+ * Marks a mailbox as needing a reconnect: the provider refused its grant, or
81
+ * its stored token cannot be opened. Nothing sends from it until the member
82
+ * connects it again, which returns it to where it was.
83
+ */ export async function markOutreachMailboxReconnectRequired(firestore, input) {
84
+ await mailboxRef(firestore, input.orgId, input.mailboxId).update({
85
+ status: 'reconnect_required',
86
+ 'health.lastErrorAtMs': input.nowMs,
87
+ 'health.lastErrorCode': input.errorCode,
88
+ updatedAtMs: input.nowMs
89
+ }).catch((error)=>{
90
+ console.error('[outreach] marking a mailbox reconnect_required failed', error);
91
+ });
92
+ }
93
+
94
+ //# sourceMappingURL=mailbox-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-transport.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 { SecretBoxError } from '@aglyn/shared-util-tools/secret-box'\nimport { createGmailClient, type GmailClient } from '../transport/gmail-client'\nimport type { TransportDeps } from '../transport/http'\nimport {\n mailboxCredentialsRef,\n mailboxRef,\n openMailboxRefreshToken,\n readMailboxCredentials,\n sealMailboxRefreshToken,\n type OutreachGoogleMailboxCredentials,\n} from './mailbox-credentials'\nimport {\n readOutreachGoogleConfig,\n type OutreachGoogleConfigResult,\n} from './outreach-config'\n\n/**\n * A CONNECTED MAILBOX, OPENED FOR SENDING (AGL-2978).\n *\n * The door the send and sync runtime, and the panel's test send, reach a\n * mailbox's Gmail through: the stored credential read, its refresh token\n * opened with the deployment's key, and a transport client built on it. The\n * token never leaves this function except inside the client.\n *\n * A token opened under an older key of the keyring is sealed again under the\n * current one on the way through, so a key rotation completes itself as\n * mailboxes are used rather than needing a migration.\n */\n\nexport type OpenedOutreachMailbox =\n | { ok: true; client: GmailClient; credential: OutreachGoogleMailboxCredentials }\n | {\n ok: false\n /**\n * - `not-configured` — this deployment has no Outreach client or key.\n * - `no-credential` — nothing is stored for the mailbox.\n * - `sealed-token-unreadable` — the stored token does not open with\n * this deployment's key (rotated away, or altered). The mailbox needs\n * connecting again.\n */\n reason: 'not-configured' | 'no-credential' | 'sealed-token-unreadable'\n }\n\nexport interface OpenMailboxDeps extends TransportDeps {\n readConfig?: () => OutreachGoogleConfigResult\n}\n\nexport async function openOutreachMailboxClient(\n firestore: FirebaseFirestore.Firestore,\n input: { mailboxId: string },\n deps: OpenMailboxDeps = {},\n): Promise<OpenedOutreachMailbox> {\n const config = (deps.readConfig ?? readOutreachGoogleConfig)()\n if (!config.configured) return { ok: false, reason: 'not-configured' }\n const snapshot = await mailboxCredentialsRef(firestore, input.mailboxId).get()\n const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null)\n if (!credential || credential.mailboxId !== input.mailboxId) {\n return { ok: false, reason: 'no-credential' }\n }\n let opened: { refreshToken: string; needsReseal: boolean }\n try {\n opened = openMailboxRefreshToken(credential, config.config.keyring)\n } catch (error) {\n if (error instanceof SecretBoxError) return { ok: false, reason: 'sealed-token-unreadable' }\n throw error\n }\n if (opened.needsReseal) {\n await resealMailboxRefreshToken(firestore, credential, opened.refreshToken, config.config.keyring, deps.now)\n }\n const client = createGmailClient({\n ...deps,\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n refreshToken: opened.refreshToken,\n })\n return { ok: true, client, credential }\n}\n\n/**\n * Seals a token again under the current key, only if the stored value is\n * still the one that was opened: a reconnect that landed in between wrote a\n * newer grant, and that one wins. Best-effort — a failure leaves the old\n * seal, which still opens.\n */\nasync function resealMailboxRefreshToken(\n firestore: FirebaseFirestore.Firestore,\n credential: OutreachGoogleMailboxCredentials,\n refreshToken: string,\n keyring: Parameters<typeof sealMailboxRefreshToken>[2],\n now: (() => number) | undefined,\n): Promise<void> {\n const ref = mailboxCredentialsRef(firestore, credential.mailboxId)\n try {\n await firestore.runTransaction(async (tx) => {\n const current = await tx.get(ref)\n if (current.get('sealedRefreshToken') !== credential.sealedRefreshToken) return\n tx.update(ref, {\n ...sealMailboxRefreshToken(refreshToken, credential.mailboxId, keyring),\n updatedAtMs: (now ?? Date.now)(),\n })\n })\n } catch (error) {\n console.error('[outreach] resealing a mailbox token failed', error)\n }\n}\n\n/**\n * Marks a mailbox as needing a reconnect: the provider refused its grant, or\n * its stored token cannot be opened. Nothing sends from it until the member\n * connects it again, which returns it to where it was.\n */\nexport async function markOutreachMailboxReconnectRequired(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; mailboxId: string; errorCode: string; nowMs: number },\n): Promise<void> {\n await mailboxRef(firestore, input.orgId, input.mailboxId)\n .update({\n status: 'reconnect_required',\n 'health.lastErrorAtMs': input.nowMs,\n 'health.lastErrorCode': input.errorCode,\n updatedAtMs: input.nowMs,\n })\n .catch((error: unknown) => {\n console.error('[outreach] marking a mailbox reconnect_required failed', error)\n })\n}\n"],"names":["SecretBoxError","createGmailClient","mailboxCredentialsRef","mailboxRef","openMailboxRefreshToken","readMailboxCredentials","sealMailboxRefreshToken","readOutreachGoogleConfig","openOutreachMailboxClient","firestore","input","deps","config","readConfig","configured","ok","reason","snapshot","mailboxId","get","credential","exists","data","opened","keyring","error","needsReseal","resealMailboxRefreshToken","refreshToken","now","client","clientId","clientSecret","ref","runTransaction","tx","current","sealedRefreshToken","update","updatedAtMs","Date","console","markOutreachMailboxReconnectRequired","orgId","status","nowMs","errorCode","catch"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,cAAc,QAAQ,sCAAqC;AACpE,SAASC,iBAAiB,QAA0B,+BAA2B;AAE/E,SACEC,qBAAqB,EACrBC,UAAU,EACVC,uBAAuB,EACvBC,sBAAsB,EACtBC,uBAAuB,QAElB,2BAAuB;AAC9B,SACEC,wBAAwB,QAEnB,uBAAmB;AAiC1B,OAAO,eAAeC,0BACpBC,SAAsC,EACtCC,KAA4B,EAC5BC,OAAwB,CAAC,CAAC;QAEVA;IAAhB,MAAMC,SAAS,EAACD,mBAAAA,KAAKE,UAAU,YAAfF,mBAAmBJ;IACnC,IAAI,CAACK,OAAOE,UAAU,EAAE,OAAO;QAAEC,IAAI;QAAOC,QAAQ;IAAiB;IACrE,MAAMC,WAAW,MAAMf,sBAAsBO,WAAWC,MAAMQ,SAAS,EAAEC,GAAG;IAC5E,MAAMC,aAAaf,uBAAuBY,SAASI,MAAM,GAAGJ,SAASK,IAAI,KAAK;IAC9E,IAAI,CAACF,cAAcA,WAAWF,SAAS,KAAKR,MAAMQ,SAAS,EAAE;QAC3D,OAAO;YAAEH,IAAI;YAAOC,QAAQ;QAAgB;IAC9C;IACA,IAAIO;IACJ,IAAI;QACFA,SAASnB,wBAAwBgB,YAAYR,OAAOA,MAAM,CAACY,OAAO;IACpE,EAAE,OAAOC,OAAO;QACd,IAAIA,iBAAiBzB,gBAAgB,OAAO;YAAEe,IAAI;YAAOC,QAAQ;QAA0B;QAC3F,MAAMS;IACR;IACA,IAAIF,OAAOG,WAAW,EAAE;QACtB,MAAMC,0BAA0BlB,WAAWW,YAAYG,OAAOK,YAAY,EAAEhB,OAAOA,MAAM,CAACY,OAAO,EAAEb,KAAKkB,GAAG;IAC7G;IACA,MAAMC,SAAS7B,kBAAkB,aAC5BU;QACHoB,UAAUnB,OAAOA,MAAM,CAACmB,QAAQ;QAChCC,cAAcpB,OAAOA,MAAM,CAACoB,YAAY;QACxCJ,cAAcL,OAAOK,YAAY;;IAEnC,OAAO;QAAEb,IAAI;QAAMe;QAAQV;IAAW;AACxC;AAEA;;;;;CAKC,GACD,eAAeO,0BACblB,SAAsC,EACtCW,UAA4C,EAC5CQ,YAAoB,EACpBJ,OAAsD,EACtDK,GAA+B;IAE/B,MAAMI,MAAM/B,sBAAsBO,WAAWW,WAAWF,SAAS;IACjE,IAAI;QACF,MAAMT,UAAUyB,cAAc,CAAC,OAAOC;YACpC,MAAMC,UAAU,MAAMD,GAAGhB,GAAG,CAACc;YAC7B,IAAIG,QAAQjB,GAAG,CAAC,0BAA0BC,WAAWiB,kBAAkB,EAAE;YACzEF,GAAGG,MAAM,CAACL,KAAK,aACV3B,wBAAwBsB,cAAcR,WAAWF,SAAS,EAAEM;gBAC/De,aAAa,CAACV,cAAAA,MAAOW,KAAKX,GAAG;;QAEjC;IACF,EAAE,OAAOJ,OAAO;QACdgB,QAAQhB,KAAK,CAAC,+CAA+CA;IAC/D;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeiB,qCACpBjC,SAAsC,EACtCC,KAA6E;IAE7E,MAAMP,WAAWM,WAAWC,MAAMiC,KAAK,EAAEjC,MAAMQ,SAAS,EACrDoB,MAAM,CAAC;QACNM,QAAQ;QACR,wBAAwBlC,MAAMmC,KAAK;QACnC,wBAAwBnC,MAAMoC,SAAS;QACvCP,aAAa7B,MAAMmC,KAAK;IAC1B,GACCE,KAAK,CAAC,CAACtB;QACNgB,QAAQhB,KAAK,CAAC,0DAA0DA;IAC1E;AACJ"}
@@ -0,0 +1,41 @@
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 GOOGLE SENDS A REP BACK TO (AGL-2978).
19
+ *
20
+ * Google redirects only to an address registered on the OAuth client, matched
21
+ * exactly, so the redirect cannot follow whichever host a rep happened to open
22
+ * the console on: it is the deployment's canonical console origin,
23
+ * `NEXT_PUBLIC_CONSOLE_URL`, plus the callback route. That is the one address
24
+ * an operator registers.
25
+ *
26
+ * Local development is the one exception, and never in production: a request
27
+ * from `http://localhost` or `127.0.0.1` is sent back to itself, so a developer
28
+ * registering their local address on a test client is not bounced to the
29
+ * deployed console after consenting. Anything else a request claims about its
30
+ * origin is ignored — the host Google redirects to is where the authorization
31
+ * code arrives, and a request header does not get to choose it.
32
+ */
33
+ /** The callback's path on the console. */
34
+ export declare const OUTREACH_OAUTH_CALLBACK_PATH: string;
35
+ /** The canonical console origin, or `null` when it is unset or not http(s). */
36
+ export declare function canonicalConsoleOrigin(): string | null;
37
+ /**
38
+ * The redirect address for a connect started by `requestUrl`, or `null` when
39
+ * the deployment names no console origin to register.
40
+ */
41
+ export declare function outreachOAuthRedirectUri(requestUrl: string): string | null;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
17
+ /**
18
+ * WHERE GOOGLE SENDS A REP BACK TO (AGL-2978).
19
+ *
20
+ * Google redirects only to an address registered on the OAuth client, matched
21
+ * exactly, so the redirect cannot follow whichever host a rep happened to open
22
+ * the console on: it is the deployment's canonical console origin,
23
+ * `NEXT_PUBLIC_CONSOLE_URL`, plus the callback route. That is the one address
24
+ * an operator registers.
25
+ *
26
+ * Local development is the one exception, and never in production: a request
27
+ * from `http://localhost` or `127.0.0.1` is sent back to itself, so a developer
28
+ * registering their local address on a test client is not bounced to the
29
+ * deployed console after consenting. Anything else a request claims about its
30
+ * origin is ignored — the host Google redirects to is where the authorization
31
+ * code arrives, and a request header does not get to choose it.
32
+ */ /** The callback's path on the console. */ export const OUTREACH_OAUTH_CALLBACK_PATH = `/api/${OUTREACH_API_ROUTES.mailboxesOAuthCallback}`;
33
+ const stripTrailingSlash = (value)=>value.replace(/\/+$/, '');
34
+ /** The canonical console origin, or `null` when it is unset or not http(s). */ export function canonicalConsoleOrigin() {
35
+ var _process_env_NEXT_PUBLIC_CONSOLE_URL;
36
+ const raw = stripTrailingSlash(String((_process_env_NEXT_PUBLIC_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL) != null ? _process_env_NEXT_PUBLIC_CONSOLE_URL : '').trim());
37
+ if (!raw) return null;
38
+ try {
39
+ const url = new URL(raw);
40
+ return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : null;
41
+ } catch (unused) {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * The redirect address for a connect started by `requestUrl`, or `null` when
47
+ * the deployment names no console origin to register.
48
+ */ export function outreachOAuthRedirectUri(requestUrl) {
49
+ if (process.env['NODE_ENV'] !== 'production') {
50
+ try {
51
+ const url = new URL(requestUrl);
52
+ if (url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) {
53
+ return `${url.origin}${OUTREACH_OAUTH_CALLBACK_PATH}`;
54
+ }
55
+ } catch (unused) {
56
+ // Not a URL: fall through to the canonical origin.
57
+ }
58
+ }
59
+ const origin = canonicalConsoleOrigin();
60
+ return origin ? `${origin}${OUTREACH_OAUTH_CALLBACK_PATH}` : null;
61
+ }
62
+
63
+ //# sourceMappingURL=oauth-redirect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/oauth-redirect.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 { OUTREACH_API_ROUTES } from '../constants/api-routes'\n\n/**\n * WHERE GOOGLE SENDS A REP BACK TO (AGL-2978).\n *\n * Google redirects only to an address registered on the OAuth client, matched\n * exactly, so the redirect cannot follow whichever host a rep happened to open\n * the console on: it is the deployment's canonical console origin,\n * `NEXT_PUBLIC_CONSOLE_URL`, plus the callback route. That is the one address\n * an operator registers.\n *\n * Local development is the one exception, and never in production: a request\n * from `http://localhost` or `127.0.0.1` is sent back to itself, so a developer\n * registering their local address on a test client is not bounced to the\n * deployed console after consenting. Anything else a request claims about its\n * origin is ignored — the host Google redirects to is where the authorization\n * code arrives, and a request header does not get to choose it.\n */\n\n/** The callback's path on the console. */\nexport const OUTREACH_OAUTH_CALLBACK_PATH = `/api/${OUTREACH_API_ROUTES.mailboxesOAuthCallback}`\n\nconst stripTrailingSlash = (value: string) => value.replace(/\\/+$/, '')\n\n/** The canonical console origin, or `null` when it is unset or not http(s). */\nexport function canonicalConsoleOrigin(): string | null {\n const raw = stripTrailingSlash(String(process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim())\n if (!raw) return null\n try {\n const url = new URL(raw)\n return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : null\n } catch {\n return null\n }\n}\n\n/**\n * The redirect address for a connect started by `requestUrl`, or `null` when\n * the deployment names no console origin to register.\n */\nexport function outreachOAuthRedirectUri(requestUrl: string): string | null {\n if (process.env['NODE_ENV'] !== 'production') {\n try {\n const url = new URL(requestUrl)\n if (url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) {\n return `${url.origin}${OUTREACH_OAUTH_CALLBACK_PATH}`\n }\n } catch {\n // Not a URL: fall through to the canonical origin.\n }\n }\n const origin = canonicalConsoleOrigin()\n return origin ? `${origin}${OUTREACH_OAUTH_CALLBACK_PATH}` : null\n}\n"],"names":["OUTREACH_API_ROUTES","OUTREACH_OAUTH_CALLBACK_PATH","mailboxesOAuthCallback","stripTrailingSlash","value","replace","canonicalConsoleOrigin","process","raw","String","env","NEXT_PUBLIC_CONSOLE_URL","trim","url","URL","protocol","origin","outreachOAuthRedirectUri","requestUrl","hostname"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,6BAAyB;AAE7D;;;;;;;;;;;;;;;CAeC,GAED,wCAAwC,GACxC,OAAO,MAAMC,+BAA+B,CAAC,KAAK,EAAED,oBAAoBE,sBAAsB,EAAE,CAAA;AAEhG,MAAMC,qBAAqB,CAACC,QAAkBA,MAAMC,OAAO,CAAC,QAAQ;AAEpE,6EAA6E,GAC7E,OAAO,SAASC;QACwBC;IAAtC,MAAMC,MAAML,mBAAmBM,QAAOF,uCAAAA,QAAQG,GAAG,CAACC,uBAAuB,YAAnCJ,uCAAuC,IAAIK,IAAI;IACrF,IAAI,CAACJ,KAAK,OAAO;IACjB,IAAI;QACF,MAAMK,MAAM,IAAIC,IAAIN;QACpB,OAAOK,IAAIE,QAAQ,KAAK,YAAYF,IAAIE,QAAQ,KAAK,UAAUF,IAAIG,MAAM,GAAG;IAC9E,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,SAASC,yBAAyBC,UAAkB;IACzD,IAAIX,QAAQG,GAAG,CAAC,WAAW,KAAK,cAAc;QAC5C,IAAI;YACF,MAAMG,MAAM,IAAIC,IAAII;YACpB,IAAIL,IAAIE,QAAQ,KAAK,WAAYF,CAAAA,IAAIM,QAAQ,KAAK,eAAeN,IAAIM,QAAQ,KAAK,WAAU,GAAI;gBAC9F,OAAO,GAAGN,IAAIG,MAAM,GAAGf,8BAA8B;YACvD;QACF,EAAE,eAAM;QACN,mDAAmD;QACrD;IACF;IACA,MAAMe,SAASV;IACf,OAAOU,SAAS,GAAGA,SAASf,8BAA8B,GAAG;AAC/D"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * THE OAUTH `state` FOR A MAILBOX CONNECT (AGL-2978): signed, short-lived,
19
+ * single-use, and bound to one member of one organization.
20
+ *
21
+ * ## Signed
22
+ *
23
+ * `os1.<payload>.<signature>`, the payload `{ o: orgId, u: uid, n: nonce,
24
+ * e: expiry }` and the signature an HMAC under the shared, fail-closed
25
+ * `TOKEN_SIGNING_SECRET` with its own `outreach-oauth-state:` context — the
26
+ * construction every other signed token here uses, and domain-separated from
27
+ * all of them. Google hands the state back verbatim, so a state that verifies
28
+ * names the org and the member it was minted for. The console dispatcher
29
+ * reads exactly that to ask its release gate about the right organization on
30
+ * a redirect that carries no bearer token.
31
+ *
32
+ * ## Single-use
33
+ *
34
+ * A signature proves who minted a state, not that it is fresh. Each connect
35
+ * also writes ONE pending record per member per organization, at
36
+ * `orgs/{orgId}/outreachOAuthStates/{sha256(orgId, uid)}`, holding the
37
+ * SHA-256 of the state's nonce, its expiry and the redirect address the code
38
+ * was issued for. Finishing a connect consumes that record in a transaction
39
+ * (`consumeOnce`), so a second use of the same state finds nothing, and
40
+ * starting another connect replaces the record, retiring every earlier state.
41
+ *
42
+ * ## Under the organization
43
+ *
44
+ * The record is the organization's, so it lives in the organization's tree:
45
+ * the workspace erasure's recursive delete and the workspace export's walk
46
+ * reach it with no sweep of their own, and nothing outside this plugin names
47
+ * the collection. No Firestore rule names it either, and the org block has no
48
+ * catch-all, so every client is refused it — a client that could write one
49
+ * could re-arm a consumed state, and one that could read one would learn
50
+ * which members have a connect open.
51
+ *
52
+ * ## Derived, not stored
53
+ *
54
+ * The PKCE verifier and the OpenID `nonce` are HMACs of the state's nonce
55
+ * under the same secret, each with its own context. Nothing secret is stored
56
+ * for them, and only this server can recompute either — a state copied off a
57
+ * redirect is not enough to redeem its code.
58
+ */
59
+ export declare const OUTREACH_OAUTH_STATES_COLLECTION = "outreachOAuthStates";
60
+ /** How long a rep has to finish Google's consent screen. */
61
+ export declare const OUTREACH_OAUTH_STATE_TTL_MS: number;
62
+ export interface OutreachOAuthStateClaims {
63
+ orgId: string;
64
+ uid: string;
65
+ nonce: string;
66
+ /** Expiry, epoch ms. */
67
+ exp: number;
68
+ }
69
+ /**
70
+ * Mints a state for a member's connect. Throws when `TOKEN_SIGNING_SECRET` is
71
+ * not configured; the route answers that as a deployment that cannot connect.
72
+ */
73
+ export declare function mintOutreachOAuthState(input: {
74
+ orgId: string;
75
+ uid: string;
76
+ nowMs: number;
77
+ /** Test seam; a random 32 bytes otherwise. */
78
+ nonce?: string;
79
+ }): {
80
+ state: string;
81
+ claims: OutreachOAuthStateClaims;
82
+ };
83
+ export type OutreachOAuthStateRead = {
84
+ ok: true;
85
+ claims: OutreachOAuthStateClaims;
86
+ }
87
+ /** Authentic and past its expiry — the claims can still say whose it was. */
88
+ | {
89
+ ok: false;
90
+ refusal: 'state-expired';
91
+ claims: OutreachOAuthStateClaims;
92
+ }
93
+ /** Tampered, truncated, another version, or no secret to check it with. */
94
+ | {
95
+ ok: false;
96
+ refusal: 'state-invalid';
97
+ };
98
+ /** Verifies a state's signature and expiry. Never throws. */
99
+ export declare function readOutreachOAuthState(state: unknown, nowMs: number): OutreachOAuthStateRead;
100
+ /** The PKCE `code_verifier` for a state's nonce: 43 base64url characters. */
101
+ export declare function outreachPkceVerifier(nonce: string): string;
102
+ /** The OpenID `nonce` Google must echo in the ID token for this connect. */
103
+ export declare function outreachOidcNonce(nonce: string): string;
104
+ /** The pending record's id: one per member per organization. */
105
+ export declare function outreachOAuthStateDocId(orgId: string, uid: string): string;
106
+ /** `orgs/{orgId}/outreachOAuthStates/{id}` for a member's pending connect. */
107
+ export declare function outreachOAuthStateRef(firestore: FirebaseFirestore.Firestore, orgId: string, uid: string): FirebaseFirestore.DocumentReference;
108
+ /** `orgs/{orgId}/outreachOAuthStates/{id}` — see the module comment. */
109
+ export interface OutreachOAuthStateRecord {
110
+ orgId: string;
111
+ uid: string;
112
+ /** SHA-256 of the state's nonce; the nonce itself is never stored. */
113
+ nonceDigest: string;
114
+ /** The redirect address the authorization code was issued for. */
115
+ redirectUri: string;
116
+ expiresAtMs: number;
117
+ createdAtMs: number;
118
+ }
119
+ /**
120
+ * Writes the pending record for a freshly minted state, replacing any earlier
121
+ * one for the same member and organization.
122
+ */
123
+ export declare function recordOutreachOAuthState(firestore: FirebaseFirestore.Firestore, input: {
124
+ claims: OutreachOAuthStateClaims;
125
+ redirectUri: string;
126
+ nowMs: number;
127
+ }): Promise<void>;
128
+ export type OutreachOAuthStateConsumed = {
129
+ ok: true;
130
+ redirectUri: string;
131
+ }
132
+ /** No pending record: this state was already used, or never recorded. */
133
+ | {
134
+ ok: false;
135
+ refusal: 'state-replayed';
136
+ }
137
+ /** A later connect by the same member replaced this state. */
138
+ | {
139
+ ok: false;
140
+ refusal: 'state-superseded';
141
+ } | {
142
+ ok: false;
143
+ refusal: 'state-expired';
144
+ };
145
+ /**
146
+ * Consumes the pending record a verified state names — once. A superseded
147
+ * state leaves the newer record standing for the connect that owns it.
148
+ */
149
+ export declare function consumeOutreachOAuthState(firestore: FirebaseFirestore.Firestore, input: {
150
+ claims: OutreachOAuthStateClaims;
151
+ nowMs: number;
152
+ }): Promise<OutreachOAuthStateConsumed>;
@@ -0,0 +1,212 @@
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 { consumeOnce } from "@aglyn/tenant-data-admin/server/consume-once";
17
+ import { tokenSigningSecret } from "@aglyn/tenant-data-admin/server/media-signing";
18
+ import { safeEqual } from "@aglyn/tenant-data-admin/server/safe-equal";
19
+ import { createHash, createHmac, randomBytes } from "node:crypto";
20
+ /**
21
+ * THE OAUTH `state` FOR A MAILBOX CONNECT (AGL-2978): signed, short-lived,
22
+ * single-use, and bound to one member of one organization.
23
+ *
24
+ * ## Signed
25
+ *
26
+ * `os1.<payload>.<signature>`, the payload `{ o: orgId, u: uid, n: nonce,
27
+ * e: expiry }` and the signature an HMAC under the shared, fail-closed
28
+ * `TOKEN_SIGNING_SECRET` with its own `outreach-oauth-state:` context — the
29
+ * construction every other signed token here uses, and domain-separated from
30
+ * all of them. Google hands the state back verbatim, so a state that verifies
31
+ * names the org and the member it was minted for. The console dispatcher
32
+ * reads exactly that to ask its release gate about the right organization on
33
+ * a redirect that carries no bearer token.
34
+ *
35
+ * ## Single-use
36
+ *
37
+ * A signature proves who minted a state, not that it is fresh. Each connect
38
+ * also writes ONE pending record per member per organization, at
39
+ * `orgs/{orgId}/outreachOAuthStates/{sha256(orgId, uid)}`, holding the
40
+ * SHA-256 of the state's nonce, its expiry and the redirect address the code
41
+ * was issued for. Finishing a connect consumes that record in a transaction
42
+ * (`consumeOnce`), so a second use of the same state finds nothing, and
43
+ * starting another connect replaces the record, retiring every earlier state.
44
+ *
45
+ * ## Under the organization
46
+ *
47
+ * The record is the organization's, so it lives in the organization's tree:
48
+ * the workspace erasure's recursive delete and the workspace export's walk
49
+ * reach it with no sweep of their own, and nothing outside this plugin names
50
+ * the collection. No Firestore rule names it either, and the org block has no
51
+ * catch-all, so every client is refused it — a client that could write one
52
+ * could re-arm a consumed state, and one that could read one would learn
53
+ * which members have a connect open.
54
+ *
55
+ * ## Derived, not stored
56
+ *
57
+ * The PKCE verifier and the OpenID `nonce` are HMACs of the state's nonce
58
+ * under the same secret, each with its own context. Nothing secret is stored
59
+ * for them, and only this server can recompute either — a state copied off a
60
+ * redirect is not enough to redeem its code.
61
+ */ export const OUTREACH_OAUTH_STATES_COLLECTION = 'outreachOAuthStates';
62
+ /** How long a rep has to finish Google's consent screen. */ export const OUTREACH_OAUTH_STATE_TTL_MS = 10 * 60 * 1000;
63
+ const STATE_VERSION = 'os1';
64
+ const STATE_CONTEXT = 'outreach-oauth-state:v1';
65
+ const PKCE_CONTEXT = 'outreach-oauth-pkce:v1';
66
+ const OIDC_NONCE_CONTEXT = 'outreach-oauth-oidc-nonce:v1';
67
+ const STATE_MAX_CHARS = 1024;
68
+ const hmac = (context, value)=>createHmac('sha256', tokenSigningSecret()).update(`${context}:${value}`).digest('base64url');
69
+ /**
70
+ * Mints a state for a member's connect. Throws when `TOKEN_SIGNING_SECRET` is
71
+ * not configured; the route answers that as a deployment that cannot connect.
72
+ */ export function mintOutreachOAuthState(input) {
73
+ var _input_nonce;
74
+ const claims = {
75
+ orgId: input.orgId,
76
+ uid: input.uid,
77
+ nonce: (_input_nonce = input.nonce) != null ? _input_nonce : randomBytes(32).toString('base64url'),
78
+ exp: input.nowMs + OUTREACH_OAUTH_STATE_TTL_MS
79
+ };
80
+ const wire = {
81
+ o: claims.orgId,
82
+ u: claims.uid,
83
+ n: claims.nonce,
84
+ e: claims.exp
85
+ };
86
+ const payload = Buffer.from(JSON.stringify(wire), 'utf8').toString('base64url');
87
+ return {
88
+ state: `${STATE_VERSION}.${payload}.${hmac(STATE_CONTEXT, payload)}`,
89
+ claims
90
+ };
91
+ }
92
+ /** Verifies a state's signature and expiry. Never throws. */ export function readOutreachOAuthState(state, nowMs) {
93
+ const invalid = {
94
+ ok: false,
95
+ refusal: 'state-invalid'
96
+ };
97
+ if (typeof state !== 'string' || !state || state.length > STATE_MAX_CHARS) return invalid;
98
+ const parts = state.split('.');
99
+ if (parts.length !== 3 || parts[0] !== STATE_VERSION) return invalid;
100
+ const [, payload, signature] = parts;
101
+ let expected;
102
+ try {
103
+ expected = hmac(STATE_CONTEXT, payload);
104
+ } catch (unused) {
105
+ return invalid;
106
+ }
107
+ if (!safeEqual(signature, expected)) return invalid;
108
+ let wire;
109
+ try {
110
+ wire = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
111
+ } catch (unused) {
112
+ return invalid;
113
+ }
114
+ const claims = {
115
+ orgId: typeof (wire == null ? void 0 : wire.o) === 'string' ? wire.o : '',
116
+ uid: typeof (wire == null ? void 0 : wire.u) === 'string' ? wire.u : '',
117
+ nonce: typeof (wire == null ? void 0 : wire.n) === 'string' ? wire.n : '',
118
+ exp: Number(wire == null ? void 0 : wire.e)
119
+ };
120
+ if (!claims.orgId || !claims.uid || !claims.nonce || !Number.isFinite(claims.exp)) return invalid;
121
+ if (claims.exp <= nowMs) return {
122
+ ok: false,
123
+ refusal: 'state-expired',
124
+ claims
125
+ };
126
+ return {
127
+ ok: true,
128
+ claims
129
+ };
130
+ }
131
+ /** The PKCE `code_verifier` for a state's nonce: 43 base64url characters. */ export function outreachPkceVerifier(nonce) {
132
+ return hmac(PKCE_CONTEXT, nonce);
133
+ }
134
+ /** The OpenID `nonce` Google must echo in the ID token for this connect. */ export function outreachOidcNonce(nonce) {
135
+ return hmac(OIDC_NONCE_CONTEXT, nonce);
136
+ }
137
+ /** The pending record's id: one per member per organization. */ export function outreachOAuthStateDocId(orgId, uid) {
138
+ return createHash('sha256').update(`${orgId}\n${uid}`).digest('hex');
139
+ }
140
+ /** `orgs/{orgId}/outreachOAuthStates/{id}` for a member's pending connect. */ export function outreachOAuthStateRef(firestore, orgId, uid) {
141
+ return firestore.collection('orgs').doc(orgId).collection(OUTREACH_OAUTH_STATES_COLLECTION).doc(outreachOAuthStateDocId(orgId, uid));
142
+ }
143
+ const digest = (nonce)=>createHash('sha256').update(nonce).digest('base64url');
144
+ /**
145
+ * Writes the pending record for a freshly minted state, replacing any earlier
146
+ * one for the same member and organization.
147
+ */ export async function recordOutreachOAuthState(firestore, input) {
148
+ const { claims } = input;
149
+ const record = {
150
+ orgId: claims.orgId,
151
+ uid: claims.uid,
152
+ nonceDigest: digest(claims.nonce),
153
+ redirectUri: input.redirectUri,
154
+ expiresAtMs: claims.exp,
155
+ createdAtMs: input.nowMs
156
+ };
157
+ await outreachOAuthStateRef(firestore, claims.orgId, claims.uid).set(record);
158
+ }
159
+ /**
160
+ * Consumes the pending record a verified state names — once. A superseded
161
+ * state leaves the newer record standing for the connect that owns it.
162
+ */ export async function consumeOutreachOAuthState(firestore, input) {
163
+ const { claims } = input;
164
+ const ref = outreachOAuthStateRef(firestore, claims.orgId, claims.uid);
165
+ const result = await consumeOnce(firestore, ref, (data)=>{
166
+ var _data_nonceDigest, _data_redirectUri;
167
+ if (data['orgId'] !== claims.orgId || data['uid'] !== claims.uid) {
168
+ return {
169
+ accept: false,
170
+ reason: 'state-superseded'
171
+ };
172
+ }
173
+ if (!safeEqual(String((_data_nonceDigest = data['nonceDigest']) != null ? _data_nonceDigest : ''), digest(claims.nonce))) {
174
+ return {
175
+ accept: false,
176
+ reason: 'state-superseded'
177
+ };
178
+ }
179
+ if (Number(data['expiresAtMs']) <= input.nowMs) {
180
+ // Nobody can accept an expired record, so removing it destroys nothing
181
+ // a concurrent consume could have used.
182
+ return {
183
+ accept: false,
184
+ reason: 'state-expired',
185
+ remove: true
186
+ };
187
+ }
188
+ return {
189
+ accept: true,
190
+ value: String((_data_redirectUri = data['redirectUri']) != null ? _data_redirectUri : ''),
191
+ remove: true
192
+ };
193
+ });
194
+ if (result.ok && result.value) return {
195
+ ok: true,
196
+ redirectUri: result.value
197
+ };
198
+ if (result.reason === 'state-superseded') return {
199
+ ok: false,
200
+ refusal: 'state-superseded'
201
+ };
202
+ if (result.reason === 'state-expired') return {
203
+ ok: false,
204
+ refusal: 'state-expired'
205
+ };
206
+ return {
207
+ ok: false,
208
+ refusal: 'state-replayed'
209
+ };
210
+ }
211
+
212
+ //# sourceMappingURL=oauth-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/oauth-state.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 { consumeOnce } from '@aglyn/tenant-data-admin/server/consume-once'\nimport { tokenSigningSecret } from '@aglyn/tenant-data-admin/server/media-signing'\nimport { safeEqual } from '@aglyn/tenant-data-admin/server/safe-equal'\nimport { createHash, createHmac, randomBytes } from 'node:crypto'\n\n/**\n * THE OAUTH `state` FOR A MAILBOX CONNECT (AGL-2978): signed, short-lived,\n * single-use, and bound to one member of one organization.\n *\n * ## Signed\n *\n * `os1.<payload>.<signature>`, the payload `{ o: orgId, u: uid, n: nonce,\n * e: expiry }` and the signature an HMAC under the shared, fail-closed\n * `TOKEN_SIGNING_SECRET` with its own `outreach-oauth-state:` context — the\n * construction every other signed token here uses, and domain-separated from\n * all of them. Google hands the state back verbatim, so a state that verifies\n * names the org and the member it was minted for. The console dispatcher\n * reads exactly that to ask its release gate about the right organization on\n * a redirect that carries no bearer token.\n *\n * ## Single-use\n *\n * A signature proves who minted a state, not that it is fresh. Each connect\n * also writes ONE pending record per member per organization, at\n * `orgs/{orgId}/outreachOAuthStates/{sha256(orgId, uid)}`, holding the\n * SHA-256 of the state's nonce, its expiry and the redirect address the code\n * was issued for. Finishing a connect consumes that record in a transaction\n * (`consumeOnce`), so a second use of the same state finds nothing, and\n * starting another connect replaces the record, retiring every earlier state.\n *\n * ## Under the organization\n *\n * The record is the organization's, so it lives in the organization's tree:\n * the workspace erasure's recursive delete and the workspace export's walk\n * reach it with no sweep of their own, and nothing outside this plugin names\n * the collection. No Firestore rule names it either, and the org block has no\n * catch-all, so every client is refused it — a client that could write one\n * could re-arm a consumed state, and one that could read one would learn\n * which members have a connect open.\n *\n * ## Derived, not stored\n *\n * The PKCE verifier and the OpenID `nonce` are HMACs of the state's nonce\n * under the same secret, each with its own context. Nothing secret is stored\n * for them, and only this server can recompute either — a state copied off a\n * redirect is not enough to redeem its code.\n */\n\nexport const OUTREACH_OAUTH_STATES_COLLECTION = 'outreachOAuthStates'\n\n/** How long a rep has to finish Google's consent screen. */\nexport const OUTREACH_OAUTH_STATE_TTL_MS = 10 * 60 * 1000\n\nconst STATE_VERSION = 'os1'\nconst STATE_CONTEXT = 'outreach-oauth-state:v1'\nconst PKCE_CONTEXT = 'outreach-oauth-pkce:v1'\nconst OIDC_NONCE_CONTEXT = 'outreach-oauth-oidc-nonce:v1'\nconst STATE_MAX_CHARS = 1024\n\nexport interface OutreachOAuthStateClaims {\n orgId: string\n uid: string\n nonce: string\n /** Expiry, epoch ms. */\n exp: number\n}\n\ninterface WireClaims {\n o: string\n u: string\n n: string\n e: number\n}\n\nconst hmac = (context: string, value: string): string =>\n createHmac('sha256', tokenSigningSecret()).update(`${context}:${value}`).digest('base64url')\n\n/**\n * Mints a state for a member's connect. Throws when `TOKEN_SIGNING_SECRET` is\n * not configured; the route answers that as a deployment that cannot connect.\n */\nexport function mintOutreachOAuthState(input: {\n orgId: string\n uid: string\n nowMs: number\n /** Test seam; a random 32 bytes otherwise. */\n nonce?: string\n}): { state: string; claims: OutreachOAuthStateClaims } {\n const claims: OutreachOAuthStateClaims = {\n orgId: input.orgId,\n uid: input.uid,\n nonce: input.nonce ?? randomBytes(32).toString('base64url'),\n exp: input.nowMs + OUTREACH_OAUTH_STATE_TTL_MS,\n }\n const wire: WireClaims = { o: claims.orgId, u: claims.uid, n: claims.nonce, e: claims.exp }\n const payload = Buffer.from(JSON.stringify(wire), 'utf8').toString('base64url')\n return { state: `${STATE_VERSION}.${payload}.${hmac(STATE_CONTEXT, payload)}`, claims }\n}\n\nexport type OutreachOAuthStateRead =\n | { ok: true; claims: OutreachOAuthStateClaims }\n /** Authentic and past its expiry — the claims can still say whose it was. */\n | { ok: false; refusal: 'state-expired'; claims: OutreachOAuthStateClaims }\n /** Tampered, truncated, another version, or no secret to check it with. */\n | { ok: false; refusal: 'state-invalid' }\n\n/** Verifies a state's signature and expiry. Never throws. */\nexport function readOutreachOAuthState(state: unknown, nowMs: number): OutreachOAuthStateRead {\n const invalid = { ok: false, refusal: 'state-invalid' } as const\n if (typeof state !== 'string' || !state || state.length > STATE_MAX_CHARS) return invalid\n const parts = state.split('.')\n if (parts.length !== 3 || parts[0] !== STATE_VERSION) return invalid\n const [, payload, signature] = parts\n let expected: string\n try {\n expected = hmac(STATE_CONTEXT, payload)\n } catch {\n return invalid\n }\n if (!safeEqual(signature, expected)) return invalid\n let wire: Partial<WireClaims>\n try {\n wire = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Partial<WireClaims>\n } catch {\n return invalid\n }\n const claims: OutreachOAuthStateClaims = {\n orgId: typeof wire?.o === 'string' ? wire.o : '',\n uid: typeof wire?.u === 'string' ? wire.u : '',\n nonce: typeof wire?.n === 'string' ? wire.n : '',\n exp: Number(wire?.e),\n }\n if (!claims.orgId || !claims.uid || !claims.nonce || !Number.isFinite(claims.exp)) return invalid\n if (claims.exp <= nowMs) return { ok: false, refusal: 'state-expired', claims }\n return { ok: true, claims }\n}\n\n/** The PKCE `code_verifier` for a state's nonce: 43 base64url characters. */\nexport function outreachPkceVerifier(nonce: string): string {\n return hmac(PKCE_CONTEXT, nonce)\n}\n\n/** The OpenID `nonce` Google must echo in the ID token for this connect. */\nexport function outreachOidcNonce(nonce: string): string {\n return hmac(OIDC_NONCE_CONTEXT, nonce)\n}\n\n/** The pending record's id: one per member per organization. */\nexport function outreachOAuthStateDocId(orgId: string, uid: string): string {\n return createHash('sha256').update(`${orgId}\\n${uid}`).digest('hex')\n}\n\n/** `orgs/{orgId}/outreachOAuthStates/{id}` for a member's pending connect. */\nexport function outreachOAuthStateRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n uid: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_OAUTH_STATES_COLLECTION)\n .doc(outreachOAuthStateDocId(orgId, uid))\n}\n\nconst digest = (nonce: string) => createHash('sha256').update(nonce).digest('base64url')\n\n/** `orgs/{orgId}/outreachOAuthStates/{id}` — see the module comment. */\nexport interface OutreachOAuthStateRecord {\n orgId: string\n uid: string\n /** SHA-256 of the state's nonce; the nonce itself is never stored. */\n nonceDigest: string\n /** The redirect address the authorization code was issued for. */\n redirectUri: string\n expiresAtMs: number\n createdAtMs: number\n}\n\n/**\n * Writes the pending record for a freshly minted state, replacing any earlier\n * one for the same member and organization.\n */\nexport async function recordOutreachOAuthState(\n firestore: FirebaseFirestore.Firestore,\n input: { claims: OutreachOAuthStateClaims; redirectUri: string; nowMs: number },\n): Promise<void> {\n const { claims } = input\n const record: OutreachOAuthStateRecord = {\n orgId: claims.orgId,\n uid: claims.uid,\n nonceDigest: digest(claims.nonce),\n redirectUri: input.redirectUri,\n expiresAtMs: claims.exp,\n createdAtMs: input.nowMs,\n }\n await outreachOAuthStateRef(firestore, claims.orgId, claims.uid).set(record)\n}\n\nexport type OutreachOAuthStateConsumed =\n | { ok: true; redirectUri: string }\n /** No pending record: this state was already used, or never recorded. */\n | { ok: false; refusal: 'state-replayed' }\n /** A later connect by the same member replaced this state. */\n | { ok: false; refusal: 'state-superseded' }\n | { ok: false; refusal: 'state-expired' }\n\n/**\n * Consumes the pending record a verified state names — once. A superseded\n * state leaves the newer record standing for the connect that owns it.\n */\nexport async function consumeOutreachOAuthState(\n firestore: FirebaseFirestore.Firestore,\n input: { claims: OutreachOAuthStateClaims; nowMs: number },\n): Promise<OutreachOAuthStateConsumed> {\n const { claims } = input\n const ref = outreachOAuthStateRef(firestore, claims.orgId, claims.uid)\n const result = await consumeOnce<string>(firestore, ref, (data) => {\n if (data['orgId'] !== claims.orgId || data['uid'] !== claims.uid) {\n return { accept: false, reason: 'state-superseded' }\n }\n if (!safeEqual(String(data['nonceDigest'] ?? ''), digest(claims.nonce))) {\n return { accept: false, reason: 'state-superseded' }\n }\n if (Number(data['expiresAtMs']) <= input.nowMs) {\n // Nobody can accept an expired record, so removing it destroys nothing\n // a concurrent consume could have used.\n return { accept: false, reason: 'state-expired', remove: true }\n }\n return { accept: true, value: String(data['redirectUri'] ?? ''), remove: true }\n })\n if (result.ok && result.value) return { ok: true, redirectUri: result.value }\n if (result.reason === 'state-superseded') return { ok: false, refusal: 'state-superseded' }\n if (result.reason === 'state-expired') return { ok: false, refusal: 'state-expired' }\n return { ok: false, refusal: 'state-replayed' }\n}\n"],"names":["consumeOnce","tokenSigningSecret","safeEqual","createHash","createHmac","randomBytes","OUTREACH_OAUTH_STATES_COLLECTION","OUTREACH_OAUTH_STATE_TTL_MS","STATE_VERSION","STATE_CONTEXT","PKCE_CONTEXT","OIDC_NONCE_CONTEXT","STATE_MAX_CHARS","hmac","context","value","update","digest","mintOutreachOAuthState","input","claims","orgId","uid","nonce","toString","exp","nowMs","wire","o","u","n","e","payload","Buffer","from","JSON","stringify","state","readOutreachOAuthState","invalid","ok","refusal","length","parts","split","signature","expected","parse","Number","isFinite","outreachPkceVerifier","outreachOidcNonce","outreachOAuthStateDocId","outreachOAuthStateRef","firestore","collection","doc","recordOutreachOAuthState","record","nonceDigest","redirectUri","expiresAtMs","createdAtMs","set","consumeOutreachOAuthState","ref","result","data","accept","reason","String","remove"],"mappings":"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"}