@aglyn/tenant-data-admin 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 (374) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/jest.integration.config.d.ts +39 -0
  4. package/jest.integration.setup.d.ts +17 -0
  5. package/package.json +51 -0
  6. package/src/index.d.ts +17 -0
  7. package/src/index.js +18 -0
  8. package/src/index.js.map +1 -0
  9. package/src/lib/render-cache.d.ts +184 -0
  10. package/src/lib/render-cache.js +198 -0
  11. package/src/lib/render-cache.js.map +1 -0
  12. package/src/lib/server/account-addresses.d.ts +160 -0
  13. package/src/lib/server/account-addresses.js +311 -0
  14. package/src/lib/server/account-addresses.js.map +1 -0
  15. package/src/lib/server/account-emails.d.ts +219 -0
  16. package/src/lib/server/account-emails.js +640 -0
  17. package/src/lib/server/account-emails.js.map +1 -0
  18. package/src/lib/server/admin-audit.d.ts +180 -0
  19. package/src/lib/server/admin-audit.js +233 -0
  20. package/src/lib/server/admin-audit.js.map +1 -0
  21. package/src/lib/server/analytics-retention.d.ts +63 -0
  22. package/src/lib/server/analytics-retention.js +66 -0
  23. package/src/lib/server/analytics-retention.js.map +1 -0
  24. package/src/lib/server/api-http.d.ts +159 -0
  25. package/src/lib/server/api-http.js +230 -0
  26. package/src/lib/server/api-http.js.map +1 -0
  27. package/src/lib/server/api-keys.d.ts +117 -0
  28. package/src/lib/server/api-keys.js +298 -0
  29. package/src/lib/server/api-keys.js.map +1 -0
  30. package/src/lib/server/auth-handoff.d.ts +165 -0
  31. package/src/lib/server/auth-handoff.js +482 -0
  32. package/src/lib/server/auth-handoff.js.map +1 -0
  33. package/src/lib/server/auth-pools.d.ts +246 -0
  34. package/src/lib/server/auth-pools.js +551 -0
  35. package/src/lib/server/auth-pools.js.map +1 -0
  36. package/src/lib/server/billing-addons.d.ts +171 -0
  37. package/src/lib/server/billing-addons.js +277 -0
  38. package/src/lib/server/billing-addons.js.map +1 -0
  39. package/src/lib/server/campaign-attribution-store.d.ts +55 -0
  40. package/src/lib/server/campaign-attribution-store.js +95 -0
  41. package/src/lib/server/campaign-attribution-store.js.map +1 -0
  42. package/src/lib/server/campaign-conversion-attribution.d.ts +205 -0
  43. package/src/lib/server/campaign-conversion-attribution.js +279 -0
  44. package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
  45. package/src/lib/server/client-error-report.d.ts +301 -0
  46. package/src/lib/server/client-error-report.js +755 -0
  47. package/src/lib/server/client-error-report.js.map +1 -0
  48. package/src/lib/server/connect-account-status.d.ts +74 -0
  49. package/src/lib/server/connect-account-status.js +94 -0
  50. package/src/lib/server/connect-account-status.js.map +1 -0
  51. package/src/lib/server/connect-payout-failure.d.ts +103 -0
  52. package/src/lib/server/connect-payout-failure.js +127 -0
  53. package/src/lib/server/connect-payout-failure.js.map +1 -0
  54. package/src/lib/server/console-api-rate-limit.d.ts +81 -0
  55. package/src/lib/server/console-api-rate-limit.js +111 -0
  56. package/src/lib/server/console-api-rate-limit.js.map +1 -0
  57. package/src/lib/server/console-domains.d.ts +326 -0
  58. package/src/lib/server/console-domains.js +779 -0
  59. package/src/lib/server/console-domains.js.map +1 -0
  60. package/src/lib/server/consume-once.d.ts +91 -0
  61. package/src/lib/server/consume-once.js +96 -0
  62. package/src/lib/server/consume-once.js.map +1 -0
  63. package/src/lib/server/contact-company-link.d.ts +85 -0
  64. package/src/lib/server/contact-company-link.js +105 -0
  65. package/src/lib/server/contact-company-link.js.map +1 -0
  66. package/src/lib/server/contact-email-engagement.d.ts +32 -0
  67. package/src/lib/server/contact-email-engagement.js +145 -0
  68. package/src/lib/server/contact-email-engagement.js.map +1 -0
  69. package/src/lib/server/contact-email-index.d.ts +56 -0
  70. package/src/lib/server/contact-email-index.js +158 -0
  71. package/src/lib/server/contact-email-index.js.map +1 -0
  72. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  73. package/src/lib/server/contact-lifecycle-floor.js +88 -0
  74. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  75. package/src/lib/server/contact-merge.d.ts +55 -0
  76. package/src/lib/server/contact-merge.js +279 -0
  77. package/src/lib/server/contact-merge.js.map +1 -0
  78. package/src/lib/server/contact-suppression.d.ts +153 -0
  79. package/src/lib/server/contact-suppression.js +259 -0
  80. package/src/lib/server/contact-suppression.js.map +1 -0
  81. package/src/lib/server/crm-booking-activity.d.ts +86 -0
  82. package/src/lib/server/crm-booking-activity.js +251 -0
  83. package/src/lib/server/crm-booking-activity.js.map +1 -0
  84. package/src/lib/server/crm-email-activity.d.ts +112 -0
  85. package/src/lib/server/crm-email-activity.js +143 -0
  86. package/src/lib/server/crm-email-activity.js.map +1 -0
  87. package/src/lib/server/crm-inbound-email.d.ts +145 -0
  88. package/src/lib/server/crm-inbound-email.js +287 -0
  89. package/src/lib/server/crm-inbound-email.js.map +1 -0
  90. package/src/lib/server/crm-next-activity.d.ts +74 -0
  91. package/src/lib/server/crm-next-activity.js +194 -0
  92. package/src/lib/server/crm-next-activity.js.map +1 -0
  93. package/src/lib/server/crm-records.d.ts +144 -0
  94. package/src/lib/server/crm-records.js +218 -0
  95. package/src/lib/server/crm-records.js.map +1 -0
  96. package/src/lib/server/csp-aggregate.d.ts +62 -0
  97. package/src/lib/server/csp-aggregate.js +276 -0
  98. package/src/lib/server/csp-aggregate.js.map +1 -0
  99. package/src/lib/server/data-storage-gate.d.ts +66 -0
  100. package/src/lib/server/data-storage-gate.js +48 -0
  101. package/src/lib/server/data-storage-gate.js.map +1 -0
  102. package/src/lib/server/dataset-live-pages.d.ts +179 -0
  103. package/src/lib/server/dataset-live-pages.js +421 -0
  104. package/src/lib/server/dataset-live-pages.js.map +1 -0
  105. package/src/lib/server/dns-probe.d.ts +53 -0
  106. package/src/lib/server/dns-probe.js +184 -0
  107. package/src/lib/server/dns-probe.js.map +1 -0
  108. package/src/lib/server/document-id.d.ts +29 -0
  109. package/src/lib/server/document-id.js +100 -0
  110. package/src/lib/server/document-id.js.map +1 -0
  111. package/src/lib/server/domain-provider-vercel.d.ts +33 -0
  112. package/src/lib/server/domain-provider-vercel.js +298 -0
  113. package/src/lib/server/domain-provider-vercel.js.map +1 -0
  114. package/src/lib/server/domain-provider-webhook.d.ts +73 -0
  115. package/src/lib/server/domain-provider-webhook.js +223 -0
  116. package/src/lib/server/domain-provider-webhook.js.map +1 -0
  117. package/src/lib/server/domain-provider-wildcard.d.ts +54 -0
  118. package/src/lib/server/domain-provider-wildcard.js +163 -0
  119. package/src/lib/server/domain-provider-wildcard.js.map +1 -0
  120. package/src/lib/server/domain-provider.d.ts +197 -0
  121. package/src/lib/server/domain-provider.js +221 -0
  122. package/src/lib/server/domain-provider.js.map +1 -0
  123. package/src/lib/server/duplicate-activity.d.ts +54 -0
  124. package/src/lib/server/duplicate-activity.js +67 -0
  125. package/src/lib/server/duplicate-activity.js.map +1 -0
  126. package/src/lib/server/duplicate-resource.d.ts +55 -0
  127. package/src/lib/server/duplicate-resource.js +474 -0
  128. package/src/lib/server/duplicate-resource.js.map +1 -0
  129. package/src/lib/server/dynamic-list-materialize.d.ts +153 -0
  130. package/src/lib/server/dynamic-list-materialize.js +581 -0
  131. package/src/lib/server/dynamic-list-materialize.js.map +1 -0
  132. package/src/lib/server/edit-access-authz.d.ts +56 -0
  133. package/src/lib/server/edit-access-authz.js +107 -0
  134. package/src/lib/server/edit-access-authz.js.map +1 -0
  135. package/src/lib/server/edit-access-token.d.ts +72 -0
  136. package/src/lib/server/edit-access-token.js +116 -0
  137. package/src/lib/server/edit-access-token.js.map +1 -0
  138. package/src/lib/server/edit-hint-token.d.ts +83 -0
  139. package/src/lib/server/edit-hint-token.js +119 -0
  140. package/src/lib/server/edit-hint-token.js.map +1 -0
  141. package/src/lib/server/email-delivery-log.d.ts +509 -0
  142. package/src/lib/server/email-delivery-log.js +1043 -0
  143. package/src/lib/server/email-delivery-log.js.map +1 -0
  144. package/src/lib/server/email-flow-gate.d.ts +80 -0
  145. package/src/lib/server/email-flow-gate.js +153 -0
  146. package/src/lib/server/email-flow-gate.js.map +1 -0
  147. package/src/lib/server/email-marketing-gate.d.ts +333 -0
  148. package/src/lib/server/email-marketing-gate.js +639 -0
  149. package/src/lib/server/email-marketing-gate.js.map +1 -0
  150. package/src/lib/server/email-metering.d.ts +293 -0
  151. package/src/lib/server/email-metering.js +359 -0
  152. package/src/lib/server/email-metering.js.map +1 -0
  153. package/src/lib/server/email-revenue-attribution.d.ts +185 -0
  154. package/src/lib/server/email-revenue-attribution.js +300 -0
  155. package/src/lib/server/email-revenue-attribution.js.map +1 -0
  156. package/src/lib/server/email-send-rate.d.ts +232 -0
  157. package/src/lib/server/email-send-rate.js +417 -0
  158. package/src/lib/server/email-send-rate.js.map +1 -0
  159. package/src/lib/server/email-sender-reputation.d.ts +317 -0
  160. package/src/lib/server/email-sender-reputation.js +464 -0
  161. package/src/lib/server/email-sender-reputation.js.map +1 -0
  162. package/src/lib/server/email-suppression.d.ts +490 -0
  163. package/src/lib/server/email-suppression.js +648 -0
  164. package/src/lib/server/email-suppression.js.map +1 -0
  165. package/src/lib/server/email-topic-confirmation.d.ts +150 -0
  166. package/src/lib/server/email-topic-confirmation.js +272 -0
  167. package/src/lib/server/email-topic-confirmation.js.map +1 -0
  168. package/src/lib/server/email-unsubscribe-link.d.ts +174 -0
  169. package/src/lib/server/email-unsubscribe-link.js +236 -0
  170. package/src/lib/server/email-unsubscribe-link.js.map +1 -0
  171. package/src/lib/server/erase-person.d.ts +116 -0
  172. package/src/lib/server/erase-person.js +272 -0
  173. package/src/lib/server/erase-person.js.map +1 -0
  174. package/src/lib/server/erase.d.ts +458 -0
  175. package/src/lib/server/erase.js +1506 -0
  176. package/src/lib/server/erase.js.map +1 -0
  177. package/src/lib/server/firebase-admin.d.ts +71 -0
  178. package/src/lib/server/firebase-admin.js +343 -0
  179. package/src/lib/server/firebase-admin.js.map +1 -0
  180. package/src/lib/server/form-dataset-binding-token.d.ts +35 -0
  181. package/src/lib/server/form-dataset-binding-token.js +107 -0
  182. package/src/lib/server/form-dataset-binding-token.js.map +1 -0
  183. package/src/lib/server/free-workspace-cap.d.ts +201 -0
  184. package/src/lib/server/free-workspace-cap.js +404 -0
  185. package/src/lib/server/free-workspace-cap.js.map +1 -0
  186. package/src/lib/server/ga4-measurement-protocol.d.ts +256 -0
  187. package/src/lib/server/ga4-measurement-protocol.js +558 -0
  188. package/src/lib/server/ga4-measurement-protocol.js.map +1 -0
  189. package/src/lib/server/host-email-tokens.d.ts +47 -0
  190. package/src/lib/server/host-email-tokens.js +72 -0
  191. package/src/lib/server/host-email-tokens.js.map +1 -0
  192. package/src/lib/server/host-memberships.d.ts +54 -0
  193. package/src/lib/server/host-memberships.js +194 -0
  194. package/src/lib/server/host-memberships.js.map +1 -0
  195. package/src/lib/server/host-sending-domain.d.ts +292 -0
  196. package/src/lib/server/host-sending-domain.js +451 -0
  197. package/src/lib/server/host-sending-domain.js.map +1 -0
  198. package/src/lib/server/host-visitor-records.d.ts +150 -0
  199. package/src/lib/server/host-visitor-records.js +262 -0
  200. package/src/lib/server/host-visitor-records.js.map +1 -0
  201. package/src/lib/server/id-token-refusal.d.ts +28 -0
  202. package/src/lib/server/id-token-refusal.js +111 -0
  203. package/src/lib/server/id-token-refusal.js.map +1 -0
  204. package/src/lib/server/legal-acceptance.d.ts +214 -0
  205. package/src/lib/server/legal-acceptance.js +305 -0
  206. package/src/lib/server/legal-acceptance.js.map +1 -0
  207. package/src/lib/server/list-members.d.ts +211 -0
  208. package/src/lib/server/list-members.js +176 -0
  209. package/src/lib/server/list-members.js.map +1 -0
  210. package/src/lib/server/live-page-usage.d.ts +202 -0
  211. package/src/lib/server/live-page-usage.js +300 -0
  212. package/src/lib/server/live-page-usage.js.map +1 -0
  213. package/src/lib/server/lockdown.d.ts +313 -0
  214. package/src/lib/server/lockdown.js +643 -0
  215. package/src/lib/server/lockdown.js.map +1 -0
  216. package/src/lib/server/media-cdn-rate-limit.d.ts +188 -0
  217. package/src/lib/server/media-cdn-rate-limit.js +204 -0
  218. package/src/lib/server/media-cdn-rate-limit.js.map +1 -0
  219. package/src/lib/server/media-delivery.d.ts +267 -0
  220. package/src/lib/server/media-delivery.js +485 -0
  221. package/src/lib/server/media-delivery.js.map +1 -0
  222. package/src/lib/server/media-download-tokens.d.ts +161 -0
  223. package/src/lib/server/media-download-tokens.js +386 -0
  224. package/src/lib/server/media-download-tokens.js.map +1 -0
  225. package/src/lib/server/media-quarantine.d.ts +127 -0
  226. package/src/lib/server/media-quarantine.js +170 -0
  227. package/src/lib/server/media-quarantine.js.map +1 -0
  228. package/src/lib/server/media-signing.d.ts +126 -0
  229. package/src/lib/server/media-signing.js +159 -0
  230. package/src/lib/server/media-signing.js.map +1 -0
  231. package/src/lib/server/media-storage-path.d.ts +96 -0
  232. package/src/lib/server/media-storage-path.js +111 -0
  233. package/src/lib/server/media-storage-path.js.map +1 -0
  234. package/src/lib/server/media-strong-digest.d.ts +85 -0
  235. package/src/lib/server/media-strong-digest.js +190 -0
  236. package/src/lib/server/media-strong-digest.js.map +1 -0
  237. package/src/lib/server/media-tombstone.d.ts +276 -0
  238. package/src/lib/server/media-tombstone.js +421 -0
  239. package/src/lib/server/media-tombstone.js.map +1 -0
  240. package/src/lib/server/media-variants.d.ts +309 -0
  241. package/src/lib/server/media-variants.js +428 -0
  242. package/src/lib/server/media-variants.js.map +1 -0
  243. package/src/lib/server/member-email-aliases.d.ts +180 -0
  244. package/src/lib/server/member-email-aliases.js +420 -0
  245. package/src/lib/server/member-email-aliases.js.map +1 -0
  246. package/src/lib/server/member-photo.d.ts +88 -0
  247. package/src/lib/server/member-photo.js +185 -0
  248. package/src/lib/server/member-photo.js.map +1 -0
  249. package/src/lib/server/membership-recover-throttle.d.ts +211 -0
  250. package/src/lib/server/membership-recover-throttle.js +231 -0
  251. package/src/lib/server/membership-recover-throttle.js.map +1 -0
  252. package/src/lib/server/notifications.d.ts +55 -0
  253. package/src/lib/server/notifications.js +134 -0
  254. package/src/lib/server/notifications.js.map +1 -0
  255. package/src/lib/server/org-billing.d.ts +97 -0
  256. package/src/lib/server/org-billing.js +236 -0
  257. package/src/lib/server/org-billing.js.map +1 -0
  258. package/src/lib/server/organizations.d.ts +837 -0
  259. package/src/lib/server/organizations.js +1814 -0
  260. package/src/lib/server/organizations.js.map +1 -0
  261. package/src/lib/server/password-reset-throttle.d.ts +84 -0
  262. package/src/lib/server/password-reset-throttle.js +102 -0
  263. package/src/lib/server/password-reset-throttle.js.map +1 -0
  264. package/src/lib/server/personal-data-export.d.ts +241 -0
  265. package/src/lib/server/personal-data-export.js +858 -0
  266. package/src/lib/server/personal-data-export.js.map +1 -0
  267. package/src/lib/server/platform-domain-names.d.ts +62 -0
  268. package/src/lib/server/platform-domain-names.js +197 -0
  269. package/src/lib/server/platform-domain-names.js.map +1 -0
  270. package/src/lib/server/platform-marketing-consent.d.ts +133 -0
  271. package/src/lib/server/platform-marketing-consent.js +209 -0
  272. package/src/lib/server/platform-marketing-consent.js.map +1 -0
  273. package/src/lib/server/rate-limit-store.d.ts +702 -0
  274. package/src/lib/server/rate-limit-store.js +1220 -0
  275. package/src/lib/server/rate-limit-store.js.map +1 -0
  276. package/src/lib/server/realm-plugins.d.ts +67 -0
  277. package/src/lib/server/realm-plugins.js +250 -0
  278. package/src/lib/server/realm-plugins.js.map +1 -0
  279. package/src/lib/server/recaptcha-allowlist.d.ts +90 -0
  280. package/src/lib/server/recaptcha-allowlist.js +409 -0
  281. package/src/lib/server/recaptcha-allowlist.js.map +1 -0
  282. package/src/lib/server/release-flags.d.ts +105 -0
  283. package/src/lib/server/release-flags.js +183 -0
  284. package/src/lib/server/release-flags.js.map +1 -0
  285. package/src/lib/server/replay-erasures.d.ts +126 -0
  286. package/src/lib/server/replay-erasures.js +217 -0
  287. package/src/lib/server/replay-erasures.js.map +1 -0
  288. package/src/lib/server/resolve-people.d.ts +66 -0
  289. package/src/lib/server/resolve-people.js +112 -0
  290. package/src/lib/server/resolve-people.js.map +1 -0
  291. package/src/lib/server/safe-equal.d.ts +45 -0
  292. package/src/lib/server/safe-equal.js +58 -0
  293. package/src/lib/server/safe-equal.js.map +1 -0
  294. package/src/lib/server/sending-domain-debt.d.ts +127 -0
  295. package/src/lib/server/sending-domain-debt.js +268 -0
  296. package/src/lib/server/sending-domain-debt.js.map +1 -0
  297. package/src/lib/server/sending-domain-recheck.d.ts +151 -0
  298. package/src/lib/server/sending-domain-recheck.js +253 -0
  299. package/src/lib/server/sending-domain-recheck.js.map +1 -0
  300. package/src/lib/server/sending-domains.d.ts +364 -0
  301. package/src/lib/server/sending-domains.js +683 -0
  302. package/src/lib/server/sending-domains.js.map +1 -0
  303. package/src/lib/server/serve-media-cdn.d.ts +433 -0
  304. package/src/lib/server/serve-media-cdn.js +1370 -0
  305. package/src/lib/server/serve-media-cdn.js.map +1 -0
  306. package/src/lib/server/serve-plugin-fetch.d.ts +61 -0
  307. package/src/lib/server/serve-plugin-fetch.js +294 -0
  308. package/src/lib/server/serve-plugin-fetch.js.map +1 -0
  309. package/src/lib/server/sms-keywords.d.ts +63 -0
  310. package/src/lib/server/sms-keywords.js +146 -0
  311. package/src/lib/server/sms-keywords.js.map +1 -0
  312. package/src/lib/server/sso-break-glass-owners.d.ts +91 -0
  313. package/src/lib/server/sso-break-glass-owners.js +227 -0
  314. package/src/lib/server/sso-break-glass-owners.js.map +1 -0
  315. package/src/lib/server/sso-domain-policy.d.ts +140 -0
  316. package/src/lib/server/sso-domain-policy.js +239 -0
  317. package/src/lib/server/sso-domain-policy.js.map +1 -0
  318. package/src/lib/server/sso-drift-logic.d.ts +106 -0
  319. package/src/lib/server/sso-drift-logic.js +95 -0
  320. package/src/lib/server/sso-drift-logic.js.map +1 -0
  321. package/src/lib/server/sso-enforcement.d.ts +204 -0
  322. package/src/lib/server/sso-enforcement.js +309 -0
  323. package/src/lib/server/sso-enforcement.js.map +1 -0
  324. package/src/lib/server/sso-provisioning.d.ts +320 -0
  325. package/src/lib/server/sso-provisioning.js +715 -0
  326. package/src/lib/server/sso-provisioning.js.map +1 -0
  327. package/src/lib/server/staff-alert-email.d.ts +41 -0
  328. package/src/lib/server/staff-alert-email.js +63 -0
  329. package/src/lib/server/staff-alert-email.js.map +1 -0
  330. package/src/lib/server/stripe-account-mode.d.ts +156 -0
  331. package/src/lib/server/stripe-account-mode.js +162 -0
  332. package/src/lib/server/stripe-account-mode.js.map +1 -0
  333. package/src/lib/server/tenant-write-lockdown.d.ts +131 -0
  334. package/src/lib/server/tenant-write-lockdown.js +176 -0
  335. package/src/lib/server/tenant-write-lockdown.js.map +1 -0
  336. package/src/lib/server/test-firestore.d.ts +45 -0
  337. package/src/lib/server/test-firestore.js +124 -0
  338. package/src/lib/server/test-firestore.js.map +1 -0
  339. package/src/lib/server/token-revocation.d.ts +140 -0
  340. package/src/lib/server/token-revocation.js +255 -0
  341. package/src/lib/server/token-revocation.js.map +1 -0
  342. package/src/lib/server/update-existing.d.ts +32 -0
  343. package/src/lib/server/update-existing.js +73 -0
  344. package/src/lib/server/update-existing.js.map +1 -0
  345. package/src/lib/server/upload-cors-reconcile.d.ts +118 -0
  346. package/src/lib/server/upload-cors-reconcile.js +234 -0
  347. package/src/lib/server/upload-cors-reconcile.js.map +1 -0
  348. package/src/lib/server/upsert-contact.d.ts +262 -0
  349. package/src/lib/server/upsert-contact.js +546 -0
  350. package/src/lib/server/upsert-contact.js.map +1 -0
  351. package/src/lib/server/usage-invoice.d.ts +203 -0
  352. package/src/lib/server/usage-invoice.js +397 -0
  353. package/src/lib/server/usage-invoice.js.map +1 -0
  354. package/src/lib/server/user-profiles.d.ts +151 -0
  355. package/src/lib/server/user-profiles.js +364 -0
  356. package/src/lib/server/user-profiles.js.map +1 -0
  357. package/src/lib/server/vercel-drain-signature.d.ts +30 -0
  358. package/src/lib/server/vercel-drain-signature.js +61 -0
  359. package/src/lib/server/vercel-drain-signature.js.map +1 -0
  360. package/src/lib/server/vercel-log-drain.d.ts +259 -0
  361. package/src/lib/server/vercel-log-drain.js +479 -0
  362. package/src/lib/server/vercel-log-drain.js.map +1 -0
  363. package/src/lib/server/verify-email-cooldown.d.ts +102 -0
  364. package/src/lib/server/verify-email-cooldown.js +114 -0
  365. package/src/lib/server/verify-email-cooldown.js.map +1 -0
  366. package/src/lib/server/visitor-write-rate-limit.d.ts +87 -0
  367. package/src/lib/server/visitor-write-rate-limit.js +117 -0
  368. package/src/lib/server/visitor-write-rate-limit.js.map +1 -0
  369. package/src/lib/server/workspace-domains.d.ts +156 -0
  370. package/src/lib/server/workspace-domains.js +271 -0
  371. package/src/lib/server/workspace-domains.js.map +1 -0
  372. package/src/tenancy.d.ts +115 -0
  373. package/src/tenancy.js +142 -0
  374. package/src/tenancy.js.map +1 -0
@@ -0,0 +1,287 @@
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 { buildCrmCapturedEmailActivity, consentGroupForHost, contactCaptureHostIds, crmActivityLogHasRoom, crmCapturedEmailKey, crmInboundCandidates, crmInboundExcerpt, crmInboundRoster, crmScopeTokens, emailAddressOf, emailDomainOf, findMemberByEmailAddress, forwardedSection, htmlToPlainText, isCrmInboundAddress, isCrmInboundToken, mintCrmInboundToken, personKey, readContactFacet } from "@aglyn/aglyn/server";
18
+ import { MEMBER_EMAIL_ALIASES_COLLECTION } from "@aglyn/aglyn/app-utils/member-email-aliases";
19
+ import { findContactByEmail } from "./contact-email-index.js";
20
+ import { createCrmEmailActivity, crmCapturedEmailActivityRef, newCrmActivityRef } from "./crm-email-activity.js";
21
+ import { countCrmActivitiesForRecord } from "./crm-records.js";
22
+ /**
23
+ * EMAIL CAPTURE (AGL-2657): the half with Firestore.
24
+ *
25
+ * The org's capture token — minted once, replaced on a rotation, found
26
+ * again from a message's recipient — and the filing of one received
27
+ * message: which record it was with, whether it has been filed before, and
28
+ * the row it becomes. The pure rules are `@aglyn/aglyn`'s
29
+ * `crm-inbound.ts`; this module is the reads and the one write.
30
+ *
31
+ * Firestore is a parameter throughout, as in `crm-records.ts` and
32
+ * `crm-email-activity.ts`: the routes hold one, and a module that reached
33
+ * for the Admin app itself would drag it into every spec that only wants
34
+ * the matching. The roster is read through that same store by
35
+ * `loadCrmInboundRoster`, and the caller writes the org's feed line, so a
36
+ * spec of this module needs a store and nothing else.
37
+ */ /** The most candidate addresses one message is matched by. */ export const CRM_INBOUND_CANDIDATE_MAX = 6;
38
+ /** The most sites whose leads are consulted for one message. */ export const CRM_INBOUND_LEAD_HOST_MAX = 50;
39
+ /**
40
+ * The org's capture token, minted the first time anybody asks and kept
41
+ * thereafter; with `rotate`, replaced. A transaction, so two members
42
+ * opening the settings page at once mint one token and not two, and a
43
+ * rotation lands on the token the reader saw.
44
+ */ export async function ensureCrmInboundToken(firestore, orgId, options = {}) {
45
+ var _options_nowMs;
46
+ const rotate = options.rotate === true;
47
+ const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
48
+ const orgRef = firestore.collection('orgs').doc(orgId);
49
+ return firestore.runTransaction(async (tx)=>{
50
+ var _snapshot_get;
51
+ const snapshot = await tx.get(orgRef);
52
+ if (!snapshot.exists) throw new Error(`[crm-inbound] unknown org ${orgId}`);
53
+ const current = (_snapshot_get = snapshot.get('crmInbound')) != null ? _snapshot_get : null;
54
+ const held = isCrmInboundToken(current == null ? void 0 : current.token);
55
+ if (held && !rotate) {
56
+ const createdAtMs = Number(current == null ? void 0 : current.createdAtMs);
57
+ return _extends({
58
+ token: String(current == null ? void 0 : current.token),
59
+ createdAtMs: Number.isFinite(createdAtMs) && createdAtMs > 0 ? createdAtMs : nowMs
60
+ }, typeof (current == null ? void 0 : current.rotatedAtMs) === 'number' ? {
61
+ rotatedAtMs: current.rotatedAtMs
62
+ } : {}, {
63
+ rotated: false
64
+ });
65
+ }
66
+ const createdAtMs = held && Number.isFinite(Number(current == null ? void 0 : current.createdAtMs)) && Number(current == null ? void 0 : current.createdAtMs) > 0 ? Number(current == null ? void 0 : current.createdAtMs) : nowMs;
67
+ const next = _extends({
68
+ token: mintCrmInboundToken(),
69
+ createdAtMs
70
+ }, held ? {
71
+ rotatedAtMs: nowMs
72
+ } : {});
73
+ tx.update(orgRef, {
74
+ crmInbound: next
75
+ });
76
+ return _extends({}, next, {
77
+ rotated: held
78
+ });
79
+ });
80
+ }
81
+ /**
82
+ * The org a capture token names, or `null`. One equality on
83
+ * `crmInbound.token`, which Firestore indexes on its own; a token that is
84
+ * not one by shape is refused before the query, so a malformed local part
85
+ * costs no read.
86
+ */ export async function findOrgByCrmInboundToken(firestore, token) {
87
+ var _doc_data;
88
+ if (!isCrmInboundToken(token)) return null;
89
+ const page = await firestore.collection('orgs').where('crmInbound.token', '==', token).limit(1).get();
90
+ const doc = page.docs[0];
91
+ if (!doc) return null;
92
+ return {
93
+ orgId: doc.id,
94
+ org: _extends({
95
+ $id: doc.id
96
+ }, (_doc_data = doc.data()) != null ? _doc_data : {})
97
+ };
98
+ }
99
+ /**
100
+ * Every site of the organization, by id, up to the ceiling — the set a
101
+ * message's lead lookup sweeps. One equality on `hosts.orgId`.
102
+ */ export async function crmInboundHostIds(firestore, orgId) {
103
+ const page = await firestore.collection('hosts').where('orgId', '==', orgId).limit(CRM_INBOUND_LEAD_HOST_MAX).get();
104
+ return page.docs.map((doc)=>String(doc.id));
105
+ }
106
+ /**
107
+ * The first candidate that is a contact of the org — through the address
108
+ * index, so a merged record's alternate address answers — or a lead on any
109
+ * of its sites. Candidates are tried in the order the rule gave them and
110
+ * the first hit wins; a contact is looked for before a lead at each
111
+ * address, because a converted lead's address is its contact's.
112
+ */ export async function matchCrmInboundCorrespondent(firestore, input) {
113
+ const { orgId, org } = input;
114
+ const orgRef = firestore.collection('orgs').doc(orgId);
115
+ const contacts = orgRef.collection('contacts');
116
+ const tokensOf = (record)=>{
117
+ const raw = record['visibleTo'];
118
+ return Array.isArray(raw) ? raw.map((token)=>String(token)) : [];
119
+ };
120
+ for (const candidate of input.candidates.slice(0, CRM_INBOUND_CANDIDATE_MAX)){
121
+ const contact = await findContactByEmail(contacts, candidate.email);
122
+ if (contact) {
123
+ var _contact_data, _record_hostId;
124
+ const record = (_contact_data = contact.data()) != null ? _contact_data : {};
125
+ const hostId = String((_record_hostId = record['hostId']) != null ? _record_hostId : '').trim() || contactCaptureHostIds(record)[0] || '';
126
+ const link = {
127
+ contactId: contact.id
128
+ };
129
+ if (hostId) {
130
+ // The company the capturing site files this person under, so the
131
+ // row shows on the company's log as a sent email's does.
132
+ const group = consentGroupForHost(org, hostId);
133
+ const facet = readContactFacet(record, group.groupId);
134
+ if (facet.companyId) link.companyId = facet.companyId;
135
+ }
136
+ return {
137
+ email: candidate.email,
138
+ direction: candidate.direction,
139
+ kind: 'contact',
140
+ link,
141
+ hostId,
142
+ visibleTo: tokensOf(record)
143
+ };
144
+ }
145
+ const key = personKey(candidate.email);
146
+ if (!key) continue;
147
+ for (const hostId of input.hostIds.slice(0, CRM_INBOUND_LEAD_HOST_MAX)){
148
+ var _lead_get;
149
+ const lead = await firestore.collection('hosts').doc(hostId).collection('leads').doc(key).get();
150
+ if (!lead.exists) continue;
151
+ const link = {
152
+ leadId: lead.id
153
+ };
154
+ // A converted lead's mail belongs on the contact it became as well.
155
+ const converted = String((_lead_get = lead.get('convertedContactId')) != null ? _lead_get : '').trim();
156
+ if (converted) link.contactId = converted;
157
+ return {
158
+ email: candidate.email,
159
+ direction: candidate.direction,
160
+ kind: 'lead',
161
+ link,
162
+ hostId,
163
+ // A lead carries no tokens of its own: the site's scope, as every
164
+ // record created from that site is stamped.
165
+ visibleTo: crmScopeTokens(org, consentGroupForHost(org, hostId))
166
+ };
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * The workspace's roster as the filer matches against it: the member
173
+ * documents and every member's stored alias document, two reads under the
174
+ * org, reduced by `crmInboundRoster` to each member's sign-in address and
175
+ * the aliases they have confirmed (AGL-2975).
176
+ */ export async function loadCrmInboundRoster(firestore, orgId) {
177
+ const orgRef = firestore.collection('orgs').doc(orgId);
178
+ const [members, aliases] = await Promise.all([
179
+ orgRef.collection('members').get(),
180
+ orgRef.collection(MEMBER_EMAIL_ALIASES_COLLECTION).get()
181
+ ]);
182
+ const rows = members.docs.map((doc)=>{
183
+ var _doc_data;
184
+ return _extends({}, (_doc_data = doc.data()) != null ? _doc_data : {}, {
185
+ $id: doc.id
186
+ });
187
+ });
188
+ const aliasDocuments = new Map(aliases.docs.map((doc)=>[
189
+ String(doc.id),
190
+ doc.data()
191
+ ]));
192
+ return crmInboundRoster(rows, aliasDocuments);
193
+ }
194
+ /**
195
+ * Files one received message on the record it was with (AGL-2657).
196
+ *
197
+ * The order is the cost: the candidate rule is arithmetic; the match is
198
+ * the reads that decide everything after it; a message filed before is
199
+ * answered off one read of the row it would write, before the ceiling's
200
+ * aggregate; the ceiling comes last and refuses rather than dropping a row
201
+ * on a full log. The write is a `create`, so two deliveries racing past
202
+ * the read still yield one row. The BODY never leaves this function except
203
+ * as the bounded excerpt in the row.
204
+ */ export async function fileCrmInboundEmail(firestore, input) {
205
+ var _ref, _message_to_map_find, _ref1, _ref2;
206
+ const { orgId, org, message, domain } = input;
207
+ const plain = (message.text.trim() ? message.text : htmlToPlainText(message.html)).replace(/\r\n?/g, '\n');
208
+ const forwarded = forwardedSection(plain);
209
+ const { sender, senderIsMember, candidates } = crmInboundCandidates({
210
+ from: message.from,
211
+ to: [
212
+ ...message.to,
213
+ ...message.receivedFor
214
+ ],
215
+ cc: [
216
+ ...message.cc,
217
+ ...message.bcc
218
+ ],
219
+ forwardedFrom: (_ref = forwarded == null ? void 0 : forwarded.from) != null ? _ref : undefined,
220
+ domain,
221
+ members: input.members
222
+ });
223
+ const senderDomain = emailDomainOf(message.from);
224
+ if (!candidates.length) return {
225
+ outcome: 'unmatched',
226
+ senderDomain
227
+ };
228
+ const match = await matchCrmInboundCorrespondent(firestore, {
229
+ orgId,
230
+ org,
231
+ candidates,
232
+ hostIds: input.hostIds
233
+ });
234
+ if (!match) return {
235
+ outcome: 'unmatched',
236
+ senderDomain
237
+ };
238
+ const key = crmCapturedEmailKey(message.messageId, message.id);
239
+ const ref = key ? crmCapturedEmailActivityRef(firestore, orgId, key) : newCrmActivityRef(firestore, orgId);
240
+ if (key) {
241
+ const existing = await ref.get();
242
+ if (existing.exists) return {
243
+ outcome: 'duplicate',
244
+ activityId: ref.id
245
+ };
246
+ }
247
+ const orgRef = firestore.collection('orgs').doc(orgId);
248
+ if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, match.link))) {
249
+ return {
250
+ outcome: 'ceiling',
251
+ match
252
+ };
253
+ }
254
+ // The member the From is — by sign-in address, or by an alias exactly one
255
+ // member confirmed. A shared alias names the team and no one person.
256
+ const author = senderIsMember ? findMemberByEmailAddress(input.members, sender) : null;
257
+ // The address the message was written to, when it is worth showing: the
258
+ // correspondent for a copied send, else the first recipient that is not
259
+ // the capture address.
260
+ const to = match.direction === 'outbound' ? match.email : (_message_to_map_find = message.to.map((value)=>emailAddressOf(value)).find((value)=>value && !isCrmInboundAddress(value, domain))) != null ? _message_to_map_find : null;
261
+ const activity = buildCrmCapturedEmailActivity({
262
+ direction: match.direction,
263
+ subject: message.subject,
264
+ excerpt: crmInboundExcerpt(message.text, message.html),
265
+ from: sender != null ? sender : message.from,
266
+ to,
267
+ messageId: message.messageId || message.id,
268
+ inReplyTo: message.inReplyTo,
269
+ atMs: message.receivedAtMs,
270
+ byUid: (_ref1 = author == null ? void 0 : author.uid) != null ? _ref1 : '',
271
+ byName: (_ref2 = author == null ? void 0 : author.name) != null ? _ref2 : null,
272
+ link: match.link,
273
+ hostId: match.hostId,
274
+ visibleTo: match.visibleTo
275
+ });
276
+ const created = await createCrmEmailActivity(ref, activity);
277
+ return created === 'created' ? {
278
+ outcome: 'filed',
279
+ activityId: ref.id,
280
+ match
281
+ } : {
282
+ outcome: 'duplicate',
283
+ activityId: ref.id
284
+ };
285
+ }
286
+
287
+ //# sourceMappingURL=crm-inbound-email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/crm-inbound-email.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 {\n buildCrmCapturedEmailActivity,\n consentGroupForHost,\n contactCaptureHostIds,\n crmActivityLogHasRoom,\n crmCapturedEmailKey,\n crmInboundCandidates,\n crmInboundExcerpt,\n crmInboundRoster,\n type CrmActivityLink,\n type CrmEmailDirection,\n type CrmInboundCandidate,\n type CrmInboundMember,\n type CrmInboundRosterRow,\n crmScopeTokens,\n emailAddressOf,\n emailDomainOf,\n findMemberByEmailAddress,\n forwardedSection,\n htmlToPlainText,\n isCrmInboundAddress,\n isCrmInboundToken,\n mintCrmInboundToken,\n personKey,\n readContactFacet,\n} from '@aglyn/aglyn/server'\nimport { MEMBER_EMAIL_ALIASES_COLLECTION } from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport type { OrgCrmInbound } from '@aglyn/aglyn/foundation'\nimport type { ReceivedEmail } from '@aglyn/shared-util-email'\nimport { findContactByEmail } from './contact-email-index'\nimport {\n createCrmEmailActivity,\n crmCapturedEmailActivityRef,\n newCrmActivityRef,\n} from './crm-email-activity'\nimport { countCrmActivitiesForRecord } from './crm-records'\n\n/**\n * EMAIL CAPTURE (AGL-2657): the half with Firestore.\n *\n * The org's capture token — minted once, replaced on a rotation, found\n * again from a message's recipient — and the filing of one received\n * message: which record it was with, whether it has been filed before, and\n * the row it becomes. The pure rules are `@aglyn/aglyn`'s\n * `crm-inbound.ts`; this module is the reads and the one write.\n *\n * Firestore is a parameter throughout, as in `crm-records.ts` and\n * `crm-email-activity.ts`: the routes hold one, and a module that reached\n * for the Admin app itself would drag it into every spec that only wants\n * the matching. The roster is read through that same store by\n * `loadCrmInboundRoster`, and the caller writes the org's feed line, so a\n * spec of this module needs a store and nothing else.\n */\n\n/** The most candidate addresses one message is matched by. */\nexport const CRM_INBOUND_CANDIDATE_MAX = 6\n\n/** The most sites whose leads are consulted for one message. */\nexport const CRM_INBOUND_LEAD_HOST_MAX = 50\n\nexport interface CrmInboundTokenResult {\n token: string\n createdAtMs: number\n rotatedAtMs?: number\n /** True when this call replaced a token that existed. */\n rotated: boolean\n}\n\n/**\n * The org's capture token, minted the first time anybody asks and kept\n * thereafter; with `rotate`, replaced. A transaction, so two members\n * opening the settings page at once mint one token and not two, and a\n * rotation lands on the token the reader saw.\n */\nexport async function ensureCrmInboundToken(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n options: { rotate?: boolean; nowMs?: number } = {},\n): Promise<CrmInboundTokenResult> {\n const rotate = options.rotate === true\n const nowMs = options.nowMs ?? Date.now()\n const orgRef = firestore.collection('orgs').doc(orgId)\n return firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(orgRef)\n if (!snapshot.exists) throw new Error(`[crm-inbound] unknown org ${orgId}`)\n const current = (snapshot.get('crmInbound') ?? null) as Partial<OrgCrmInbound> | null\n const held = isCrmInboundToken(current?.token)\n if (held && !rotate) {\n const createdAtMs = Number(current?.createdAtMs)\n return {\n token: String(current?.token),\n createdAtMs: Number.isFinite(createdAtMs) && createdAtMs > 0 ? createdAtMs : nowMs,\n ...(typeof current?.rotatedAtMs === 'number' ? { rotatedAtMs: current.rotatedAtMs } : {}),\n rotated: false,\n }\n }\n const createdAtMs =\n held && Number.isFinite(Number(current?.createdAtMs)) && Number(current?.createdAtMs) > 0\n ? Number(current?.createdAtMs)\n : nowMs\n const next: OrgCrmInbound = {\n token: mintCrmInboundToken(),\n createdAtMs,\n ...(held ? { rotatedAtMs: nowMs } : {}),\n }\n tx.update(orgRef, { crmInbound: next })\n return { ...next, rotated: held }\n })\n}\n\n/**\n * The org a capture token names, or `null`. One equality on\n * `crmInbound.token`, which Firestore indexes on its own; a token that is\n * not one by shape is refused before the query, so a malformed local part\n * costs no read.\n */\nexport async function findOrgByCrmInboundToken(\n firestore: FirebaseFirestore.Firestore,\n token: unknown,\n): Promise<{ orgId: string; org: Record<string, unknown> } | null> {\n if (!isCrmInboundToken(token)) return null\n const page = await firestore\n .collection('orgs')\n .where('crmInbound.token', '==', token)\n .limit(1)\n .get()\n const doc = page.docs[0]\n if (!doc) return null\n return { orgId: doc.id, org: { $id: doc.id, ...(doc.data() ?? {}) } }\n}\n\n/**\n * Every site of the organization, by id, up to the ceiling — the set a\n * message's lead lookup sweeps. One equality on `hosts.orgId`.\n */\nexport async function crmInboundHostIds(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<string[]> {\n const page = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(CRM_INBOUND_LEAD_HOST_MAX)\n .get()\n return page.docs.map((doc) => String(doc.id))\n}\n\n/** The record one of a message's addresses resolved to. */\nexport interface CrmInboundMatch {\n email: string\n direction: CrmEmailDirection\n kind: 'contact' | 'lead'\n /** Where the row is filed. */\n link: CrmActivityLink\n /** The site the row is stamped with — the record's own. */\n hostId: string\n /** Who may list the row — the record's own tokens. */\n visibleTo: string[]\n}\n\n/**\n * The first candidate that is a contact of the org — through the address\n * index, so a merged record's alternate address answers — or a lead on any\n * of its sites. Candidates are tried in the order the rule gave them and\n * the first hit wins; a contact is looked for before a lead at each\n * address, because a converted lead's address is its contact's.\n */\nexport async function matchCrmInboundCorrespondent(\n firestore: FirebaseFirestore.Firestore,\n input: {\n orgId: string\n org: Record<string, unknown>\n candidates: readonly CrmInboundCandidate[]\n hostIds: readonly string[]\n },\n): Promise<CrmInboundMatch | null> {\n const { orgId, org } = input\n const orgRef = firestore.collection('orgs').doc(orgId)\n const contacts = orgRef.collection('contacts')\n const tokensOf = (record: Record<string, unknown>): string[] => {\n const raw = record['visibleTo']\n return Array.isArray(raw) ? raw.map((token) => String(token)) : []\n }\n for (const candidate of input.candidates.slice(0, CRM_INBOUND_CANDIDATE_MAX)) {\n const contact = await findContactByEmail(contacts, candidate.email)\n if (contact) {\n const record = (contact.data() ?? {}) as Record<string, unknown>\n const hostId =\n String(record['hostId'] ?? '').trim() || contactCaptureHostIds(record)[0] || ''\n const link: CrmActivityLink = { contactId: contact.id }\n if (hostId) {\n // The company the capturing site files this person under, so the\n // row shows on the company's log as a sent email's does.\n const group = consentGroupForHost(org, hostId)\n const facet = readContactFacet(record, group.groupId)\n if (facet.companyId) link.companyId = facet.companyId\n }\n return {\n email: candidate.email,\n direction: candidate.direction,\n kind: 'contact',\n link,\n hostId,\n visibleTo: tokensOf(record),\n }\n }\n const key = personKey(candidate.email)\n if (!key) continue\n for (const hostId of input.hostIds.slice(0, CRM_INBOUND_LEAD_HOST_MAX)) {\n const lead = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('leads')\n .doc(key)\n .get()\n if (!lead.exists) continue\n const link: CrmActivityLink = { leadId: lead.id }\n // A converted lead's mail belongs on the contact it became as well.\n const converted = String(lead.get('convertedContactId') ?? '').trim()\n if (converted) link.contactId = converted\n return {\n email: candidate.email,\n direction: candidate.direction,\n kind: 'lead',\n link,\n hostId,\n // A lead carries no tokens of its own: the site's scope, as every\n // record created from that site is stamped.\n visibleTo: crmScopeTokens(org, consentGroupForHost(org, hostId)),\n }\n }\n }\n return null\n}\n\n/**\n * The workspace's roster as the filer matches against it: the member\n * documents and every member's stored alias document, two reads under the\n * org, reduced by `crmInboundRoster` to each member's sign-in address and\n * the aliases they have confirmed (AGL-2975).\n */\nexport async function loadCrmInboundRoster(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<CrmInboundMember[]> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const [members, aliases] = await Promise.all([\n orgRef.collection('members').get(),\n orgRef.collection(MEMBER_EMAIL_ALIASES_COLLECTION).get(),\n ])\n const rows: CrmInboundRosterRow[] = members.docs.map((doc) => ({\n ...((doc.data() ?? {}) as Omit<CrmInboundRosterRow, '$id'>),\n $id: doc.id,\n }))\n const aliasDocuments = new Map<string, unknown>(\n aliases.docs.map((doc) => [String(doc.id), doc.data()]),\n )\n return crmInboundRoster(rows, aliasDocuments)\n}\n\nexport type CrmInboundFileOutcome =\n /** A row was written. */\n | { outcome: 'filed'; activityId: string; match: CrmInboundMatch }\n /** The message had been filed before; nothing written. */\n | { outcome: 'duplicate'; activityId: string }\n /** No address on the message is a record of the org; nothing written. */\n | { outcome: 'unmatched'; senderDomain: string | null }\n /** The record's activity log is at its ceiling; nothing written. */\n | { outcome: 'ceiling'; match: CrmInboundMatch }\n\n/**\n * Files one received message on the record it was with (AGL-2657).\n *\n * The order is the cost: the candidate rule is arithmetic; the match is\n * the reads that decide everything after it; a message filed before is\n * answered off one read of the row it would write, before the ceiling's\n * aggregate; the ceiling comes last and refuses rather than dropping a row\n * on a full log. The write is a `create`, so two deliveries racing past\n * the read still yield one row. The BODY never leaves this function except\n * as the bounded excerpt in the row.\n */\nexport async function fileCrmInboundEmail(\n firestore: FirebaseFirestore.Firestore,\n input: {\n orgId: string\n org: Record<string, unknown>\n message: ReceivedEmail\n /** The capture domain, so the capture address is never the correspondent. */\n domain: string\n /** The roster, from `loadCrmInboundRoster`: sign-in addresses and confirmed aliases. */\n members: readonly CrmInboundMember[]\n hostIds: readonly string[]\n },\n): Promise<CrmInboundFileOutcome> {\n const { orgId, org, message, domain } = input\n const plain = (message.text.trim() ? message.text : htmlToPlainText(message.html)).replace(\n /\\r\\n?/g,\n '\\n',\n )\n const forwarded = forwardedSection(plain)\n const { sender, senderIsMember, candidates } = crmInboundCandidates({\n from: message.from,\n to: [...message.to, ...message.receivedFor],\n cc: [...message.cc, ...message.bcc],\n forwardedFrom: forwarded?.from ?? undefined,\n domain,\n members: input.members,\n })\n const senderDomain = emailDomainOf(message.from)\n if (!candidates.length) return { outcome: 'unmatched', senderDomain }\n\n const match = await matchCrmInboundCorrespondent(firestore, {\n orgId,\n org,\n candidates,\n hostIds: input.hostIds,\n })\n if (!match) return { outcome: 'unmatched', senderDomain }\n\n const key = crmCapturedEmailKey(message.messageId, message.id)\n const ref = key\n ? crmCapturedEmailActivityRef(firestore, orgId, key)\n : newCrmActivityRef(firestore, orgId)\n if (key) {\n const existing = await ref.get()\n if (existing.exists) return { outcome: 'duplicate', activityId: ref.id }\n }\n\n const orgRef = firestore.collection('orgs').doc(orgId)\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, match.link))) {\n return { outcome: 'ceiling', match }\n }\n\n // The member the From is — by sign-in address, or by an alias exactly one\n // member confirmed. A shared alias names the team and no one person.\n const author = senderIsMember ? findMemberByEmailAddress(input.members, sender) : null\n // The address the message was written to, when it is worth showing: the\n // correspondent for a copied send, else the first recipient that is not\n // the capture address.\n const to =\n match.direction === 'outbound'\n ? match.email\n : (message.to\n .map((value) => emailAddressOf(value))\n .find((value) => value && !isCrmInboundAddress(value, domain)) ?? null)\n const activity = buildCrmCapturedEmailActivity({\n direction: match.direction,\n subject: message.subject,\n excerpt: crmInboundExcerpt(message.text, message.html),\n from: sender ?? message.from,\n to,\n messageId: message.messageId || message.id,\n inReplyTo: message.inReplyTo,\n atMs: message.receivedAtMs,\n byUid: author?.uid ?? '',\n byName: author?.name ?? null,\n link: match.link,\n hostId: match.hostId,\n visibleTo: match.visibleTo,\n })\n const created = await createCrmEmailActivity(ref, activity)\n return created === 'created'\n ? { outcome: 'filed', activityId: ref.id, match }\n : { outcome: 'duplicate', activityId: ref.id }\n}\n"],"names":["buildCrmCapturedEmailActivity","consentGroupForHost","contactCaptureHostIds","crmActivityLogHasRoom","crmCapturedEmailKey","crmInboundCandidates","crmInboundExcerpt","crmInboundRoster","crmScopeTokens","emailAddressOf","emailDomainOf","findMemberByEmailAddress","forwardedSection","htmlToPlainText","isCrmInboundAddress","isCrmInboundToken","mintCrmInboundToken","personKey","readContactFacet","MEMBER_EMAIL_ALIASES_COLLECTION","findContactByEmail","createCrmEmailActivity","crmCapturedEmailActivityRef","newCrmActivityRef","countCrmActivitiesForRecord","CRM_INBOUND_CANDIDATE_MAX","CRM_INBOUND_LEAD_HOST_MAX","ensureCrmInboundToken","firestore","orgId","options","rotate","nowMs","Date","now","orgRef","collection","doc","runTransaction","tx","snapshot","get","exists","Error","current","held","token","createdAtMs","Number","String","isFinite","rotatedAtMs","rotated","next","update","crmInbound","findOrgByCrmInboundToken","page","where","limit","docs","id","org","$id","data","crmInboundHostIds","map","matchCrmInboundCorrespondent","input","contacts","tokensOf","record","raw","Array","isArray","candidate","candidates","slice","contact","email","hostId","trim","link","contactId","group","facet","groupId","companyId","direction","kind","visibleTo","key","hostIds","lead","leadId","converted","loadCrmInboundRoster","members","aliases","Promise","all","rows","aliasDocuments","Map","fileCrmInboundEmail","message","domain","plain","text","html","replace","forwarded","sender","senderIsMember","from","to","receivedFor","cc","bcc","forwardedFrom","undefined","senderDomain","length","outcome","match","messageId","ref","existing","activityId","author","value","find","activity","subject","excerpt","inReplyTo","atMs","receivedAtMs","byUid","uid","byName","name","created"],"mappings":"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"}
@@ -0,0 +1,74 @@
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 CrmNextActivityLink, type CrmNextActivityLinkField } from '@aglyn/aglyn/app-utils/crm-next-activity';
18
+ /** The collection each link field's record lives in, under `orgs/{orgId}/`. */
19
+ export declare const CRM_NEXT_ACTIVITY_COLLECTIONS: Readonly<Record<CrmNextActivityLinkField, string>>;
20
+ export interface CrmNextActivityRecompute {
21
+ /** Records whose stored value was written (or rewritten to the same figure). */
22
+ records: number;
23
+ /** Records that no longer exist — a task can outlive what it was filed against. */
24
+ missing: number;
25
+ }
26
+ /**
27
+ * Recompute `nextTaskAtMs` for every record the links name, once each.
28
+ *
29
+ * Hand in BOTH sides of a change — the links a task carried before an
30
+ * update and the ones it carries after — so a task moved from one deal to
31
+ * another leaves neither stale. A record the task named that has since been
32
+ * deleted is counted in `missing` and skipped; nothing here creates one.
33
+ */
34
+ export declare function recomputeCrmNextTaskAt(firestore: FirebaseFirestore.Firestore, orgId: string, links: readonly (CrmNextActivityLink | null | undefined)[]): Promise<CrmNextActivityRecompute>;
35
+ /**
36
+ * The same, from a task document rather than a link — what a writer holds
37
+ * after it has read the task it is about to change or has just changed.
38
+ */
39
+ export declare function crmNextActivityLinksOf(task: Record<string, unknown> | null | undefined): CrmNextActivityLink;
40
+ /** The most open tasks one org-wide recompute walks. */
41
+ export declare const CRM_NEXT_ACTIVITY_SWEEP_MAX = 20000;
42
+ export interface CrmNextActivitySweep {
43
+ /** Open tasks read. */
44
+ tasks: number;
45
+ /** Records that now carry a time. */
46
+ scheduled: number;
47
+ /** Records that carried a time and now carry `null`. */
48
+ cleared: number;
49
+ /** Whether the org held more open tasks than the sweep reads. */
50
+ truncated: boolean;
51
+ }
52
+ /**
53
+ * The one-off recompute for a whole organization — the Fields section's
54
+ * "Recompute next activity" (AGL-2661), for the records written before the
55
+ * field existed and for whatever a client-direct write left behind.
56
+ *
57
+ * Two passes and no per-record task query: every OPEN task once, grouped
58
+ * by the records it names, so each record with something scheduled is
59
+ * written its earliest due time; then every record in the three
60
+ * collections that CARRIES a time and was not in that group, cleared to
61
+ * `null`. A record that was never written is left alone — every reader
62
+ * already treats absent as `null`, and writing a null onto every contact
63
+ * an org holds would cost a write per person to say nothing.
64
+ *
65
+ * The clearing pass runs only when the first pass read EVERY open task.
66
+ * "No open task names this record" is a statement about all of them, and
67
+ * an organization holding more than the cap has open tasks the sweep never
68
+ * saw — clearing on a partial read would blank records that are perfectly
69
+ * scheduled. Past the cap the sweep is the writing half alone, and
70
+ * `truncated` says so.
71
+ */
72
+ export declare function sweepCrmNextTaskAt(firestore: FirebaseFirestore.Firestore, orgId: string, options?: {
73
+ max?: number;
74
+ }): Promise<CrmNextActivitySweep>;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The `nextTaskAtMs` a record carries, as the Admin SDK writes it (AGL-2661).
18
+ *
19
+ * A contact, a company and a deal each denormalize when their earliest OPEN
20
+ * task is due, so a list can draw a "Next activity" column and answer
21
+ * "which deals have nothing scheduled" without a task query per row. This
22
+ * is the writer: given the records a task names, read that record's tasks
23
+ * back and store the answer `nextTaskAtMsOf` gives. Every server-side task
24
+ * writer — the console's task routes, the REST resource, the workflow step
25
+ * that files a task, the merge that repoints one — calls it after its own
26
+ * write; a client-direct task write asks the console's `crm/next-activity`
27
+ * route, which calls it too.
28
+ *
29
+ * Imported by module path rather than from the `@aglyn/aglyn/server`
30
+ * barrel, for the reason `contact-company-link.ts` gives.
31
+ *
32
+ * The task query is ONE equality on the link field, the query the record
33
+ * cards already run, and the status is judged in memory — no composite
34
+ * index, and a record with a hundred done tasks reads a hundred small
35
+ * documents once per write against it, which is the price of never being
36
+ * wrong. The record write is a bare `update()` of the one field, so
37
+ * `updatedAt` stays where the person's last edit left it: a task moving is
38
+ * not the record being edited, and the lists order by `updatedAt`.
39
+ */ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
40
+ import { CRM_NEXT_ACTIVITY_FIELD, mergeNextActivityLinks, nextActivityLinksOfTask, nextTaskAtMsOf, readNextTaskAtMs } from "@aglyn/aglyn/app-utils/crm-next-activity";
41
+ /** The collection each link field's record lives in, under `orgs/{orgId}/`. */ export const CRM_NEXT_ACTIVITY_COLLECTIONS = {
42
+ contactId: 'contacts',
43
+ companyId: CRM_COLLECTIONS.companies,
44
+ dealId: CRM_COLLECTIONS.deals
45
+ };
46
+ const isNotFound = (error)=>{
47
+ const { code, message } = error != null ? error : {};
48
+ return code === 5 || code === 'not-found' || code === 'NOT_FOUND' || typeof message === 'string' && message.includes('NOT_FOUND');
49
+ };
50
+ /**
51
+ * Store one record's `nextTaskAtMs` from its tasks. `null` for a record
52
+ * that no longer exists, else the value written.
53
+ */ async function recomputeOne(orgRef, field, id) {
54
+ const tasks = await orgRef.collection(CRM_COLLECTIONS.tasks).where(field, '==', id).get();
55
+ const value = nextTaskAtMsOf(tasks.docs.map((doc)=>({
56
+ status: doc.get('status'),
57
+ dueAtMs: doc.get('dueAtMs')
58
+ })));
59
+ try {
60
+ await orgRef.collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field]).doc(id).update({
61
+ [CRM_NEXT_ACTIVITY_FIELD]: value
62
+ });
63
+ } catch (error) {
64
+ if (isNotFound(error)) return {
65
+ written: false
66
+ };
67
+ throw error;
68
+ }
69
+ return {
70
+ written: true,
71
+ value
72
+ };
73
+ }
74
+ /**
75
+ * Recompute `nextTaskAtMs` for every record the links name, once each.
76
+ *
77
+ * Hand in BOTH sides of a change — the links a task carried before an
78
+ * update and the ones it carries after — so a task moved from one deal to
79
+ * another leaves neither stale. A record the task named that has since been
80
+ * deleted is counted in `missing` and skipped; nothing here creates one.
81
+ */ export async function recomputeCrmNextTaskAt(firestore, orgId, links) {
82
+ const orgRef = firestore.collection('orgs').doc(orgId);
83
+ const result = {
84
+ records: 0,
85
+ missing: 0
86
+ };
87
+ for (const { field, id } of mergeNextActivityLinks(links)){
88
+ const outcome = await recomputeOne(orgRef, field, id);
89
+ if (outcome.written) result.records += 1;
90
+ else result.missing += 1;
91
+ }
92
+ return result;
93
+ }
94
+ /**
95
+ * The same, from a task document rather than a link — what a writer holds
96
+ * after it has read the task it is about to change or has just changed.
97
+ */ export function crmNextActivityLinksOf(task) {
98
+ return nextActivityLinksOfTask(task);
99
+ }
100
+ /** The most open tasks one org-wide recompute walks. */ export const CRM_NEXT_ACTIVITY_SWEEP_MAX = 20000;
101
+ /**
102
+ * The one-off recompute for a whole organization — the Fields section's
103
+ * "Recompute next activity" (AGL-2661), for the records written before the
104
+ * field existed and for whatever a client-direct write left behind.
105
+ *
106
+ * Two passes and no per-record task query: every OPEN task once, grouped
107
+ * by the records it names, so each record with something scheduled is
108
+ * written its earliest due time; then every record in the three
109
+ * collections that CARRIES a time and was not in that group, cleared to
110
+ * `null`. A record that was never written is left alone — every reader
111
+ * already treats absent as `null`, and writing a null onto every contact
112
+ * an org holds would cost a write per person to say nothing.
113
+ *
114
+ * The clearing pass runs only when the first pass read EVERY open task.
115
+ * "No open task names this record" is a statement about all of them, and
116
+ * an organization holding more than the cap has open tasks the sweep never
117
+ * saw — clearing on a partial read would blank records that are perfectly
118
+ * scheduled. Past the cap the sweep is the writing half alone, and
119
+ * `truncated` says so.
120
+ */ export async function sweepCrmNextTaskAt(firestore, orgId, options = {}) {
121
+ var _options_max;
122
+ const max = (_options_max = options.max) != null ? _options_max : CRM_NEXT_ACTIVITY_SWEEP_MAX;
123
+ const orgRef = firestore.collection('orgs').doc(orgId);
124
+ const open = await orgRef.collection(CRM_COLLECTIONS.tasks).where('status', '==', 'open').limit(max + 1).get();
125
+ const truncated = open.docs.length > max;
126
+ const docs = open.docs.slice(0, max);
127
+ const earliest = new Map();
128
+ for (const field of Object.keys(CRM_NEXT_ACTIVITY_COLLECTIONS)){
129
+ earliest.set(field, new Map());
130
+ }
131
+ for (const doc of docs){
132
+ const task = {
133
+ status: doc.get('status'),
134
+ dueAtMs: doc.get('dueAtMs')
135
+ };
136
+ const links = nextActivityLinksOfTask({
137
+ contactId: doc.get('contactId'),
138
+ companyId: doc.get('companyId'),
139
+ dealId: doc.get('dealId')
140
+ });
141
+ for (const { field, id } of mergeNextActivityLinks([
142
+ links
143
+ ])){
144
+ var _byId_get;
145
+ const byId = earliest.get(field);
146
+ byId.set(id, nextTaskAtMsOf([
147
+ task,
148
+ {
149
+ status: 'open',
150
+ dueAtMs: (_byId_get = byId.get(id)) != null ? _byId_get : null
151
+ }
152
+ ]));
153
+ }
154
+ }
155
+ const sweep = {
156
+ tasks: docs.length,
157
+ scheduled: 0,
158
+ cleared: 0,
159
+ truncated
160
+ };
161
+ for (const [field, byId] of earliest){
162
+ const records = orgRef.collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field]);
163
+ for (const [id, value] of byId){
164
+ try {
165
+ await records.doc(id).update({
166
+ [CRM_NEXT_ACTIVITY_FIELD]: value
167
+ });
168
+ sweep.scheduled += 1;
169
+ } catch (error) {
170
+ if (!isNotFound(error)) throw error;
171
+ }
172
+ }
173
+ /*
174
+ * The stale half: a record that says something is scheduled when no
175
+ * open task names it any more. `> 0` is the one query a stored time
176
+ * answers without an index of its own, and matches what
177
+ * `readNextTaskAtMs` calls a usable value.
178
+ */ if (truncated) continue;
179
+ const carrying = await records.where(CRM_NEXT_ACTIVITY_FIELD, '>', 0).get();
180
+ for (const doc of carrying.docs){
181
+ if (byId.has(doc.id)) continue;
182
+ if (readNextTaskAtMs({
183
+ nextTaskAtMs: doc.get(CRM_NEXT_ACTIVITY_FIELD)
184
+ }) === null) continue;
185
+ await doc.ref.update({
186
+ [CRM_NEXT_ACTIVITY_FIELD]: null
187
+ });
188
+ sweep.cleared += 1;
189
+ }
190
+ }
191
+ return sweep;
192
+ }
193
+
194
+ //# sourceMappingURL=crm-next-activity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/crm-next-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The `nextTaskAtMs` a record carries, as the Admin SDK writes it (AGL-2661).\n *\n * A contact, a company and a deal each denormalize when their earliest OPEN\n * task is due, so a list can draw a \"Next activity\" column and answer\n * \"which deals have nothing scheduled\" without a task query per row. This\n * is the writer: given the records a task names, read that record's tasks\n * back and store the answer `nextTaskAtMsOf` gives. Every server-side task\n * writer — the console's task routes, the REST resource, the workflow step\n * that files a task, the merge that repoints one — calls it after its own\n * write; a client-direct task write asks the console's `crm/next-activity`\n * route, which calls it too.\n *\n * Imported by module path rather than from the `@aglyn/aglyn/server`\n * barrel, for the reason `contact-company-link.ts` gives.\n *\n * The task query is ONE equality on the link field, the query the record\n * cards already run, and the status is judged in memory — no composite\n * index, and a record with a hundred done tasks reads a hundred small\n * documents once per write against it, which is the price of never being\n * wrong. The record write is a bare `update()` of the one field, so\n * `updatedAt` stays where the person's last edit left it: a task moving is\n * not the record being edited, and the lists order by `updatedAt`.\n */\n\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n CRM_NEXT_ACTIVITY_FIELD,\n type CrmNextActivityLink,\n type CrmNextActivityLinkField,\n mergeNextActivityLinks,\n nextActivityLinksOfTask,\n nextTaskAtMsOf,\n readNextTaskAtMs,\n} from '@aglyn/aglyn/app-utils/crm-next-activity'\n\n/** The collection each link field's record lives in, under `orgs/{orgId}/`. */\nexport const CRM_NEXT_ACTIVITY_COLLECTIONS: Readonly<Record<CrmNextActivityLinkField, string>> = {\n contactId: 'contacts',\n companyId: CRM_COLLECTIONS.companies,\n dealId: CRM_COLLECTIONS.deals,\n}\n\nexport interface CrmNextActivityRecompute {\n /** Records whose stored value was written (or rewritten to the same figure). */\n records: number\n /** Records that no longer exist — a task can outlive what it was filed against. */\n missing: number\n}\n\nconst isNotFound = (error: unknown): boolean => {\n const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown }\n return (\n code === 5 ||\n code === 'not-found' ||\n code === 'NOT_FOUND' ||\n (typeof message === 'string' && message.includes('NOT_FOUND'))\n )\n}\n\n/**\n * Store one record's `nextTaskAtMs` from its tasks. `null` for a record\n * that no longer exists, else the value written.\n */\nasync function recomputeOne(\n orgRef: FirebaseFirestore.DocumentReference,\n field: CrmNextActivityLinkField,\n id: string,\n): Promise<{ written: true; value: number | null } | { written: false }> {\n const tasks = await orgRef\n .collection(CRM_COLLECTIONS.tasks)\n .where(field, '==', id)\n .get()\n const value = nextTaskAtMsOf(\n tasks.docs.map((doc) => ({\n status: doc.get('status'),\n dueAtMs: doc.get('dueAtMs'),\n })),\n )\n try {\n await orgRef\n .collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field])\n .doc(id)\n .update({ [CRM_NEXT_ACTIVITY_FIELD]: value })\n } catch (error) {\n if (isNotFound(error)) return { written: false }\n throw error\n }\n return { written: true, value }\n}\n\n/**\n * Recompute `nextTaskAtMs` for every record the links name, once each.\n *\n * Hand in BOTH sides of a change — the links a task carried before an\n * update and the ones it carries after — so a task moved from one deal to\n * another leaves neither stale. A record the task named that has since been\n * deleted is counted in `missing` and skipped; nothing here creates one.\n */\nexport async function recomputeCrmNextTaskAt(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n links: readonly (CrmNextActivityLink | null | undefined)[],\n): Promise<CrmNextActivityRecompute> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const result: CrmNextActivityRecompute = { records: 0, missing: 0 }\n for (const { field, id } of mergeNextActivityLinks(links)) {\n const outcome = await recomputeOne(orgRef, field, id)\n if (outcome.written) result.records += 1\n else result.missing += 1\n }\n return result\n}\n\n/**\n * The same, from a task document rather than a link — what a writer holds\n * after it has read the task it is about to change or has just changed.\n */\nexport function crmNextActivityLinksOf(\n task: Record<string, unknown> | null | undefined,\n): CrmNextActivityLink {\n return nextActivityLinksOfTask(task as Parameters<typeof nextActivityLinksOfTask>[0])\n}\n\n/** The most open tasks one org-wide recompute walks. */\nexport const CRM_NEXT_ACTIVITY_SWEEP_MAX = 20_000\n\nexport interface CrmNextActivitySweep {\n /** Open tasks read. */\n tasks: number\n /** Records that now carry a time. */\n scheduled: number\n /** Records that carried a time and now carry `null`. */\n cleared: number\n /** Whether the org held more open tasks than the sweep reads. */\n truncated: boolean\n}\n\n/**\n * The one-off recompute for a whole organization — the Fields section's\n * \"Recompute next activity\" (AGL-2661), for the records written before the\n * field existed and for whatever a client-direct write left behind.\n *\n * Two passes and no per-record task query: every OPEN task once, grouped\n * by the records it names, so each record with something scheduled is\n * written its earliest due time; then every record in the three\n * collections that CARRIES a time and was not in that group, cleared to\n * `null`. A record that was never written is left alone — every reader\n * already treats absent as `null`, and writing a null onto every contact\n * an org holds would cost a write per person to say nothing.\n *\n * The clearing pass runs only when the first pass read EVERY open task.\n * \"No open task names this record\" is a statement about all of them, and\n * an organization holding more than the cap has open tasks the sweep never\n * saw — clearing on a partial read would blank records that are perfectly\n * scheduled. Past the cap the sweep is the writing half alone, and\n * `truncated` says so.\n */\nexport async function sweepCrmNextTaskAt(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n options: { max?: number } = {},\n): Promise<CrmNextActivitySweep> {\n const max = options.max ?? CRM_NEXT_ACTIVITY_SWEEP_MAX\n const orgRef = firestore.collection('orgs').doc(orgId)\n const open = await orgRef\n .collection(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .limit(max + 1)\n .get()\n const truncated = open.docs.length > max\n const docs = open.docs.slice(0, max)\n\n const earliest = new Map<CrmNextActivityLinkField, Map<string, number | null>>()\n for (const field of Object.keys(CRM_NEXT_ACTIVITY_COLLECTIONS) as CrmNextActivityLinkField[]) {\n earliest.set(field, new Map())\n }\n for (const doc of docs) {\n const task = { status: doc.get('status'), dueAtMs: doc.get('dueAtMs') }\n const links = nextActivityLinksOfTask({\n contactId: doc.get('contactId'),\n companyId: doc.get('companyId'),\n dealId: doc.get('dealId'),\n })\n for (const { field, id } of mergeNextActivityLinks([links])) {\n const byId = earliest.get(field) as Map<string, number | null>\n byId.set(id, nextTaskAtMsOf([task, { status: 'open', dueAtMs: byId.get(id) ?? null }]))\n }\n }\n\n const sweep: CrmNextActivitySweep = { tasks: docs.length, scheduled: 0, cleared: 0, truncated }\n for (const [field, byId] of earliest) {\n const records = orgRef.collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field])\n for (const [id, value] of byId) {\n try {\n await records.doc(id).update({ [CRM_NEXT_ACTIVITY_FIELD]: value })\n sweep.scheduled += 1\n } catch (error) {\n if (!isNotFound(error)) throw error\n }\n }\n /*\n * The stale half: a record that says something is scheduled when no\n * open task names it any more. `> 0` is the one query a stored time\n * answers without an index of its own, and matches what\n * `readNextTaskAtMs` calls a usable value.\n */\n if (truncated) continue\n const carrying = await records.where(CRM_NEXT_ACTIVITY_FIELD, '>', 0).get()\n for (const doc of carrying.docs) {\n if (byId.has(doc.id)) continue\n if (readNextTaskAtMs({ nextTaskAtMs: doc.get(CRM_NEXT_ACTIVITY_FIELD) }) === null) continue\n await doc.ref.update({ [CRM_NEXT_ACTIVITY_FIELD]: null })\n sweep.cleared += 1\n }\n }\n return sweep\n}\n"],"names":["CRM_COLLECTIONS","CRM_NEXT_ACTIVITY_FIELD","mergeNextActivityLinks","nextActivityLinksOfTask","nextTaskAtMsOf","readNextTaskAtMs","CRM_NEXT_ACTIVITY_COLLECTIONS","contactId","companyId","companies","dealId","deals","isNotFound","error","code","message","includes","recomputeOne","orgRef","field","id","tasks","collection","where","get","value","docs","map","doc","status","dueAtMs","update","written","recomputeCrmNextTaskAt","firestore","orgId","links","result","records","missing","outcome","crmNextActivityLinksOf","task","CRM_NEXT_ACTIVITY_SWEEP_MAX","sweepCrmNextTaskAt","options","max","open","limit","truncated","length","slice","earliest","Map","Object","keys","set","byId","sweep","scheduled","cleared","carrying","has","nextTaskAtMs","ref"],"mappings":"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"}