@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,300 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { FieldValue } from "firebase-admin/firestore";
18
+ import { EMAIL_ATTRIBUTION_MODEL, EMAIL_ATTRIBUTION_WINDOW_DAYS, EMAIL_ATTRIBUTION_WINDOW_MS, emailTouchIsInWindow } from "@aglyn/shared-util-email";
19
+ import { readEmailCampaignTouch } from "./email-delivery-log.js";
20
+ import { isDocumentId } from "./document-id.js";
21
+ import firebaseAdmin from "./firebase-admin.js";
22
+ const defaultFirestore = ()=>firebaseAdmin.app().firestore();
23
+ /**
24
+ * THE COMMERCE↔EMAIL JOIN — an order, credited to the campaign that led to it.
25
+ *
26
+ * ## Why this is a join
27
+ *
28
+ * Every compared ESP attributes revenue through an integration into a store
29
+ * it does not own: a catalog sync, an on-site tracking snippet, an identity
30
+ * graph, and a window wide enough to absorb what the reconciliation misses.
31
+ * Commerce here is first-party and runs on the merchant's own Stripe Connect
32
+ * account, so the click and the order are two rows in one database keyed the
33
+ * same way. The window is not a fudge factor here — it is the model, and
34
+ * nothing else about the number is estimated.
35
+ *
36
+ * ## Three writes, and what each one is for
37
+ *
38
+ * - **The touch**, `emailDeliveries/{personKey}.campaignTouches[hostId]`,
39
+ * written by the delivery webhook on a click. Owned by
40
+ * `email-delivery-log.ts`, because it lives on the person's document and
41
+ * the erasure path has to be able to remove it.
42
+ * - **The attribution record**, `hosts/{hostId}/emailAttributions/{orderId}`,
43
+ * written here when an order is credited. It is the audit trail — which
44
+ * campaign, which click, which model, which window — and it is what the
45
+ * refund path reads to find out which campaign to take the money back off.
46
+ * - **The rollup**, `campaigns/{campaignId}/reports/revenue`, incremented
47
+ * here. One document per campaign, so the report reads it whole.
48
+ *
49
+ * ## Why the record is a document and not a field on the order
50
+ *
51
+ * Three reasons, and the first is decisive. `create()` fails when the
52
+ * document already exists, which is exact idempotency for free: webhook
53
+ * delivery is at-least-once, and a redelivered purchase must not be able to
54
+ * credit a campaign twice. Second, not every purchase door writes an order —
55
+ * a booking is a sale with no order document — and a field would have had to
56
+ * either skip those or conjure the document, and conjuring is the
57
+ * phantom-document shape this codebase spent a sweep removing. Third, the
58
+ * order document is read by the console list, the fulfilment path, the
59
+ * supplier outbox and the CSV export, and none of them wants a field about
60
+ * email.
61
+ *
62
+ * ## Never throws
63
+ *
64
+ * Same contract as `upsertHostContact`, which calls it, and for the same
65
+ * reason: the money has already moved and the order already records it, so
66
+ * nothing here may fail a sale. A lost attribution understates a campaign; a
67
+ * thrown one loses a checkout.
68
+ */ /** The per-host collection of attribution records. */ export const EMAIL_ATTRIBUTIONS_COLLECTION = 'emailAttributions';
69
+ /** The single rollup document under a campaign. */ export const CAMPAIGN_REVENUE_REPORT_DOC = 'revenue';
70
+ /**
71
+ * The currency an amount is recorded under when the caller does not say.
72
+ *
73
+ * Every checkout door in this repo — cart, buy-now, POS, draft orders,
74
+ * reservations, subscriptions — writes `currency: 'usd'` onto the Stripe line
75
+ * items, and no order document carries a currency field to read back. So this
76
+ * is a statement about what the code charges rather than a guess about the
77
+ * money. The parameter exists so a door that ever charges in something else
78
+ * says so and lands in its own bucket, because the one thing the report may
79
+ * never do is add two currencies together.
80
+ */ export const DEFAULT_ATTRIBUTION_CURRENCY = 'usd';
81
+ /** Normalizes a currency code to the key its bucket is stored under. */ function currencyKey(raw) {
82
+ const code = String(raw != null ? raw : '').trim().toLowerCase();
83
+ // Letters only: the code becomes a MAP KEY, and a key carrying a dot would
84
+ // be unreachable by any dotted field path a later reader wants to use.
85
+ return /^[a-z]{3}$/.test(code) ? code : DEFAULT_ATTRIBUTION_CURRENCY;
86
+ }
87
+ /** A positive integer number of minor units, or 0. */ function minorUnits(raw) {
88
+ const value = Math.round(Number(raw));
89
+ return Number.isFinite(value) && value > 0 ? value : 0;
90
+ }
91
+ /**
92
+ * Credits one order to the campaign whose link the buyer last clicked.
93
+ *
94
+ * ## What happens when there is nobody to credit
95
+ *
96
+ * Each case answers `null` and writes NOTHING, which is the whole design:
97
+ *
98
+ * - **No email on the order.** A guest checkout that never identified its
99
+ * buyer cannot be joined to anybody's clicks. There is no fallback and
100
+ * there deliberately is not one — the alternatives are an IP or a device
101
+ * guess, which is the probabilistic attribution owning the checkout exists
102
+ * to avoid.
103
+ * - **A guest with an email but no contact record.** This is ATTRIBUTED
104
+ * normally. The join keys on the address hash, exactly as the touch and
105
+ * the suppression list do, so it never asks whether a contact document
106
+ * exists — which matters, because contact creation is audience-band gated
107
+ * and a Free org's dropped contact would otherwise silently drop the
108
+ * revenue with it.
109
+ * - **No touch, or a touch on another site.** Nobody clicked, so nobody is
110
+ * credited. The touch map is keyed by host and the send path refuses
111
+ * cross-site reach; the revenue join agrees with it.
112
+ * - **A touch outside the window**, in either direction. See
113
+ * {@link emailTouchIsInWindow} — a click AFTER the order is the receipt,
114
+ * not the cause.
115
+ *
116
+ * None of these is counted anywhere. A miss costs no write, which is what
117
+ * keeps the ordinary order — placed by somebody who is not on the mailing
118
+ * list at all — at exactly one document read. What it means for the report is
119
+ * stated on the report: the figure counts orders it could join, and is a
120
+ * floor.
121
+ *
122
+ * @returns the record written, or `null` when nothing was credited.
123
+ */ export async function attributeOrderToEmail(options, firestore) {
124
+ try {
125
+ var _options_hostId, _options_orderId, _options_orderedAtMs, _options_email;
126
+ const hostId = String((_options_hostId = options.hostId) != null ? _options_hostId : '');
127
+ const orderId = String((_options_orderId = options.orderId) != null ? _options_orderId : '');
128
+ const amountCents = minorUnits(options.amountCents);
129
+ if (!isDocumentId(hostId) || !isDocumentId(orderId)) return null;
130
+ if (!amountCents) return null;
131
+ const orderedAtMs = Number((_options_orderedAtMs = options.orderedAtMs) != null ? _options_orderedAtMs : Date.now());
132
+ const db = firestore != null ? firestore : defaultFirestore();
133
+ /*
134
+ * `String()` on an `unknown`, then let `emailSuppressionKey` decide.
135
+ *
136
+ * The buyer's address arrives as the ORDER recorded it, which is not
137
+ * consistently a string — the same rawness `recordContactRefund` takes
138
+ * `email: unknown` for. The key derivation is the one place that knows
139
+ * how an address becomes a document id, so nothing here tries to
140
+ * pre-clean it; a value that is not an address answers `null` there and
141
+ * the order is credited to nobody.
142
+ */ const rawEmail = typeof options.email === 'string' ? options.email : String((_options_email = options.email) != null ? _options_email : '');
143
+ const touch = await readEmailCampaignTouch(rawEmail, hostId, db);
144
+ if (!touch) return null;
145
+ if (!isDocumentId(touch.campaignId)) return null;
146
+ if (!emailTouchIsInWindow(touch.clickedAtMs, orderedAtMs)) return null;
147
+ const record = {
148
+ campaignId: touch.campaignId,
149
+ clickedAtMs: touch.clickedAtMs,
150
+ orderedAtMs,
151
+ amountCents,
152
+ currency: currencyKey(options.currency),
153
+ model: EMAIL_ATTRIBUTION_MODEL,
154
+ windowDays: EMAIL_ATTRIBUTION_WINDOW_DAYS
155
+ };
156
+ const hostRef = db.collection('hosts').doc(hostId);
157
+ /*
158
+ * `create()`, never `set()`. It fails with ALREADY_EXISTS when this order
159
+ * has been credited before, and that failure is the idempotency: a
160
+ * webhook redelivery, a retried checkout completion and a replayed event
161
+ * all land here a second time, and all three must leave the rollup where
162
+ * they found it. The increment below is reached only when the create
163
+ * succeeded, so the two can never disagree about whether this order was
164
+ * counted.
165
+ */ try {
166
+ await hostRef.collection(EMAIL_ATTRIBUTIONS_COLLECTION).doc(orderId).create(_extends({}, record, {
167
+ createdAt: FieldValue.serverTimestamp()
168
+ }));
169
+ } catch (unused) {
170
+ return null;
171
+ }
172
+ /*
173
+ * A merge-set that CREATES, unlike the campaign counters the delivery
174
+ * webhook writes through `updateExisting`.
175
+ *
176
+ * The distinction is which document is being conjured. Those counters
177
+ * refuse to create because a merge-set against a DELETED CAMPAIGN
178
+ * resurrects the campaign itself, as a husk holding a stats map and no
179
+ * subject. This writes a subcollection document UNDER a campaign, so a
180
+ * campaign that no longer exists gains an orphaned report rather than
181
+ * coming back to life in the merchant's history — and the campaign was
182
+ * proven to exist a moment ago, when its click wrote the touch.
183
+ *
184
+ * Every amount is an increment, so two orders settling at once both land.
185
+ */ const currency = record.currency;
186
+ await hostRef.collection('campaigns').doc(touch.campaignId).collection('reports').doc(CAMPAIGN_REVENUE_REPORT_DOC).set({
187
+ model: EMAIL_ATTRIBUTION_MODEL,
188
+ windowDays: EMAIL_ATTRIBUTION_WINDOW_DAYS,
189
+ byCurrency: {
190
+ [currency]: {
191
+ grossCents: FieldValue.increment(amountCents),
192
+ orders: FieldValue.increment(1)
193
+ }
194
+ },
195
+ updatedAt: FieldValue.serverTimestamp()
196
+ }, {
197
+ merge: true
198
+ });
199
+ return record;
200
+ } catch (error) {
201
+ console.error('attributeOrderToEmail failed', error);
202
+ return null;
203
+ }
204
+ }
205
+ /**
206
+ * Takes back revenue a campaign was credited with, when it is refunded.
207
+ *
208
+ * ## Recorded beside the gross, never subtracted from it
209
+ *
210
+ * `grossCents` is left exactly as it is and the reversal lands in
211
+ * `refundedCents` next to it. This is the shape `contact-refund.ts` chose for
212
+ * `ltvCents`/`refundedCents` and the orders CSV chose for
213
+ * `amountUsd`/`refundedUsd`, and it is chosen a third time here so all three
214
+ * answer "what did this earn, net" identically. Decrementing would make a
215
+ * stored number mean one thing for rollups written before a refund and
216
+ * another after, with nothing on the document to tell them apart — and would
217
+ * put the question "can it go negative" onto storage, where the answer cannot
218
+ * be clamped without destroying evidence. Both stored figures are monotonic
219
+ * counters of money that really moved in one direction; the derived net is
220
+ * clamped at the point of display and nowhere else.
221
+ *
222
+ * ## The currency comes from the RECORD, not from the caller
223
+ *
224
+ * A refund reverses a specific sale, and the bucket it comes out of has to be
225
+ * the bucket it went into. Reading the currency back off the attribution
226
+ * record is what guarantees that, and it is the second thing the record is
227
+ * for.
228
+ *
229
+ * ## Chargebacks come through here too
230
+ *
231
+ * Money reversed is money reversed, whichever door it left by, so a lost
232
+ * dispute reverses the credit the same way a refund does. `kind` exists for
233
+ * the caller's clarity and changes nothing about the arithmetic — the same
234
+ * choice `recordContactRefund` made one field along.
235
+ *
236
+ * @returns whether a reversal was recorded.
237
+ */ export async function reverseEmailAttributedRevenue(options, firestore) {
238
+ try {
239
+ var _options_hostId, _options_orderId, _snapshot_data, _record_campaignId;
240
+ const hostId = String((_options_hostId = options.hostId) != null ? _options_hostId : '');
241
+ const orderId = String((_options_orderId = options.orderId) != null ? _options_orderId : '');
242
+ const amountCents = minorUnits(options.amountCents);
243
+ if (!isDocumentId(hostId) || !isDocumentId(orderId)) return false;
244
+ if (!amountCents) return false;
245
+ const db = firestore != null ? firestore : defaultFirestore();
246
+ const hostRef = db.collection('hosts').doc(hostId);
247
+ const snapshot = await hostRef.collection(EMAIL_ATTRIBUTIONS_COLLECTION).doc(orderId).get();
248
+ // The ordinary case, and not an error: most orders were never credited to
249
+ // a campaign, so most refunds have nothing to reverse.
250
+ if (!snapshot.exists) return false;
251
+ const record = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
252
+ const campaignId = String((_record_campaignId = record.campaignId) != null ? _record_campaignId : '');
253
+ if (!isDocumentId(campaignId)) return false;
254
+ const currency = currencyKey(record.currency);
255
+ /*
256
+ * NOT capped against what was credited, and that is deliberate. The
257
+ * caller already caps each attempt against what is left on the ORDER, so
258
+ * several partials sum to at most the order total; capping again here
259
+ * against the attributed amount would silently discard the reversal of an
260
+ * order refunded for more than the amount the campaign was credited with
261
+ * — which happens when the credit was the charge and the refund includes
262
+ * something the credit did not. The stored pair keeps both true figures
263
+ * and the reader clamps the net it prints.
264
+ */ await hostRef.collection('campaigns').doc(campaignId).collection('reports').doc(CAMPAIGN_REVENUE_REPORT_DOC).set({
265
+ byCurrency: {
266
+ [currency]: _extends({
267
+ refundedCents: FieldValue.increment(amountCents)
268
+ }, options.closedTheOrder ? {
269
+ refundedOrders: FieldValue.increment(1)
270
+ } : {})
271
+ },
272
+ updatedAt: FieldValue.serverTimestamp()
273
+ }, {
274
+ merge: true
275
+ });
276
+ /*
277
+ * Stamped on the record as well as counted in the rollup. The rollup is a
278
+ * sum and cannot say WHICH orders came back; this is the per-order half
279
+ * of the same fact, and it is what a merchant asking "why did this
280
+ * campaign's revenue drop" is eventually going to need. Increments, so
281
+ * two partials on one order both land.
282
+ */ await snapshot.ref.set(_extends({
283
+ refundedCents: FieldValue.increment(amountCents),
284
+ lastRefundAtMs: Date.now()
285
+ }, options.kind === 'chargeback' ? {
286
+ chargedBack: true
287
+ } : {}, options.closedTheOrder ? {
288
+ fullyRefunded: true
289
+ } : {}), {
290
+ merge: true
291
+ }).catch(()=>undefined);
292
+ return true;
293
+ } catch (error) {
294
+ console.error('reverseEmailAttributedRevenue failed', error);
295
+ return false;
296
+ }
297
+ }
298
+ /** The window, re-exported so a caller needs one import for the whole join. */ export { EMAIL_ATTRIBUTION_WINDOW_MS };
299
+
300
+ //# sourceMappingURL=email-revenue-attribution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/email-revenue-attribution.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n EMAIL_ATTRIBUTION_MODEL,\n EMAIL_ATTRIBUTION_WINDOW_DAYS,\n EMAIL_ATTRIBUTION_WINDOW_MS,\n emailTouchIsInWindow,\n} from '@aglyn/shared-util-email'\nimport { readEmailCampaignTouch } from './email-delivery-log'\nimport { isDocumentId } from './document-id'\nimport firebaseAdmin from './firebase-admin'\n\nconst defaultFirestore = () => firebaseAdmin.app().firestore()\n\n/**\n * THE COMMERCE↔EMAIL JOIN — an order, credited to the campaign that led to it.\n *\n * ## Why this is a join\n *\n * Every compared ESP attributes revenue through an integration into a store\n * it does not own: a catalog sync, an on-site tracking snippet, an identity\n * graph, and a window wide enough to absorb what the reconciliation misses.\n * Commerce here is first-party and runs on the merchant's own Stripe Connect\n * account, so the click and the order are two rows in one database keyed the\n * same way. The window is not a fudge factor here — it is the model, and\n * nothing else about the number is estimated.\n *\n * ## Three writes, and what each one is for\n *\n * - **The touch**, `emailDeliveries/{personKey}.campaignTouches[hostId]`,\n * written by the delivery webhook on a click. Owned by\n * `email-delivery-log.ts`, because it lives on the person's document and\n * the erasure path has to be able to remove it.\n * - **The attribution record**, `hosts/{hostId}/emailAttributions/{orderId}`,\n * written here when an order is credited. It is the audit trail — which\n * campaign, which click, which model, which window — and it is what the\n * refund path reads to find out which campaign to take the money back off.\n * - **The rollup**, `campaigns/{campaignId}/reports/revenue`, incremented\n * here. One document per campaign, so the report reads it whole.\n *\n * ## Why the record is a document and not a field on the order\n *\n * Three reasons, and the first is decisive. `create()` fails when the\n * document already exists, which is exact idempotency for free: webhook\n * delivery is at-least-once, and a redelivered purchase must not be able to\n * credit a campaign twice. Second, not every purchase door writes an order —\n * a booking is a sale with no order document — and a field would have had to\n * either skip those or conjure the document, and conjuring is the\n * phantom-document shape this codebase spent a sweep removing. Third, the\n * order document is read by the console list, the fulfilment path, the\n * supplier outbox and the CSV export, and none of them wants a field about\n * email.\n *\n * ## Never throws\n *\n * Same contract as `upsertHostContact`, which calls it, and for the same\n * reason: the money has already moved and the order already records it, so\n * nothing here may fail a sale. A lost attribution understates a campaign; a\n * thrown one loses a checkout.\n */\n\n/** The per-host collection of attribution records. */\nexport const EMAIL_ATTRIBUTIONS_COLLECTION = 'emailAttributions'\n\n/** The single rollup document under a campaign. */\nexport const CAMPAIGN_REVENUE_REPORT_DOC = 'revenue'\n\n/**\n * The currency an amount is recorded under when the caller does not say.\n *\n * Every checkout door in this repo — cart, buy-now, POS, draft orders,\n * reservations, subscriptions — writes `currency: 'usd'` onto the Stripe line\n * items, and no order document carries a currency field to read back. So this\n * is a statement about what the code charges rather than a guess about the\n * money. The parameter exists so a door that ever charges in something else\n * says so and lands in its own bucket, because the one thing the report may\n * never do is add two currencies together.\n */\nexport const DEFAULT_ATTRIBUTION_CURRENCY = 'usd'\n\n/** What one attribution record holds. */\nexport interface EmailAttributionRecord {\n campaignId: string\n /** When the credited click happened. */\n clickedAtMs: number\n /** When the order was placed. */\n orderedAtMs: number\n /** Minor units credited, gross — the amount the buyer was charged. */\n amountCents: number\n /** Lowercase currency code the amount is in. */\n currency: string\n /** The model this credit was decided under. */\n model: string\n /** The window, in days, it was decided inside. */\n windowDays: number\n}\n\n/** Normalizes a currency code to the key its bucket is stored under. */\nfunction currencyKey(raw: unknown): string {\n const code = String(raw ?? '')\n .trim()\n .toLowerCase()\n // Letters only: the code becomes a MAP KEY, and a key carrying a dot would\n // be unreachable by any dotted field path a later reader wants to use.\n return /^[a-z]{3}$/.test(code) ? code : DEFAULT_ATTRIBUTION_CURRENCY\n}\n\n/** A positive integer number of minor units, or 0. */\nfunction minorUnits(raw: unknown): number {\n const value = Math.round(Number(raw))\n return Number.isFinite(value) && value > 0 ? value : 0\n}\n\n/**\n * Credits one order to the campaign whose link the buyer last clicked.\n *\n * ## What happens when there is nobody to credit\n *\n * Each case answers `null` and writes NOTHING, which is the whole design:\n *\n * - **No email on the order.** A guest checkout that never identified its\n * buyer cannot be joined to anybody's clicks. There is no fallback and\n * there deliberately is not one — the alternatives are an IP or a device\n * guess, which is the probabilistic attribution owning the checkout exists\n * to avoid.\n * - **A guest with an email but no contact record.** This is ATTRIBUTED\n * normally. The join keys on the address hash, exactly as the touch and\n * the suppression list do, so it never asks whether a contact document\n * exists — which matters, because contact creation is audience-band gated\n * and a Free org's dropped contact would otherwise silently drop the\n * revenue with it.\n * - **No touch, or a touch on another site.** Nobody clicked, so nobody is\n * credited. The touch map is keyed by host and the send path refuses\n * cross-site reach; the revenue join agrees with it.\n * - **A touch outside the window**, in either direction. See\n * {@link emailTouchIsInWindow} — a click AFTER the order is the receipt,\n * not the cause.\n *\n * None of these is counted anywhere. A miss costs no write, which is what\n * keeps the ordinary order — placed by somebody who is not on the mailing\n * list at all — at exactly one document read. What it means for the report is\n * stated on the report: the figure counts orders it could join, and is a\n * floor.\n *\n * @returns the record written, or `null` when nothing was credited.\n */\nexport async function attributeOrderToEmail(\n options: {\n hostId: string\n /** The order, booking or invoice the money came in on. */\n orderId: string\n /** The buyer as the sale recorded them, raw — normalized downstream. */\n email: unknown\n /** Gross minor units the buyer was charged. */\n amountCents: number\n /** Lowercase currency code, when the door knows one. */\n currency?: string\n /** When the order was placed. Defaults to now. */\n orderedAtMs?: number\n },\n firestore?: any,\n): Promise<EmailAttributionRecord | null> {\n try {\n const hostId = String(options.hostId ?? '')\n const orderId = String(options.orderId ?? '')\n const amountCents = minorUnits(options.amountCents)\n if (!isDocumentId(hostId) || !isDocumentId(orderId)) return null\n if (!amountCents) return null\n\n const orderedAtMs = Number(options.orderedAtMs ?? Date.now())\n const db = firestore ?? defaultFirestore()\n\n /*\n * `String()` on an `unknown`, then let `emailSuppressionKey` decide.\n *\n * The buyer's address arrives as the ORDER recorded it, which is not\n * consistently a string — the same rawness `recordContactRefund` takes\n * `email: unknown` for. The key derivation is the one place that knows\n * how an address becomes a document id, so nothing here tries to\n * pre-clean it; a value that is not an address answers `null` there and\n * the order is credited to nobody.\n */\n const rawEmail =\n typeof options.email === 'string' ? options.email : String(options.email ?? '')\n const touch = await readEmailCampaignTouch(rawEmail, hostId, db)\n if (!touch) return null\n if (!isDocumentId(touch.campaignId)) return null\n if (!emailTouchIsInWindow(touch.clickedAtMs, orderedAtMs)) return null\n\n const record: EmailAttributionRecord = {\n campaignId: touch.campaignId,\n clickedAtMs: touch.clickedAtMs,\n orderedAtMs,\n amountCents,\n currency: currencyKey(options.currency),\n model: EMAIL_ATTRIBUTION_MODEL,\n windowDays: EMAIL_ATTRIBUTION_WINDOW_DAYS,\n }\n\n const hostRef = db.collection('hosts').doc(hostId)\n /*\n * `create()`, never `set()`. It fails with ALREADY_EXISTS when this order\n * has been credited before, and that failure is the idempotency: a\n * webhook redelivery, a retried checkout completion and a replayed event\n * all land here a second time, and all three must leave the rollup where\n * they found it. The increment below is reached only when the create\n * succeeded, so the two can never disagree about whether this order was\n * counted.\n */\n try {\n await hostRef\n .collection(EMAIL_ATTRIBUTIONS_COLLECTION)\n .doc(orderId)\n .create({ ...record, createdAt: FieldValue.serverTimestamp() })\n } catch {\n return null\n }\n\n /*\n * A merge-set that CREATES, unlike the campaign counters the delivery\n * webhook writes through `updateExisting`.\n *\n * The distinction is which document is being conjured. Those counters\n * refuse to create because a merge-set against a DELETED CAMPAIGN\n * resurrects the campaign itself, as a husk holding a stats map and no\n * subject. This writes a subcollection document UNDER a campaign, so a\n * campaign that no longer exists gains an orphaned report rather than\n * coming back to life in the merchant's history — and the campaign was\n * proven to exist a moment ago, when its click wrote the touch.\n *\n * Every amount is an increment, so two orders settling at once both land.\n */\n const currency = record.currency\n await hostRef\n .collection('campaigns')\n .doc(touch.campaignId)\n .collection('reports')\n .doc(CAMPAIGN_REVENUE_REPORT_DOC)\n .set(\n {\n model: EMAIL_ATTRIBUTION_MODEL,\n windowDays: EMAIL_ATTRIBUTION_WINDOW_DAYS,\n byCurrency: {\n [currency]: {\n grossCents: FieldValue.increment(amountCents),\n orders: FieldValue.increment(1),\n },\n },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n\n return record\n } catch (error) {\n console.error('attributeOrderToEmail failed', error)\n return null\n }\n}\n\n/**\n * Takes back revenue a campaign was credited with, when it is refunded.\n *\n * ## Recorded beside the gross, never subtracted from it\n *\n * `grossCents` is left exactly as it is and the reversal lands in\n * `refundedCents` next to it. This is the shape `contact-refund.ts` chose for\n * `ltvCents`/`refundedCents` and the orders CSV chose for\n * `amountUsd`/`refundedUsd`, and it is chosen a third time here so all three\n * answer \"what did this earn, net\" identically. Decrementing would make a\n * stored number mean one thing for rollups written before a refund and\n * another after, with nothing on the document to tell them apart — and would\n * put the question \"can it go negative\" onto storage, where the answer cannot\n * be clamped without destroying evidence. Both stored figures are monotonic\n * counters of money that really moved in one direction; the derived net is\n * clamped at the point of display and nowhere else.\n *\n * ## The currency comes from the RECORD, not from the caller\n *\n * A refund reverses a specific sale, and the bucket it comes out of has to be\n * the bucket it went into. Reading the currency back off the attribution\n * record is what guarantees that, and it is the second thing the record is\n * for.\n *\n * ## Chargebacks come through here too\n *\n * Money reversed is money reversed, whichever door it left by, so a lost\n * dispute reverses the credit the same way a refund does. `kind` exists for\n * the caller's clarity and changes nothing about the arithmetic — the same\n * choice `recordContactRefund` made one field along.\n *\n * @returns whether a reversal was recorded.\n */\nexport async function reverseEmailAttributedRevenue(\n options: {\n hostId: string\n orderId: string\n /** Minor units reversed by THIS attempt, never the order total. */\n amountCents: number\n /** True only for the write that moved the order into `refunded`. */\n closedTheOrder: boolean\n kind?: 'refund' | 'chargeback'\n },\n firestore?: any,\n): Promise<boolean> {\n try {\n const hostId = String(options.hostId ?? '')\n const orderId = String(options.orderId ?? '')\n const amountCents = minorUnits(options.amountCents)\n if (!isDocumentId(hostId) || !isDocumentId(orderId)) return false\n if (!amountCents) return false\n\n const db = firestore ?? defaultFirestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const snapshot = await hostRef\n .collection(EMAIL_ATTRIBUTIONS_COLLECTION)\n .doc(orderId)\n .get()\n // The ordinary case, and not an error: most orders were never credited to\n // a campaign, so most refunds have nothing to reverse.\n if (!snapshot.exists) return false\n\n const record = snapshot.data() ?? {}\n const campaignId = String(record.campaignId ?? '')\n if (!isDocumentId(campaignId)) return false\n const currency = currencyKey(record.currency)\n\n /*\n * NOT capped against what was credited, and that is deliberate. The\n * caller already caps each attempt against what is left on the ORDER, so\n * several partials sum to at most the order total; capping again here\n * against the attributed amount would silently discard the reversal of an\n * order refunded for more than the amount the campaign was credited with\n * — which happens when the credit was the charge and the refund includes\n * something the credit did not. The stored pair keeps both true figures\n * and the reader clamps the net it prints.\n */\n await hostRef\n .collection('campaigns')\n .doc(campaignId)\n .collection('reports')\n .doc(CAMPAIGN_REVENUE_REPORT_DOC)\n .set(\n {\n byCurrency: {\n [currency]: {\n refundedCents: FieldValue.increment(amountCents),\n ...(options.closedTheOrder\n ? { refundedOrders: FieldValue.increment(1) }\n : {}),\n },\n },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n\n /*\n * Stamped on the record as well as counted in the rollup. The rollup is a\n * sum and cannot say WHICH orders came back; this is the per-order half\n * of the same fact, and it is what a merchant asking \"why did this\n * campaign's revenue drop\" is eventually going to need. Increments, so\n * two partials on one order both land.\n */\n await snapshot.ref\n .set(\n {\n refundedCents: FieldValue.increment(amountCents),\n lastRefundAtMs: Date.now(),\n ...(options.kind === 'chargeback' ? { chargedBack: true } : {}),\n ...(options.closedTheOrder ? { fullyRefunded: true } : {}),\n },\n { merge: true },\n )\n .catch(() => undefined)\n\n return true\n } catch (error) {\n console.error('reverseEmailAttributedRevenue failed', error)\n return false\n }\n}\n\n/** The window, re-exported so a caller needs one import for the whole join. */\nexport { EMAIL_ATTRIBUTION_WINDOW_MS }\n"],"names":["FieldValue","EMAIL_ATTRIBUTION_MODEL","EMAIL_ATTRIBUTION_WINDOW_DAYS","EMAIL_ATTRIBUTION_WINDOW_MS","emailTouchIsInWindow","readEmailCampaignTouch","isDocumentId","firebaseAdmin","defaultFirestore","app","firestore","EMAIL_ATTRIBUTIONS_COLLECTION","CAMPAIGN_REVENUE_REPORT_DOC","DEFAULT_ATTRIBUTION_CURRENCY","currencyKey","raw","code","String","trim","toLowerCase","test","minorUnits","value","Math","round","Number","isFinite","attributeOrderToEmail","options","hostId","orderId","amountCents","orderedAtMs","Date","now","db","rawEmail","email","touch","campaignId","clickedAtMs","record","currency","model","windowDays","hostRef","collection","doc","create","createdAt","serverTimestamp","set","byCurrency","grossCents","increment","orders","updatedAt","merge","error","console","reverseEmailAttributedRevenue","snapshot","get","exists","data","refundedCents","closedTheOrder","refundedOrders","ref","lastRefundAtMs","kind","chargedBack","fullyRefunded","catch","undefined"],"mappings":"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"}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * THE PLATFORM SEND-RATE GOVERNOR — durable half (AGL-2409).
19
+ *
20
+ * The policy is pure and lives in `@aglyn/shared-util-email`
21
+ * (`send-rate.ts`); this adds the Firestore counter that makes the ceiling
22
+ * global rather than per-instance, the configuration read that makes a ramp a
23
+ * value change instead of a deploy, and the installation that puts the whole
24
+ * thing on `sendEmail`'s path.
25
+ *
26
+ * ## Storage: the `rateLimits` collection, on purpose
27
+ *
28
+ * Both documents live in `rateLimits`, the same collection AGL-794's counters,
29
+ * AGL-1679's degradation markers and AGL-1907's signup-refusal markers already
30
+ * use, for the reason those two state: it inherits the deny-all security rule
31
+ * and the `expiresAt` TTL policy that already exist rather than needing a new
32
+ * collection, a rules deploy and a second TTL policy.
33
+ *
34
+ * - `rateLimits/sendRate_{windowStartMs}` — one document per hour, holding
35
+ * the count. Carries `expiresAt`, so the TTL policy sweeps it.
36
+ * - `rateLimits/sendRateConfig` — the ceiling. **Carries NO `expiresAt`, and
37
+ * must never be given one**, or the TTL policy that serves the counters
38
+ * would quietly delete the configuration and the platform would silently
39
+ * revert to the compiled-in default. `emailSendRateConfigWrite` is the one
40
+ * writer and `email-send-rate.spec.ts` asserts the field is absent.
41
+ *
42
+ * The window document does NOT carry `lastAtMs`. AGL-1693's rate-limiter
43
+ * health probe queries this collection with `where('lastAtMs', '>=', cutoff)`;
44
+ * a per-hour document in that range would compete with the degradation
45
+ * markers the probe exists to find. It uses `sentAtMs`, which keeps the two
46
+ * queries disjoint at the index level — the same reasoning, and the same
47
+ * mistake avoided, as `refusedAtMs` on the signup markers.
48
+ *
49
+ * ## Failure posture: OPEN
50
+ *
51
+ * If the counter or the config cannot be read, every send is granted. That is
52
+ * the opposite of `consumeRateLimit`'s fail-closed-on-contention rule, and the
53
+ * difference is what the two controls protect. A brute-force limiter that
54
+ * fails open lets an attacker in; this one failing open lets a campaign out.
55
+ * The cost of failing CLOSED here is refusing a paying customer's campaign
56
+ * because of an unrelated Firestore blip — and, worse, refusing a bulk sweep
57
+ * that a customer's month depends on. Neither is worth the hour of ramp it
58
+ * would buy back, and the operator can see it: a degraded grant is reported
59
+ * and surfaced on /admin/emails.
60
+ */
61
+ import { type EmailSendPriority, type EmailSendRateConfig, type EmailSendRateVerdict } from '@aglyn/shared-util-email';
62
+ /** Document id of the live ceiling. NEVER written with `expiresAt`. */
63
+ export declare const EMAIL_SEND_RATE_CONFIG_DOC = "sendRateConfig";
64
+ /** Id prefix for the per-hour counters. */
65
+ export declare const EMAIL_SEND_RATE_WINDOW_PREFIX = "sendRate_";
66
+ /** The counter document id for a window. */
67
+ export declare function emailSendRateWindowDocId(windowStartMs: number): string;
68
+ /** Drop the in-process config cache — called by the console after a write. */
69
+ export declare function invalidateEmailSendRateConfigCache(): void;
70
+ /**
71
+ * The live ceiling. Falls back to the compiled-in default on any read error,
72
+ * which is the fail-open posture stated above: an unreachable config must not
73
+ * be read as a ceiling of zero.
74
+ */
75
+ export declare function readEmailSendRateConfig(options?: {
76
+ firestore?: any;
77
+ now?: number;
78
+ }): Promise<EmailSendRateConfig>;
79
+ export interface ConsumeEmailSendBudgetOptions {
80
+ priority: EmailSendPriority;
81
+ /** Messages this send would add. */
82
+ count: number;
83
+ now?: number;
84
+ firestore?: any;
85
+ /** Injectable for tests; otherwise read (and cached) from Firestore. */
86
+ config?: EmailSendRateConfig;
87
+ }
88
+ export interface ConsumeEmailSendBudgetResult extends EmailSendRateVerdict {
89
+ windowStartMs: number;
90
+ /** True when the counter was unreachable and this failed open. */
91
+ degraded: boolean;
92
+ }
93
+ /**
94
+ * Counts `count` messages against the current hour, globally, and answers
95
+ * whether they may go.
96
+ *
97
+ * The transaction reads the counter and writes an ABSOLUTE value derived from
98
+ * that read — not `FieldValue.increment` — because the read is the authority
99
+ * for the decision. Firestore aborts and re-runs the callback when a
100
+ * document it read has moved, so two concurrent sweeps cannot both see the
101
+ * same headroom and both take it. An increment would be atomic on the number
102
+ * and useless for the decision, which is exactly the read-then-write shape
103
+ * AGL-2267 is about one surface over.
104
+ *
105
+ * A refused send writes NOTHING. A caller that will retry next hour must not
106
+ * have spent budget on being told no.
107
+ */
108
+ export declare function consumeEmailSendBudget(options: ConsumeEmailSendBudgetOptions): Promise<ConsumeEmailSendBudgetResult>;
109
+ /**
110
+ * What the current hour looks like. Read-only, for the staff console.
111
+ *
112
+ * Returns `used: 0` rather than throwing when the window document does not
113
+ * exist, which is the ordinary state of a quiet hour.
114
+ */
115
+ export declare function readEmailSendRateWindow(options?: {
116
+ firestore?: any;
117
+ now?: number;
118
+ }): Promise<{
119
+ windowStartMs: number;
120
+ resetMs: number;
121
+ used: number;
122
+ }>;
123
+ /** Id prefix for the per-org, per-hour campaign counters. */
124
+ export declare const EMAIL_ORG_SEND_RATE_WINDOW_PREFIX = "sendRateOrg_";
125
+ /**
126
+ * The per-org counter document id for a window.
127
+ *
128
+ * A separate document per org rather than a map field on the platform window,
129
+ * so two orgs sending in the same hour contend on their own documents instead
130
+ * of serialising on one. The platform window is already a single hot document
131
+ * and adding N org fields to it would make every campaign in the hour a write
132
+ * conflict with every other.
133
+ */
134
+ export declare function emailOrgSendRateWindowDocId(windowStartMs: number, orgId: string): string;
135
+ /** The current hour for one org. Read-only, for a usage surface. */
136
+ export declare function readOrgEmailSendWindow(options: {
137
+ orgId: string;
138
+ firestore?: any;
139
+ now?: number;
140
+ }): Promise<{
141
+ windowStartMs: number;
142
+ resetMs: number;
143
+ used: number;
144
+ }>;
145
+ /** The answer to a per-org hourly claim. Every field is a stated number. */
146
+ export interface OrgEmailSendClaimResult {
147
+ allowed: boolean;
148
+ /** Count in this org's window BEFORE this send. */
149
+ used: number;
150
+ /** What this org may send in an hour. */
151
+ ceiling: number;
152
+ /** Headroom after this send, floored at 0. */
153
+ remaining: number;
154
+ /** When the window rolls and a deferred campaign may go. */
155
+ retryAtMs: number;
156
+ /** True when the counter was unreachable and this failed open. */
157
+ degraded: boolean;
158
+ }
159
+ /**
160
+ * THE PER-ORG SHARE OF THE PLATFORM HOUR.
161
+ *
162
+ * The platform governor bounds total volume; it does not bound how much of
163
+ * that total ONE tenant may take. Without this, a single org with a large
164
+ * audience occupies the whole hour and every other customer's campaigns are
165
+ * refused by a ceiling they did nothing to reach — one tenant denying service
166
+ * to the rest, on a limit they cannot see.
167
+ *
168
+ * The ceiling is derived, not configured: `orgHourlyCampaignCeiling` is a
169
+ * share of whatever the live platform ceiling currently is, so a staff ramp
170
+ * moves both together and the two can never drift into contradiction. See
171
+ * `send-ceilings.ts` for the arithmetic and the relations it maintains.
172
+ *
173
+ * **Campaigns only.** This function is not on `sendEmail`'s path and is called
174
+ * from the campaign sender alone. A transactional message can never reach it,
175
+ * which is the same boundary `emailSendRateVerdict` enforces one layer down
176
+ * and for the same reason: a password reset refused by a throttle converts a
177
+ * reputation risk into an outage on somebody else's business.
178
+ *
179
+ * ## Fails OPEN
180
+ *
181
+ * An unreachable counter grants the send, matching `consumeEmailSendBudget`.
182
+ * A refusal produced by a Firestore blip is a refused campaign for a paying
183
+ * customer, and the hour of pacing it buys back is not worth it.
184
+ *
185
+ * ## Claimed, not reconciled
186
+ *
187
+ * The claim is taken for the whole batch and never refunded, unlike the
188
+ * monthly reservation. The window is one hour and TTL-swept, so an
189
+ * undelivered remainder costs the org the rest of that hour and nothing
190
+ * after it — where an unreconciled MONTHLY claim would cost the rest of the
191
+ * month, which is why that one is reconciled and this one is not. The error
192
+ * is in the direction that can only ever pace mail more, never let more out.
193
+ */
194
+ export declare function claimOrgEmailSendBudget(options: {
195
+ orgId: string;
196
+ /** Messages this campaign would send. */
197
+ count: number;
198
+ /** The live platform ceiling; the org share is derived from it. */
199
+ platformPerHour: number;
200
+ /** False parks the control, exactly as the platform governor's flag does. */
201
+ enabled?: boolean;
202
+ now?: number;
203
+ firestore?: any;
204
+ }): Promise<OrgEmailSendClaimResult>;
205
+ /**
206
+ * The document the console writes when staff move the ramp.
207
+ *
208
+ * Returned rather than written so the route owns the write (and the audit row
209
+ * beside it), and so a spec can assert the SHAPE without a Firestore. The one
210
+ * thing it must never contain is `expiresAt`.
211
+ */
212
+ export declare function emailSendRateConfigWrite(input: {
213
+ perHour: number;
214
+ enabled: boolean;
215
+ actorEmail?: string | null;
216
+ note?: string;
217
+ now?: number;
218
+ }): Partial<EmailSendRateConfig>;
219
+ /**
220
+ * Puts the durable governor on `sendEmail`'s path.
221
+ *
222
+ * **Called at module load**, from the bottom of this file, so that importing
223
+ * `@aglyn/tenant-data-admin` anywhere is enough — every server surface in the
224
+ * product already imports that barrel, and `export *` forces this module to
225
+ * evaluate. The alternative, an `installEmailSendGovernor()` call at each
226
+ * server entrypoint, is the 37-places-to-remember shape this codebase already
227
+ * rejected once for the `context` tag.
228
+ *
229
+ * Idempotent: installing twice replaces the same closure with an equivalent
230
+ * one, and the closure holds no state — the state is the Firestore document.
231
+ */
232
+ export declare function installEmailSendGovernor(): void;