@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,359 @@
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
+ */ import { firebaseAdmin } from "./firebase-admin.js";
17
+ /**
18
+ * The email meters (AGL-1438). **Two of them, deliberately.**
19
+ *
20
+ * Before this, `hosts/{hostId}/counters/emailSends` was written by exactly one
21
+ * caller — the marketing campaign sender. Workflows, commerce, bookings and
22
+ * invites all called `sendEmail` and counted nothing, so the counter that named
23
+ * itself "email sends" held campaign sends alone. Two things followed from
24
+ * that: `emailSendsPerMonth` under-enforced, and the COGS figure AGL-1134
25
+ * started recording off this counter under-reported real email cost by however
26
+ * much non-campaign volume an org sent.
27
+ *
28
+ * UNITS: plain COUNTS — one per recipient address handed to the sender —
29
+ * for the calendar month named by the `YYYY-MM` FIELD on the counter document.
30
+ * Not bytes, not currency, not a running total: each month's field is
31
+ * independent, so summing months is a legitimate year-to-date and reading one
32
+ * is that month alone. Same unit as `workflowRuns`/`actionRuns` beside it, and
33
+ * the same unit `orgCounterTotals` sums, so a campaign of 300 and 300 workflow
34
+ * notifications are the same 300 on the meter.
35
+ *
36
+ * ### `emailSends` — the COST meter. Counts everything. Gates nothing.
37
+ *
38
+ * Every `sendEmail` call site increments this, whatever sent it. It is what the
39
+ * monthly rollup carries for COGS, and it is RECORDED, NOT PRICED — there is
40
+ * still no per-email rate anywhere, so it enters neither `billedCents` nor
41
+ * `costUsd` nor `ORG_COGS_UNIT_RATES_USD`. Pricing it is a decision with an
42
+ * invoice month behind it.
43
+ *
44
+ * ### `campaignEmailSends` — the ENFORCEABLE meter. Counts campaigns only.
45
+ *
46
+ * `emailSendsPerMonth` is checked against this and nothing else. A campaign is
47
+ * discretionary: refusing one at the cap is the outcome the plan sells, the
48
+ * customer sees a clear message, and they upgrade or wait.
49
+ *
50
+ * **Transactional mail is never blocked by a quota, at any tier.** Password
51
+ * resets, invites, order confirmations, booking reminders and workflow
52
+ * notifications send regardless of counter state; they count toward cost and
53
+ * cannot be refused. The failure modes are not symmetric — a blocked password
54
+ * reset locks somebody out of their own account, and the message explaining
55
+ * why is itself an email that will not send; a dropped order confirmation
56
+ * reads to the buyer as a failed order. That converts a billing event into an
57
+ * outage on somebody else's business, where the overage it saved was bounded
58
+ * and billable. It is the same fail-open posture the rest of this codebase
59
+ * takes: rate limiting fails soft, robots/sitemap fail open so a transient
60
+ * error cannot de-index a customer's site.
61
+ *
62
+ * ### Not double-counted
63
+ *
64
+ * One call site, one call to this function, and a campaign send increments BOTH
65
+ * counters from that single call rather than incrementing `emailSends` here and
66
+ * again somewhere else. The counter is keyed by month ON THE DOCUMENT, so
67
+ * nothing accumulates across a re-read, and the rollup re-derives rather than
68
+ * adds. A meter that double-counts is worse than no meter, because it looks
69
+ * authoritative (AGL-1402).
70
+ *
71
+ * ### Transition note
72
+ *
73
+ * `campaignEmailSends` starts empty. For the month this ships, campaign volume
74
+ * already sent is recorded only under the old `emailSends`, so the campaign cap
75
+ * effectively restarts once. That loosens for at most one month, which is the
76
+ * correct direction to be wrong in — the alternative, seeding the enforceable
77
+ * meter from a counter that now also holds transactional mail, would refuse
78
+ * campaigns because of order confirmations.
79
+ */ /** Every send, whatever produced it. The cost meter. */ export const EMAIL_SENDS_COUNTER = 'emailSends';
80
+ /** Campaign sends alone. The only meter `emailSendsPerMonth` may refuse. */ export const CAMPAIGN_EMAIL_SENDS_COUNTER = 'campaignEmailSends';
81
+ /** Top-level home for platform-scoped meters: `meters/platform/counters/*`. */ export const PLATFORM_METER_COLLECTION = 'meters';
82
+ export const PLATFORM_METER_DOC = 'platform';
83
+ /** The counters document for a scope, e.g. `hosts/{id}/counters/emailSends`. */ export function emailMeterRef(firestore, scope, counter) {
84
+ if (scope.kind === 'host') {
85
+ return firestore.collection('hosts').doc(scope.hostId).collection('counters').doc(counter);
86
+ }
87
+ if (scope.kind === 'org') {
88
+ return firestore.collection('orgs').doc(scope.orgId).collection('counters').doc(counter);
89
+ }
90
+ return firestore.collection(PLATFORM_METER_COLLECTION).doc(PLATFORM_METER_DOC).collection('counters').doc(counter);
91
+ }
92
+ /** Current calendar month as `YYYY-MM`, matching every other counter here. */ export function currentMeterMonth(now = new Date()) {
93
+ return now.toISOString().slice(0, 7);
94
+ }
95
+ /**
96
+ * Records `count` emails against the cost meter, and against the campaign
97
+ * meter too when the send was a campaign.
98
+ *
99
+ * **Never throws.** Metering is bookkeeping that runs after mail has already
100
+ * gone out — a counter write that fails must not turn a delivered receipt into
101
+ * a 500 for the buyer, and must certainly not be able to stop the next send.
102
+ * Failures are logged and swallowed, the same posture `sendEmail` itself takes.
103
+ */ export async function recordEmailSends(options) {
104
+ const { scope, sendClass } = options;
105
+ const count = Math.floor(Number(options.count));
106
+ // A send that did not happen is not a cost. `sendEmail` reports `sent` per
107
+ // message, and callers pass the delivered count, so 0 is the ordinary
108
+ // outcome of an unconfigured environment rather than an error.
109
+ if (!Number.isFinite(count) || count <= 0) return;
110
+ if (scope.kind === 'host' && !scope.hostId) return;
111
+ if (scope.kind === 'org' && !scope.orgId) return;
112
+ const month = options.month || currentMeterMonth();
113
+ try {
114
+ var _options_firestore;
115
+ const firestore = (_options_firestore = options.firestore) != null ? _options_firestore : firebaseAdmin.app().firestore();
116
+ const increment = firebaseAdmin.firestore.FieldValue.increment(count);
117
+ const counters = sendClass === 'campaign' ? [
118
+ EMAIL_SENDS_COUNTER,
119
+ CAMPAIGN_EMAIL_SENDS_COUNTER
120
+ ] : [
121
+ EMAIL_SENDS_COUNTER
122
+ ];
123
+ await Promise.all(counters.map((counter)=>emailMeterRef(firestore, scope, counter).set({
124
+ [month]: increment
125
+ }, {
126
+ merge: true
127
+ })));
128
+ } catch (error) {
129
+ console.error('email meter write failed', error);
130
+ }
131
+ }
132
+ /**
133
+ * One transactional (or, with `sendClass`, campaign) send attributed to a site.
134
+ *
135
+ * The overwhelmingly common call — `await meterHostEmail(hostId)` right after
136
+ * a `sendEmail` — so it reads as one line at every call site and there is one
137
+ * place to change if the counter ever moves.
138
+ */ export function meterHostEmail(hostId, count = 1, sendClass = 'transactional') {
139
+ return recordEmailSends({
140
+ scope: {
141
+ kind: 'host',
142
+ hostId
143
+ },
144
+ count,
145
+ sendClass
146
+ });
147
+ }
148
+ /** Org-scoped transactional mail: invites, welcome, usage summaries. */ export function meterOrgEmail(orgId, count = 1) {
149
+ return recordEmailSends({
150
+ scope: {
151
+ kind: 'org',
152
+ orgId
153
+ },
154
+ count,
155
+ sendClass: 'transactional'
156
+ });
157
+ }
158
+ /**
159
+ * Account and staff mail that belongs to no org at send time — a password
160
+ * reset knows only an address, and a staff alert has no customer at all.
161
+ */ export function meterPlatformEmail(count = 1) {
162
+ return recordEmailSends({
163
+ scope: {
164
+ kind: 'platform'
165
+ },
166
+ count,
167
+ sendClass: 'transactional'
168
+ });
169
+ }
170
+ /**
171
+ * The enforceable figure for a site this month: campaign sends alone.
172
+ *
173
+ * Reads the campaign meter and NOT `emailSends`, which now also holds every
174
+ * receipt and password reset the site sent. Enforcing the plan's cap against
175
+ * that total is exactly the behaviour this issue exists to prevent.
176
+ */ export async function campaignEmailSendsForMonth(hostRef, month) {
177
+ var _snapshot_get;
178
+ const snapshot = await hostRef.collection('counters').doc(CAMPAIGN_EMAIL_SENDS_COUNTER).get();
179
+ const used = Number((_snapshot_get = snapshot.get(month)) != null ? _snapshot_get : 0);
180
+ // An absent counter is 0, and a corrupt negative must not read as unused
181
+ // headroom that a cap then honours.
182
+ return Number.isFinite(used) && used > 0 ? used : 0;
183
+ }
184
+ /**
185
+ * The org-level campaign meter, and the ONLY figure `emailSendsPerMonth` is
186
+ * enforced against since AGL-2267.
187
+ *
188
+ * ## Why a second counter and not the per-host one
189
+ *
190
+ * `hosts/{hostId}/counters/campaignEmailSends` is per SITE. `emailSendsPerMonth`
191
+ * is an ORG entitlement. An org with N sites therefore received N × the cap it
192
+ * bought — invisible on Free and Starter (`hostLimit: 1`) and growing with the
193
+ * plan, so the customers who paid most got the most cap they had not paid for.
194
+ *
195
+ * The per-host counter is UNCHANGED and still written, by `meterHostEmail`, on
196
+ * the delivered count. It is per-site history and it feeds the cost meter;
197
+ * nothing about it was wrong except being asked a question about an org.
198
+ *
199
+ * ## Why folding the existing per-site counters in was rejected
200
+ *
201
+ * Summing N site counters into one org total retroactively puts multi-site
202
+ * paying customers over a limit they have been under all along — a limit that
203
+ * then refuses a campaign they had every reason to expect to send, days after
204
+ * they bought the plan. This counter therefore STARTS EMPTY, and the campaign
205
+ * cap effectively restarts once for the month this ships.
206
+ *
207
+ * That is the same transition `campaignEmailSends` itself was given, and the
208
+ * same reasoning: it loosens for at most one month, which is the correct
209
+ * direction to be wrong in, and it needs no production measurement of who is
210
+ * currently over — a measurement that could only be taken against live
211
+ * customer data and would have blocked the fix indefinitely.
212
+ */ export const ORG_CAMPAIGN_EMAIL_SENDS_COUNTER = CAMPAIGN_EMAIL_SENDS_COUNTER;
213
+ /** Reads an org-scoped monthly counter field, clamped like every other. */ function readMonthField(snapshot, month) {
214
+ var _ref;
215
+ const used = Number((_ref = (snapshot == null ? void 0 : snapshot.exists) ? snapshot.get(month) : 0) != null ? _ref : 0);
216
+ return Number.isFinite(used) && used > 0 ? Math.floor(used) : 0;
217
+ }
218
+ /**
219
+ * The enforceable figure for an ORG this month: campaign sends alone.
220
+ *
221
+ * Read-only. Used by the campaign composer's recipient preview, which must
222
+ * not reserve anything (AGL-2178 — "nothing has been written above this
223
+ * line"), and by anything that reports headroom.
224
+ */ export async function orgCampaignEmailSendsForMonth(orgId, month, firestore) {
225
+ if (!orgId) return 0;
226
+ const db = firestore != null ? firestore : firebaseAdmin.app().firestore();
227
+ const snapshot = await db.collection('orgs').doc(orgId).collection('counters').doc(ORG_CAMPAIGN_EMAIL_SENDS_COUNTER).get();
228
+ return readMonthField(snapshot, month);
229
+ }
230
+ /**
231
+ * Claims `count` campaign sends against the org's monthly allowance, ATOMICALLY.
232
+ *
233
+ * ## Why a reservation and not a check
234
+ *
235
+ * The cap used to be read before the send and incremented after delivery, so
236
+ * two concurrent campaigns both passed the same reading and both sent — the
237
+ * cap was advisory in exactly the conditions it existed for. **A read-then-write
238
+ * cap is not a cap.**
239
+ *
240
+ * The transaction reads the counter and writes an ABSOLUTE value derived from
241
+ * that read, deliberately not `FieldValue.increment`. Firestore aborts and
242
+ * re-runs the callback when a document the transaction read has moved, so a
243
+ * second sender that starts inside the first one's window re-reads the raised
244
+ * figure and is refused. An increment would be atomic on the number and
245
+ * useless for the decision, because the decision is made from a value the
246
+ * write never proves it still held.
247
+ *
248
+ * A refused reservation writes NOTHING.
249
+ *
250
+ * ## Why reserve-then-reconcile rather than reserve-and-keep
251
+ *
252
+ * A campaign reserves against a PARTIAL delivery: `sendEmail` reports per
253
+ * message, and only some of a 500-address batch may go out. Keeping the whole
254
+ * reservation would charge a customer's allowance for mail that never left.
255
+ * So the claim is taken in full up front — that is what makes it a cap — and
256
+ * {@link reconcileCampaignSendReservation} gives back the difference once the
257
+ * delivered count is known.
258
+ *
259
+ * **The failure mode is stated rather than hidden**: if the process dies
260
+ * between reserving and reconciling, the org is charged for the undelivered
261
+ * remainder for the rest of that calendar month. That is conservative in the
262
+ * direction this issue cares about — it can only ever refuse more mail, never
263
+ * let more out — and it self-heals at the month boundary, because each month
264
+ * is an independent field on the document.
265
+ *
266
+ * `limit` may be `Infinity` (`UNLIMITED`), which admits everything.
267
+ */ export async function reserveCampaignEmailSends(options) {
268
+ var _options_firestore;
269
+ const count = Math.max(0, Math.floor(Number(options.count) || 0));
270
+ const limit = Number(options.limit);
271
+ // A plan-less or unresolvable org must not be a bypass. The caller resolves
272
+ // free-tier entitlements for it (AGL-247) and the cap is 0, so an empty
273
+ // orgId that reached here means the counter has nowhere to live — refuse
274
+ // rather than send unbounded.
275
+ if (!options.orgId) {
276
+ return {
277
+ ok: false,
278
+ used: 0,
279
+ limit: Number.isFinite(limit) ? limit : 0
280
+ };
281
+ }
282
+ const db = (_options_firestore = options.firestore) != null ? _options_firestore : firebaseAdmin.app().firestore();
283
+ const ref = db.collection('orgs').doc(options.orgId).collection('counters').doc(ORG_CAMPAIGN_EMAIL_SENDS_COUNTER);
284
+ return db.runTransaction(async (tx)=>{
285
+ const snapshot = await tx.get(ref);
286
+ const used = readMonthField(snapshot, options.month);
287
+ if (used + count > limit) return {
288
+ ok: false,
289
+ used,
290
+ limit
291
+ };
292
+ tx.set(ref, {
293
+ [options.month]: used + count
294
+ }, {
295
+ merge: true
296
+ });
297
+ return {
298
+ ok: true,
299
+ reservation: {
300
+ orgId: options.orgId,
301
+ month: options.month,
302
+ reserved: count
303
+ },
304
+ used,
305
+ limit
306
+ };
307
+ });
308
+ }
309
+ /**
310
+ * Returns the undelivered part of a reservation.
311
+ *
312
+ * Also a transaction, and also an absolute write from its own read: a refund
313
+ * computed from a stale figure would undo a reservation another campaign took
314
+ * in the meantime, which is the same defect one direction over.
315
+ *
316
+ * **Never throws** and never drives the counter below zero. This runs after
317
+ * mail has already gone out; a bookkeeping failure must not turn a delivered
318
+ * campaign into a 500, exactly like `recordEmailSends` above. The cost of
319
+ * swallowing it is that the org keeps a claim it did not use for the rest of
320
+ * the month, which is the safe direction.
321
+ */ export async function reconcileCampaignSendReservation(reservation, delivered, firestore) {
322
+ if (!(reservation == null ? void 0 : reservation.orgId)) return;
323
+ const sent = Math.max(0, Math.floor(Number(delivered) || 0));
324
+ const refund = Math.max(0, reservation.reserved - sent);
325
+ if (refund <= 0) return;
326
+ try {
327
+ const db = firestore != null ? firestore : firebaseAdmin.app().firestore();
328
+ const ref = db.collection('orgs').doc(reservation.orgId).collection('counters').doc(ORG_CAMPAIGN_EMAIL_SENDS_COUNTER);
329
+ await db.runTransaction(async (tx)=>{
330
+ const snapshot = await tx.get(ref);
331
+ const used = readMonthField(snapshot, reservation.month);
332
+ tx.set(ref, {
333
+ [reservation.month]: Math.max(0, used - refund)
334
+ }, {
335
+ merge: true
336
+ });
337
+ });
338
+ } catch (error) {
339
+ console.error('campaign reservation reconcile failed', error);
340
+ }
341
+ }
342
+ /**
343
+ * Volume above the plan's included band, in emails — RE-EXPORTED, not defined
344
+ * here, the same move AGL-2155 made for the bandwidth helpers.
345
+ *
346
+ * The definition moved down to `@aglyn/aglyn/app-utils/plan-entitlements`
347
+ * because the billing page has to render the overage BEFORE it is charged,
348
+ * and that page is a client component which cannot import this module: the
349
+ * Admin SDK comes with it. A second copy of the subtraction on the client is
350
+ * the shape where the readout and the invoice quietly stop agreeing.
351
+ *
352
+ * Re-exported so every server caller keeps importing it from the module it
353
+ * always did — one definition, no drift, no import churn.
354
+ *
355
+ * @see priceEmailSendOverage — what the excess costs, at the plan's rate.
356
+ */ export { emailSendsOverage } from "@aglyn/aglyn/app-utils/plan-entitlements";
357
+ export default recordEmailSends;
358
+
359
+ //# sourceMappingURL=email-metering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/email-metering.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 { firebaseAdmin } from './firebase-admin'\n\n/**\n * The email meters (AGL-1438). **Two of them, deliberately.**\n *\n * Before this, `hosts/{hostId}/counters/emailSends` was written by exactly one\n * caller — the marketing campaign sender. Workflows, commerce, bookings and\n * invites all called `sendEmail` and counted nothing, so the counter that named\n * itself \"email sends\" held campaign sends alone. Two things followed from\n * that: `emailSendsPerMonth` under-enforced, and the COGS figure AGL-1134\n * started recording off this counter under-reported real email cost by however\n * much non-campaign volume an org sent.\n *\n * UNITS: plain COUNTS — one per recipient address handed to the sender —\n * for the calendar month named by the `YYYY-MM` FIELD on the counter document.\n * Not bytes, not currency, not a running total: each month's field is\n * independent, so summing months is a legitimate year-to-date and reading one\n * is that month alone. Same unit as `workflowRuns`/`actionRuns` beside it, and\n * the same unit `orgCounterTotals` sums, so a campaign of 300 and 300 workflow\n * notifications are the same 300 on the meter.\n *\n * ### `emailSends` — the COST meter. Counts everything. Gates nothing.\n *\n * Every `sendEmail` call site increments this, whatever sent it. It is what the\n * monthly rollup carries for COGS, and it is RECORDED, NOT PRICED — there is\n * still no per-email rate anywhere, so it enters neither `billedCents` nor\n * `costUsd` nor `ORG_COGS_UNIT_RATES_USD`. Pricing it is a decision with an\n * invoice month behind it.\n *\n * ### `campaignEmailSends` — the ENFORCEABLE meter. Counts campaigns only.\n *\n * `emailSendsPerMonth` is checked against this and nothing else. A campaign is\n * discretionary: refusing one at the cap is the outcome the plan sells, the\n * customer sees a clear message, and they upgrade or wait.\n *\n * **Transactional mail is never blocked by a quota, at any tier.** Password\n * resets, invites, order confirmations, booking reminders and workflow\n * notifications send regardless of counter state; they count toward cost and\n * cannot be refused. The failure modes are not symmetric — a blocked password\n * reset locks somebody out of their own account, and the message explaining\n * why is itself an email that will not send; a dropped order confirmation\n * reads to the buyer as a failed order. That converts a billing event into an\n * outage on somebody else's business, where the overage it saved was bounded\n * and billable. It is the same fail-open posture the rest of this codebase\n * takes: rate limiting fails soft, robots/sitemap fail open so a transient\n * error cannot de-index a customer's site.\n *\n * ### Not double-counted\n *\n * One call site, one call to this function, and a campaign send increments BOTH\n * counters from that single call rather than incrementing `emailSends` here and\n * again somewhere else. The counter is keyed by month ON THE DOCUMENT, so\n * nothing accumulates across a re-read, and the rollup re-derives rather than\n * adds. A meter that double-counts is worse than no meter, because it looks\n * authoritative (AGL-1402).\n *\n * ### Transition note\n *\n * `campaignEmailSends` starts empty. For the month this ships, campaign volume\n * already sent is recorded only under the old `emailSends`, so the campaign cap\n * effectively restarts once. That loosens for at most one month, which is the\n * correct direction to be wrong in — the alternative, seeding the enforceable\n * meter from a counter that now also holds transactional mail, would refuse\n * campaigns because of order confirmations.\n */\n\n/** Every send, whatever produced it. The cost meter. */\nexport const EMAIL_SENDS_COUNTER = 'emailSends'\n\n/** Campaign sends alone. The only meter `emailSendsPerMonth` may refuse. */\nexport const CAMPAIGN_EMAIL_SENDS_COUNTER = 'campaignEmailSends'\n\n/** Top-level home for platform-scoped meters: `meters/platform/counters/*`. */\nexport const PLATFORM_METER_COLLECTION = 'meters'\nexport const PLATFORM_METER_DOC = 'platform'\n\n/**\n * Who the send is attributed to.\n *\n * - `host` — a site sent it: campaigns, receipts, booking mail, workflow\n * notifications, member mail. Rolls up to the owning org.\n * - `org` — the org sent it with no site involved: invites, member-added,\n * the welcome mail, usage summaries, erasure notices.\n * - `platform` — Aglyn's own account and staff mail: password resets and\n * verification (which happen before any org is known), new-device and\n * passkey alerts, staff alerts, the system-email test send. Real cost, but\n * not any one customer's, so it is counted apart from every org rollup and\n * never reaches a COGS figure or an invoice.\n */\nexport type EmailMeterScope =\n | { kind: 'host'; hostId: string }\n | { kind: 'org'; orgId: string }\n | { kind: 'platform' }\n\n/**\n * Whether a quota may refuse this send.\n *\n * `campaign` is the ONLY discretionary class. Everything else is\n * `transactional`, which means: count it, never gate on it.\n */\nexport type EmailSendClass = 'campaign' | 'transactional'\n\nexport interface RecordEmailSendsOptions {\n scope: EmailMeterScope\n /** Emails actually handed to the sender. Zero and negatives are no-ops. */\n count: number\n sendClass: EmailSendClass\n /** `YYYY-MM`; defaults to the current UTC month. */\n month?: string\n /** Injectable for tests; defaults to the Admin SDK's Firestore. */\n firestore?: any\n}\n\n/** The counters document for a scope, e.g. `hosts/{id}/counters/emailSends`. */\nexport function emailMeterRef(\n firestore: any,\n scope: EmailMeterScope,\n counter: string,\n): any {\n if (scope.kind === 'host') {\n return firestore\n .collection('hosts')\n .doc(scope.hostId)\n .collection('counters')\n .doc(counter)\n }\n if (scope.kind === 'org') {\n return firestore\n .collection('orgs')\n .doc(scope.orgId)\n .collection('counters')\n .doc(counter)\n }\n return firestore\n .collection(PLATFORM_METER_COLLECTION)\n .doc(PLATFORM_METER_DOC)\n .collection('counters')\n .doc(counter)\n}\n\n/** Current calendar month as `YYYY-MM`, matching every other counter here. */\nexport function currentMeterMonth(now: Date = new Date()): string {\n return now.toISOString().slice(0, 7)\n}\n\n/**\n * Records `count` emails against the cost meter, and against the campaign\n * meter too when the send was a campaign.\n *\n * **Never throws.** Metering is bookkeeping that runs after mail has already\n * gone out — a counter write that fails must not turn a delivered receipt into\n * a 500 for the buyer, and must certainly not be able to stop the next send.\n * Failures are logged and swallowed, the same posture `sendEmail` itself takes.\n */\nexport async function recordEmailSends(\n options: RecordEmailSendsOptions,\n): Promise<void> {\n const { scope, sendClass } = options\n const count = Math.floor(Number(options.count))\n // A send that did not happen is not a cost. `sendEmail` reports `sent` per\n // message, and callers pass the delivered count, so 0 is the ordinary\n // outcome of an unconfigured environment rather than an error.\n if (!Number.isFinite(count) || count <= 0) return\n if (scope.kind === 'host' && !scope.hostId) return\n if (scope.kind === 'org' && !scope.orgId) return\n\n const month = options.month || currentMeterMonth()\n try {\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const increment = firebaseAdmin.firestore.FieldValue.increment(count)\n const counters =\n sendClass === 'campaign'\n ? [EMAIL_SENDS_COUNTER, CAMPAIGN_EMAIL_SENDS_COUNTER]\n : [EMAIL_SENDS_COUNTER]\n await Promise.all(\n counters.map((counter) =>\n emailMeterRef(firestore, scope, counter).set(\n { [month]: increment },\n { merge: true },\n ),\n ),\n )\n } catch (error) {\n console.error('email meter write failed', error)\n }\n}\n\n/**\n * One transactional (or, with `sendClass`, campaign) send attributed to a site.\n *\n * The overwhelmingly common call — `await meterHostEmail(hostId)` right after\n * a `sendEmail` — so it reads as one line at every call site and there is one\n * place to change if the counter ever moves.\n */\nexport function meterHostEmail(\n hostId: string,\n count = 1,\n sendClass: EmailSendClass = 'transactional',\n): Promise<void> {\n return recordEmailSends({\n scope: { kind: 'host', hostId },\n count,\n sendClass,\n })\n}\n\n/** Org-scoped transactional mail: invites, welcome, usage summaries. */\nexport function meterOrgEmail(orgId: string, count = 1): Promise<void> {\n return recordEmailSends({\n scope: { kind: 'org', orgId },\n count,\n sendClass: 'transactional',\n })\n}\n\n/**\n * Account and staff mail that belongs to no org at send time — a password\n * reset knows only an address, and a staff alert has no customer at all.\n */\nexport function meterPlatformEmail(count = 1): Promise<void> {\n return recordEmailSends({\n scope: { kind: 'platform' },\n count,\n sendClass: 'transactional',\n })\n}\n\n/**\n * The enforceable figure for a site this month: campaign sends alone.\n *\n * Reads the campaign meter and NOT `emailSends`, which now also holds every\n * receipt and password reset the site sent. Enforcing the plan's cap against\n * that total is exactly the behaviour this issue exists to prevent.\n */\nexport async function campaignEmailSendsForMonth(\n hostRef: any,\n month: string,\n): Promise<number> {\n const snapshot = await hostRef\n .collection('counters')\n .doc(CAMPAIGN_EMAIL_SENDS_COUNTER)\n .get()\n const used = Number(snapshot.get(month) ?? 0)\n // An absent counter is 0, and a corrupt negative must not read as unused\n // headroom that a cap then honours.\n return Number.isFinite(used) && used > 0 ? used : 0\n}\n\n/**\n * The org-level campaign meter, and the ONLY figure `emailSendsPerMonth` is\n * enforced against since AGL-2267.\n *\n * ## Why a second counter and not the per-host one\n *\n * `hosts/{hostId}/counters/campaignEmailSends` is per SITE. `emailSendsPerMonth`\n * is an ORG entitlement. An org with N sites therefore received N × the cap it\n * bought — invisible on Free and Starter (`hostLimit: 1`) and growing with the\n * plan, so the customers who paid most got the most cap they had not paid for.\n *\n * The per-host counter is UNCHANGED and still written, by `meterHostEmail`, on\n * the delivered count. It is per-site history and it feeds the cost meter;\n * nothing about it was wrong except being asked a question about an org.\n *\n * ## Why folding the existing per-site counters in was rejected\n *\n * Summing N site counters into one org total retroactively puts multi-site\n * paying customers over a limit they have been under all along — a limit that\n * then refuses a campaign they had every reason to expect to send, days after\n * they bought the plan. This counter therefore STARTS EMPTY, and the campaign\n * cap effectively restarts once for the month this ships.\n *\n * That is the same transition `campaignEmailSends` itself was given, and the\n * same reasoning: it loosens for at most one month, which is the correct\n * direction to be wrong in, and it needs no production measurement of who is\n * currently over — a measurement that could only be taken against live\n * customer data and would have blocked the fix indefinitely.\n */\nexport const ORG_CAMPAIGN_EMAIL_SENDS_COUNTER = CAMPAIGN_EMAIL_SENDS_COUNTER\n\n/** A granted claim on the org's monthly campaign allowance. */\nexport interface CampaignSendReservation {\n orgId: string\n month: string\n /** Messages claimed up front. */\n reserved: number\n}\n\nexport type ReserveCampaignSendsResult =\n | { ok: true; reservation: CampaignSendReservation; used: number; limit: number }\n | { ok: false; used: number; limit: number }\n\n/** Reads an org-scoped monthly counter field, clamped like every other. */\nfunction readMonthField(snapshot: any, month: string): number {\n const used = Number((snapshot?.exists ? snapshot.get(month) : 0) ?? 0)\n return Number.isFinite(used) && used > 0 ? Math.floor(used) : 0\n}\n\n/**\n * The enforceable figure for an ORG this month: campaign sends alone.\n *\n * Read-only. Used by the campaign composer's recipient preview, which must\n * not reserve anything (AGL-2178 — \"nothing has been written above this\n * line\"), and by anything that reports headroom.\n */\nexport async function orgCampaignEmailSendsForMonth(\n orgId: string,\n month: string,\n firestore?: any,\n): Promise<number> {\n if (!orgId) return 0\n const db = firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await db\n .collection('orgs')\n .doc(orgId)\n .collection('counters')\n .doc(ORG_CAMPAIGN_EMAIL_SENDS_COUNTER)\n .get()\n return readMonthField(snapshot, month)\n}\n\n/**\n * Claims `count` campaign sends against the org's monthly allowance, ATOMICALLY.\n *\n * ## Why a reservation and not a check\n *\n * The cap used to be read before the send and incremented after delivery, so\n * two concurrent campaigns both passed the same reading and both sent — the\n * cap was advisory in exactly the conditions it existed for. **A read-then-write\n * cap is not a cap.**\n *\n * The transaction reads the counter and writes an ABSOLUTE value derived from\n * that read, deliberately not `FieldValue.increment`. Firestore aborts and\n * re-runs the callback when a document the transaction read has moved, so a\n * second sender that starts inside the first one's window re-reads the raised\n * figure and is refused. An increment would be atomic on the number and\n * useless for the decision, because the decision is made from a value the\n * write never proves it still held.\n *\n * A refused reservation writes NOTHING.\n *\n * ## Why reserve-then-reconcile rather than reserve-and-keep\n *\n * A campaign reserves against a PARTIAL delivery: `sendEmail` reports per\n * message, and only some of a 500-address batch may go out. Keeping the whole\n * reservation would charge a customer's allowance for mail that never left.\n * So the claim is taken in full up front — that is what makes it a cap — and\n * {@link reconcileCampaignSendReservation} gives back the difference once the\n * delivered count is known.\n *\n * **The failure mode is stated rather than hidden**: if the process dies\n * between reserving and reconciling, the org is charged for the undelivered\n * remainder for the rest of that calendar month. That is conservative in the\n * direction this issue cares about — it can only ever refuse more mail, never\n * let more out — and it self-heals at the month boundary, because each month\n * is an independent field on the document.\n *\n * `limit` may be `Infinity` (`UNLIMITED`), which admits everything.\n */\nexport async function reserveCampaignEmailSends(options: {\n orgId: string\n month: string\n count: number\n limit: number\n firestore?: any\n}): Promise<ReserveCampaignSendsResult> {\n const count = Math.max(0, Math.floor(Number(options.count) || 0))\n const limit = Number(options.limit)\n // A plan-less or unresolvable org must not be a bypass. The caller resolves\n // free-tier entitlements for it (AGL-247) and the cap is 0, so an empty\n // orgId that reached here means the counter has nowhere to live — refuse\n // rather than send unbounded.\n if (!options.orgId) {\n return { ok: false, used: 0, limit: Number.isFinite(limit) ? limit : 0 }\n }\n const db = options.firestore ?? firebaseAdmin.app().firestore()\n const ref = db\n .collection('orgs')\n .doc(options.orgId)\n .collection('counters')\n .doc(ORG_CAMPAIGN_EMAIL_SENDS_COUNTER)\n\n return db.runTransaction(async (tx: any) => {\n const snapshot = await tx.get(ref)\n const used = readMonthField(snapshot, options.month)\n if (used + count > limit) return { ok: false, used, limit }\n tx.set(ref, { [options.month]: used + count }, { merge: true })\n return {\n ok: true,\n reservation: { orgId: options.orgId, month: options.month, reserved: count },\n used,\n limit,\n }\n })\n}\n\n/**\n * Returns the undelivered part of a reservation.\n *\n * Also a transaction, and also an absolute write from its own read: a refund\n * computed from a stale figure would undo a reservation another campaign took\n * in the meantime, which is the same defect one direction over.\n *\n * **Never throws** and never drives the counter below zero. This runs after\n * mail has already gone out; a bookkeeping failure must not turn a delivered\n * campaign into a 500, exactly like `recordEmailSends` above. The cost of\n * swallowing it is that the org keeps a claim it did not use for the rest of\n * the month, which is the safe direction.\n */\nexport async function reconcileCampaignSendReservation(\n reservation: CampaignSendReservation | null | undefined,\n delivered: number,\n firestore?: any,\n): Promise<void> {\n if (!reservation?.orgId) return\n const sent = Math.max(0, Math.floor(Number(delivered) || 0))\n const refund = Math.max(0, reservation.reserved - sent)\n if (refund <= 0) return\n try {\n const db = firestore ?? firebaseAdmin.app().firestore()\n const ref = db\n .collection('orgs')\n .doc(reservation.orgId)\n .collection('counters')\n .doc(ORG_CAMPAIGN_EMAIL_SENDS_COUNTER)\n await db.runTransaction(async (tx: any) => {\n const snapshot = await tx.get(ref)\n const used = readMonthField(snapshot, reservation.month)\n tx.set(\n ref,\n { [reservation.month]: Math.max(0, used - refund) },\n { merge: true },\n )\n })\n } catch (error) {\n console.error('campaign reservation reconcile failed', error)\n }\n}\n\n/**\n * Volume above the plan's included band, in emails — RE-EXPORTED, not defined\n * here, the same move AGL-2155 made for the bandwidth helpers.\n *\n * The definition moved down to `@aglyn/aglyn/app-utils/plan-entitlements`\n * because the billing page has to render the overage BEFORE it is charged,\n * and that page is a client component which cannot import this module: the\n * Admin SDK comes with it. A second copy of the subtraction on the client is\n * the shape where the readout and the invoice quietly stop agreeing.\n *\n * Re-exported so every server caller keeps importing it from the module it\n * always did — one definition, no drift, no import churn.\n *\n * @see priceEmailSendOverage — what the excess costs, at the plan's rate.\n */\nexport { emailSendsOverage } from '@aglyn/aglyn/app-utils/plan-entitlements'\n\nexport default recordEmailSends\n"],"names":["firebaseAdmin","EMAIL_SENDS_COUNTER","CAMPAIGN_EMAIL_SENDS_COUNTER","PLATFORM_METER_COLLECTION","PLATFORM_METER_DOC","emailMeterRef","firestore","scope","counter","kind","collection","doc","hostId","orgId","currentMeterMonth","now","Date","toISOString","slice","recordEmailSends","options","sendClass","count","Math","floor","Number","isFinite","month","app","increment","FieldValue","counters","Promise","all","map","set","merge","error","console","meterHostEmail","meterOrgEmail","meterPlatformEmail","campaignEmailSendsForMonth","hostRef","snapshot","get","used","ORG_CAMPAIGN_EMAIL_SENDS_COUNTER","readMonthField","exists","orgCampaignEmailSendsForMonth","db","reserveCampaignEmailSends","max","limit","ok","ref","runTransaction","tx","reservation","reserved","reconcileCampaignSendReservation","delivered","sent","refund","emailSendsOverage"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,sBAAkB;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DC,GAED,sDAAsD,GACtD,OAAO,MAAMC,sBAAsB,aAAY;AAE/C,0EAA0E,GAC1E,OAAO,MAAMC,+BAA+B,qBAAoB;AAEhE,6EAA6E,GAC7E,OAAO,MAAMC,4BAA4B,SAAQ;AACjD,OAAO,MAAMC,qBAAqB,WAAU;AAuC5C,8EAA8E,GAC9E,OAAO,SAASC,cACdC,SAAc,EACdC,KAAsB,EACtBC,OAAe;IAEf,IAAID,MAAME,IAAI,KAAK,QAAQ;QACzB,OAAOH,UACJI,UAAU,CAAC,SACXC,GAAG,CAACJ,MAAMK,MAAM,EAChBF,UAAU,CAAC,YACXC,GAAG,CAACH;IACT;IACA,IAAID,MAAME,IAAI,KAAK,OAAO;QACxB,OAAOH,UACJI,UAAU,CAAC,QACXC,GAAG,CAACJ,MAAMM,KAAK,EACfH,UAAU,CAAC,YACXC,GAAG,CAACH;IACT;IACA,OAAOF,UACJI,UAAU,CAACP,2BACXQ,GAAG,CAACP,oBACJM,UAAU,CAAC,YACXC,GAAG,CAACH;AACT;AAEA,4EAA4E,GAC5E,OAAO,SAASM,kBAAkBC,MAAY,IAAIC,MAAM;IACtD,OAAOD,IAAIE,WAAW,GAAGC,KAAK,CAAC,GAAG;AACpC;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,iBACpBC,OAAgC;IAEhC,MAAM,EAAEb,KAAK,EAAEc,SAAS,EAAE,GAAGD;IAC7B,MAAME,QAAQC,KAAKC,KAAK,CAACC,OAAOL,QAAQE,KAAK;IAC7C,2EAA2E;IAC3E,sEAAsE;IACtE,+DAA+D;IAC/D,IAAI,CAACG,OAAOC,QAAQ,CAACJ,UAAUA,SAAS,GAAG;IAC3C,IAAIf,MAAME,IAAI,KAAK,UAAU,CAACF,MAAMK,MAAM,EAAE;IAC5C,IAAIL,MAAME,IAAI,KAAK,SAAS,CAACF,MAAMM,KAAK,EAAE;IAE1C,MAAMc,QAAQP,QAAQO,KAAK,IAAIb;IAC/B,IAAI;YACgBM;QAAlB,MAAMd,aAAYc,qBAAAA,QAAQd,SAAS,YAAjBc,qBAAqBpB,cAAc4B,GAAG,GAAGtB,SAAS;QACpE,MAAMuB,YAAY7B,cAAcM,SAAS,CAACwB,UAAU,CAACD,SAAS,CAACP;QAC/D,MAAMS,WACJV,cAAc,aACV;YAACpB;YAAqBC;SAA6B,GACnD;YAACD;SAAoB;QAC3B,MAAM+B,QAAQC,GAAG,CACfF,SAASG,GAAG,CAAC,CAAC1B,UACZH,cAAcC,WAAWC,OAAOC,SAAS2B,GAAG,CAC1C;gBAAE,CAACR,MAAM,EAAEE;YAAU,GACrB;gBAAEO,OAAO;YAAK;IAItB,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,4BAA4BA;IAC5C;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,eACd3B,MAAc,EACdU,QAAQ,CAAC,EACTD,YAA4B,eAAe;IAE3C,OAAOF,iBAAiB;QACtBZ,OAAO;YAAEE,MAAM;YAAQG;QAAO;QAC9BU;QACAD;IACF;AACF;AAEA,sEAAsE,GACtE,OAAO,SAASmB,cAAc3B,KAAa,EAAES,QAAQ,CAAC;IACpD,OAAOH,iBAAiB;QACtBZ,OAAO;YAAEE,MAAM;YAAOI;QAAM;QAC5BS;QACAD,WAAW;IACb;AACF;AAEA;;;CAGC,GACD,OAAO,SAASoB,mBAAmBnB,QAAQ,CAAC;IAC1C,OAAOH,iBAAiB;QACtBZ,OAAO;YAAEE,MAAM;QAAW;QAC1Ba;QACAD,WAAW;IACb;AACF;AAEA;;;;;;CAMC,GACD,OAAO,eAAeqB,2BACpBC,OAAY,EACZhB,KAAa;QAMOiB;IAJpB,MAAMA,WAAW,MAAMD,QACpBjC,UAAU,CAAC,YACXC,GAAG,CAACT,8BACJ2C,GAAG;IACN,MAAMC,OAAOrB,QAAOmB,gBAAAA,SAASC,GAAG,CAAClB,kBAAbiB,gBAAuB;IAC3C,yEAAyE;IACzE,oCAAoC;IACpC,OAAOnB,OAAOC,QAAQ,CAACoB,SAASA,OAAO,IAAIA,OAAO;AACpD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMC,mCAAmC7C,6BAA4B;AAc5E,yEAAyE,GACzE,SAAS8C,eAAeJ,QAAa,EAAEjB,KAAa;QAC7BiB;IAArB,MAAME,OAAOrB,QAAQmB,OAAAA,CAAAA,4BAAAA,SAAUK,MAAM,IAAGL,SAASC,GAAG,CAAClB,SAAS,aAAzCiB,OAA+C;IACpE,OAAOnB,OAAOC,QAAQ,CAACoB,SAASA,OAAO,IAAIvB,KAAKC,KAAK,CAACsB,QAAQ;AAChE;AAEA;;;;;;CAMC,GACD,OAAO,eAAeI,8BACpBrC,KAAa,EACbc,KAAa,EACbrB,SAAe;IAEf,IAAI,CAACO,OAAO,OAAO;IACnB,MAAMsC,KAAK7C,oBAAAA,YAAaN,cAAc4B,GAAG,GAAGtB,SAAS;IACrD,MAAMsC,WAAW,MAAMO,GACpBzC,UAAU,CAAC,QACXC,GAAG,CAACE,OACJH,UAAU,CAAC,YACXC,GAAG,CAACoC,kCACJF,GAAG;IACN,OAAOG,eAAeJ,UAAUjB;AAClC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GACD,OAAO,eAAeyB,0BAA0BhC,OAM/C;QAUYA;IATX,MAAME,QAAQC,KAAK8B,GAAG,CAAC,GAAG9B,KAAKC,KAAK,CAACC,OAAOL,QAAQE,KAAK,KAAK;IAC9D,MAAMgC,QAAQ7B,OAAOL,QAAQkC,KAAK;IAClC,4EAA4E;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,8BAA8B;IAC9B,IAAI,CAAClC,QAAQP,KAAK,EAAE;QAClB,OAAO;YAAE0C,IAAI;YAAOT,MAAM;YAAGQ,OAAO7B,OAAOC,QAAQ,CAAC4B,SAASA,QAAQ;QAAE;IACzE;IACA,MAAMH,MAAK/B,qBAAAA,QAAQd,SAAS,YAAjBc,qBAAqBpB,cAAc4B,GAAG,GAAGtB,SAAS;IAC7D,MAAMkD,MAAML,GACTzC,UAAU,CAAC,QACXC,GAAG,CAACS,QAAQP,KAAK,EACjBH,UAAU,CAAC,YACXC,GAAG,CAACoC;IAEP,OAAOI,GAAGM,cAAc,CAAC,OAAOC;QAC9B,MAAMd,WAAW,MAAMc,GAAGb,GAAG,CAACW;QAC9B,MAAMV,OAAOE,eAAeJ,UAAUxB,QAAQO,KAAK;QACnD,IAAImB,OAAOxB,QAAQgC,OAAO,OAAO;YAAEC,IAAI;YAAOT;YAAMQ;QAAM;QAC1DI,GAAGvB,GAAG,CAACqB,KAAK;YAAE,CAACpC,QAAQO,KAAK,CAAC,EAAEmB,OAAOxB;QAAM,GAAG;YAAEc,OAAO;QAAK;QAC7D,OAAO;YACLmB,IAAI;YACJI,aAAa;gBAAE9C,OAAOO,QAAQP,KAAK;gBAAEc,OAAOP,QAAQO,KAAK;gBAAEiC,UAAUtC;YAAM;YAC3EwB;YACAQ;QACF;IACF;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeO,iCACpBF,WAAuD,EACvDG,SAAiB,EACjBxD,SAAe;IAEf,IAAI,EAACqD,+BAAAA,YAAa9C,KAAK,GAAE;IACzB,MAAMkD,OAAOxC,KAAK8B,GAAG,CAAC,GAAG9B,KAAKC,KAAK,CAACC,OAAOqC,cAAc;IACzD,MAAME,SAASzC,KAAK8B,GAAG,CAAC,GAAGM,YAAYC,QAAQ,GAAGG;IAClD,IAAIC,UAAU,GAAG;IACjB,IAAI;QACF,MAAMb,KAAK7C,oBAAAA,YAAaN,cAAc4B,GAAG,GAAGtB,SAAS;QACrD,MAAMkD,MAAML,GACTzC,UAAU,CAAC,QACXC,GAAG,CAACgD,YAAY9C,KAAK,EACrBH,UAAU,CAAC,YACXC,GAAG,CAACoC;QACP,MAAMI,GAAGM,cAAc,CAAC,OAAOC;YAC7B,MAAMd,WAAW,MAAMc,GAAGb,GAAG,CAACW;YAC9B,MAAMV,OAAOE,eAAeJ,UAAUe,YAAYhC,KAAK;YACvD+B,GAAGvB,GAAG,CACJqB,KACA;gBAAE,CAACG,YAAYhC,KAAK,CAAC,EAAEJ,KAAK8B,GAAG,CAAC,GAAGP,OAAOkB;YAAQ,GAClD;gBAAE5B,OAAO;YAAK;QAElB;IACF,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,yCAAyCA;IACzD;AACF;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAAS4B,iBAAiB,QAAQ,2CAA0C;AAE5E,eAAe9C,iBAAgB"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { EMAIL_ATTRIBUTION_WINDOW_MS } from '@aglyn/shared-util-email';
18
+ /**
19
+ * THE COMMERCE↔EMAIL JOIN — an order, credited to the campaign that led to it.
20
+ *
21
+ * ## Why this is a join
22
+ *
23
+ * Every compared ESP attributes revenue through an integration into a store
24
+ * it does not own: a catalog sync, an on-site tracking snippet, an identity
25
+ * graph, and a window wide enough to absorb what the reconciliation misses.
26
+ * Commerce here is first-party and runs on the merchant's own Stripe Connect
27
+ * account, so the click and the order are two rows in one database keyed the
28
+ * same way. The window is not a fudge factor here — it is the model, and
29
+ * nothing else about the number is estimated.
30
+ *
31
+ * ## Three writes, and what each one is for
32
+ *
33
+ * - **The touch**, `emailDeliveries/{personKey}.campaignTouches[hostId]`,
34
+ * written by the delivery webhook on a click. Owned by
35
+ * `email-delivery-log.ts`, because it lives on the person's document and
36
+ * the erasure path has to be able to remove it.
37
+ * - **The attribution record**, `hosts/{hostId}/emailAttributions/{orderId}`,
38
+ * written here when an order is credited. It is the audit trail — which
39
+ * campaign, which click, which model, which window — and it is what the
40
+ * refund path reads to find out which campaign to take the money back off.
41
+ * - **The rollup**, `campaigns/{campaignId}/reports/revenue`, incremented
42
+ * here. One document per campaign, so the report reads it whole.
43
+ *
44
+ * ## Why the record is a document and not a field on the order
45
+ *
46
+ * Three reasons, and the first is decisive. `create()` fails when the
47
+ * document already exists, which is exact idempotency for free: webhook
48
+ * delivery is at-least-once, and a redelivered purchase must not be able to
49
+ * credit a campaign twice. Second, not every purchase door writes an order —
50
+ * a booking is a sale with no order document — and a field would have had to
51
+ * either skip those or conjure the document, and conjuring is the
52
+ * phantom-document shape this codebase spent a sweep removing. Third, the
53
+ * order document is read by the console list, the fulfilment path, the
54
+ * supplier outbox and the CSV export, and none of them wants a field about
55
+ * email.
56
+ *
57
+ * ## Never throws
58
+ *
59
+ * Same contract as `upsertHostContact`, which calls it, and for the same
60
+ * reason: the money has already moved and the order already records it, so
61
+ * nothing here may fail a sale. A lost attribution understates a campaign; a
62
+ * thrown one loses a checkout.
63
+ */
64
+ /** The per-host collection of attribution records. */
65
+ export declare const EMAIL_ATTRIBUTIONS_COLLECTION = "emailAttributions";
66
+ /** The single rollup document under a campaign. */
67
+ export declare const CAMPAIGN_REVENUE_REPORT_DOC = "revenue";
68
+ /**
69
+ * The currency an amount is recorded under when the caller does not say.
70
+ *
71
+ * Every checkout door in this repo — cart, buy-now, POS, draft orders,
72
+ * reservations, subscriptions — writes `currency: 'usd'` onto the Stripe line
73
+ * items, and no order document carries a currency field to read back. So this
74
+ * is a statement about what the code charges rather than a guess about the
75
+ * money. The parameter exists so a door that ever charges in something else
76
+ * says so and lands in its own bucket, because the one thing the report may
77
+ * never do is add two currencies together.
78
+ */
79
+ export declare const DEFAULT_ATTRIBUTION_CURRENCY = "usd";
80
+ /** What one attribution record holds. */
81
+ export interface EmailAttributionRecord {
82
+ campaignId: string;
83
+ /** When the credited click happened. */
84
+ clickedAtMs: number;
85
+ /** When the order was placed. */
86
+ orderedAtMs: number;
87
+ /** Minor units credited, gross — the amount the buyer was charged. */
88
+ amountCents: number;
89
+ /** Lowercase currency code the amount is in. */
90
+ currency: string;
91
+ /** The model this credit was decided under. */
92
+ model: string;
93
+ /** The window, in days, it was decided inside. */
94
+ windowDays: number;
95
+ }
96
+ /**
97
+ * Credits one order to the campaign whose link the buyer last clicked.
98
+ *
99
+ * ## What happens when there is nobody to credit
100
+ *
101
+ * Each case answers `null` and writes NOTHING, which is the whole design:
102
+ *
103
+ * - **No email on the order.** A guest checkout that never identified its
104
+ * buyer cannot be joined to anybody's clicks. There is no fallback and
105
+ * there deliberately is not one — the alternatives are an IP or a device
106
+ * guess, which is the probabilistic attribution owning the checkout exists
107
+ * to avoid.
108
+ * - **A guest with an email but no contact record.** This is ATTRIBUTED
109
+ * normally. The join keys on the address hash, exactly as the touch and
110
+ * the suppression list do, so it never asks whether a contact document
111
+ * exists — which matters, because contact creation is audience-band gated
112
+ * and a Free org's dropped contact would otherwise silently drop the
113
+ * revenue with it.
114
+ * - **No touch, or a touch on another site.** Nobody clicked, so nobody is
115
+ * credited. The touch map is keyed by host and the send path refuses
116
+ * cross-site reach; the revenue join agrees with it.
117
+ * - **A touch outside the window**, in either direction. See
118
+ * {@link emailTouchIsInWindow} — a click AFTER the order is the receipt,
119
+ * not the cause.
120
+ *
121
+ * None of these is counted anywhere. A miss costs no write, which is what
122
+ * keeps the ordinary order — placed by somebody who is not on the mailing
123
+ * list at all — at exactly one document read. What it means for the report is
124
+ * stated on the report: the figure counts orders it could join, and is a
125
+ * floor.
126
+ *
127
+ * @returns the record written, or `null` when nothing was credited.
128
+ */
129
+ export declare function attributeOrderToEmail(options: {
130
+ hostId: string;
131
+ /** The order, booking or invoice the money came in on. */
132
+ orderId: string;
133
+ /** The buyer as the sale recorded them, raw — normalized downstream. */
134
+ email: unknown;
135
+ /** Gross minor units the buyer was charged. */
136
+ amountCents: number;
137
+ /** Lowercase currency code, when the door knows one. */
138
+ currency?: string;
139
+ /** When the order was placed. Defaults to now. */
140
+ orderedAtMs?: number;
141
+ }, firestore?: any): Promise<EmailAttributionRecord | null>;
142
+ /**
143
+ * Takes back revenue a campaign was credited with, when it is refunded.
144
+ *
145
+ * ## Recorded beside the gross, never subtracted from it
146
+ *
147
+ * `grossCents` is left exactly as it is and the reversal lands in
148
+ * `refundedCents` next to it. This is the shape `contact-refund.ts` chose for
149
+ * `ltvCents`/`refundedCents` and the orders CSV chose for
150
+ * `amountUsd`/`refundedUsd`, and it is chosen a third time here so all three
151
+ * answer "what did this earn, net" identically. Decrementing would make a
152
+ * stored number mean one thing for rollups written before a refund and
153
+ * another after, with nothing on the document to tell them apart — and would
154
+ * put the question "can it go negative" onto storage, where the answer cannot
155
+ * be clamped without destroying evidence. Both stored figures are monotonic
156
+ * counters of money that really moved in one direction; the derived net is
157
+ * clamped at the point of display and nowhere else.
158
+ *
159
+ * ## The currency comes from the RECORD, not from the caller
160
+ *
161
+ * A refund reverses a specific sale, and the bucket it comes out of has to be
162
+ * the bucket it went into. Reading the currency back off the attribution
163
+ * record is what guarantees that, and it is the second thing the record is
164
+ * for.
165
+ *
166
+ * ## Chargebacks come through here too
167
+ *
168
+ * Money reversed is money reversed, whichever door it left by, so a lost
169
+ * dispute reverses the credit the same way a refund does. `kind` exists for
170
+ * the caller's clarity and changes nothing about the arithmetic — the same
171
+ * choice `recordContactRefund` made one field along.
172
+ *
173
+ * @returns whether a reversal was recorded.
174
+ */
175
+ export declare function reverseEmailAttributedRevenue(options: {
176
+ hostId: string;
177
+ orderId: string;
178
+ /** Minor units reversed by THIS attempt, never the order total. */
179
+ amountCents: number;
180
+ /** True only for the write that moved the order into `refunded`. */
181
+ closedTheOrder: boolean;
182
+ kind?: 'refund' | 'chargeback';
183
+ }, firestore?: any): Promise<boolean>;
184
+ /** The window, re-exported so a caller needs one import for the whole join. */
185
+ export { EMAIL_ATTRIBUTION_WINDOW_MS };