@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,171 @@
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 { resolveOrgEntitlements, type OrgPlan, type OrgSeatAddons } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Self-serve add-on price map (AGL-525): the bridge between
20
+ * `STRIPE_PRICE_*` env price ids, `org.seatAddons` keys, and
21
+ * `PLAN_PRICING` USD amounts. Add-ons bill as extra items on the org's
22
+ * one subscription, so every kind has a monthly and a yearly price
23
+ * (Stripe allows a single interval per subscription; annual orgs attach
24
+ * the `_YEARLY` variants). Seat/member/dataset/host prices and the Aglyn AI
25
+ * add-on (AGL-2896) are per-plan; POS registers and the Event Calendar are
26
+ * flat. Prices are minted by `tools/scripts/setup-stripe.mjs` — keep the
27
+ * three in sync.
28
+ */
29
+ export type AddonKind = keyof Required<OrgSeatAddons>;
30
+ export declare const ADDON_KINDS: readonly AddonKind[];
31
+ export type BillingInterval = 'month' | 'year';
32
+ /**
33
+ * Every plan that is sold with a Stripe price — DERIVED, never hand-listed
34
+ * (AGL-1340).
35
+ *
36
+ * The literal that used to sit here listed starter, pro, business and
37
+ * advanced only — it predated the Scale and Agency tiers, so
38
+ * `addonKindFromPriceId()` could not recognise a single Scale/Agency add-on
39
+ * price coming back from Stripe. `addonPriceId()` still SOLD them (it only
40
+ * uppercases the plan), so the money moved and the read-back returned
41
+ * nothing: the webhook wrote `seatAddons: {all zeros}` for the
42
+ * highest-paying orgs on the next subscription event, and `seatAddons` is an
43
+ * ENTITLEMENT INPUT (`plan-entitlements.ts` — it raises `hostLimit` and
44
+ * `posRegisters`, and flips the `eventCalendar` feature).
45
+ *
46
+ * Deriving from `SELF_SERVE_PLANS` is what stops the next tier from
47
+ * reintroducing it: `free` has nothing to sell and `enterprise` is quoted
48
+ * per deal (no `STRIPE_PRICE_ENTERPRISE_*` exists), so everything else is
49
+ * by definition a paid, priced plan.
50
+ */
51
+ export declare const PAID_PLANS: readonly OrgPlan[];
52
+ /**
53
+ * The Stripe price id for an add-on kind on a plan at a billing interval;
54
+ * null when the plan doesn't sell it (free) or the env isn't configured.
55
+ */
56
+ export declare function addonPriceId(kind: AddonKind, plan: OrgPlan, interval: BillingInterval): string | null;
57
+ /**
58
+ * Reverse map: which add-on kind a subscription item's price id sells.
59
+ * Null for non-add-on prices (the base plan item, unknown prices).
60
+ */
61
+ export declare function addonKindFromPriceId(priceId: string | null | undefined): AddonKind | null;
62
+ /**
63
+ * The BASE plan's Stripe price id at an interval
64
+ * (`STRIPE_PRICE_{PLAN}[_YEARLY]`); null for `free`, for the custom-priced
65
+ * `enterprise`, and for an env that isn't configured. Read at call time, so
66
+ * it never freezes a stale value at module load the way the routes'
67
+ * `PRICE_ENV` maps do.
68
+ */
69
+ export declare function planPriceId(plan: OrgPlan, interval: BillingInterval): string | null;
70
+ /**
71
+ * Which plan a subscription item's price sells AND on which interval; null for
72
+ * an add-on price, the metered price, an ad-hoc enterprise price, and anything
73
+ * unknown.
74
+ *
75
+ * The interval half exists because a schedule phase read back from Stripe
76
+ * carries price IDS and nothing else (AGL-2150): rebuilding that phase's item
77
+ * list needs to know which interval it is priced on, and guessing it from the
78
+ * live subscription is wrong exactly when the pending change is also an
79
+ * interval change.
80
+ */
81
+ export declare function planAndIntervalFromPriceId(priceId: string | null | undefined): {
82
+ plan: OrgPlan;
83
+ interval: BillingInterval;
84
+ } | null;
85
+ /**
86
+ * Which plan a subscription item's price sells; null for an add-on price,
87
+ * the metered price, an ad-hoc enterprise price, and anything unknown.
88
+ */
89
+ export declare function planFromPriceId(priceId: string | null | undefined): OrgPlan | null;
90
+ /**
91
+ * The shared metered-usage price (AGL-635) that makes reported usage
92
+ * billable, for a given billing interval.
93
+ *
94
+ * Stripe rejects a subscription whose items mix `recurring.interval`, so the
95
+ * metered item has to recur the same way the plan does. There are two prices
96
+ * — `aglyn_metered_usage` (monthly) and `aglyn_metered_usage_yearly` — and
97
+ * both bill $0.01 per aggregated unit against the SAME meter, because the
98
+ * rollup posts an already-computed value in CENTS. The interval is therefore
99
+ * the only thing that differs, and it is the only thing a caller must get
100
+ * right.
101
+ *
102
+ * `interval` is REQUIRED on purpose (AGL-1280). It used to be monthly-only
103
+ * with no argument, so the failure mode of forgetting it was attaching a
104
+ * monthly price to a yearly subscription — the exact mixed-interval crash
105
+ * this signature now makes unrepresentable. A default would hand that
106
+ * footgun straight back.
107
+ */
108
+ export declare function meteredPriceId(interval: 'month' | 'year'): string | null;
109
+ /**
110
+ * Whether a price id is EITHER shared metered-usage price.
111
+ *
112
+ * Interval-agnostic by design: callers ask this to exclude the metered item
113
+ * from plan/add-on reasoning, and an annual subscription's metered item must
114
+ * be excluded just as thoroughly as a monthly one.
115
+ */
116
+ export declare function isMeteredPriceId(priceId: string | null | undefined): boolean;
117
+ /**
118
+ * The subscription's BASE PLAN item, identified POSITIVELY (AGL-1340).
119
+ *
120
+ * This used to be "whatever no add-on price claims" — identification by
121
+ * elimination, which quietly returns the wrong item the moment a
122
+ * subscription carries anything the add-on map doesn't recognise: the
123
+ * metered item, a grandfathered v1 price, a coupon-era SKU, an item added
124
+ * by hand in the Stripe dashboard. Picking an add-on item as the plan item
125
+ * means a plan switch re-prices the ADD-ON, and reads the billing interval
126
+ * off it.
127
+ *
128
+ * So: match a known `STRIPE_PRICE_{PLAN}[_YEARLY]` first. Only when nothing
129
+ * matches — an enterprise deal bills on an ad-hoc price, and grandfathered
130
+ * subscriptions predate the current SKUs — fall back to the old rule,
131
+ * minus the metered item, and finally to `items[0]` so behaviour for those
132
+ * subscriptions is unchanged.
133
+ */
134
+ export declare function findPlanItem<T extends {
135
+ price?: {
136
+ id?: string;
137
+ } | null;
138
+ }>(items: readonly T[] | null | undefined): T | null;
139
+ /**
140
+ * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of
141
+ * this); null when the plan doesn't sell the kind. Single pricing source
142
+ * of truth: `PLAN_PRICING` + the two flat add-on constants.
143
+ */
144
+ export declare function addonUnitUsd(kind: AddonKind, plan: OrgPlan): number | null;
145
+ /**
146
+ * Full add-on quantity map (explicit zeros for absent kinds) from a
147
+ * subscription's items — the shape written to `org.seatAddons` so
148
+ * removals and dashboard edits converge on sync (AGL-527).
149
+ */
150
+ export declare function addonQuantitiesFromItems(items: Array<{
151
+ price?: {
152
+ id?: string;
153
+ } | null;
154
+ quantity?: number | null;
155
+ }> | null | undefined): Record<AddonKind, number>;
156
+ /**
157
+ * Max purchasable quantity per kind, from a purchases-free entitlement
158
+ * resolution (plan defaults + staff overrides only) so the ceiling does not
159
+ * drift as the org buys: seat/dataset kinds stop at the plan's hard max,
160
+ * hosts/registers use flat ceilings, the Event Calendar and Aglyn AI are 0/1
161
+ * toggles. POS registers additionally require the `pos` feature.
162
+ *
163
+ * Lives here rather than inside `/api/billing/addons` because a PLAN CHANGE
164
+ * needs the same ceiling for a different plan. The addons route asks "how many
165
+ * may this org buy on the plan it is on"; `buildTargetItems` asks "how many of
166
+ * what it already bought can the plan it is moving to actually deliver". Two
167
+ * readers, one definition — a second copy is how they answer differently.
168
+ */
169
+ export declare function addonMaxForBaseline(kind: AddonKind, baseline: ReturnType<typeof resolveOrgEntitlements>): number;
170
+ /** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */
171
+ export declare function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number;
@@ -0,0 +1,277 @@
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 { EVENT_CALENDAR_ADDON_MONTHLY_USD, EXTRA_HOSTS_ADDON_MAX, PLAN_PRICING, POS_REGISTER_ADDON_MONTHLY_USD, POS_REGISTERS_ADDON_MAX, resolveOrgEntitlements, SELF_SERVE_PLANS, UNLIMITED } from "@aglyn/aglyn/server";
17
+ export const ADDON_KINDS = [
18
+ 'managers',
19
+ 'members',
20
+ 'datasets',
21
+ 'hosts',
22
+ 'posRegisters',
23
+ 'eventCalendar',
24
+ 'aiAddon'
25
+ ];
26
+ /** Kinds priced per plan (env `STRIPE_PRICE_{PLAN}_{SUFFIX}[_YEARLY]`). */ const PER_PLAN_ENV_SUFFIX = {
27
+ managers: 'EXTRA_SEAT',
28
+ members: 'EXTRA_MEMBER',
29
+ datasets: 'EXTRA_DATASET',
30
+ hosts: 'EXTRA_HOST',
31
+ aiAddon: 'AI_ADDON'
32
+ };
33
+ /** Kinds priced flat across plans (env `STRIPE_PRICE_{SUFFIX}[_YEARLY]`). */ const FLAT_ENV_NAME = {
34
+ posRegisters: 'POS_REGISTER',
35
+ eventCalendar: 'EVENT_CALENDAR'
36
+ };
37
+ /**
38
+ * Every plan that is sold with a Stripe price — DERIVED, never hand-listed
39
+ * (AGL-1340).
40
+ *
41
+ * The literal that used to sit here listed starter, pro, business and
42
+ * advanced only — it predated the Scale and Agency tiers, so
43
+ * `addonKindFromPriceId()` could not recognise a single Scale/Agency add-on
44
+ * price coming back from Stripe. `addonPriceId()` still SOLD them (it only
45
+ * uppercases the plan), so the money moved and the read-back returned
46
+ * nothing: the webhook wrote `seatAddons: {all zeros}` for the
47
+ * highest-paying orgs on the next subscription event, and `seatAddons` is an
48
+ * ENTITLEMENT INPUT (`plan-entitlements.ts` — it raises `hostLimit` and
49
+ * `posRegisters`, and flips the `eventCalendar` feature).
50
+ *
51
+ * Deriving from `SELF_SERVE_PLANS` is what stops the next tier from
52
+ * reintroducing it: `free` has nothing to sell and `enterprise` is quoted
53
+ * per deal (no `STRIPE_PRICE_ENTERPRISE_*` exists), so everything else is
54
+ * by definition a paid, priced plan.
55
+ */ export const PAID_PLANS = SELF_SERVE_PLANS.filter((plan)=>plan !== 'free');
56
+ function env(name) {
57
+ const value = process.env[name];
58
+ return value ? value : null;
59
+ }
60
+ /**
61
+ * The Stripe price id for an add-on kind on a plan at a billing interval;
62
+ * null when the plan doesn't sell it (free) or the env isn't configured.
63
+ */ export function addonPriceId(kind, plan, interval) {
64
+ const yearly = interval === 'year' ? '_YEARLY' : '';
65
+ const flat = FLAT_ENV_NAME[kind];
66
+ if (flat) return env(`STRIPE_PRICE_${flat}${yearly}`);
67
+ const suffix = PER_PLAN_ENV_SUFFIX[kind];
68
+ if (!suffix || plan === 'free') return null;
69
+ return env(`STRIPE_PRICE_${plan.toUpperCase()}_${suffix}${yearly}`);
70
+ }
71
+ /**
72
+ * Reverse map: which add-on kind a subscription item's price id sells.
73
+ * Null for non-add-on prices (the base plan item, unknown prices).
74
+ */ export function addonKindFromPriceId(priceId) {
75
+ if (!priceId) return null;
76
+ for (const kind of ADDON_KINDS){
77
+ for (const interval of [
78
+ 'month',
79
+ 'year'
80
+ ]){
81
+ if (FLAT_ENV_NAME[kind]) {
82
+ if (addonPriceId(kind, 'starter', interval) === priceId) return kind;
83
+ continue;
84
+ }
85
+ for (const plan of PAID_PLANS){
86
+ if (addonPriceId(kind, plan, interval) === priceId) return kind;
87
+ }
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * The BASE plan's Stripe price id at an interval
94
+ * (`STRIPE_PRICE_{PLAN}[_YEARLY]`); null for `free`, for the custom-priced
95
+ * `enterprise`, and for an env that isn't configured. Read at call time, so
96
+ * it never freezes a stale value at module load the way the routes'
97
+ * `PRICE_ENV` maps do.
98
+ */ export function planPriceId(plan, interval) {
99
+ if (!PAID_PLANS.includes(plan)) return null;
100
+ const yearly = interval === 'year' ? '_YEARLY' : '';
101
+ return env(`STRIPE_PRICE_${plan.toUpperCase()}${yearly}`);
102
+ }
103
+ /**
104
+ * Which plan a subscription item's price sells AND on which interval; null for
105
+ * an add-on price, the metered price, an ad-hoc enterprise price, and anything
106
+ * unknown.
107
+ *
108
+ * The interval half exists because a schedule phase read back from Stripe
109
+ * carries price IDS and nothing else (AGL-2150): rebuilding that phase's item
110
+ * list needs to know which interval it is priced on, and guessing it from the
111
+ * live subscription is wrong exactly when the pending change is also an
112
+ * interval change.
113
+ */ export function planAndIntervalFromPriceId(priceId) {
114
+ if (!priceId) return null;
115
+ for (const plan of PAID_PLANS){
116
+ for (const interval of [
117
+ 'month',
118
+ 'year'
119
+ ]){
120
+ if (planPriceId(plan, interval) === priceId) return {
121
+ plan,
122
+ interval
123
+ };
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ /**
129
+ * Which plan a subscription item's price sells; null for an add-on price,
130
+ * the metered price, an ad-hoc enterprise price, and anything unknown.
131
+ */ export function planFromPriceId(priceId) {
132
+ var _ref;
133
+ var _planAndIntervalFromPriceId;
134
+ return (_ref = (_planAndIntervalFromPriceId = planAndIntervalFromPriceId(priceId)) == null ? void 0 : _planAndIntervalFromPriceId.plan) != null ? _ref : null;
135
+ }
136
+ /**
137
+ * The shared metered-usage price (AGL-635) that makes reported usage
138
+ * billable, for a given billing interval.
139
+ *
140
+ * Stripe rejects a subscription whose items mix `recurring.interval`, so the
141
+ * metered item has to recur the same way the plan does. There are two prices
142
+ * — `aglyn_metered_usage` (monthly) and `aglyn_metered_usage_yearly` — and
143
+ * both bill $0.01 per aggregated unit against the SAME meter, because the
144
+ * rollup posts an already-computed value in CENTS. The interval is therefore
145
+ * the only thing that differs, and it is the only thing a caller must get
146
+ * right.
147
+ *
148
+ * `interval` is REQUIRED on purpose (AGL-1280). It used to be monthly-only
149
+ * with no argument, so the failure mode of forgetting it was attaching a
150
+ * monthly price to a yearly subscription — the exact mixed-interval crash
151
+ * this signature now makes unrepresentable. A default would hand that
152
+ * footgun straight back.
153
+ */ export function meteredPriceId(interval) {
154
+ return env(interval === 'year' ? 'STRIPE_PRICE_METERED_YEARLY' : 'STRIPE_PRICE_METERED');
155
+ }
156
+ /**
157
+ * Whether a price id is EITHER shared metered-usage price.
158
+ *
159
+ * Interval-agnostic by design: callers ask this to exclude the metered item
160
+ * from plan/add-on reasoning, and an annual subscription's metered item must
161
+ * be excluded just as thoroughly as a monthly one.
162
+ */ export function isMeteredPriceId(priceId) {
163
+ if (!priceId) return false;
164
+ return priceId === meteredPriceId('month') || priceId === meteredPriceId('year');
165
+ }
166
+ /**
167
+ * The subscription's BASE PLAN item, identified POSITIVELY (AGL-1340).
168
+ *
169
+ * This used to be "whatever no add-on price claims" — identification by
170
+ * elimination, which quietly returns the wrong item the moment a
171
+ * subscription carries anything the add-on map doesn't recognise: the
172
+ * metered item, a grandfathered v1 price, a coupon-era SKU, an item added
173
+ * by hand in the Stripe dashboard. Picking an add-on item as the plan item
174
+ * means a plan switch re-prices the ADD-ON, and reads the billing interval
175
+ * off it.
176
+ *
177
+ * So: match a known `STRIPE_PRICE_{PLAN}[_YEARLY]` first. Only when nothing
178
+ * matches — an enterprise deal bills on an ad-hoc price, and grandfathered
179
+ * subscriptions predate the current SKUs — fall back to the old rule,
180
+ * minus the metered item, and finally to `items[0]` so behaviour for those
181
+ * subscriptions is unchanged.
182
+ */ export function findPlanItem(items) {
183
+ var _ref;
184
+ const list = items != null ? items : [];
185
+ const byPlanPrice = list.find((item)=>{
186
+ var _item_price;
187
+ return planFromPriceId(item == null ? void 0 : (_item_price = item.price) == null ? void 0 : _item_price.id);
188
+ });
189
+ if (byPlanPrice) return byPlanPrice;
190
+ const notAnAddon = list.find((item)=>{
191
+ var _item_price, _item_price1;
192
+ return !addonKindFromPriceId(item == null ? void 0 : (_item_price = item.price) == null ? void 0 : _item_price.id) && !isMeteredPriceId(item == null ? void 0 : (_item_price1 = item.price) == null ? void 0 : _item_price1.id);
193
+ });
194
+ return (_ref = notAnAddon != null ? notAnAddon : list[0]) != null ? _ref : null;
195
+ }
196
+ /**
197
+ * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of
198
+ * this); null when the plan doesn't sell the kind. Single pricing source
199
+ * of truth: `PLAN_PRICING` + the two flat add-on constants.
200
+ */ export function addonUnitUsd(kind, plan) {
201
+ const pricing = PLAN_PRICING[plan];
202
+ switch(kind){
203
+ case 'managers':
204
+ return pricing.extraSeatMonthlyUsd;
205
+ case 'members':
206
+ return pricing.extraCollaboratorMonthlyUsd;
207
+ case 'datasets':
208
+ return pricing.extraDatasetMonthlyUsd;
209
+ case 'hosts':
210
+ return pricing.extraHostMonthlyUsd;
211
+ case 'posRegisters':
212
+ return POS_REGISTER_ADDON_MONTHLY_USD;
213
+ case 'eventCalendar':
214
+ return EVENT_CALENDAR_ADDON_MONTHLY_USD;
215
+ case 'aiAddon':
216
+ return pricing.aiAddonMonthlyUsd;
217
+ }
218
+ }
219
+ /**
220
+ * Full add-on quantity map (explicit zeros for absent kinds) from a
221
+ * subscription's items — the shape written to `org.seatAddons` so
222
+ * removals and dashboard edits converge on sync (AGL-527).
223
+ */ export function addonQuantitiesFromItems(items) {
224
+ const quantities = Object.fromEntries(ADDON_KINDS.map((kind)=>[
225
+ kind,
226
+ 0
227
+ ]));
228
+ for (const item of items != null ? items : []){
229
+ var _item_price;
230
+ const kind = addonKindFromPriceId(item == null ? void 0 : (_item_price = item.price) == null ? void 0 : _item_price.id);
231
+ if (kind) {
232
+ var _ref;
233
+ quantities[kind] += Math.max(0, Math.floor(Number((_ref = item == null ? void 0 : item.quantity) != null ? _ref : 0)));
234
+ }
235
+ }
236
+ return quantities;
237
+ }
238
+ /**
239
+ * Max purchasable quantity per kind, from a purchases-free entitlement
240
+ * resolution (plan defaults + staff overrides only) so the ceiling does not
241
+ * drift as the org buys: seat/dataset kinds stop at the plan's hard max,
242
+ * hosts/registers use flat ceilings, the Event Calendar and Aglyn AI are 0/1
243
+ * toggles. POS registers additionally require the `pos` feature.
244
+ *
245
+ * Lives here rather than inside `/api/billing/addons` because a PLAN CHANGE
246
+ * needs the same ceiling for a different plan. The addons route asks "how many
247
+ * may this org buy on the plan it is on"; `buildTargetItems` asks "how many of
248
+ * what it already bought can the plan it is moving to actually deliver". Two
249
+ * readers, one definition — a second copy is how they answer differently.
250
+ */ export function addonMaxForBaseline(kind, baseline) {
251
+ const bounded = (included, max)=>Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX;
252
+ switch(kind){
253
+ case 'managers':
254
+ return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg);
255
+ case 'members':
256
+ return bounded(baseline.membersPerHost, baseline.maxMembersPerHost);
257
+ case 'datasets':
258
+ return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg);
259
+ case 'hosts':
260
+ return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX;
261
+ case 'posRegisters':
262
+ return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0;
263
+ case 'eventCalendar':
264
+ return 1;
265
+ case 'aiAddon':
266
+ // Org-wide like the Event Calendar: one purchase, and the price map
267
+ // (null on Free and Enterprise) says whether the plan sells it at all.
268
+ return 1;
269
+ }
270
+ }
271
+ /** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */ export function addonMaxForPlan(kind, plan) {
272
+ return addonMaxForBaseline(kind, resolveOrgEntitlements({
273
+ plan
274
+ }));
275
+ }
276
+
277
+ //# sourceMappingURL=billing-addons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/billing-addons.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 {\n EVENT_CALENDAR_ADDON_MONTHLY_USD,\n EXTRA_HOSTS_ADDON_MAX,\n PLAN_PRICING,\n POS_REGISTER_ADDON_MONTHLY_USD,\n POS_REGISTERS_ADDON_MAX,\n resolveOrgEntitlements,\n SELF_SERVE_PLANS,\n UNLIMITED,\n type OrgPlan,\n type OrgSeatAddons,\n} from '@aglyn/aglyn/server'\n\n/**\n * Self-serve add-on price map (AGL-525): the bridge between\n * `STRIPE_PRICE_*` env price ids, `org.seatAddons` keys, and\n * `PLAN_PRICING` USD amounts. Add-ons bill as extra items on the org's\n * one subscription, so every kind has a monthly and a yearly price\n * (Stripe allows a single interval per subscription; annual orgs attach\n * the `_YEARLY` variants). Seat/member/dataset/host prices and the Aglyn AI\n * add-on (AGL-2896) are per-plan; POS registers and the Event Calendar are\n * flat. Prices are minted by `tools/scripts/setup-stripe.mjs` — keep the\n * three in sync.\n */\nexport type AddonKind = keyof Required<OrgSeatAddons>\n\nexport const ADDON_KINDS: readonly AddonKind[] = [\n 'managers',\n 'members',\n 'datasets',\n 'hosts',\n 'posRegisters',\n 'eventCalendar',\n 'aiAddon',\n]\n\nexport type BillingInterval = 'month' | 'year'\n\n/** Kinds priced per plan (env `STRIPE_PRICE_{PLAN}_{SUFFIX}[_YEARLY]`). */\nconst PER_PLAN_ENV_SUFFIX: Partial<Record<AddonKind, string>> = {\n managers: 'EXTRA_SEAT',\n members: 'EXTRA_MEMBER',\n datasets: 'EXTRA_DATASET',\n hosts: 'EXTRA_HOST',\n aiAddon: 'AI_ADDON',\n}\n\n/** Kinds priced flat across plans (env `STRIPE_PRICE_{SUFFIX}[_YEARLY]`). */\nconst FLAT_ENV_NAME: Partial<Record<AddonKind, string>> = {\n posRegisters: 'POS_REGISTER',\n eventCalendar: 'EVENT_CALENDAR',\n}\n\n/**\n * Every plan that is sold with a Stripe price — DERIVED, never hand-listed\n * (AGL-1340).\n *\n * The literal that used to sit here listed starter, pro, business and\n * advanced only — it predated the Scale and Agency tiers, so\n * `addonKindFromPriceId()` could not recognise a single Scale/Agency add-on\n * price coming back from Stripe. `addonPriceId()` still SOLD them (it only\n * uppercases the plan), so the money moved and the read-back returned\n * nothing: the webhook wrote `seatAddons: {all zeros}` for the\n * highest-paying orgs on the next subscription event, and `seatAddons` is an\n * ENTITLEMENT INPUT (`plan-entitlements.ts` — it raises `hostLimit` and\n * `posRegisters`, and flips the `eventCalendar` feature).\n *\n * Deriving from `SELF_SERVE_PLANS` is what stops the next tier from\n * reintroducing it: `free` has nothing to sell and `enterprise` is quoted\n * per deal (no `STRIPE_PRICE_ENTERPRISE_*` exists), so everything else is\n * by definition a paid, priced plan.\n */\nexport const PAID_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter(\n (plan) => plan !== 'free',\n)\n\nfunction env(name: string): string | null {\n const value = process.env[name]\n return value ? value : null\n}\n\n/**\n * The Stripe price id for an add-on kind on a plan at a billing interval;\n * null when the plan doesn't sell it (free) or the env isn't configured.\n */\nexport function addonPriceId(\n kind: AddonKind,\n plan: OrgPlan,\n interval: BillingInterval,\n): string | null {\n const yearly = interval === 'year' ? '_YEARLY' : ''\n const flat = FLAT_ENV_NAME[kind]\n if (flat) return env(`STRIPE_PRICE_${flat}${yearly}`)\n const suffix = PER_PLAN_ENV_SUFFIX[kind]\n if (!suffix || plan === 'free') return null\n return env(`STRIPE_PRICE_${plan.toUpperCase()}_${suffix}${yearly}`)\n}\n\n/**\n * Reverse map: which add-on kind a subscription item's price id sells.\n * Null for non-add-on prices (the base plan item, unknown prices).\n */\nexport function addonKindFromPriceId(\n priceId: string | null | undefined,\n): AddonKind | null {\n if (!priceId) return null\n for (const kind of ADDON_KINDS) {\n for (const interval of ['month', 'year'] as const) {\n if (FLAT_ENV_NAME[kind]) {\n if (addonPriceId(kind, 'starter', interval) === priceId) return kind\n continue\n }\n for (const plan of PAID_PLANS) {\n if (addonPriceId(kind, plan, interval) === priceId) return kind\n }\n }\n }\n return null\n}\n\n/**\n * The BASE plan's Stripe price id at an interval\n * (`STRIPE_PRICE_{PLAN}[_YEARLY]`); null for `free`, for the custom-priced\n * `enterprise`, and for an env that isn't configured. Read at call time, so\n * it never freezes a stale value at module load the way the routes'\n * `PRICE_ENV` maps do.\n */\nexport function planPriceId(\n plan: OrgPlan,\n interval: BillingInterval,\n): string | null {\n if (!PAID_PLANS.includes(plan)) return null\n const yearly = interval === 'year' ? '_YEARLY' : ''\n return env(`STRIPE_PRICE_${plan.toUpperCase()}${yearly}`)\n}\n\n/**\n * Which plan a subscription item's price sells AND on which interval; null for\n * an add-on price, the metered price, an ad-hoc enterprise price, and anything\n * unknown.\n *\n * The interval half exists because a schedule phase read back from Stripe\n * carries price IDS and nothing else (AGL-2150): rebuilding that phase's item\n * list needs to know which interval it is priced on, and guessing it from the\n * live subscription is wrong exactly when the pending change is also an\n * interval change.\n */\nexport function planAndIntervalFromPriceId(\n priceId: string | null | undefined,\n): { plan: OrgPlan; interval: BillingInterval } | null {\n if (!priceId) return null\n for (const plan of PAID_PLANS) {\n for (const interval of ['month', 'year'] as const) {\n if (planPriceId(plan, interval) === priceId) return { plan, interval }\n }\n }\n return null\n}\n\n/**\n * Which plan a subscription item's price sells; null for an add-on price,\n * the metered price, an ad-hoc enterprise price, and anything unknown.\n */\nexport function planFromPriceId(\n priceId: string | null | undefined,\n): OrgPlan | null {\n return planAndIntervalFromPriceId(priceId)?.plan ?? null\n}\n\n/**\n * The shared metered-usage price (AGL-635) that makes reported usage\n * billable, for a given billing interval.\n *\n * Stripe rejects a subscription whose items mix `recurring.interval`, so the\n * metered item has to recur the same way the plan does. There are two prices\n * — `aglyn_metered_usage` (monthly) and `aglyn_metered_usage_yearly` — and\n * both bill $0.01 per aggregated unit against the SAME meter, because the\n * rollup posts an already-computed value in CENTS. The interval is therefore\n * the only thing that differs, and it is the only thing a caller must get\n * right.\n *\n * `interval` is REQUIRED on purpose (AGL-1280). It used to be monthly-only\n * with no argument, so the failure mode of forgetting it was attaching a\n * monthly price to a yearly subscription — the exact mixed-interval crash\n * this signature now makes unrepresentable. A default would hand that\n * footgun straight back.\n */\nexport function meteredPriceId(interval: 'month' | 'year'): string | null {\n return env(\n interval === 'year' ? 'STRIPE_PRICE_METERED_YEARLY' : 'STRIPE_PRICE_METERED',\n )\n}\n\n/**\n * Whether a price id is EITHER shared metered-usage price.\n *\n * Interval-agnostic by design: callers ask this to exclude the metered item\n * from plan/add-on reasoning, and an annual subscription's metered item must\n * be excluded just as thoroughly as a monthly one.\n */\nexport function isMeteredPriceId(priceId: string | null | undefined): boolean {\n if (!priceId) return false\n return (\n priceId === meteredPriceId('month') || priceId === meteredPriceId('year')\n )\n}\n\n/**\n * The subscription's BASE PLAN item, identified POSITIVELY (AGL-1340).\n *\n * This used to be \"whatever no add-on price claims\" — identification by\n * elimination, which quietly returns the wrong item the moment a\n * subscription carries anything the add-on map doesn't recognise: the\n * metered item, a grandfathered v1 price, a coupon-era SKU, an item added\n * by hand in the Stripe dashboard. Picking an add-on item as the plan item\n * means a plan switch re-prices the ADD-ON, and reads the billing interval\n * off it.\n *\n * So: match a known `STRIPE_PRICE_{PLAN}[_YEARLY]` first. Only when nothing\n * matches — an enterprise deal bills on an ad-hoc price, and grandfathered\n * subscriptions predate the current SKUs — fall back to the old rule,\n * minus the metered item, and finally to `items[0]` so behaviour for those\n * subscriptions is unchanged.\n */\nexport function findPlanItem<T extends { price?: { id?: string } | null }>(\n items: readonly T[] | null | undefined,\n): T | null {\n const list = items ?? []\n const byPlanPrice = list.find((item) => planFromPriceId(item?.price?.id))\n if (byPlanPrice) return byPlanPrice\n const notAnAddon = list.find(\n (item) =>\n !addonKindFromPriceId(item?.price?.id) &&\n !isMeteredPriceId(item?.price?.id),\n )\n return notAnAddon ?? list[0] ?? null\n}\n\n/**\n * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of\n * this); null when the plan doesn't sell the kind. Single pricing source\n * of truth: `PLAN_PRICING` + the two flat add-on constants.\n */\nexport function addonUnitUsd(kind: AddonKind, plan: OrgPlan): number | null {\n const pricing = PLAN_PRICING[plan]\n switch (kind) {\n case 'managers':\n return pricing.extraSeatMonthlyUsd\n case 'members':\n return pricing.extraCollaboratorMonthlyUsd\n case 'datasets':\n return pricing.extraDatasetMonthlyUsd\n case 'hosts':\n return pricing.extraHostMonthlyUsd\n case 'posRegisters':\n return POS_REGISTER_ADDON_MONTHLY_USD\n case 'eventCalendar':\n return EVENT_CALENDAR_ADDON_MONTHLY_USD\n case 'aiAddon':\n return pricing.aiAddonMonthlyUsd\n }\n}\n\n/**\n * Full add-on quantity map (explicit zeros for absent kinds) from a\n * subscription's items — the shape written to `org.seatAddons` so\n * removals and dashboard edits converge on sync (AGL-527).\n */\nexport function addonQuantitiesFromItems(\n items:\n | Array<{ price?: { id?: string } | null; quantity?: number | null }>\n | null\n | undefined,\n): Record<AddonKind, number> {\n const quantities = Object.fromEntries(\n ADDON_KINDS.map((kind) => [kind, 0]),\n ) as Record<AddonKind, number>\n for (const item of items ?? []) {\n const kind = addonKindFromPriceId(item?.price?.id)\n if (kind) {\n quantities[kind] += Math.max(0, Math.floor(Number(item?.quantity ?? 0)))\n }\n }\n return quantities\n}\n\n/**\n * Max purchasable quantity per kind, from a purchases-free entitlement\n * resolution (plan defaults + staff overrides only) so the ceiling does not\n * drift as the org buys: seat/dataset kinds stop at the plan's hard max,\n * hosts/registers use flat ceilings, the Event Calendar and Aglyn AI are 0/1\n * toggles. POS registers additionally require the `pos` feature.\n *\n * Lives here rather than inside `/api/billing/addons` because a PLAN CHANGE\n * needs the same ceiling for a different plan. The addons route asks \"how many\n * may this org buy on the plan it is on\"; `buildTargetItems` asks \"how many of\n * what it already bought can the plan it is moving to actually deliver\". Two\n * readers, one definition — a second copy is how they answer differently.\n */\nexport function addonMaxForBaseline(\n kind: AddonKind,\n baseline: ReturnType<typeof resolveOrgEntitlements>,\n): number {\n const bounded = (included: number, max: number) =>\n Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX\n switch (kind) {\n case 'managers':\n return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg)\n case 'members':\n return bounded(baseline.membersPerHost, baseline.maxMembersPerHost)\n case 'datasets':\n return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg)\n case 'hosts':\n return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX\n case 'posRegisters':\n return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0\n case 'eventCalendar':\n return 1\n case 'aiAddon':\n // Org-wide like the Event Calendar: one purchase, and the price map\n // (null on Free and Enterprise) says whether the plan sells it at all.\n return 1\n }\n}\n\n/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */\nexport function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number {\n return addonMaxForBaseline(kind, resolveOrgEntitlements({ plan } as never))\n}\n"],"names":["EVENT_CALENDAR_ADDON_MONTHLY_USD","EXTRA_HOSTS_ADDON_MAX","PLAN_PRICING","POS_REGISTER_ADDON_MONTHLY_USD","POS_REGISTERS_ADDON_MAX","resolveOrgEntitlements","SELF_SERVE_PLANS","UNLIMITED","ADDON_KINDS","PER_PLAN_ENV_SUFFIX","managers","members","datasets","hosts","aiAddon","FLAT_ENV_NAME","posRegisters","eventCalendar","PAID_PLANS","filter","plan","env","name","value","process","addonPriceId","kind","interval","yearly","flat","suffix","toUpperCase","addonKindFromPriceId","priceId","planPriceId","includes","planAndIntervalFromPriceId","planFromPriceId","meteredPriceId","isMeteredPriceId","findPlanItem","items","notAnAddon","list","byPlanPrice","find","item","price","id","addonUnitUsd","pricing","extraSeatMonthlyUsd","extraCollaboratorMonthlyUsd","extraDatasetMonthlyUsd","extraHostMonthlyUsd","aiAddonMonthlyUsd","addonQuantitiesFromItems","quantities","Object","fromEntries","map","Math","max","floor","Number","quantity","addonMaxForBaseline","baseline","bounded","included","isFinite","managersPerOrg","maxManagersPerOrg","membersPerHost","maxMembersPerHost","datasetsPerOrg","maxDatasetsPerOrg","hostLimit","features","pos","addonMaxForPlan"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** The per-host collection of conversion attribution records. */
18
+ export declare const CAMPAIGN_ATTRIBUTIONS_COLLECTION = "campaignAttributions";
19
+ /**
20
+ * Removes every conversion attributed to one person, across every site.
21
+ *
22
+ * ## Why an erasure reaches these at all
23
+ *
24
+ * An attribution is a claim about a person: "this human came from that
25
+ * campaign and then did this". It is the same personal fact as the click
26
+ * stamp it was derived from, and the erasure path already deletes those —
27
+ * `eraseEmailDeliveriesForAddresses` drops `campaignTouches` alongside the
28
+ * engagement stamps and says why. A summary that outlived its source would
29
+ * leave an erasure that removed the evidence and kept the conclusion.
30
+ *
31
+ * ## Why a collection-group query
32
+ *
33
+ * The record is per HOST, because that is where a site's own reporting reads
34
+ * it. An erasure arrives as an ADDRESS and knows nothing about which sites
35
+ * that person ever visited. The collection group is the join between the two,
36
+ * and `personKey` is on the record precisely so one exists — declared in
37
+ * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,
38
+ * because a collection-group query gets no automatic single-field index and
39
+ * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked
40
+ * to be forgotten.
41
+ *
42
+ * ## The order's own record is NOT touched
43
+ *
44
+ * `emailAttributions` holds a commercial fact about a sale, keyed by the
45
+ * order's id and carrying no address — the revenue join says so where it
46
+ * writes it. This holds a record of a person's behavior under a key derived
47
+ * from their address. Different facts, different answers, and the split is
48
+ * why both are defensible.
49
+ *
50
+ * Never throws: an erasure that fails on one leg must still complete the
51
+ * others, and the caller reports what it removed.
52
+ *
53
+ * @returns how many records were removed.
54
+ */
55
+ export declare function eraseCampaignAttributionsForPersonKey(key: string | null | undefined, firestore?: any): Promise<number>;
@@ -0,0 +1,95 @@
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
+ * Where conversion attributions are kept, and how an erasure reaches them.
18
+ *
19
+ * ## Why this is a leaf module and not part of the join
20
+ *
21
+ * The erasure runs from `email-delivery-log.ts`, in the same function that
22
+ * drops a person's campaign touches — which is the point, because an
23
+ * attribution is a conclusion drawn from those touches and a conclusion that
24
+ * outlives its evidence is an erasure that did not happen. But the join
25
+ * itself READS that module (`readEmailCampaignTouch` is one half of the
26
+ * last-touch comparison), so putting the erasure beside the join would make
27
+ * the two modules import each other. Under jest that cycle resolves to
28
+ * `undefined` at import time rather than failing loudly, which is how an
29
+ * erasure comes to silently do nothing while its test passes.
30
+ *
31
+ * So the collection name and the sweep live here, importing nothing but
32
+ * Firestore, and both sides import this.
33
+ */ import firebaseAdmin from "./firebase-admin.js";
34
+ const defaultFirestore = ()=>firebaseAdmin.app().firestore();
35
+ /** The per-host collection of conversion attribution records. */ export const CAMPAIGN_ATTRIBUTIONS_COLLECTION = 'campaignAttributions';
36
+ /** How many records one erasure pass removes per batch. */ const ERASURE_BATCH = 400;
37
+ /**
38
+ * Removes every conversion attributed to one person, across every site.
39
+ *
40
+ * ## Why an erasure reaches these at all
41
+ *
42
+ * An attribution is a claim about a person: "this human came from that
43
+ * campaign and then did this". It is the same personal fact as the click
44
+ * stamp it was derived from, and the erasure path already deletes those —
45
+ * `eraseEmailDeliveriesForAddresses` drops `campaignTouches` alongside the
46
+ * engagement stamps and says why. A summary that outlived its source would
47
+ * leave an erasure that removed the evidence and kept the conclusion.
48
+ *
49
+ * ## Why a collection-group query
50
+ *
51
+ * The record is per HOST, because that is where a site's own reporting reads
52
+ * it. An erasure arrives as an ADDRESS and knows nothing about which sites
53
+ * that person ever visited. The collection group is the join between the two,
54
+ * and `personKey` is on the record precisely so one exists — declared in
55
+ * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,
56
+ * because a collection-group query gets no automatic single-field index and
57
+ * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked
58
+ * to be forgotten.
59
+ *
60
+ * ## The order's own record is NOT touched
61
+ *
62
+ * `emailAttributions` holds a commercial fact about a sale, keyed by the
63
+ * order's id and carrying no address — the revenue join says so where it
64
+ * writes it. This holds a record of a person's behavior under a key derived
65
+ * from their address. Different facts, different answers, and the split is
66
+ * why both are defensible.
67
+ *
68
+ * Never throws: an erasure that fails on one leg must still complete the
69
+ * others, and the caller reports what it removed.
70
+ *
71
+ * @returns how many records were removed.
72
+ */ export async function eraseCampaignAttributionsForPersonKey(key, firestore) {
73
+ if (!key || typeof key !== 'string') return 0;
74
+ const db = firestore != null ? firestore : defaultFirestore();
75
+ let removed = 0;
76
+ try {
77
+ // Bounded rather than `while (true)`: a pathological volume must not be
78
+ // able to hold an erasure request open indefinitely, and the pass that
79
+ // runs next picks up whatever a bounded one left.
80
+ for(let pass = 0; pass < 20; pass += 1){
81
+ const snapshot = await db.collectionGroup(CAMPAIGN_ATTRIBUTIONS_COLLECTION).where('personKey', '==', key).limit(ERASURE_BATCH).get();
82
+ if (snapshot.empty) break;
83
+ const batch = db.batch();
84
+ snapshot.docs.forEach((doc)=>batch.delete(doc.ref));
85
+ await batch.commit();
86
+ removed += snapshot.size;
87
+ if (snapshot.size < ERASURE_BATCH) break;
88
+ }
89
+ } catch (error) {
90
+ console.error('eraseCampaignAttributionsForPersonKey failed', error);
91
+ }
92
+ return removed;
93
+ }
94
+
95
+ //# sourceMappingURL=campaign-attribution-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/campaign-attribution-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Where conversion attributions are kept, and how an erasure reaches them.\n *\n * ## Why this is a leaf module and not part of the join\n *\n * The erasure runs from `email-delivery-log.ts`, in the same function that\n * drops a person's campaign touches — which is the point, because an\n * attribution is a conclusion drawn from those touches and a conclusion that\n * outlives its evidence is an erasure that did not happen. But the join\n * itself READS that module (`readEmailCampaignTouch` is one half of the\n * last-touch comparison), so putting the erasure beside the join would make\n * the two modules import each other. Under jest that cycle resolves to\n * `undefined` at import time rather than failing loudly, which is how an\n * erasure comes to silently do nothing while its test passes.\n *\n * So the collection name and the sweep live here, importing nothing but\n * Firestore, and both sides import this.\n */\n\nimport firebaseAdmin from './firebase-admin'\n\nconst defaultFirestore = () => firebaseAdmin.app().firestore()\n\n/** The per-host collection of conversion attribution records. */\nexport const CAMPAIGN_ATTRIBUTIONS_COLLECTION = 'campaignAttributions'\n\n/** How many records one erasure pass removes per batch. */\nconst ERASURE_BATCH = 400\n\n/**\n * Removes every conversion attributed to one person, across every site.\n *\n * ## Why an erasure reaches these at all\n *\n * An attribution is a claim about a person: \"this human came from that\n * campaign and then did this\". It is the same personal fact as the click\n * stamp it was derived from, and the erasure path already deletes those —\n * `eraseEmailDeliveriesForAddresses` drops `campaignTouches` alongside the\n * engagement stamps and says why. A summary that outlived its source would\n * leave an erasure that removed the evidence and kept the conclusion.\n *\n * ## Why a collection-group query\n *\n * The record is per HOST, because that is where a site's own reporting reads\n * it. An erasure arrives as an ADDRESS and knows nothing about which sites\n * that person ever visited. The collection group is the join between the two,\n * and `personKey` is on the record precisely so one exists — declared in\n * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,\n * because a collection-group query gets no automatic single-field index and\n * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked\n * to be forgotten.\n *\n * ## The order's own record is NOT touched\n *\n * `emailAttributions` holds a commercial fact about a sale, keyed by the\n * order's id and carrying no address — the revenue join says so where it\n * writes it. This holds a record of a person's behavior under a key derived\n * from their address. Different facts, different answers, and the split is\n * why both are defensible.\n *\n * Never throws: an erasure that fails on one leg must still complete the\n * others, and the caller reports what it removed.\n *\n * @returns how many records were removed.\n */\nexport async function eraseCampaignAttributionsForPersonKey(\n key: string | null | undefined,\n firestore?: any,\n): Promise<number> {\n if (!key || typeof key !== 'string') return 0\n const db = firestore ?? defaultFirestore()\n let removed = 0\n try {\n // Bounded rather than `while (true)`: a pathological volume must not be\n // able to hold an erasure request open indefinitely, and the pass that\n // runs next picks up whatever a bounded one left.\n for (let pass = 0; pass < 20; pass += 1) {\n const snapshot = await db\n .collectionGroup(CAMPAIGN_ATTRIBUTIONS_COLLECTION)\n .where('personKey', '==', key)\n .limit(ERASURE_BATCH)\n .get()\n if (snapshot.empty) break\n const batch = db.batch()\n snapshot.docs.forEach((doc: any) => batch.delete(doc.ref))\n await batch.commit()\n removed += snapshot.size\n if (snapshot.size < ERASURE_BATCH) break\n }\n } catch (error) {\n console.error('eraseCampaignAttributionsForPersonKey failed', error)\n }\n return removed\n}\n"],"names":["firebaseAdmin","defaultFirestore","app","firestore","CAMPAIGN_ATTRIBUTIONS_COLLECTION","ERASURE_BATCH","eraseCampaignAttributionsForPersonKey","key","db","removed","pass","snapshot","collectionGroup","where","limit","get","empty","batch","docs","forEach","doc","delete","ref","commit","size","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAED,OAAOA,mBAAmB,sBAAkB;AAE5C,MAAMC,mBAAmB,IAAMD,cAAcE,GAAG,GAAGC,SAAS;AAE5D,+DAA+D,GAC/D,OAAO,MAAMC,mCAAmC,uBAAsB;AAEtE,yDAAyD,GACzD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,eAAeC,sCACpBC,GAA8B,EAC9BJ,SAAe;IAEf,IAAI,CAACI,OAAO,OAAOA,QAAQ,UAAU,OAAO;IAC5C,MAAMC,KAAKL,oBAAAA,YAAaF;IACxB,IAAIQ,UAAU;IACd,IAAI;QACF,wEAAwE;QACxE,uEAAuE;QACvE,kDAAkD;QAClD,IAAK,IAAIC,OAAO,GAAGA,OAAO,IAAIA,QAAQ,EAAG;YACvC,MAAMC,WAAW,MAAMH,GACpBI,eAAe,CAACR,kCAChBS,KAAK,CAAC,aAAa,MAAMN,KACzBO,KAAK,CAACT,eACNU,GAAG;YACN,IAAIJ,SAASK,KAAK,EAAE;YACpB,MAAMC,QAAQT,GAAGS,KAAK;YACtBN,SAASO,IAAI,CAACC,OAAO,CAAC,CAACC,MAAaH,MAAMI,MAAM,CAACD,IAAIE,GAAG;YACxD,MAAML,MAAMM,MAAM;YAClBd,WAAWE,SAASa,IAAI;YACxB,IAAIb,SAASa,IAAI,GAAGnB,eAAe;QACrC;IACF,EAAE,OAAOoB,OAAO;QACdC,QAAQD,KAAK,CAAC,gDAAgDA;IAChE;IACA,OAAOhB;AACT"}