@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,145 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * THE CONTACT'S OWN ENGAGEMENT STAMP (AGL-2616):
18
+ * `facets.{groupId}.lastEmailEngagementAtMs` on `orgs/{orgId}/contacts`.
19
+ *
20
+ * ## What it answers
21
+ *
22
+ * "When did this person last read one of OUR campaigns?" — on the record
23
+ * page as "Last engaged", as a column on the list, and as the audience rule
24
+ * `engagedWithinDays`. The per-person rollup on `emailDeliveries/{key}`
25
+ * already answers "when did this address last read anything from anybody",
26
+ * which is the right question for a sunset that refuses to mail cold
27
+ * addresses and the wrong one for a re-engagement audience: it moves when
28
+ * the person opens a receipt, an invite, or a sibling business's newsletter
29
+ * on the same shared row. This stamp moves only when they open or click a
30
+ * campaign this holder's own sites sent.
31
+ *
32
+ * ## What one webhook event costs
33
+ *
34
+ * The same bound the rollup keeps: only an event that is the FIRST of its
35
+ * type for its message reaches here, which `recordEmailDeliveryEvent`'s
36
+ * transaction already decided. A reader opening one newsletter six times is
37
+ * one transaction, and a replayed event finds its type already recorded and
38
+ * contributes nothing. Inside that bound the cost per person is one org
39
+ * resolution and one group resolution per batch, then one keyed query and
40
+ * at most one update per person.
41
+ *
42
+ * ## Forward-only, in a transaction
43
+ *
44
+ * Provider events are unordered and at-least-once, so an old instant
45
+ * arriving late must not overwrite a fresh stamp — the same reasoning
46
+ * `recordPersonEngagement` gives. The query for the contact rides inside
47
+ * the transaction so the compare-and-write is atomic against a second event
48
+ * for the same person landing in another instance.
49
+ *
50
+ * ## Only a contact this site may see, and only this holder's facet
51
+ *
52
+ * The contact row is shared by every site in the org. The stamp is written
53
+ * under the sending site's consent group, never at the top of the document,
54
+ * and only onto a row whose `visibleTo` admits the site — a person another
55
+ * holder captured and this site never met is not this site's contact, and a
56
+ * write onto their row would mint a facet for a holder that does not hold
57
+ * them. `updatedAt` is left alone: an open is something the person did, not
58
+ * an edit the team made, and a list sorted on recency must not reshuffle on
59
+ * every mailbox prefetch.
60
+ *
61
+ * ## Never throws
62
+ *
63
+ * Best-effort for the reason everything on the webhook path is: a stamp that
64
+ * failed loses a fact a page can live without, and a stamp that threw would
65
+ * lose the provider's acknowledgement and teach it to retry the whole event.
66
+ */ import { contactFacetPath, normalizeContactEmail, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
67
+ import { findContactByEmail } from "./contact-email-index.js";
68
+ import { firebaseAdmin } from "./firebase-admin.js";
69
+ import { consentGroupForSite, orgDataCollectionForHost } from "./organizations.js";
70
+ /** The facet field the stamp lives under. */ export const CONTACT_EMAIL_ENGAGEMENT_FIELD = 'lastEmailEngagementAtMs';
71
+ /** The event types that count as a person engaging with a campaign. */ const ENGAGEMENT_TYPES = new Set([
72
+ 'opened',
73
+ 'clicked'
74
+ ]);
75
+ /**
76
+ * Stamps the sending site's contact facet for every person these outcomes
77
+ * say engaged for the first time with a message.
78
+ *
79
+ * @param hostId the site the campaign went out from — the `hostId` tag the
80
+ * send stamped, which is the only tenant identity a delivery event carries.
81
+ * @returns how many contact documents were written.
82
+ */ export async function recordContactEmailEngagement(args) {
83
+ var _args_hostId, _args_firestore;
84
+ const hostId = String((_args_hostId = args.hostId) != null ? _args_hostId : '');
85
+ if (!hostId) return 0;
86
+ /** Address → the newest engagement instant in this batch. */ const byEmail = new Map();
87
+ for (const outcome of args.outcomes){
88
+ var _byEmail_get;
89
+ if (!outcome.firstOfType) continue;
90
+ if (!ENGAGEMENT_TYPES.has(outcome.type)) continue;
91
+ const email = normalizeContactEmail(outcome.to);
92
+ const at = Number(outcome.at);
93
+ if (!email || !Number.isFinite(at) || at <= 0) continue;
94
+ byEmail.set(email, Math.max((_byEmail_get = byEmail.get(email)) != null ? _byEmail_get : 0, at));
95
+ }
96
+ if (!byEmail.size) return 0;
97
+ let contactsRef;
98
+ let groupId;
99
+ try {
100
+ contactsRef = await orgDataCollectionForHost(hostId, 'contacts');
101
+ groupId = (await consentGroupForSite(hostId)).groupId;
102
+ } catch (error) {
103
+ console.error('[contact-email-engagement] site could not be resolved', hostId, error);
104
+ return 0;
105
+ }
106
+ const db = (_args_firestore = args.firestore) != null ? _args_firestore : firebaseAdmin.app().firestore();
107
+ const field = contactFacetPath(groupId, CONTACT_EMAIL_ENGAGEMENT_FIELD);
108
+ let written = 0;
109
+ for (const [email, at] of byEmail){
110
+ try {
111
+ await db.runTransaction(async (transaction)=>{
112
+ var _snapshot_data, _readContactFacet_CONTACT_EMAIL_ENGAGEMENT_FIELD;
113
+ /*
114
+ * The same unscoped lookup the capture door makes — one human is one
115
+ * row whichever site met them, and through the address index
116
+ * (AGL-2633) the row is found under an address a merge folded into
117
+ * it too — followed by the scope check the capture door's
118
+ * `visibleTo` write is the source of. Read THROUGH the transaction:
119
+ * the stamp below is a compare-and-set against the instant this
120
+ * read saw.
121
+ */ const snapshot = await findContactByEmail(contactsRef, email, {
122
+ transaction
123
+ });
124
+ if (!snapshot) return;
125
+ const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
126
+ if (!visibleToHost(data['visibleTo'], hostId)) {
127
+ return;
128
+ }
129
+ const stored = Number((_readContactFacet_CONTACT_EMAIL_ENGAGEMENT_FIELD = readContactFacet(data, groupId)[CONTACT_EMAIL_ENGAGEMENT_FIELD]) != null ? _readContactFacet_CONTACT_EMAIL_ENGAGEMENT_FIELD : 0);
130
+ // Nothing moved forward, so nothing is written — the out-of-order
131
+ // and the replayed event are the ordinary cases this skips.
132
+ if (Number.isFinite(stored) && stored >= at) return;
133
+ transaction.update(snapshot.ref, {
134
+ [field]: at
135
+ });
136
+ written += 1;
137
+ });
138
+ } catch (error) {
139
+ console.error('[contact-email-engagement] stamp failed', hostId, error);
140
+ }
141
+ }
142
+ return written;
143
+ }
144
+
145
+ //# sourceMappingURL=contact-email-engagement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-email-engagement.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 CONTACT'S OWN ENGAGEMENT STAMP (AGL-2616):\n * `facets.{groupId}.lastEmailEngagementAtMs` on `orgs/{orgId}/contacts`.\n *\n * ## What it answers\n *\n * \"When did this person last read one of OUR campaigns?\" — on the record\n * page as \"Last engaged\", as a column on the list, and as the audience rule\n * `engagedWithinDays`. The per-person rollup on `emailDeliveries/{key}`\n * already answers \"when did this address last read anything from anybody\",\n * which is the right question for a sunset that refuses to mail cold\n * addresses and the wrong one for a re-engagement audience: it moves when\n * the person opens a receipt, an invite, or a sibling business's newsletter\n * on the same shared row. This stamp moves only when they open or click a\n * campaign this holder's own sites sent.\n *\n * ## What one webhook event costs\n *\n * The same bound the rollup keeps: only an event that is the FIRST of its\n * type for its message reaches here, which `recordEmailDeliveryEvent`'s\n * transaction already decided. A reader opening one newsletter six times is\n * one transaction, and a replayed event finds its type already recorded and\n * contributes nothing. Inside that bound the cost per person is one org\n * resolution and one group resolution per batch, then one keyed query and\n * at most one update per person.\n *\n * ## Forward-only, in a transaction\n *\n * Provider events are unordered and at-least-once, so an old instant\n * arriving late must not overwrite a fresh stamp — the same reasoning\n * `recordPersonEngagement` gives. The query for the contact rides inside\n * the transaction so the compare-and-write is atomic against a second event\n * for the same person landing in another instance.\n *\n * ## Only a contact this site may see, and only this holder's facet\n *\n * The contact row is shared by every site in the org. The stamp is written\n * under the sending site's consent group, never at the top of the document,\n * and only onto a row whose `visibleTo` admits the site — a person another\n * holder captured and this site never met is not this site's contact, and a\n * write onto their row would mint a facet for a holder that does not hold\n * them. `updatedAt` is left alone: an open is something the person did, not\n * an edit the team made, and a list sorted on recency must not reshuffle on\n * every mailbox prefetch.\n *\n * ## Never throws\n *\n * Best-effort for the reason everything on the webhook path is: a stamp that\n * failed loses a fact a page can live without, and a stamp that threw would\n * lose the provider's acknowledgement and teach it to retry the whole event.\n */\n\nimport {\n contactFacetPath,\n normalizeContactEmail,\n readContactFacet,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport { findContactByEmail } from './contact-email-index'\nimport type { EmailDeliveryEventOutcome } from './email-delivery-log'\nimport { firebaseAdmin } from './firebase-admin'\nimport { consentGroupForSite, orgDataCollectionForHost } from './organizations'\n\n/** The facet field the stamp lives under. */\nexport const CONTACT_EMAIL_ENGAGEMENT_FIELD = 'lastEmailEngagementAtMs'\n\n/** The event types that count as a person engaging with a campaign. */\nconst ENGAGEMENT_TYPES: ReadonlySet<EmailDeliveryEventOutcome['type']> = new Set([\n 'opened',\n 'clicked',\n])\n\n/**\n * Stamps the sending site's contact facet for every person these outcomes\n * say engaged for the first time with a message.\n *\n * @param hostId the site the campaign went out from — the `hostId` tag the\n * send stamped, which is the only tenant identity a delivery event carries.\n * @returns how many contact documents were written.\n */\nexport async function recordContactEmailEngagement(args: {\n hostId: string\n outcomes: readonly EmailDeliveryEventOutcome[]\n firestore?: any\n}): Promise<number> {\n const hostId = String(args.hostId ?? '')\n if (!hostId) return 0\n\n /** Address → the newest engagement instant in this batch. */\n const byEmail = new Map<string, number>()\n for (const outcome of args.outcomes) {\n if (!outcome.firstOfType) continue\n if (!ENGAGEMENT_TYPES.has(outcome.type)) continue\n const email = normalizeContactEmail(outcome.to)\n const at = Number(outcome.at)\n if (!email || !Number.isFinite(at) || at <= 0) continue\n byEmail.set(email, Math.max(byEmail.get(email) ?? 0, at))\n }\n if (!byEmail.size) return 0\n\n let contactsRef: FirebaseFirestore.CollectionReference\n let groupId: string\n try {\n contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n groupId = (await consentGroupForSite(hostId)).groupId\n } catch (error) {\n console.error('[contact-email-engagement] site could not be resolved', hostId, error)\n return 0\n }\n\n const db = args.firestore ?? firebaseAdmin.app().firestore()\n const field = contactFacetPath(groupId, CONTACT_EMAIL_ENGAGEMENT_FIELD)\n let written = 0\n for (const [email, at] of byEmail) {\n try {\n await db.runTransaction(async (transaction: any) => {\n /*\n * The same unscoped lookup the capture door makes — one human is one\n * row whichever site met them, and through the address index\n * (AGL-2633) the row is found under an address a merge folded into\n * it too — followed by the scope check the capture door's\n * `visibleTo` write is the source of. Read THROUGH the transaction:\n * the stamp below is a compare-and-set against the instant this\n * read saw.\n */\n const snapshot = await findContactByEmail(contactsRef, email, { transaction })\n if (!snapshot) return\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n if (!visibleToHost(data['visibleTo'] as string[] | undefined, hostId)) {\n return\n }\n const stored = Number(\n readContactFacet(data, groupId)[CONTACT_EMAIL_ENGAGEMENT_FIELD] ?? 0,\n )\n // Nothing moved forward, so nothing is written — the out-of-order\n // and the replayed event are the ordinary cases this skips.\n if (Number.isFinite(stored) && stored >= at) return\n transaction.update(snapshot.ref, { [field]: at })\n written += 1\n })\n } catch (error) {\n console.error('[contact-email-engagement] stamp failed', hostId, error)\n }\n }\n return written\n}\n"],"names":["contactFacetPath","normalizeContactEmail","readContactFacet","visibleToHost","findContactByEmail","firebaseAdmin","consentGroupForSite","orgDataCollectionForHost","CONTACT_EMAIL_ENGAGEMENT_FIELD","ENGAGEMENT_TYPES","Set","recordContactEmailEngagement","args","hostId","String","byEmail","Map","outcome","outcomes","firstOfType","has","type","email","to","at","Number","isFinite","set","Math","max","get","size","contactsRef","groupId","error","console","db","firestore","app","field","written","runTransaction","transaction","snapshot","data","stored","update","ref"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDC,GAED,SACEA,gBAAgB,EAChBC,qBAAqB,EACrBC,gBAAgB,EAChBC,aAAa,QACR,sBAAqB;AAC5B,SAASC,kBAAkB,QAAQ,2BAAuB;AAE1D,SAASC,aAAa,QAAQ,sBAAkB;AAChD,SAASC,mBAAmB,EAAEC,wBAAwB,QAAQ,qBAAiB;AAE/E,2CAA2C,GAC3C,OAAO,MAAMC,iCAAiC,0BAAyB;AAEvE,qEAAqE,GACrE,MAAMC,mBAAmE,IAAIC,IAAI;IAC/E;IACA;CACD;AAED;;;;;;;CAOC,GACD,OAAO,eAAeC,6BAA6BC,IAIlD;QACuBA,cAyBXA;IAzBX,MAAMC,SAASC,QAAOF,eAAAA,KAAKC,MAAM,YAAXD,eAAe;IACrC,IAAI,CAACC,QAAQ,OAAO;IAEpB,2DAA2D,GAC3D,MAAME,UAAU,IAAIC;IACpB,KAAK,MAAMC,WAAWL,KAAKM,QAAQ,CAAE;YAMPH;QAL5B,IAAI,CAACE,QAAQE,WAAW,EAAE;QAC1B,IAAI,CAACV,iBAAiBW,GAAG,CAACH,QAAQI,IAAI,GAAG;QACzC,MAAMC,QAAQrB,sBAAsBgB,QAAQM,EAAE;QAC9C,MAAMC,KAAKC,OAAOR,QAAQO,EAAE;QAC5B,IAAI,CAACF,SAAS,CAACG,OAAOC,QAAQ,CAACF,OAAOA,MAAM,GAAG;QAC/CT,QAAQY,GAAG,CAACL,OAAOM,KAAKC,GAAG,EAACd,eAAAA,QAAQe,GAAG,CAACR,kBAAZP,eAAsB,GAAGS;IACvD;IACA,IAAI,CAACT,QAAQgB,IAAI,EAAE,OAAO;IAE1B,IAAIC;IACJ,IAAIC;IACJ,IAAI;QACFD,cAAc,MAAMzB,yBAAyBM,QAAQ;QACrDoB,UAAU,AAAC,CAAA,MAAM3B,oBAAoBO,OAAM,EAAGoB,OAAO;IACvD,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,yDAAyDrB,QAAQqB;QAC/E,OAAO;IACT;IAEA,MAAME,MAAKxB,kBAAAA,KAAKyB,SAAS,YAAdzB,kBAAkBP,cAAciC,GAAG,GAAGD,SAAS;IAC1D,MAAME,QAAQvC,iBAAiBiC,SAASzB;IACxC,IAAIgC,UAAU;IACd,KAAK,MAAM,CAAClB,OAAOE,GAAG,IAAIT,QAAS;QACjC,IAAI;YACF,MAAMqB,GAAGK,cAAc,CAAC,OAAOC;oBAYfC,gBAKZzC;gBAhBF;;;;;;;;SAQC,GACD,MAAMyC,WAAW,MAAMvC,mBAAmB4B,aAAaV,OAAO;oBAAEoB;gBAAY;gBAC5E,IAAI,CAACC,UAAU;gBACf,MAAMC,QAAQD,iBAAAA,SAASC,IAAI,cAAbD,iBAAmB,CAAC;gBAClC,IAAI,CAACxC,cAAcyC,IAAI,CAAC,YAAY,EAA0B/B,SAAS;oBACrE;gBACF;gBACA,MAAMgC,SAASpB,QACbvB,mDAAAA,iBAAiB0C,MAAMX,QAAQ,CAACzB,+BAA+B,YAA/DN,mDAAmE;gBAErE,kEAAkE;gBAClE,4DAA4D;gBAC5D,IAAIuB,OAAOC,QAAQ,CAACmB,WAAWA,UAAUrB,IAAI;gBAC7CkB,YAAYI,MAAM,CAACH,SAASI,GAAG,EAAE;oBAAE,CAACR,MAAM,EAAEf;gBAAG;gBAC/CgB,WAAW;YACb;QACF,EAAE,OAAON,OAAO;YACdC,QAAQD,KAAK,CAAC,2CAA2CrB,QAAQqB;QACnE;IACF;IACA,OAAOM;AACT"}
@@ -0,0 +1,56 @@
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
+ /** One entry: the address as stored, and the contact it resolves to. */
18
+ export interface ContactEmailIndexEntry {
19
+ email: string;
20
+ contactId: string;
21
+ }
22
+ /**
23
+ * The org's index collection, beside its contacts one — or `null` when the
24
+ * handle has no parent, which is a fixture's contacts reference and never
25
+ * a real org subcollection. `null` means "no index": the caller falls back
26
+ * to the query and writes nothing.
27
+ */
28
+ export declare function emailIndexBeside(contactsRef: FirebaseFirestore.CollectionReference): FirebaseFirestore.CollectionReference | null;
29
+ /**
30
+ * Point every address in `emails` at `contactId`. Never rejects: an entry
31
+ * that could not be written is one the next lookup writes again.
32
+ */
33
+ export declare function writeContactEmailIndex(index: FirebaseFirestore.CollectionReference | null, contactId: string, emails: readonly string[]): Promise<void>;
34
+ /** How one door reads differently from the capture; see the module notes. */
35
+ export interface FindContactByEmailOptions {
36
+ /**
37
+ * Answer only a contact THIS site may see. A document the address names
38
+ * but the site cannot read is `null` — the same answer the scoped
39
+ * `email ==` query gave, since one address names one document.
40
+ */
41
+ hostId?: string;
42
+ /**
43
+ * Read through this transaction. Every read — the index entry, the
44
+ * contact it names, the fallback query — goes through it; the lazy index
45
+ * write does not, so the caller may still read after this returns.
46
+ */
47
+ transaction?: FirebaseFirestore.Transaction;
48
+ }
49
+ /**
50
+ * The contact an address belongs to, as a document snapshot — or `null`.
51
+ *
52
+ * The index first, then the per-document query it summarizes. A query hit
53
+ * writes the entry the index lacked, so the read that paid for the miss is
54
+ * the last one that has to.
55
+ */
56
+ export declare function findContactByEmail(contactsRef: FirebaseFirestore.CollectionReference, email: unknown, options?: FindContactByEmailOptions): Promise<FirebaseFirestore.DocumentSnapshot | null>;
@@ -0,0 +1,158 @@
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 ADDRESS INDEX (AGL-2625): `orgs/{orgId}/emailIndex/{personKey}`.
18
+ *
19
+ * `upsertHostContact` finds a person by `where('email', '==', …)` on the
20
+ * contacts collection, and that query answers one document per address by
21
+ * construction — which is exactly what stops a merged record's address from
22
+ * ever finding the survivor. The survivor's `email` is the identity that
23
+ * stayed; the merged address is in its `alternateEmails`, and Firestore
24
+ * cannot answer "the document whose primary OR alternate address is X" as
25
+ * one indexed query. So the address is indexed on its own: one document per
26
+ * address, naming the contact it belongs to.
27
+ *
28
+ * ## Consulted first, never trusted alone
29
+ *
30
+ * The lookup reads the index entry and then the contact it names, and
31
+ * takes the contact only if it still exists. A stale entry — a contact
32
+ * detached and deleted after its entry was written — falls through to the
33
+ * query, which is the truth the index only summarizes.
34
+ *
35
+ * ## Written lazily, never backfilled
36
+ *
37
+ * An entry is written when a capture CREATES a contact, and when a lookup
38
+ * finds one through the query rather than the index — so every existing
39
+ * contact gains an entry on its next write, and no job has to walk the
40
+ * collection. A merge writes one entry per address the survivor answers to.
41
+ *
42
+ * ## Never in the way of a capture
43
+ *
44
+ * Every read and write here is wrapped: a form submission or an order must
45
+ * succeed whatever the index did, exactly as the capture itself swallows
46
+ * its own failures. An index that cannot be reached is a miss, and a miss
47
+ * is the query.
48
+ *
49
+ * ## One lookup for every door (AGL-2633)
50
+ *
51
+ * The capture door was the first reader; every other place that finds a
52
+ * person by address — an automation step, the flow-email consent gate, the
53
+ * delivery webhook's engagement stamp, the Inbox add-to-list check, a
54
+ * refund, a campaign's proof and consent reads, the REST create and
55
+ * `?email=` list — resolves through this same function, so a merged
56
+ * record's alternate address answers the same person everywhere. Two of
57
+ * those doors read differently from the capture, and the options carry it:
58
+ *
59
+ * - **`hostId`** narrows the answer to what ONE site may see. The index is
60
+ * org-wide, so the lookup is too; the scope check is applied to the
61
+ * document it names, in memory, which answers exactly what the scoped
62
+ * `email ==` query used to — one document per address, so a contact the
63
+ * site cannot see is `null` rather than a reason to look further.
64
+ * - **`transaction`** routes every read through the caller's transaction,
65
+ * for a door that updates the contact it finds inside one. The lazy index
66
+ * write stays OUTSIDE the transaction, deliberately: a queued write would
67
+ * forbid the caller any later read, and the entry is a cache fill that
68
+ * never has to be atomic with anything. It is idempotent, so a retried
69
+ * transaction body writing it twice is harmless.
70
+ *
71
+ * Imported by module path rather than through the `@aglyn/aglyn/server`
72
+ * barrel, for the reason `upsert-contact.ts` gives: the door's specs
73
+ * substitute a fixture barrel, and a helper that reached the normalizer only
74
+ * through it would find `undefined` there. Callers outside this library
75
+ * import THIS module by its leaf path for the mirror-image reason: most of
76
+ * their specs substitute the `@aglyn/tenant-data-admin` barrel wholesale.
77
+ */ import { CONTACT_EMAIL_INDEX_COLLECTION, normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
78
+ import { personKey } from "@aglyn/aglyn/app-utils/person-key";
79
+ import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
80
+ import { FieldValue } from "firebase-admin/firestore";
81
+ /**
82
+ * The org's index collection, beside its contacts one — or `null` when the
83
+ * handle has no parent, which is a fixture's contacts reference and never
84
+ * a real org subcollection. `null` means "no index": the caller falls back
85
+ * to the query and writes nothing.
86
+ */ export function emailIndexBeside(contactsRef) {
87
+ try {
88
+ const parent = contactsRef.parent;
89
+ return parent ? parent.collection(CONTACT_EMAIL_INDEX_COLLECTION) : null;
90
+ } catch (unused) {
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Point every address in `emails` at `contactId`. Never rejects: an entry
96
+ * that could not be written is one the next lookup writes again.
97
+ */ export async function writeContactEmailIndex(index, contactId, emails) {
98
+ if (!index || !contactId) return;
99
+ await Promise.all(emails.map(async (raw)=>{
100
+ const email = normalizeContactEmail(raw);
101
+ const key = email ? personKey(email) : null;
102
+ if (!email || !key) return;
103
+ const entry = {
104
+ email,
105
+ contactId,
106
+ updatedAt: FieldValue.serverTimestamp()
107
+ };
108
+ await index.doc(key).set(entry, {
109
+ merge: true
110
+ });
111
+ })).catch((error)=>{
112
+ console.error('[contact-email-index] write failed', contactId, error);
113
+ });
114
+ }
115
+ /**
116
+ * The contact an address belongs to, as a document snapshot — or `null`.
117
+ *
118
+ * The index first, then the per-document query it summarizes. A query hit
119
+ * writes the entry the index lacked, so the read that paid for the miss is
120
+ * the last one that has to.
121
+ */ export async function findContactByEmail(contactsRef, email, options = {}) {
122
+ const normalized = normalizeContactEmail(email);
123
+ if (!normalized) return null;
124
+ const { hostId, transaction } = options;
125
+ const readDoc = (ref)=>transaction ? transaction.get(ref) : ref.get();
126
+ const readQuery = (query)=>transaction ? transaction.get(query) : query.get();
127
+ const index = emailIndexBeside(contactsRef);
128
+ const key = index ? personKey(normalized) : null;
129
+ let hit = null;
130
+ if (index && key) {
131
+ try {
132
+ var _entry_get;
133
+ const entry = await readDoc(index.doc(key));
134
+ const contactId = entry.exists ? String((_entry_get = entry.get('contactId')) != null ? _entry_get : '') : '';
135
+ if (contactId) {
136
+ const contact = await readDoc(contactsRef.doc(contactId));
137
+ if (contact.exists) hit = contact;
138
+ }
139
+ } catch (error) {
140
+ console.error('[contact-email-index] lookup failed', error);
141
+ }
142
+ }
143
+ if (!hit) {
144
+ var _found_docs_;
145
+ const found = await readQuery(contactsRef.where('email', '==', normalized).limit(1));
146
+ hit = (_found_docs_ = found.docs[0]) != null ? _found_docs_ : null;
147
+ if (!hit) return null;
148
+ await writeContactEmailIndex(index, hit.id, [
149
+ normalized
150
+ ]);
151
+ }
152
+ if (hostId && !visibleToHost(hit.get('visibleTo'), hostId)) {
153
+ return null;
154
+ }
155
+ return hit;
156
+ }
157
+
158
+ //# sourceMappingURL=contact-email-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-email-index.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 ADDRESS INDEX (AGL-2625): `orgs/{orgId}/emailIndex/{personKey}`.\n *\n * `upsertHostContact` finds a person by `where('email', '==', …)` on the\n * contacts collection, and that query answers one document per address by\n * construction — which is exactly what stops a merged record's address from\n * ever finding the survivor. The survivor's `email` is the identity that\n * stayed; the merged address is in its `alternateEmails`, and Firestore\n * cannot answer \"the document whose primary OR alternate address is X\" as\n * one indexed query. So the address is indexed on its own: one document per\n * address, naming the contact it belongs to.\n *\n * ## Consulted first, never trusted alone\n *\n * The lookup reads the index entry and then the contact it names, and\n * takes the contact only if it still exists. A stale entry — a contact\n * detached and deleted after its entry was written — falls through to the\n * query, which is the truth the index only summarizes.\n *\n * ## Written lazily, never backfilled\n *\n * An entry is written when a capture CREATES a contact, and when a lookup\n * finds one through the query rather than the index — so every existing\n * contact gains an entry on its next write, and no job has to walk the\n * collection. A merge writes one entry per address the survivor answers to.\n *\n * ## Never in the way of a capture\n *\n * Every read and write here is wrapped: a form submission or an order must\n * succeed whatever the index did, exactly as the capture itself swallows\n * its own failures. An index that cannot be reached is a miss, and a miss\n * is the query.\n *\n * ## One lookup for every door (AGL-2633)\n *\n * The capture door was the first reader; every other place that finds a\n * person by address — an automation step, the flow-email consent gate, the\n * delivery webhook's engagement stamp, the Inbox add-to-list check, a\n * refund, a campaign's proof and consent reads, the REST create and\n * `?email=` list — resolves through this same function, so a merged\n * record's alternate address answers the same person everywhere. Two of\n * those doors read differently from the capture, and the options carry it:\n *\n * - **`hostId`** narrows the answer to what ONE site may see. The index is\n * org-wide, so the lookup is too; the scope check is applied to the\n * document it names, in memory, which answers exactly what the scoped\n * `email ==` query used to — one document per address, so a contact the\n * site cannot see is `null` rather than a reason to look further.\n * - **`transaction`** routes every read through the caller's transaction,\n * for a door that updates the contact it finds inside one. The lazy index\n * write stays OUTSIDE the transaction, deliberately: a queued write would\n * forbid the caller any later read, and the entry is a cache fill that\n * never has to be atomic with anything. It is idempotent, so a retried\n * transaction body writing it twice is harmless.\n *\n * Imported by module path rather than through the `@aglyn/aglyn/server`\n * barrel, for the reason `upsert-contact.ts` gives: the door's specs\n * substitute a fixture barrel, and a helper that reached the normalizer only\n * through it would find `undefined` there. Callers outside this library\n * import THIS module by its leaf path for the mirror-image reason: most of\n * their specs substitute the `@aglyn/tenant-data-admin` barrel wholesale.\n */\n\nimport {\n CONTACT_EMAIL_INDEX_COLLECTION,\n normalizeContactEmail,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** One entry: the address as stored, and the contact it resolves to. */\nexport interface ContactEmailIndexEntry {\n email: string\n contactId: string\n}\n\n/**\n * The org's index collection, beside its contacts one — or `null` when the\n * handle has no parent, which is a fixture's contacts reference and never\n * a real org subcollection. `null` means \"no index\": the caller falls back\n * to the query and writes nothing.\n */\nexport function emailIndexBeside(\n contactsRef: FirebaseFirestore.CollectionReference,\n): FirebaseFirestore.CollectionReference | null {\n try {\n const parent = contactsRef.parent\n return parent ? parent.collection(CONTACT_EMAIL_INDEX_COLLECTION) : null\n } catch {\n return null\n }\n}\n\n/**\n * Point every address in `emails` at `contactId`. Never rejects: an entry\n * that could not be written is one the next lookup writes again.\n */\nexport async function writeContactEmailIndex(\n index: FirebaseFirestore.CollectionReference | null,\n contactId: string,\n emails: readonly string[],\n): Promise<void> {\n if (!index || !contactId) return\n await Promise.all(\n emails.map(async (raw) => {\n const email = normalizeContactEmail(raw)\n const key = email ? personKey(email) : null\n if (!email || !key) return\n const entry: ContactEmailIndexEntry & { updatedAt: unknown } = {\n email,\n contactId,\n updatedAt: FieldValue.serverTimestamp(),\n }\n await index.doc(key).set(entry, { merge: true })\n }),\n ).catch((error: unknown) => {\n console.error('[contact-email-index] write failed', contactId, error)\n })\n}\n\n/** How one door reads differently from the capture; see the module notes. */\nexport interface FindContactByEmailOptions {\n /**\n * Answer only a contact THIS site may see. A document the address names\n * but the site cannot read is `null` — the same answer the scoped\n * `email ==` query gave, since one address names one document.\n */\n hostId?: string\n /**\n * Read through this transaction. Every read — the index entry, the\n * contact it names, the fallback query — goes through it; the lazy index\n * write does not, so the caller may still read after this returns.\n */\n transaction?: FirebaseFirestore.Transaction\n}\n\n/**\n * The contact an address belongs to, as a document snapshot — or `null`.\n *\n * The index first, then the per-document query it summarizes. A query hit\n * writes the entry the index lacked, so the read that paid for the miss is\n * the last one that has to.\n */\nexport async function findContactByEmail(\n contactsRef: FirebaseFirestore.CollectionReference,\n email: unknown,\n options: FindContactByEmailOptions = {},\n): Promise<FirebaseFirestore.DocumentSnapshot | null> {\n const normalized = normalizeContactEmail(email)\n if (!normalized) return null\n const { hostId, transaction } = options\n const readDoc = (ref: FirebaseFirestore.DocumentReference) =>\n transaction ? transaction.get(ref) : ref.get()\n const readQuery = (query: FirebaseFirestore.Query) =>\n transaction ? transaction.get(query) : query.get()\n\n const index = emailIndexBeside(contactsRef)\n const key = index ? personKey(normalized) : null\n let hit: FirebaseFirestore.DocumentSnapshot | null = null\n if (index && key) {\n try {\n const entry = await readDoc(index.doc(key))\n const contactId = entry.exists ? String(entry.get('contactId') ?? '') : ''\n if (contactId) {\n const contact = await readDoc(contactsRef.doc(contactId))\n if (contact.exists) hit = contact\n }\n } catch (error) {\n console.error('[contact-email-index] lookup failed', error)\n }\n }\n if (!hit) {\n const found = await readQuery(\n contactsRef.where('email', '==', normalized).limit(1),\n )\n hit = found.docs[0] ?? null\n if (!hit) return null\n await writeContactEmailIndex(index, hit.id, [normalized])\n }\n if (hostId && !visibleToHost(hit.get('visibleTo') as string[] | undefined, hostId)) {\n return null\n }\n return hit\n}\n"],"names":["CONTACT_EMAIL_INDEX_COLLECTION","normalizeContactEmail","personKey","visibleToHost","FieldValue","emailIndexBeside","contactsRef","parent","collection","writeContactEmailIndex","index","contactId","emails","Promise","all","map","raw","email","key","entry","updatedAt","serverTimestamp","doc","set","merge","catch","error","console","findContactByEmail","options","normalized","hostId","transaction","readDoc","ref","get","readQuery","query","hit","exists","String","contact","found","where","limit","docs","id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DC,GAED,SACEA,8BAA8B,EAC9BC,qBAAqB,QAChB,kCAAiC;AACxC,SAASC,SAAS,QAAQ,oCAAmC;AAC7D,SAASC,aAAa,QAAQ,sCAAqC;AACnE,SAASC,UAAU,QAAQ,2BAA0B;AAQrD;;;;;CAKC,GACD,OAAO,SAASC,iBACdC,WAAkD;IAElD,IAAI;QACF,MAAMC,SAASD,YAAYC,MAAM;QACjC,OAAOA,SAASA,OAAOC,UAAU,CAACR,kCAAkC;IACtE,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeS,uBACpBC,KAAmD,EACnDC,SAAiB,EACjBC,MAAyB;IAEzB,IAAI,CAACF,SAAS,CAACC,WAAW;IAC1B,MAAME,QAAQC,GAAG,CACfF,OAAOG,GAAG,CAAC,OAAOC;QAChB,MAAMC,QAAQhB,sBAAsBe;QACpC,MAAME,MAAMD,QAAQf,UAAUe,SAAS;QACvC,IAAI,CAACA,SAAS,CAACC,KAAK;QACpB,MAAMC,QAAyD;YAC7DF;YACAN;YACAS,WAAWhB,WAAWiB,eAAe;QACvC;QACA,MAAMX,MAAMY,GAAG,CAACJ,KAAKK,GAAG,CAACJ,OAAO;YAAEK,OAAO;QAAK;IAChD,IACAC,KAAK,CAAC,CAACC;QACPC,QAAQD,KAAK,CAAC,sCAAsCf,WAAWe;IACjE;AACF;AAkBA;;;;;;CAMC,GACD,OAAO,eAAeE,mBACpBtB,WAAkD,EAClDW,KAAc,EACdY,UAAqC,CAAC,CAAC;IAEvC,MAAMC,aAAa7B,sBAAsBgB;IACzC,IAAI,CAACa,YAAY,OAAO;IACxB,MAAM,EAAEC,MAAM,EAAEC,WAAW,EAAE,GAAGH;IAChC,MAAMI,UAAU,CAACC,MACfF,cAAcA,YAAYG,GAAG,CAACD,OAAOA,IAAIC,GAAG;IAC9C,MAAMC,YAAY,CAACC,QACjBL,cAAcA,YAAYG,GAAG,CAACE,SAASA,MAAMF,GAAG;IAElD,MAAMzB,QAAQL,iBAAiBC;IAC/B,MAAMY,MAAMR,QAAQR,UAAU4B,cAAc;IAC5C,IAAIQ,MAAiD;IACrD,IAAI5B,SAASQ,KAAK;QAChB,IAAI;gBAEsCC;YADxC,MAAMA,QAAQ,MAAMc,QAAQvB,MAAMY,GAAG,CAACJ;YACtC,MAAMP,YAAYQ,MAAMoB,MAAM,GAAGC,QAAOrB,aAAAA,MAAMgB,GAAG,CAAC,wBAAVhB,aAA0B,MAAM;YACxE,IAAIR,WAAW;gBACb,MAAM8B,UAAU,MAAMR,QAAQ3B,YAAYgB,GAAG,CAACX;gBAC9C,IAAI8B,QAAQF,MAAM,EAAED,MAAMG;YAC5B;QACF,EAAE,OAAOf,OAAO;YACdC,QAAQD,KAAK,CAAC,uCAAuCA;QACvD;IACF;IACA,IAAI,CAACY,KAAK;YAIFI;QAHN,MAAMA,QAAQ,MAAMN,UAClB9B,YAAYqC,KAAK,CAAC,SAAS,MAAMb,YAAYc,KAAK,CAAC;QAErDN,OAAMI,eAAAA,MAAMG,IAAI,CAAC,EAAE,YAAbH,eAAiB;QACvB,IAAI,CAACJ,KAAK,OAAO;QACjB,MAAM7B,uBAAuBC,OAAO4B,IAAIQ,EAAE,EAAE;YAAChB;SAAW;IAC1D;IACA,IAAIC,UAAU,CAAC5B,cAAcmC,IAAIH,GAAG,CAAC,cAAsCJ,SAAS;QAClF,OAAO;IACT;IACA,OAAOO;AACT"}
@@ -0,0 +1,89 @@
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
+ * A person's lifecycle stage, FLOORED at what just happened to them
19
+ * (AGL-2641).
20
+ *
21
+ * The capture doors floor a stage inside `upsertHostContact`: an order
22
+ * names `customer` as its `initialLifecycleStage`, and the door fills an
23
+ * empty stage or advances an earlier one and never moves anybody back. A
24
+ * won deal is the same fact as a purchase — the business has decided this
25
+ * person bought — but it arrives at a record that already exists, through a
26
+ * writer that is not a capture door: the deal-stage route and the REST
27
+ * deals resource. This is the door's rule for them, over a contact document
28
+ * they already hold a reference to.
29
+ *
30
+ * The write goes into ONE holder's facet, because a stage is one business's
31
+ * reading of a person and the row is shared by every site in the org. The
32
+ * holder is the site the deal was made on, resolved to its consent group
33
+ * the way every other facet writer resolves it; a deal that names no site
34
+ * falls back to the site that captured the person, which is the one
35
+ * holder a contact always has. A contact that names none either is left
36
+ * alone and the caller told, rather than written into a facet nobody reads.
37
+ *
38
+ * The write is skipped when it would change nothing — a customer stays a
39
+ * customer, an evangelist stays an evangelist, `other` is never overwritten
40
+ * — so a caller can announce `contactStageChanged` exactly when `advanced`
41
+ * says the stage moved, and an automation listening for the change never
42
+ * hears one that did not happen.
43
+ */
44
+ import { type ContactLifecycleStage } from '@aglyn/aglyn/server';
45
+ export type ContactLifecycleFloor = {
46
+ /** The stage was raised to the floor. */
47
+ outcome: 'advanced';
48
+ contactId: string;
49
+ /** The address on the row, for the event a caller announces. */
50
+ email: string;
51
+ /** The site whose facet was written. */
52
+ hostId: string;
53
+ groupId: string;
54
+ /** The stage before the write, or `''` for a person who had none. */
55
+ previousStage: ContactLifecycleStage | '';
56
+ lifecycleStage: ContactLifecycleStage;
57
+ } | {
58
+ /** The person already held the floor or a later stage; nothing was written. */
59
+ outcome: 'held';
60
+ contactId: string;
61
+ email: string;
62
+ hostId: string;
63
+ groupId: string;
64
+ lifecycleStage: ContactLifecycleStage;
65
+ } | {
66
+ /** No such contact. */
67
+ outcome: 'missing';
68
+ contactId: string;
69
+ } | {
70
+ /** Neither the caller nor the contact names a site, so there is no facet to write. */
71
+ outcome: 'unheld';
72
+ contactId: string;
73
+ };
74
+ export interface ContactLifecycleFloorOptions {
75
+ contactRef: FirebaseFirestore.DocumentReference;
76
+ /**
77
+ * The org the contact belongs to, for the consent groups — the same
78
+ * document every facet writer resolves the holder through.
79
+ */
80
+ org: Record<string, unknown> | null | undefined;
81
+ /**
82
+ * The site the act happened on, whose facet takes the stage. `null` for a
83
+ * record no site captured; the contact's own capturing site is used then.
84
+ */
85
+ hostId: string | null | undefined;
86
+ /** The EARLIEST stage that describes what happened — see `advanceContactLifecycleStage`. */
87
+ floor: ContactLifecycleStage;
88
+ }
89
+ export declare function floorContactLifecycleStage(options: ContactLifecycleFloorOptions): Promise<ContactLifecycleFloor>;
@@ -0,0 +1,88 @@
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
+ * A person's lifecycle stage, FLOORED at what just happened to them
18
+ * (AGL-2641).
19
+ *
20
+ * The capture doors floor a stage inside `upsertHostContact`: an order
21
+ * names `customer` as its `initialLifecycleStage`, and the door fills an
22
+ * empty stage or advances an earlier one and never moves anybody back. A
23
+ * won deal is the same fact as a purchase — the business has decided this
24
+ * person bought — but it arrives at a record that already exists, through a
25
+ * writer that is not a capture door: the deal-stage route and the REST
26
+ * deals resource. This is the door's rule for them, over a contact document
27
+ * they already hold a reference to.
28
+ *
29
+ * The write goes into ONE holder's facet, because a stage is one business's
30
+ * reading of a person and the row is shared by every site in the org. The
31
+ * holder is the site the deal was made on, resolved to its consent group
32
+ * the way every other facet writer resolves it; a deal that names no site
33
+ * falls back to the site that captured the person, which is the one
34
+ * holder a contact always has. A contact that names none either is left
35
+ * alone and the caller told, rather than written into a facet nobody reads.
36
+ *
37
+ * The write is skipped when it would change nothing — a customer stays a
38
+ * customer, an evangelist stays an evangelist, `other` is never overwritten
39
+ * — so a caller can announce `contactStageChanged` exactly when `advanced`
40
+ * says the stage moved, and an automation listening for the change never
41
+ * hears one that did not happen.
42
+ */ import { consentGroupForHost, advanceContactLifecycleStage, contactFacetPath, isContactLifecycleStage, readContactFacet } from "@aglyn/aglyn/server";
43
+ import { FieldValue } from "firebase-admin/firestore";
44
+ export async function floorContactLifecycleStage(options) {
45
+ var _snapshot_data, _options_hostId, _data_hostId, _advanceContactLifecycleStage, _data_email;
46
+ const { contactRef, org, floor } = options;
47
+ const snapshot = await contactRef.get();
48
+ if (!snapshot.exists) return {
49
+ outcome: 'missing',
50
+ contactId: contactRef.id
51
+ };
52
+ const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
53
+ const hostId = String((_options_hostId = options.hostId) != null ? _options_hostId : '').trim() || String((_data_hostId = data['hostId']) != null ? _data_hostId : '').trim();
54
+ if (!hostId) return {
55
+ outcome: 'unheld',
56
+ contactId: snapshot.id
57
+ };
58
+ const { groupId } = consentGroupForHost(org != null ? org : null, hostId);
59
+ const facet = readContactFacet(data, groupId);
60
+ const held = isContactLifecycleStage(facet.lifecycleStage) ? facet.lifecycleStage : undefined;
61
+ const lifecycleStage = (_advanceContactLifecycleStage = advanceContactLifecycleStage(held, floor)) != null ? _advanceContactLifecycleStage : floor;
62
+ const email = String((_data_email = data['email']) != null ? _data_email : '');
63
+ if (lifecycleStage === held) {
64
+ return {
65
+ outcome: 'held',
66
+ contactId: snapshot.id,
67
+ email,
68
+ hostId,
69
+ groupId,
70
+ lifecycleStage
71
+ };
72
+ }
73
+ await snapshot.ref.update({
74
+ [contactFacetPath(groupId, 'lifecycleStage')]: lifecycleStage,
75
+ updatedAt: FieldValue.serverTimestamp()
76
+ });
77
+ return {
78
+ outcome: 'advanced',
79
+ contactId: snapshot.id,
80
+ email,
81
+ hostId,
82
+ groupId,
83
+ previousStage: held != null ? held : '',
84
+ lifecycleStage
85
+ };
86
+ }
87
+
88
+ //# sourceMappingURL=contact-lifecycle-floor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-lifecycle-floor.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 * A person's lifecycle stage, FLOORED at what just happened to them\n * (AGL-2641).\n *\n * The capture doors floor a stage inside `upsertHostContact`: an order\n * names `customer` as its `initialLifecycleStage`, and the door fills an\n * empty stage or advances an earlier one and never moves anybody back. A\n * won deal is the same fact as a purchase — the business has decided this\n * person bought — but it arrives at a record that already exists, through a\n * writer that is not a capture door: the deal-stage route and the REST\n * deals resource. This is the door's rule for them, over a contact document\n * they already hold a reference to.\n *\n * The write goes into ONE holder's facet, because a stage is one business's\n * reading of a person and the row is shared by every site in the org. The\n * holder is the site the deal was made on, resolved to its consent group\n * the way every other facet writer resolves it; a deal that names no site\n * falls back to the site that captured the person, which is the one\n * holder a contact always has. A contact that names none either is left\n * alone and the caller told, rather than written into a facet nobody reads.\n *\n * The write is skipped when it would change nothing — a customer stays a\n * customer, an evangelist stays an evangelist, `other` is never overwritten\n * — so a caller can announce `contactStageChanged` exactly when `advanced`\n * says the stage moved, and an automation listening for the change never\n * hears one that did not happen.\n */\n\nimport {\n consentGroupForHost,\n advanceContactLifecycleStage,\n contactFacetPath,\n type ContactLifecycleStage,\n isContactLifecycleStage,\n readContactFacet,\n} from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\n\nexport type ContactLifecycleFloor =\n | {\n /** The stage was raised to the floor. */\n outcome: 'advanced'\n contactId: string\n /** The address on the row, for the event a caller announces. */\n email: string\n /** The site whose facet was written. */\n hostId: string\n groupId: string\n /** The stage before the write, or `''` for a person who had none. */\n previousStage: ContactLifecycleStage | ''\n lifecycleStage: ContactLifecycleStage\n }\n | {\n /** The person already held the floor or a later stage; nothing was written. */\n outcome: 'held'\n contactId: string\n email: string\n hostId: string\n groupId: string\n lifecycleStage: ContactLifecycleStage\n }\n | {\n /** No such contact. */\n outcome: 'missing'\n contactId: string\n }\n | {\n /** Neither the caller nor the contact names a site, so there is no facet to write. */\n outcome: 'unheld'\n contactId: string\n }\n\nexport interface ContactLifecycleFloorOptions {\n contactRef: FirebaseFirestore.DocumentReference\n /**\n * The org the contact belongs to, for the consent groups — the same\n * document every facet writer resolves the holder through.\n */\n org: Record<string, unknown> | null | undefined\n /**\n * The site the act happened on, whose facet takes the stage. `null` for a\n * record no site captured; the contact's own capturing site is used then.\n */\n hostId: string | null | undefined\n /** The EARLIEST stage that describes what happened — see `advanceContactLifecycleStage`. */\n floor: ContactLifecycleStage\n}\n\nexport async function floorContactLifecycleStage(\n options: ContactLifecycleFloorOptions,\n): Promise<ContactLifecycleFloor> {\n const { contactRef, org, floor } = options\n const snapshot = await contactRef.get()\n if (!snapshot.exists) return { outcome: 'missing', contactId: contactRef.id }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const hostId = String(options.hostId ?? '').trim() || String(data['hostId'] ?? '').trim()\n if (!hostId) return { outcome: 'unheld', contactId: snapshot.id }\n\n const { groupId } = consentGroupForHost(org ?? null, hostId)\n const facet = readContactFacet(data, groupId)\n const held = isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : undefined\n const lifecycleStage = advanceContactLifecycleStage(held, floor) ?? floor\n const email = String(data['email'] ?? '')\n if (lifecycleStage === held) {\n return { outcome: 'held', contactId: snapshot.id, email, hostId, groupId, lifecycleStage }\n }\n await snapshot.ref.update({\n [contactFacetPath(groupId, 'lifecycleStage')]: lifecycleStage,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return {\n outcome: 'advanced',\n contactId: snapshot.id,\n email,\n hostId,\n groupId,\n previousStage: held ?? '',\n lifecycleStage,\n }\n}\n"],"names":["consentGroupForHost","advanceContactLifecycleStage","contactFacetPath","isContactLifecycleStage","readContactFacet","FieldValue","floorContactLifecycleStage","options","snapshot","data","contactRef","org","floor","get","exists","outcome","contactId","id","hostId","String","trim","groupId","facet","held","lifecycleStage","undefined","email","ref","update","updatedAt","serverTimestamp","previousStage"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,SACEA,mBAAmB,EACnBC,4BAA4B,EAC5BC,gBAAgB,EAEhBC,uBAAuB,EACvBC,gBAAgB,QACX,sBAAqB;AAC5B,SAASC,UAAU,QAAQ,2BAA0B;AAoDrD,OAAO,eAAeC,2BACpBC,OAAqC;QAKvBC,gBACQD,iBAAuCE,cAQtCR,+BACFQ;IAbrB,MAAM,EAAEC,UAAU,EAAEC,GAAG,EAAEC,KAAK,EAAE,GAAGL;IACnC,MAAMC,WAAW,MAAME,WAAWG,GAAG;IACrC,IAAI,CAACL,SAASM,MAAM,EAAE,OAAO;QAAEC,SAAS;QAAWC,WAAWN,WAAWO,EAAE;IAAC;IAC5E,MAAMR,QAAQD,iBAAAA,SAASC,IAAI,cAAbD,iBAAmB,CAAC;IAClC,MAAMU,SAASC,QAAOZ,kBAAAA,QAAQW,MAAM,YAAdX,kBAAkB,IAAIa,IAAI,MAAMD,QAAOV,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB,IAAIW,IAAI;IACvF,IAAI,CAACF,QAAQ,OAAO;QAAEH,SAAS;QAAUC,WAAWR,SAASS,EAAE;IAAC;IAEhE,MAAM,EAAEI,OAAO,EAAE,GAAGrB,oBAAoBW,cAAAA,MAAO,MAAMO;IACrD,MAAMI,QAAQlB,iBAAiBK,MAAMY;IACrC,MAAME,OAAOpB,wBAAwBmB,MAAME,cAAc,IACrDF,MAAME,cAAc,GACpBC;IACJ,MAAMD,kBAAiBvB,gCAAAA,6BAA6BsB,MAAMX,kBAAnCX,gCAA6CW;IACpE,MAAMc,QAAQP,QAAOV,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB;IACtC,IAAIe,mBAAmBD,MAAM;QAC3B,OAAO;YAAER,SAAS;YAAQC,WAAWR,SAASS,EAAE;YAAES;YAAOR;YAAQG;YAASG;QAAe;IAC3F;IACA,MAAMhB,SAASmB,GAAG,CAACC,MAAM,CAAC;QACxB,CAAC1B,iBAAiBmB,SAAS,kBAAkB,EAAEG;QAC/CK,WAAWxB,WAAWyB,eAAe;IACvC;IACA,OAAO;QACLf,SAAS;QACTC,WAAWR,SAASS,EAAE;QACtBS;QACAR;QACAG;QACAU,aAAa,EAAER,eAAAA,OAAQ;QACvBC;IACF;AACF"}