@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,581 @@
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
+ */ /**
18
+ * Materializes a `kind: 'dynamic'` list's rule into its `members`
19
+ * subcollection (`docs/specs/email-overhaul.md` §3b/§3e).
20
+ *
21
+ * A dynamic list does NOT resolve at send time. It stores a rule, a sweep
22
+ * writes the matching people into the same `members` collection a manual list
23
+ * uses, and the send path keeps reading one deterministic collection. Three
24
+ * reasons, all of them things that have already gone wrong here:
25
+ *
26
+ * 1. The send must not run an unbounded scan — that is what produced an
27
+ * arbitrary, unstable 500 of a site's 3,000 leads.
28
+ * 2. The composer needs a cheap count, not a 5,000-document scan per
29
+ * keystroke.
30
+ * 3. "Who did this campaign go to" is a support question and a compliance
31
+ * question. A materialized membership answers it; a rule re-evaluated
32
+ * later does not.
33
+ *
34
+ * ## ⛔ A CEILING NEVER REMOVES A PERSON
35
+ *
36
+ * This function has no quota comparison and must never grow one. Capacity in
37
+ * this product is enforced at the REDUCTION, not at the use: a limit that
38
+ * refuses a person or their data leaks, because the person is already there
39
+ * and dropping them is not a refusal, it is deletion. So a rule that matches
40
+ * more people than a plan's audience band, or than a single send's recipient
41
+ * cap, materializes ALL of them. The ceiling that a large audience meets is
42
+ * `performCampaignSend`'s, which refuses the SEND and states the count it
43
+ * found — a refusal the merchant can see, argue with, and act on, where a
44
+ * trimmed membership is silent and unreproducible.
45
+ *
46
+ * What bounds this function is the per-run SCAN budget below, which is a
47
+ * different thing: it bounds how much work one sweep does, and the cursor it
48
+ * returns resumes the same sweep on the next run. Running out of budget
49
+ * postpones enrollments; it never removes anybody, and it never finishes a
50
+ * sweep it did not complete.
51
+ */ import { FieldValue } from "firebase-admin/firestore";
52
+ import { soloConsentGroup, candidateMatchesDynamicListRule, dynamicListRuleIsEmpty, dynamicListRuleListIds, dynamicListRuleWithoutListReference, dynamicListRuleNeedsContactFacet, dynamicListDimensionsForCrmView, dynamicListPlanningRule, CRM_COLLECTIONS, normalizeCrmViewFilters, dynamicListRuleNeedsEngagement, extractEmailFromFields, isContactLifecycleStage, normalizeContactEmail, normalizeDynamicListRule, personKey, readCampaignIds, readContactCampaignIds, readContactFacet } from "@aglyn/aglyn/server";
53
+ import { firebaseAdmin } from "./firebase-admin.js";
54
+ import { readPersonEngagementByKeys } from "./email-delivery-log.js";
55
+ import { enrollListMember } from "./list-members.js";
56
+ import { consentGroupForSite, orgDataCollectionForHost, scopedToHost } from "./organizations.js";
57
+ /**
58
+ * Documents one sweep run may READ across all sources.
59
+ *
60
+ * A bound on work, not on membership. When a run exhausts it the sweep stops
61
+ * where it is, returns `complete: false` with the cursor it reached, and the
62
+ * next run continues — the shape the import-history sweep already uses. The
63
+ * alternative, an unbounded scan of every silo per list per run, is the
64
+ * expensive-read pattern this codebase refuses.
65
+ *
66
+ * ⚠️ Membership reconciliation is SKIPPED on an incomplete run. A partial
67
+ * scan has only seen part of the population, so anybody it did not reach
68
+ * would look like a non-match — and acting on that would delete real members
69
+ * because a budget ran out.
70
+ */ export const DYNAMIC_LIST_SCAN_BUDGET = 5000;
71
+ /** Documents read per source page. */ const PAGE_SIZE = 500;
72
+ /**
73
+ * The silo collections, as queries.
74
+ *
75
+ * `contacts` is org-scoped and must go through `scopedToHost` — the Admin SDK
76
+ * does not evaluate rules, so an unfiltered read would let one site's list
77
+ * materialize another site's contacts. The other three are host-owned and
78
+ * carry no `visibleTo` to filter on.
79
+ */ async function sourceQuery(source, hostId) {
80
+ const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
81
+ if (source === 'contacts') {
82
+ const ref = await orgDataCollectionForHost(hostId, 'contacts');
83
+ return scopedToHost(ref, hostId);
84
+ }
85
+ return hostRef.collection(source);
86
+ }
87
+ const millis = (value)=>{
88
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
89
+ const toMillis = value == null ? void 0 : value.toMillis;
90
+ if (typeof toMillis === 'function') {
91
+ const parsed = Number(toMillis.call(value));
92
+ return Number.isFinite(parsed) ? parsed : null;
93
+ }
94
+ return null;
95
+ };
96
+ /**
97
+ * The CRM fields a rule may read, out of ONE holder's facet (AGL-2603).
98
+ *
99
+ * The owner, the stage, the company and the custom values are this holder's
100
+ * business records on a shared row. They are read through `readContactFacet`
101
+ * for the group the sweep runs as — never off the top of the document, where
102
+ * a pre-facet row could still carry somebody's fields, and never out of
103
+ * another group's facet. A field the facet does not carry is left ABSENT on
104
+ * the candidate, which the matcher reads as "does not match" for the first
105
+ * three and as "unset" for a custom clause; a stage the model does not name
106
+ * is dropped for the reason the normalizer drops one from a rule.
107
+ */ function crmFieldsFromFacet(data, groupId) {
108
+ var _facet_lastEmailEngagementAtMs;
109
+ const facet = readContactFacet(data, groupId);
110
+ const custom = facet.custom && typeof facet.custom === 'object' && !Array.isArray(facet.custom) ? facet.custom : undefined;
111
+ // The campaign engagement stamp (AGL-2616), on the same footing as the
112
+ // owner: this holder's fact about the person, read out of this holder's
113
+ // facet, absent when the webhook has never stamped one.
114
+ const engaged = Number((_facet_lastEmailEngagementAtMs = facet.lastEmailEngagementAtMs) != null ? _facet_lastEmailEngagementAtMs : 0);
115
+ return _extends({}, typeof facet.ownerUid === 'string' && facet.ownerUid ? {
116
+ ownerUid: facet.ownerUid
117
+ } : {}, isContactLifecycleStage(facet.lifecycleStage) ? {
118
+ lifecycleStage: facet.lifecycleStage
119
+ } : {}, typeof facet.companyId === 'string' && facet.companyId ? {
120
+ companyId: facet.companyId
121
+ } : {}, custom ? {
122
+ custom
123
+ } : {}, Number.isFinite(engaged) && engaged > 0 ? {
124
+ lastEmailEngagementAtMs: engaged
125
+ } : {});
126
+ }
127
+ /**
128
+ * Reads one silo document into the shape the rule matcher understands.
129
+ *
130
+ * @param groupId the consent group whose contact facet the campaign membership
131
+ * and the CRM fields are read from, or `null` when the rule names none of
132
+ * them and no group was resolved. A contact row is shared by every site in
133
+ * the org and its membership, its owner, its stage, its company and its
134
+ * custom values all live inside one holder's facet, so there is no
135
+ * expression here that returns another holder's filing.
136
+ */ function toCandidate(source, doc, groupId) {
137
+ var _data_tags, _data_sources, _data_ordersCount, _data_ltvCents, _data_formName;
138
+ const data = doc.data();
139
+ const email = source === 'formSubmissions' ? // same best-effort extraction the submissions route already uses to
140
+ // decide whether a submission produces a contact at all.
141
+ extractEmailFromFields(data['fields']) : normalizeContactEmail(data['email']);
142
+ if (!email) return null;
143
+ const createdAtMs = millis(data['createdAt']);
144
+ return _extends({
145
+ silo: source,
146
+ email,
147
+ // `siteMembers` stores `displayName`; the rest store `name`. Reading only
148
+ // `name` is what left every member campaign's merge tags blank.
149
+ name: typeof data['name'] === 'string' && data['name'] || typeof data['displayName'] === 'string' && data['displayName'] || undefined,
150
+ createdAtMs
151
+ }, source === 'contacts' ? _extends({
152
+ tags: (_data_tags = data['tags']) != null ? _data_tags : [],
153
+ sources: (_data_sources = data['sources']) != null ? _data_sources : {},
154
+ ordersCount: Number((_data_ordersCount = data['ordersCount']) != null ? _data_ordersCount : 0),
155
+ ltvCents: Number((_data_ltvCents = data['ltvCents']) != null ? _data_ltvCents : 0),
156
+ lastPurchaseAtMs: millis(data['lastPurchaseAtMs'])
157
+ }, groupId ? _extends({
158
+ campaignIds: readContactCampaignIds(data, groupId)
159
+ }, crmFieldsFromFacet(data, groupId)) : {}) : {}, source === 'formSubmissions' ? {
160
+ formName: String((_data_formName = data['formName']) != null ? _data_formName : ''),
161
+ // At the TOP of a submission, because a submission belongs to one
162
+ // site — the same shape every other host resource carries it in.
163
+ campaignIds: readCampaignIds(data)
164
+ } : {});
165
+ }
166
+ /**
167
+ * Resolves what a rule needs enriched.
168
+ *
169
+ * A named list that no longer exists resolves to a collection with no
170
+ * documents, which reads as "nobody is a member" — the same direction a
171
+ * deleted `segmentId` takes, and the safe one: an `inListIds` naming a
172
+ * deleted list selects nobody rather than everybody, and a `notInListIds`
173
+ * naming one excludes nobody rather than the whole audience.
174
+ */ async function planEnrichment(rule, hostId) {
175
+ const listIds = dynamicListRuleListIds(rule);
176
+ const listMembers = new Map();
177
+ if (listIds.length) {
178
+ const lists = await orgDataCollectionForHost(hostId, 'lists');
179
+ for (const listId of listIds){
180
+ listMembers.set(listId, lists.doc(listId).collection('members'));
181
+ }
182
+ }
183
+ return {
184
+ engagement: dynamicListRuleNeedsEngagement(rule),
185
+ listMembers
186
+ };
187
+ }
188
+ /** True when nothing needs looking up, so the page loop can skip the work. */ function planIsEmpty(plan) {
189
+ return !plan.engagement && plan.listMembers.size === 0;
190
+ }
191
+ /**
192
+ * Fills the enrichment fields on one page's candidates.
193
+ *
194
+ * Keys are de-duplicated first: two silos can produce the same person on one
195
+ * page, and looking them up twice would spend budget on an answer already in
196
+ * hand.
197
+ *
198
+ * @returns how many documents were read, for the caller's budget.
199
+ */ async function enrichCandidates(candidates, plan) {
200
+ if (!candidates.length || planIsEmpty(plan)) return 0;
201
+ const keyed = new Map();
202
+ for (const candidate of candidates){
203
+ const key = personKey(candidate.email);
204
+ if (!key) continue;
205
+ const held = keyed.get(key);
206
+ if (held) held.push(candidate);
207
+ else keyed.set(key, [
208
+ candidate
209
+ ]);
210
+ }
211
+ const keys = [
212
+ ...keyed.keys()
213
+ ];
214
+ if (!keys.length) return 0;
215
+ const firestore = firebaseAdmin.app().firestore();
216
+ let read = 0;
217
+ if (plan.engagement) {
218
+ // The scan's own Firestore handle, passed rather than defaulted: the
219
+ // rollup lives in another module with its own default, and two handles in
220
+ // one sweep is two things a caller has to configure to reach one store.
221
+ const engagement = await readPersonEngagementByKeys(keys, firestore);
222
+ read += keys.length;
223
+ for (const [key, rows] of keyed){
224
+ const found = engagement.get(key);
225
+ for (const candidate of rows){
226
+ var _ref, _ref1;
227
+ candidate.lastOpenedAtMs = (_ref = found == null ? void 0 : found.lastOpenedAtMs) != null ? _ref : null;
228
+ candidate.lastClickedAtMs = (_ref1 = found == null ? void 0 : found.lastClickedAtMs) != null ? _ref1 : null;
229
+ }
230
+ }
231
+ }
232
+ if (plan.listMembers.size) {
233
+ // No pre-seeding of `listIds`. A candidate found on no list keeps the
234
+ // field absent, and the matcher reads absent as "on no list" — see
235
+ // `candidateMatchesDynamicListRule` for why that direction is the safe
236
+ // one. Writing an empty array first would be a line saying the same thing
237
+ // the matcher already says.
238
+ for (const [listId, members] of plan.listMembers){
239
+ let snapshots = [];
240
+ try {
241
+ snapshots = await firestore.getAll(...keys.map((key)=>members.doc(key)));
242
+ } catch (error) {
243
+ // A failed membership lookup leaves every candidate reading as a
244
+ // non-member of this list. That keeps an `inListIds` rule from
245
+ // enrolling people it could not confirm, and it is the direction a
246
+ // reader can recover from — the next sweep asks again.
247
+ console.error('[dynamic-list] list membership lookup failed', listId, error);
248
+ }
249
+ read += keys.length;
250
+ for (const snapshot of snapshots){
251
+ var _keyed_get;
252
+ if (!(snapshot == null ? void 0 : snapshot.exists)) continue;
253
+ for (const candidate of (_keyed_get = keyed.get(snapshot.id)) != null ? _keyed_get : []){
254
+ var _candidate_listIds;
255
+ candidate.listIds = [
256
+ ...(_candidate_listIds = candidate.listIds) != null ? _candidate_listIds : [],
257
+ listId
258
+ ];
259
+ }
260
+ }
261
+ }
262
+ }
263
+ return read;
264
+ }
265
+ /**
266
+ * WHO A RULE SELECTS, without writing anything.
267
+ *
268
+ * The scan and the enrollment used to be one function, so "who would this
269
+ * match" could only be answered by materializing it. That is the wrong shape
270
+ * for two callers that now exist: a merchant asking to see an audience before
271
+ * committing to it, and a fixed list using the filters to FIND people to add.
272
+ * Neither may write memberships as a side effect of a question.
273
+ *
274
+ * One scanner rather than two. Every subtlety here is a place a second
275
+ * implementation would drift — the in-memory date filter (an `orderBy` on a
276
+ * data field would DROP undated documents rather than merely not matching
277
+ * them), the `__name__` paging order, `scopedToHost` on `contacts` and on
278
+ * nothing else, and a deleted `segmentId` narrowing to nobody rather than
279
+ * widening to everybody. `materializeDynamicList` reads this and then enrolls.
280
+ *
281
+ * NOTHING here writes, and nothing here compares a count against a limit.
282
+ */ export async function collectDynamicListCandidates(options) {
283
+ var _options_nowMs, _options_scanBudget;
284
+ const rule = normalizeDynamicListRule(options.rule);
285
+ const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
286
+ const budget = (_options_scanBudget = options.scanBudget) != null ? _options_scanBudget : DYNAMIC_LIST_SCAN_BUDGET;
287
+ if (dynamicListRuleIsEmpty(rule)) {
288
+ return {
289
+ candidates: [],
290
+ complete: true,
291
+ cursor: null,
292
+ empty: true,
293
+ read: 0
294
+ };
295
+ }
296
+ // A saved segment's filters, resolved ONCE rather than per candidate.
297
+ let segment = null;
298
+ if (rule.segmentId) {
299
+ var _snapshot_get, _snapshot_get1;
300
+ const segments = await orgDataCollectionForHost(options.hostId, 'contactSegments');
301
+ const snapshot = await segments.doc(rule.segmentId).get();
302
+ // A deleted segment narrows to nothing rather than widening to everybody:
303
+ // a rule whose filter vanished must not start matching the whole org.
304
+ if (!snapshot.exists) {
305
+ return {
306
+ candidates: [],
307
+ complete: true,
308
+ cursor: null,
309
+ empty: true,
310
+ read: 0
311
+ };
312
+ }
313
+ segment = {
314
+ tags: (_snapshot_get = snapshot.get('tags')) != null ? _snapshot_get : [],
315
+ sources: (_snapshot_get1 = snapshot.get('sources')) != null ? _snapshot_get1 : []
316
+ };
317
+ }
318
+ /*
319
+ * A saved VIEW's filters, resolved ONCE into this language (AGL-2617).
320
+ *
321
+ * The same two leans the segment takes, for the same reason: a view that
322
+ * was deleted narrows to nothing, and so does one whose clauses this
323
+ * language cannot carry — `dynamicListDimensionsForCrmView` names them
324
+ * rather than dropping them, and dropping one would make the audience
325
+ * wider than the view the merchant picked. The picker offers only views
326
+ * that translate whole, so the second case is a view edited after the
327
+ * rule named it, and a view of another section — a list of companies is
328
+ * not an audience of people — reads the same way.
329
+ */ let view = null;
330
+ if (rule.viewId) {
331
+ const views = await orgDataCollectionForHost(options.hostId, CRM_COLLECTIONS.views);
332
+ const snapshot = await views.doc(rule.viewId).get();
333
+ const translation = snapshot.exists && snapshot.get('section') === 'contacts' ? dynamicListDimensionsForCrmView(normalizeCrmViewFilters(snapshot.get('filters'))) : null;
334
+ if (!translation || translation.unsupported.length) {
335
+ return {
336
+ candidates: [],
337
+ complete: true,
338
+ cursor: null,
339
+ empty: true,
340
+ read: 0
341
+ };
342
+ }
343
+ view = translation.dimensions;
344
+ }
345
+ /*
346
+ * Resolved once for the whole scan: it names collections, and re-resolving
347
+ * the org for every page would be a read per page for an unchanging answer.
348
+ * Planned with the view's dimensions beside the rule's: a view naming an
349
+ * owner needs the facet exactly as a rule naming one does.
350
+ */ const planning = dynamicListPlanningRule(rule, view);
351
+ const enrichment = await planEnrichment(planning, options.hostId);
352
+ /*
353
+ * THE HOLDER whose contact facet a campaign or CRM clause is read from.
354
+ *
355
+ * The same group the writers use — the sites declared to be one sender, or
356
+ * this site alone — because a facet written under one group id and read
357
+ * under another is a membership that silently matches nobody. Resolved once,
358
+ * and only for a rule that names something the facet holds: a campaign, an
359
+ * owner, a stage, a company or a custom field. A rule naming none of them
360
+ * pays no org read at all, which is the shape the engagement and list
361
+ * lookups already take.
362
+ */ const facetGroupId = dynamicListRuleNeedsContactFacet(planning) ? (await consentGroupForSite(options.hostId)).groupId : null;
363
+ /** Matched people, de-duplicated across silos by their person key. */ const matches = new Map();
364
+ let read = 0;
365
+ let cursor = null;
366
+ let complete = true;
367
+ const ordered = rule.sources;
368
+ const resumeIndex = options.resume ? ordered.indexOf(options.resume.source) : -1;
369
+ scan: for(let index = 0; index < ordered.length; index += 1){
370
+ var _ref;
371
+ var _options_resume;
372
+ const source = ordered[index];
373
+ // A resume skips the sources already finished and re-enters the one that
374
+ // was interrupted at the id it stopped on.
375
+ if (resumeIndex >= 0 && index < resumeIndex) continue;
376
+ let after = resumeIndex === index ? (_ref = (_options_resume = options.resume) == null ? void 0 : _options_resume.afterId) != null ? _ref : '' : '';
377
+ const base = await sourceQuery(source, options.hostId);
378
+ for(;;){
379
+ /*
380
+ * Ordered by document id, and paged with a `startAfter` cursor.
381
+ *
382
+ * `orderBy(FieldPath.documentId())` rather than a data field on
383
+ * purpose: an `orderBy` on a field DROPS every document missing it, and
384
+ * these four collections have writers old enough that no data field is
385
+ * guaranteed on every row. The id is on all of them, so this is a total
386
+ * order that cannot silently omit a person — which is the whole point
387
+ * of the exercise, since the defect being fixed is an unordered
388
+ * `limit()` returning an arbitrary subset.
389
+ */ let page = base.orderBy(firebaseAdmin.firestore.FieldPath.documentId()).limit(PAGE_SIZE);
390
+ if (after) page = page.startAfter(after);
391
+ const snapshot = await page.get();
392
+ if (snapshot.empty) break;
393
+ /*
394
+ * The page is read into candidates FIRST, then enriched, then matched.
395
+ *
396
+ * A per-document match would mean a per-document enrichment lookup, and
397
+ * the whole reason enrichment is affordable is that one page's people
398
+ * are fetched in one `getAll`. Only candidates are enriched — a
399
+ * document with no usable address is dropped by `toCandidate` before it
400
+ * can cost a lookup.
401
+ */ const pageCandidates = [];
402
+ for (const doc of snapshot.docs){
403
+ read += 1;
404
+ after = doc.id;
405
+ const candidate = toCandidate(source, doc, facetGroupId);
406
+ if (candidate) pageCandidates.push(candidate);
407
+ }
408
+ read += await enrichCandidates(pageCandidates, enrichment);
409
+ for (const candidate of pageCandidates){
410
+ if (!candidateMatchesDynamicListRule(candidate, rule, {
411
+ segment,
412
+ view,
413
+ nowMs
414
+ })) {
415
+ continue;
416
+ }
417
+ const key = personKey(candidate.email);
418
+ if (key && !matches.has(key)) matches.set(key, candidate);
419
+ }
420
+ if (read >= budget) {
421
+ complete = false;
422
+ cursor = {
423
+ source,
424
+ afterId: after
425
+ };
426
+ break scan;
427
+ }
428
+ if (snapshot.size < PAGE_SIZE) break;
429
+ }
430
+ }
431
+ return {
432
+ candidates: [
433
+ ...matches.values()
434
+ ],
435
+ complete,
436
+ cursor,
437
+ empty: false,
438
+ read
439
+ };
440
+ }
441
+ /**
442
+ * Re-evaluates one dynamic list and writes the result into its members.
443
+ *
444
+ * @param listRef `orgs/{orgId}/lists/{listId}` — the caller has proved it
445
+ * exists and that it is `kind: 'dynamic'`.
446
+ * @param hostId the site whose silos the rule draws from, and the scope the
447
+ * org-owned contacts read is narrowed to.
448
+ * @param resume a cursor from a previous incomplete run.
449
+ */ export async function materializeDynamicList(options) {
450
+ /*
451
+ * The rule cannot refer to the list it materializes — see
452
+ * `dynamicListRuleWithoutListReference` for the oscillation that produces,
453
+ * and note that half of its beats are DELETIONS of rows this sweep created.
454
+ * Stripped here rather than at authoring time because the rule outlives any
455
+ * one form: an API caller, an import and a copied list would each need to
456
+ * remember, and the sweep is the one place all of them arrive.
457
+ */ const rule = dynamicListRuleWithoutListReference(normalizeDynamicListRule(options.rule), options.listRef.id);
458
+ const scan = await collectDynamicListCandidates(_extends({
459
+ hostId: options.hostId,
460
+ rule
461
+ }, options.resume ? {
462
+ resume: options.resume
463
+ } : {}, options.nowMs !== undefined ? {
464
+ nowMs: options.nowMs
465
+ } : {}, options.scanBudget !== undefined ? {
466
+ scanBudget: options.scanBudget
467
+ } : {}));
468
+ if (scan.empty) {
469
+ return {
470
+ matched: 0,
471
+ enrolled: 0,
472
+ removed: 0,
473
+ complete: true,
474
+ cursor: null,
475
+ empty: true
476
+ };
477
+ }
478
+ const { complete, cursor } = scan;
479
+ /*
480
+ * Re-keyed by person rather than carried as a Map across the seam.
481
+ *
482
+ * Reconciliation below asks `matches.has(key)` about a stored row's id and
483
+ * about its address, so it needs the same key the scan de-duplicated on —
484
+ * computed by the same function, from the same addresses.
485
+ */ const matches = new Map();
486
+ for (const candidate of scan.candidates){
487
+ const key = personKey(candidate.email);
488
+ if (key) matches.set(key, candidate);
489
+ }
490
+ let enrolled = 0;
491
+ for (const candidate of matches.values()){
492
+ const result = await enrollListMember(_extends({
493
+ listRef: options.listRef,
494
+ // A group of one, and it costs nothing: the materializer passes NO
495
+ // consent (see below), so the group decides only which site is recorded
496
+ // as having captured the row.
497
+ group: soloConsentGroup(options.hostId),
498
+ email: candidate.email
499
+ }, candidate.name ? {
500
+ name: candidate.name
501
+ } : {}, {
502
+ source: `rule:${candidate.silo}`,
503
+ via: 'rule'
504
+ }));
505
+ // A refusal is not a materialization failure: an unusable address and a
506
+ // person whose row records a refusal are both simply not on this list,
507
+ // and a rule that selected them does not overrule either.
508
+ if (result.enrolled && result.created) enrolled += 1;
509
+ }
510
+ /*
511
+ * Reconciliation is allowed only for a run that saw the WHOLE population:
512
+ * one that started at the beginning AND finished.
513
+ *
514
+ * `complete` alone is not enough, and that is the trap. A run that RESUMES
515
+ * from a cursor finishes the tail of the scan and reports `complete: true`
516
+ * having examined only the tail — so its match set is the remainder, and
517
+ * reconciling against it would delete every member found by the earlier
518
+ * runs of the same sweep. That is a quota drop wearing the costume of a
519
+ * membership change: nothing about it looks like a limit, and it removes
520
+ * people for no reason but how the work was chunked.
521
+ *
522
+ * The cost is that a list whose population exceeds one run's scan budget
523
+ * never reconciles, so somebody who stops matching stays enrolled until a
524
+ * sweep fits in one run. That is the right way round: an over-full list is
525
+ * a wrong count, which is recoverable, and a deleted enrollment destroys
526
+ * the record that says the person asked to be there, which is not.
527
+ */ const sawWholePopulation = complete && !options.resume;
528
+ let removed = 0;
529
+ if (sawWholePopulation) {
530
+ /*
531
+ * Reconciliation.
532
+ *
533
+ * Only rows this materializer created are eligible (`via === 'rule'`). A
534
+ * row somebody enrolled by hand, or that an automation's `enrollList`
535
+ * step wrote, stays on a dynamic list forever: it was put there by a
536
+ * decision, and a rule that does not happen to select that person is not
537
+ * a decision to remove them. Rows written before `via` existed carry no
538
+ * `via` at all and are therefore never eligible either, which is the
539
+ * conservative direction for a field introduced after the data.
540
+ *
541
+ * This is the one removal in this file and it is a MEMBERSHIP change —
542
+ * the person stopped matching the rule. It is not a capacity decision and
543
+ * there is no count in the condition.
544
+ */ const existing = await options.listRef.collection('members').where('via', '==', 'rule').get();
545
+ for (const doc of existing.docs){
546
+ if (matches.has(doc.id)) continue;
547
+ // A row adopted under a legacy id is keyed by neither `personKey` nor
548
+ // anything this sweep computed, so it is matched by ADDRESS as well
549
+ // before being removed. Getting that wrong un-enrolls a person who
550
+ // still matches the rule.
551
+ const email = normalizeContactEmail(doc.get('email'));
552
+ const key = email ? personKey(email) : null;
553
+ if (key && matches.has(key)) continue;
554
+ await doc.ref.delete();
555
+ removed += 1;
556
+ }
557
+ }
558
+ await options.listRef.set(_extends({
559
+ kind: 'dynamic'
560
+ }, sawWholePopulation ? {
561
+ memberCount: matches.size
562
+ } : {}, {
563
+ lastEvaluatedAt: FieldValue.serverTimestamp()
564
+ }, cursor ? {
565
+ evaluationCursor: cursor
566
+ } : {
567
+ evaluationCursor: null
568
+ }), {
569
+ merge: true
570
+ });
571
+ return {
572
+ matched: matches.size,
573
+ enrolled,
574
+ removed,
575
+ complete,
576
+ cursor,
577
+ empty: false
578
+ };
579
+ }
580
+
581
+ //# sourceMappingURL=dynamic-list-materialize.js.map