@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,639 @@
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
+ * MARKETING MAIL — the durable half.
19
+ *
20
+ * `marketing-send.ts` in `@aglyn/shared-util-email` states the policy and
21
+ * holds the injection seam; this answers the question, because answering it
22
+ * needs Firestore and that library may not hold the Admin SDK.
23
+ *
24
+ * Five things, in the order they cost:
25
+ *
26
+ * 1. **Both suppression lists**, through the one shared helper. A person who
27
+ * unsubscribed from this site, hard-bounced anywhere in the product, or
28
+ * pressed "report spam" is not mailed. Asked first because it is the only
29
+ * one whose answer is permanent.
30
+ * 2. **The pace the RECIPIENT asked for** on the preference page, if they
31
+ * asked for one.
32
+ * 3. **An engagement sunset**, when an operator has configured a window —
33
+ * off by default, and it costs a read only when it is on.
34
+ * 4. **A frequency ceiling**, per recipient per site, over a rolling day.
35
+ * 5. **The signed unsubscribe URL**, so the message carries a way out.
36
+ *
37
+ * ## The order of the three pace refusals, and who each one is for
38
+ *
39
+ * Suppression is first and needs no argument: its answer is permanent, and
40
+ * the frequency record must not count a message that was never going to
41
+ * leave. The other three are all refusals about PACE, and they are ordered by
42
+ * whose refusal it is, then by what asking costs.
43
+ *
44
+ * **The recipient's own cadence is second — above both platform controls.**
45
+ * It is the only one of the three a PERSON asked for; the sunset is an
46
+ * inference this platform drew about them and the ceiling is a guard against
47
+ * the merchant. When more than one applies, the honest answer to "why did
48
+ * this not send" is the fact somebody stated, not the guess we made. It is
49
+ * also the cheapest to ask: it is already on the counter document the ceiling
50
+ * reads, so a recipient who asked for monthly mail never pays for the
51
+ * sunset's engagement read. And it is the only one that binds a campaign, so
52
+ * putting it first makes the campaign path and the automated paths evaluate
53
+ * the same refusals in the same order rather than interleaving one control a
54
+ * campaign skips with one it does not.
55
+ *
56
+ * **The sunset is third, above the ceiling.** Its refusal is TERMINAL and the
57
+ * ceiling's is not: a sweep defers a `frequency-capped` message and retries
58
+ * it, so reporting the retryable refusal for a person the sunset would refuse
59
+ * anyway means the same doomed row comes back on every beat. That argument
60
+ * does not reach past the cadence, because a cadence gap is a day, a week or
61
+ * a month — a sweep retries such a row once per interval, not once per beat.
62
+ *
63
+ * ## Which refusals bind a campaign
64
+ *
65
+ * `MarketingSendContext.capped` is `false` for a campaign, and it governs
66
+ * exactly two of the four:
67
+ *
68
+ * | Refusal | A campaign is |
69
+ * | ------------------ | ------------- |
70
+ * | `suppressed` | **bound** |
71
+ * | `cadence-limited` | **bound** |
72
+ * | `unengaged` | exempt (`capped`) |
73
+ * | `frequency-capped` | exempt (`capped`) |
74
+ *
75
+ * The two exemptions share one reason: a campaign is a merchant's reviewed,
76
+ * one-shot act with a recipient count on screen before they press Send, and a
77
+ * platform control that silently removed people from that number would make
78
+ * it a lie. That reason is about a control the merchant did not ask for and
79
+ * cannot see. It does not reach a request the RECIPIENT made — a campaign
80
+ * that overrode the preference page would make it a form recording a choice
81
+ * nothing honors, which is ignoring an unsubscribe one notch quieter.
82
+ *
83
+ * A campaign COUNTS toward the ceiling either way; it is exempt from the
84
+ * refusal, never from the counting.
85
+ *
86
+ * ## The counter is a WINDOW, not a total
87
+ *
88
+ * `hosts/{hostId}/emailFrequency/{sha256(address)}` holds the instants of the
89
+ * marketing messages this site sent this person, trimmed to the window on
90
+ * every touch. A rolling window needs the instants; a running total would
91
+ * need a reset nobody triggers, and a per-day bucket would let a merchant
92
+ * send the whole ceiling at 23:55 and the whole ceiling again at 00:05.
93
+ *
94
+ * The record is bounded by the cap — a handful of numbers — because
95
+ * `marketingFrequencyVerdict` returns the trimmed window and this writes back
96
+ * only that. It is server-owned: the address is hashed exactly as the two
97
+ * suppression lists hash it, and no client writes or reads it.
98
+ *
99
+ * ## The check and the write are not one transaction, on purpose
100
+ *
101
+ * Two concurrent marketing sends to the same person can both read a window
102
+ * with room and both append, so the cap can be exceeded by the number of
103
+ * messages genuinely in flight at that instant. That is accepted rather than
104
+ * papered over: a transaction per recipient would double the round trips on a
105
+ * path that is already one awaited HTTP POST per person, and being off by one
106
+ * on a runaway guard costs a recipient one extra message. Being slow costs
107
+ * every recipient.
108
+ *
109
+ * ## Never a reduction of the person or their data
110
+ *
111
+ * A cap here refuses a SEND. It never removes a contact, never unsubscribes
112
+ * anybody, never trims an audience and never deletes a counter row belonging
113
+ * to somebody. `over-limit.ts` states the rule for capacity limits and this is
114
+ * the same instrument pointed at time: the message does not go, the person
115
+ * stays exactly where they were, and the next window mails them again. The
116
+ * recipient's own cadence is the same shape — a person who asked for monthly
117
+ * mail stays on every audience they were on and is mailed again next month.
118
+ */ import { getMarketingSendGate, marketingCadenceVerdict, marketingFrequencyVerdict, marketingSunsetDays, marketingSunsetVerdict, normalizeMarketingCadence, setMarketingSendGate } from "@aglyn/shared-util-email";
119
+ import firebaseAdmin from "./firebase-admin.js";
120
+ import { readPersonEngagement } from "./email-delivery-log.js";
121
+ import { emailSuppressionKey, filterSendableForHost, filterTopicSendable } from "./email-suppression.js";
122
+ import { buildUnsubscribeUrl } from "./email-unsubscribe-link.js";
123
+ const defaultFirestore = ()=>firebaseAdmin.app().firestore();
124
+ /**
125
+ * Where the per-recipient window lives.
126
+ *
127
+ * Under the host rather than the org: the ceiling is "how much mail this SITE
128
+ * sends one person", which is the unit a recipient experiences and the unit
129
+ * the unsubscribe link is scoped to. An org-wide counter would let one site's
130
+ * newsletter suppress a sibling site's order-related marketing, and the two
131
+ * sites may be unrelated brands.
132
+ */ export const EMAIL_FREQUENCY_SUBCOLLECTION = 'emailFrequency';
133
+ /** How each cadence reads inside a refusal sentence. */ const CADENCE_PHRASES = {
134
+ all: 'at this pace',
135
+ daily: 'a day',
136
+ weekly: 'a week',
137
+ monthly: 'a month'
138
+ };
139
+ /** What an unreadable, absent or unkeyable counter reads as. */ const NO_RECORD = {
140
+ window: [],
141
+ firstSentAtMs: null,
142
+ lastSentAtMs: null,
143
+ cadence: 'all'
144
+ };
145
+ /**
146
+ * One counter snapshot, decoded.
147
+ *
148
+ * Shared by {@link readMarketingFrequencyState} and
149
+ * {@link filterCadenceSendable} so the two cannot come to different
150
+ * conclusions about the same document — in particular about the
151
+ * `lastSentAtMs` fallback, where a per-message answer and a per-campaign
152
+ * answer that disagreed would refuse a recipient on one path and mail them on
153
+ * the other.
154
+ */ function stateFromSnapshot(snapshot) {
155
+ const stored = snapshot.get('sentAtMs');
156
+ const window = Array.isArray(stored) ? stored.map((at)=>Number(at)) : [];
157
+ const first = Number(snapshot.get('firstSentAtMs'));
158
+ const last = Number(snapshot.get('lastSentAtMs'));
159
+ return {
160
+ window,
161
+ firstSentAtMs: Number.isFinite(first) && first > 0 ? first : null,
162
+ /*
163
+ * The stored instant, or the newest entry still inside the window.
164
+ *
165
+ * The fallback is what makes this work on every record written before
166
+ * `lastSentAtMs` existed: those carry a window and nothing else, and
167
+ * reading `null` for them would let a monthly cadence pass on the first
168
+ * message after this ships for anybody mailed in the last day.
169
+ *
170
+ * `firstSentAtMs` deliberately has NO such fallback. The window would be
171
+ * the wrong answer for it in exactly the direction that matters: a
172
+ * relationship dated from a send inside the last day is younger than any
173
+ * sunset window, so the sunset could never fire — and a relationship
174
+ * dated from the oldest entry in a rolling DAY is not the first send
175
+ * either. Absent means "no record", and no record refuses nobody.
176
+ */ lastSentAtMs: Number.isFinite(last) ? last : window.length ? Math.max(...window) : null,
177
+ cadence: normalizeMarketingCadence(snapshot.get('cadence'))
178
+ };
179
+ }
180
+ function frequencyDoc(hostId, emailKey, firestore) {
181
+ return (firestore != null ? firestore : defaultFirestore()).collection('hosts').doc(hostId).collection(EMAIL_FREQUENCY_SUBCOLLECTION).doc(emailKey);
182
+ }
183
+ /**
184
+ * The instants inside the window for one recipient of one site's mail.
185
+ *
186
+ * FAILS OPEN — an empty window, meaning "nothing recorded, so there is room".
187
+ * The opposite of {@link filterSendableForHost}, and the difference is what
188
+ * each answer costs when it is wrong. A suppression read that fails open
189
+ * mails somebody who told us to stop; a frequency read that fails closed
190
+ * refuses a message nobody objected to, over a counter that is a runaway
191
+ * guard rather than a consent record.
192
+ */ export async function readMarketingFrequency(hostId, email, firestore) {
193
+ return (await readMarketingFrequencyState(hostId, email, firestore)).window;
194
+ }
195
+ /**
196
+ * The whole counter document — the window, the first send, the last send and
197
+ * the recipient's chosen pace — in ONE round trip.
198
+ *
199
+ * **One read, four facts, and that is a requirement rather than a tidiness.**
200
+ * Three separate refusals in {@link marketingSendVerdict} read this document,
201
+ * and a `get` per refusal would be three awaited round trips on a path that
202
+ * is already one awaited HTTP POST per recipient. Which is also why the
203
+ * recipient's cadence is stored HERE rather than beside the topic opt-outs:
204
+ * honoring a preference this gate has to consult on every marketing message
205
+ * has to cost nothing, and riding on a document already being read is the
206
+ * only shape that does. See {@link EmailFrequencyRecord.cadence}.
207
+ *
208
+ * FAILS OPEN on every field, and in the same direction each time: an empty
209
+ * window means "there is room", a null `firstSentAtMs` means "no record", a
210
+ * null `lastSentAtMs` means "never mailed", and an unreadable cadence reads
211
+ * as no preference expressed. An unreadable counter is not evidence that
212
+ * somebody asked for less, and it is not evidence that they have gone cold —
213
+ * the opposite of {@link filterSendableForHost}, for the reason
214
+ * {@link readMarketingFrequency} states.
215
+ */ export async function readMarketingFrequencyState(hostId, email, firestore) {
216
+ const key = emailSuppressionKey(email);
217
+ if (!key) return _extends({}, NO_RECORD);
218
+ try {
219
+ const snapshot = await frequencyDoc(hostId, key, firestore).get();
220
+ if (!snapshot.exists) return _extends({}, NO_RECORD);
221
+ return stateFromSnapshot(snapshot);
222
+ } catch (error) {
223
+ console.error('[email-marketing] frequency read failed; allowing', error);
224
+ return _extends({}, NO_RECORD);
225
+ }
226
+ }
227
+ /**
228
+ * Records the pace a recipient asked for.
229
+ *
230
+ * Written from the preference page, which is unauthenticated and reached by a
231
+ * signed link — so the CALLER has already proved the request is this
232
+ * address's. A merge, because the document is the send counter and this must
233
+ * not disturb the window it shares with.
234
+ *
235
+ * Never throws: a preference that failed to store is a page that should say
236
+ * so, not a 500 on a screen a recipient reached in order to leave.
237
+ *
238
+ * @returns whether it was stored.
239
+ */ export async function setMarketingCadence(hostId, email, cadence, options) {
240
+ const key = emailSuppressionKey(email);
241
+ if (!key || !hostId) return false;
242
+ try {
243
+ var _ref;
244
+ await frequencyDoc(hostId, key, options == null ? void 0 : options.firestore).set({
245
+ email: String(email).trim().toLowerCase(),
246
+ cadence: normalizeMarketingCadence(cadence),
247
+ cadenceSetAtMs: (_ref = options == null ? void 0 : options.nowMs) != null ? _ref : Date.now()
248
+ }, {
249
+ merge: true
250
+ });
251
+ return true;
252
+ } catch (error) {
253
+ console.error('[email-marketing] cadence write failed', error);
254
+ return false;
255
+ }
256
+ }
257
+ /**
258
+ * Records marketing messages against the recipients' windows.
259
+ *
260
+ * Takes a LIST because the campaign sender records a whole batch at once: it
261
+ * is exempt from the refusal (a reviewed, one-shot act whose recipient count
262
+ * is on screen before it goes) but not from the counting, since a campaign is
263
+ * most of the mail a person receives from a site. Doing that per recipient
264
+ * inside the send loop would add a round trip to each of up to 500 sequential
265
+ * sends; doing it here adds one batch after them.
266
+ *
267
+ * Never throws. A lost counter increment means one recipient's ceiling is
268
+ * measured a message low, which is not worth failing a delivered send over.
269
+ */ export async function recordMarketingSends(hostId, emails, options) {
270
+ var _ref;
271
+ const nowMs = (_ref = options == null ? void 0 : options.nowMs) != null ? _ref : Date.now();
272
+ const keyed = new Map();
273
+ for (const email of emails){
274
+ const key = emailSuppressionKey(email);
275
+ if (key) keyed.set(key, String(email).trim().toLowerCase());
276
+ }
277
+ if (!keyed.size || !hostId) return 0;
278
+ let recorded = 0;
279
+ try {
280
+ var _ref1;
281
+ const db = (_ref1 = options == null ? void 0 : options.firestore) != null ? _ref1 : defaultFirestore();
282
+ for (const [key, email] of keyed){
283
+ const ref = frequencyDoc(hostId, key, db);
284
+ const existing = await ref.get().then((snapshot)=>snapshot.exists ? stateFromSnapshot(snapshot) : _extends({}, NO_RECORD)).catch(()=>_extends({}, NO_RECORD));
285
+ const window = marketingFrequencyVerdict([
286
+ ...existing.window,
287
+ nowMs
288
+ ], nowMs);
289
+ await ref.set(_extends({
290
+ email,
291
+ sentAtMs: window.inWindow,
292
+ lastSentAtMs: nowMs
293
+ }, existing.firstSentAtMs ? {} : {
294
+ firstSentAtMs: nowMs
295
+ }), {
296
+ merge: true
297
+ });
298
+ recorded += 1;
299
+ }
300
+ } catch (error) {
301
+ console.error('[email-marketing] frequency record failed', error);
302
+ }
303
+ return recorded;
304
+ }
305
+ /**
306
+ * The subset of `emails` that has NOT asked this site for mail less often
307
+ * than right now.
308
+ *
309
+ * The fourth filter a campaign passes, after the platform suppression list,
310
+ * the site's own and the topic opt-outs — and the only one that is not in
311
+ * `email-suppression.ts` beside those three, because it reads the counter
312
+ * document this module owns and that module is this module's dependency.
313
+ *
314
+ * {@link marketingSendVerdict} enforces the same rule per message, for the
315
+ * senders that reach the gate. THIS is the campaign path's copy, and it is
316
+ * the enforcement there rather than only a count: a campaign carries no
317
+ * `marketing` context — it mints its own unsubscribe URL upstream — so the
318
+ * gate is not on its path at all, and a rule asked only there would not be
319
+ * asked of the sender that produces most of a person's mail.
320
+ *
321
+ * Answering it as a FILTER rather than per message is also the only placement
322
+ * that keeps the composer honest. The argument for exempting a campaign from
323
+ * the platform ceiling is that a control which silently removed people from a
324
+ * reviewed one-shot send would make the number on screen a lie; subtracting
325
+ * a request the recipient actually made, where every other refusal is already
326
+ * subtracted, is what keeps this one from having that problem.
327
+ *
328
+ * Keyed and read with one `getAll`, matching its three neighbors: one round
329
+ * trip bounded by the size of the send, and no composite index to go missing.
330
+ *
331
+ * ## Fails OPEN, like the topic filter beside it
332
+ *
333
+ * A cadence is a PACE, not a stop. Guessing wrong on an unreadable counter
334
+ * costs one recipient one message sooner than they asked for; guessing wrong
335
+ * the other way withholds a whole campaign on a transient read failure. The
336
+ * two suppression lists have already refused everybody who asked us to stop
337
+ * entirely, so nobody who said "no" reaches this line.
338
+ */ export async function filterCadenceSendable(hostId, emails, options) {
339
+ var _ref;
340
+ if (!emails.length || !hostId) return [
341
+ ...emails
342
+ ];
343
+ const nowMs = (_ref = options == null ? void 0 : options.nowMs) != null ? _ref : Date.now();
344
+ // An unkeyable address carries no counter, so it has expressed no pace. It
345
+ // is dropped from the LOOKUP and kept in the answer, exactly as the topic
346
+ // filter keeps one: the stricter filters above have already had their say.
347
+ const lookups = [];
348
+ for (const email of emails){
349
+ const key = emailSuppressionKey(email);
350
+ if (key) lookups.push({
351
+ email,
352
+ key
353
+ });
354
+ }
355
+ if (!lookups.length) return [
356
+ ...emails
357
+ ];
358
+ try {
359
+ var _ref1;
360
+ const db = (_ref1 = options == null ? void 0 : options.firestore) != null ? _ref1 : defaultFirestore();
361
+ const counters = db.collection('hosts').doc(hostId).collection(EMAIL_FREQUENCY_SUBCOLLECTION);
362
+ const snapshots = await db.getAll(...lookups.map((entry)=>counters.doc(entry.key)));
363
+ const holding = new Set();
364
+ lookups.forEach((entry, index)=>{
365
+ const snapshot = snapshots[index];
366
+ if (!(snapshot == null ? void 0 : snapshot.exists)) return;
367
+ // The same decoder the per-message path uses, so the two cannot
368
+ // disagree about one document. The sunset's `firstSentAtMs` comes back
369
+ // with it and is deliberately unused: a campaign is exempt from that
370
+ // refusal, so subtracting on it here would remove people from a count
371
+ // the gate is going to mail anyway.
372
+ const state = stateFromSnapshot(snapshot);
373
+ const verdict = marketingCadenceVerdict(state.cadence, state.lastSentAtMs, nowMs);
374
+ if (!verdict.allowed) holding.add(entry.email);
375
+ });
376
+ return emails.filter((email)=>!holding.has(email));
377
+ } catch (error) {
378
+ console.error('[email-marketing] cadence lookup failed; failing open', error);
379
+ return [
380
+ ...emails
381
+ ];
382
+ }
383
+ }
384
+ /**
385
+ * The gate itself, exported so it can be exercised without installing it.
386
+ *
387
+ * ORDER MATTERS. The suppression check is first because its answer is
388
+ * permanent and the frequency record must not count a message that was never
389
+ * going to leave — a suppressed address whose window kept growing would stay
390
+ * capped for a day after being released.
391
+ */ export async function marketingSendVerdict(request, options) {
392
+ var _ref, _request_email, _request_topicId;
393
+ const nowMs = (_ref = options == null ? void 0 : options.nowMs) != null ? _ref : Date.now();
394
+ const email = String((_request_email = request.email) != null ? _request_email : '').trim().toLowerCase();
395
+ const sendable = await filterSendableForHost(request.hostId, [
396
+ email
397
+ ], options == null ? void 0 : options.firestore);
398
+ if (!sendable.length) {
399
+ return {
400
+ allowed: false,
401
+ refusal: 'suppressed',
402
+ detail: 'This address has unsubscribed, bounced permanently, or reported a ' + 'message as spam.'
403
+ };
404
+ }
405
+ // Normalized once, and read by both the links below and the topic check
406
+ // further down: two spellings of "which stream is this" is how a link comes
407
+ // to carry a topic the gate did not check.
408
+ const topicId = String((_request_topicId = request.topicId) != null ? _request_topicId : '').trim();
409
+ /*
410
+ * TWO URLS OVER ONE SIGNATURE, the same split `campaign-send.ts` makes and
411
+ * for the same reason (RFC 8058).
412
+ *
413
+ * `oneClickUrl` is what `List-Unsubscribe` names: a mailbox provider POSTs
414
+ * it with nobody present and expects the act to have happened when it reads
415
+ * the 200, so it points at the route whose POST writes immediately.
416
+ *
417
+ * `unsubscribeUrl` is the link a PERSON clicks in the footer, and it points
418
+ * at the preference page. Every caller on this path — the abandoned-cart
419
+ * sweep, the restock notice, the newsletter welcome, the automation step —
420
+ * names a topic, and a footer pointing at the one-click route gives the
421
+ * recipient of one of those exactly one choice: stop hearing from this site
422
+ * entirely. The page offers leaving that one stream instead, with
423
+ * "Unsubscribe from everything" still on it.
424
+ *
425
+ * The topic rides both links, so the page opens on the stream the message
426
+ * belonged to rather than on a list the recipient has to search.
427
+ */ const link = _extends({
428
+ siteBase: request.siteBase,
429
+ hostId: request.hostId,
430
+ email
431
+ }, topicId ? {
432
+ topicId
433
+ } : {});
434
+ const unsubscribeUrl = buildUnsubscribeUrl(_extends({}, link, {
435
+ surface: 'preferences'
436
+ }));
437
+ const oneClickUrl = buildUnsubscribeUrl(_extends({}, link, {
438
+ surface: 'one-click'
439
+ }));
440
+ /*
441
+ * THE STREAM THIS MESSAGE BELONGS TO — the third list, and the narrowest.
442
+ *
443
+ * ONLY when the caller named one. An absent topic is not "the default
444
+ * topic": it is a message that belongs to no stream, and there is nothing
445
+ * for a person to have left. That is what keeps this check off the mail it
446
+ * must never touch — a receipt, a password reset, a booking confirmation,
447
+ * none of which declare a `marketing` context at all and none of which name
448
+ * a stream if they somehow did.
449
+ *
450
+ * After the suppression lists and before the counter read below, matching
451
+ * the order `campaign-send.ts` filters in, for two reasons that agree. It
452
+ * is the weaker fact, and the weaker fact should never be the one that
453
+ * decides — a person who unticked "Promotions and offers" is still a
454
+ * subscriber, where a person on either suppression list is not. And it is a
455
+ * TERMINAL refusal, so answering it before the counter read means a
456
+ * recipient this message was never going to reach costs one lookup rather
457
+ * than three.
458
+ *
459
+ * Fails OPEN, because `filterTopicSendable` does: a topic preference is a
460
+ * narrower fact than a suppression, and a read that failed for an unrelated
461
+ * reason is no reason to withhold a message from somebody the two lists
462
+ * above already cleared.
463
+ */ if (topicId) {
464
+ const onTopic = await filterTopicSendable(request.hostId, topicId, [
465
+ email
466
+ ], options == null ? void 0 : options.firestore);
467
+ if (!onTopic.length) {
468
+ /*
469
+ * ⛔ A REFUSAL AND NOTHING ELSE, as with the pace refusals below.
470
+ *
471
+ * No suppression is written, no membership changes, and the frequency
472
+ * window is not appended to — a message that never left must not count
473
+ * against what this person has received. They stay on every other
474
+ * stream they subscribe to, and the next message on one of those goes.
475
+ */ return {
476
+ allowed: false,
477
+ refusal: 'topic-unsubscribed',
478
+ detail: 'This address has left the email topic this message belongs to. ' + 'They still receive the other streams from this site.',
479
+ unsubscribeUrl,
480
+ oneClickUrl
481
+ };
482
+ }
483
+ }
484
+ // ONE read, and every refusal below is answered from it — except the
485
+ // sunset's engagement half, which is a different document and is fetched
486
+ // only when a window is configured and nothing above has already refused.
487
+ const state = await readMarketingFrequencyState(request.hostId, email, options == null ? void 0 : options.firestore);
488
+ /*
489
+ * THE RECIPIENT'S OWN REQUEST, first among the three pace refusals and the
490
+ * only one not subject to `capped`.
491
+ *
492
+ * `capped: false` exempts a campaign from the platform CEILING and from the
493
+ * SUNSET, because a control the merchant did not ask for and cannot see,
494
+ * silently removing people from a reviewed one-shot send, would make the
495
+ * recipient count on screen a lie. That argument does not reach this one: a
496
+ * person used the preference page to ask this site for less mail, and a
497
+ * campaign that overrode them would make that page a form that records a
498
+ * request nobody honors — the same failure as ignoring an unsubscribe, one
499
+ * notch quieter.
500
+ *
501
+ * Above the sunset for three reasons. It is the only one of the three a
502
+ * PERSON asked for, and when both apply the honest answer to "why did this
503
+ * not send" is the fact somebody stated rather than the inference we drew.
504
+ * It is answered entirely from the counter document already in hand, so a
505
+ * recipient who asked for monthly mail never pays for the sunset's second
506
+ * read. And it is the one refusal a campaign is bound by, so asking it
507
+ * first gives the campaign path and the automated paths the same order.
508
+ *
509
+ * The sunset's terminality argument does not out-rank any of that: a sweep
510
+ * that defers this row retries it once per cadence interval — a day, a week
511
+ * or a month — not once per beat, which is what that argument is about.
512
+ *
513
+ * A campaign is bound by the rule but not by THIS enforcement of it: it
514
+ * carries no `marketing` context, so nothing on its path reaches this
515
+ * function. {@link filterCadenceSendable} is where it is asked there, and
516
+ * asked as a filter so the recipient count reflects it before Send.
517
+ */ const cadence = marketingCadenceVerdict(state.cadence, state.lastSentAtMs, nowMs);
518
+ if (!cadence.allowed) {
519
+ /*
520
+ * ⛔ A REFUSAL AND NOTHING ELSE, exactly as below.
521
+ *
522
+ * Nobody is unsubscribed, no membership changes, no contact is touched,
523
+ * and the frequency window is not appended to — a message that never left
524
+ * must not count against what this person has received. Somebody who
525
+ * asked for monthly mail stays on every audience they were on and is
526
+ * mailed again next month.
527
+ */ return {
528
+ allowed: false,
529
+ refusal: 'cadence-limited',
530
+ detail: `This address asked this site for no more than one marketing ` + `message ${CADENCE_PHRASES[state.cadence]}. The next one may go on ` + `${new Date(cadence.nextAllowedAtMs).toISOString()}.`,
531
+ unsubscribeUrl,
532
+ oneClickUrl
533
+ };
534
+ }
535
+ /*
536
+ * THE SUNSET, ahead of the frequency ceiling.
537
+ *
538
+ * Ordered above the ceiling because its refusal is TERMINAL and the
539
+ * ceiling's is not: a sweep defers a `frequency-capped` message and retries
540
+ * it, so reporting the retryable refusal for a person the sunset would
541
+ * refuse anyway means the same doomed row comes back on every beat.
542
+ *
543
+ * `request.capped` governs it, the same flag the ceiling reads, so a
544
+ * campaign — a reviewed act with its recipient count on screen — is exempt
545
+ * for the reason recorded on `MarketingSendContext.capped`. That leaves the
546
+ * sunset governing the automated paths, which fire with no human present.
547
+ *
548
+ * The engagement read is the ONLY second round trip on this path, and it is
549
+ * spent only when a window is configured, the caller is capped, and the
550
+ * cadence above has already allowed. Off is the default, and off costs
551
+ * nothing.
552
+ */ const sunsetDays = marketingSunsetDays();
553
+ if (request.capped && sunsetDays > 0) {
554
+ const engagement = await readPersonEngagement(email, options == null ? void 0 : options.firestore);
555
+ const sunset = marketingSunsetVerdict({
556
+ firstSentAtMs: state.firstSentAtMs,
557
+ lastEngagedAtMs: engagement.lastEngagedAtMs
558
+ }, nowMs, sunsetDays);
559
+ if (!sunset.allowed) {
560
+ /*
561
+ * ⛔ A REFUSAL AND NOTHING ELSE.
562
+ *
563
+ * No suppression is written, no membership is touched, no contact is
564
+ * changed, and the frequency window below is not appended to — a
565
+ * message that never left must not count against what this person has
566
+ * received. The person is exactly where they were, and the next send
567
+ * after they open anything goes.
568
+ */ return {
569
+ allowed: false,
570
+ refusal: 'unengaged',
571
+ detail: `This address has not opened or clicked anything for ` + `${sunset.quietForDays} days, and this site has been mailing it ` + `for longer than the ${sunset.days}-day engagement window. It ` + `becomes mailable again as soon as they engage.`,
572
+ unsubscribeUrl,
573
+ oneClickUrl
574
+ };
575
+ }
576
+ }
577
+ const verdict = marketingFrequencyVerdict(state.window, nowMs);
578
+ if (request.capped && !verdict.allowed) {
579
+ return {
580
+ allowed: false,
581
+ refusal: 'frequency-capped',
582
+ detail: `This address has already received ${verdict.used} marketing ` + `messages from this site today (the ceiling is ${verdict.cap}).`,
583
+ unsubscribeUrl,
584
+ oneClickUrl
585
+ };
586
+ }
587
+ /*
588
+ * The send is granted, so it is counted — including for a campaign, which
589
+ * is exempt from the refusal above and not from this. `sentAtMs` therefore
590
+ * measures what the recipient actually receives rather than only the part
591
+ * of it a cap may stop, which is the difference between a ceiling and a
592
+ * number that describes nothing.
593
+ */ const appended = marketingFrequencyVerdict([
594
+ ...verdict.inWindow,
595
+ nowMs
596
+ ], nowMs);
597
+ const key = emailSuppressionKey(email);
598
+ if (key) {
599
+ await frequencyDoc(request.hostId, key, options == null ? void 0 : options.firestore).set(_extends({
600
+ email,
601
+ sentAtMs: appended.inWindow,
602
+ lastSentAtMs: nowMs
603
+ }, state.firstSentAtMs ? {} : {
604
+ firstSentAtMs: nowMs
605
+ }), {
606
+ merge: true
607
+ })// Never blocks the send. A counter write that failed is a ceiling
608
+ // measured one message low, and refusing delivery over it would let a
609
+ // Firestore hiccup become an outage on a merchant's mail.
610
+ .catch((error)=>{
611
+ console.error('[email-marketing] frequency write failed', error);
612
+ });
613
+ }
614
+ return {
615
+ allowed: true,
616
+ unsubscribeUrl,
617
+ oneClickUrl
618
+ };
619
+ }
620
+ /**
621
+ * Puts the gate on `sendEmail`'s path.
622
+ *
623
+ * **Called at module load**, from the bottom of this file, so that importing
624
+ * `@aglyn/tenant-data-admin` anywhere is enough — every server surface in the
625
+ * product already imports that barrel, and `export *` forces this module to
626
+ * evaluate. The alternative, a call at each server entrypoint, is the
627
+ * many-places-to-remember shape this codebase has rejected twice already.
628
+ *
629
+ * Idempotent: installing twice replaces the same closure with an equivalent
630
+ * one, and the closure holds no state — the state is the Firestore document.
631
+ */ export function installMarketingSendGate() {
632
+ setMarketingSendGate((request)=>marketingSendVerdict(request));
633
+ }
634
+ /** Whether a gate is installed. Reads the shared seam, not a local flag. */ export function isMarketingSendGateInstalled() {
635
+ return getMarketingSendGate() !== null;
636
+ }
637
+ installMarketingSendGate();
638
+
639
+ //# sourceMappingURL=email-marketing-gate.js.map