@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,205 @@
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 { CAMPAIGN_ATTRIBUTIONS_COLLECTION, eraseCampaignAttributionsForPersonKey } from './campaign-attribution-store';
18
+ /**
19
+ * THE IDENTIFY-MOMENT JOIN — a form, a lead, a contact or a booking, credited
20
+ * to the campaign that led to it.
21
+ *
22
+ * ## Why there is a second join at all, and why it is not a second SCHEME
23
+ *
24
+ * `email-revenue-attribution.ts` credits an ORDER by looking the buyer's
25
+ * address up in the touch map a click wrote. That works because an order
26
+ * names its buyer. Every other outcome a campaign causes is produced by
27
+ * somebody who was anonymous until the instant they produced it: they arrive
28
+ * from a campaign link, browse, and only become identifiable when they submit
29
+ * a form, sign up, book or check out. There is no address to look up until
30
+ * the very moment being attributed.
31
+ *
32
+ * So the touch is carried on the VISITOR (`campaign-touch.ts`) and attached
33
+ * here, at each moment they become somebody. What is NOT rebuilt is the
34
+ * model: the same seven-day window, the same last-click rule, the same
35
+ * `model`/`windowDays` stamped onto every record so a report can say what it
36
+ * counted. The email channel is not re-derived either — it is READ from the
37
+ * touch the delivery webhook already wrote, so a campaign email and a
38
+ * campaign ad are two inputs to one comparison rather than two schemes with
39
+ * two answers.
40
+ *
41
+ * ## Last touch, across both channels
42
+ *
43
+ * At the identify moment there can be two candidate touches:
44
+ *
45
+ * - the WEB touch the visitor's device carried, from an ad, a partner link
46
+ * or a social post; and
47
+ * - the EMAIL touch on `emailDeliveries/{personKey}.campaignTouches[hostId]`,
48
+ * written server-side when they clicked a campaign's mail.
49
+ *
50
+ * Both are window-checked and the LATER one wins. Splitting the outcome
51
+ * between them is the multi-touch model the revenue work rejected for the
52
+ * reason that applies here unchanged: a rule the merchant did not choose
53
+ * produces a figure nobody can check. One outcome, one campaign, and a rule
54
+ * stated on the record.
55
+ *
56
+ * ## No campaign touch means NO RECORD
57
+ *
58
+ * Direct traffic writes nothing at all, and there is deliberately no fallback
59
+ * — no "most recent campaign on this site", no referrer inference, no
60
+ * `utm_source=direct`. A conversion nobody can be credited with is a
61
+ * conversion nobody is credited with, and the absence of a record is how the
62
+ * report says so. The cost of a miss is one uncounted outcome; the cost of a
63
+ * guess is a campaign that reads as effective because it was the last one to
64
+ * run.
65
+ *
66
+ * ## Three reads at most, and usually one
67
+ *
68
+ * Resolving a touch costs ONE keyed document read, and only when the visitor
69
+ * gave an address — no query, no index, nothing that can be truncated. A door
70
+ * resolves once per request and hands the result to every writer beneath it,
71
+ * so a form submission that creates a submission, a contact and a lead pays
72
+ * for the lookup once rather than three times.
73
+ *
74
+ * ## Never throws
75
+ *
76
+ * The same contract as the revenue join and for the same reason: the visitor
77
+ * has already done the thing being attributed and the record of it is already
78
+ * written. A lost attribution understates a campaign; a thrown one loses a
79
+ * lead.
80
+ */
81
+ /**
82
+ * The collection and the erasure sweep, re-exported so a caller needs one
83
+ * import for the whole join. They are DEFINED in a leaf module because the
84
+ * sweep runs from `email-delivery-log.ts`, which this file reads — see
85
+ * `campaign-attribution-store.ts` for why that cycle is not merely untidy.
86
+ */
87
+ export { CAMPAIGN_ATTRIBUTIONS_COLLECTION, eraseCampaignAttributionsForPersonKey, };
88
+ /** The single conversion rollup document under an email campaign. */
89
+ export declare const CAMPAIGN_CONVERSIONS_REPORT_DOC = "conversions";
90
+ /**
91
+ * Which identify moment a record credits.
92
+ *
93
+ * Kept apart rather than summed, which is the whole reason the kind is on the
94
+ * record. One form submission by a new person creates a submission, a contact
95
+ * and possibly a lead — three true statements about one visitor action — and
96
+ * a report that added them would treble every campaign's conversions. A
97
+ * reader asks for one kind at a time, exactly as the revenue rollup buckets
98
+ * by currency and never totals across them.
99
+ */
100
+ export type CampaignConversionKind = 'form' | 'lead' | 'contact' | 'booking';
101
+ /** Which channel the credited touch arrived through. */
102
+ export type CampaignTouchChannel = 'email' | 'web';
103
+ /** The touch a conversion is credited to, once both channels have been asked. */
104
+ export interface ResolvedCampaignTouch {
105
+ channel: CampaignTouchChannel;
106
+ /** The campaign document, when the touch was a click on our own mail. */
107
+ campaignId?: string;
108
+ /** `utm_source`, when the touch was a link on the web. */
109
+ source?: string;
110
+ /** `utm_medium`, when the touch was a link on the web. */
111
+ medium?: string;
112
+ /** `utm_campaign`, when the touch was a link on the web. */
113
+ campaign?: string;
114
+ /** When the visitor followed the campaign link, epoch ms. */
115
+ touchedAtMs: number;
116
+ /**
117
+ * `sha256` of the normalized address the touch was resolved against, when
118
+ * the visitor gave one. The handle an erasure erases by — see
119
+ * {@link eraseCampaignAttributionsForPersonKey}.
120
+ */
121
+ personKey?: string;
122
+ }
123
+ /** What one conversion attribution record holds. */
124
+ export interface CampaignConversionRecord extends ResolvedCampaignTouch {
125
+ kind: CampaignConversionKind;
126
+ /** The submission, lead, contact or booking this credits. */
127
+ refId: string;
128
+ /** When the visitor became identifiable. */
129
+ convertedAtMs: number;
130
+ /** The model this credit was decided under. */
131
+ model: string;
132
+ /** The window, in days, it was decided inside. */
133
+ windowDays: number;
134
+ }
135
+ /**
136
+ * The document id for one conversion.
137
+ *
138
+ * `{kind}:{refId}`, so the same submission credited twice is the same
139
+ * document — which is what makes {@link attributeCampaignConversion}'s
140
+ * `create()` idempotent. Scoped by kind because the ids come from different
141
+ * collections and a booking id has no reason to be distinct from a form
142
+ * submission's.
143
+ */
144
+ export declare function campaignConversionId(kind: CampaignConversionKind, refId: string): string;
145
+ /**
146
+ * The campaign touch to credit a conversion happening now, or `null`.
147
+ *
148
+ * Called ONCE per conversion request, at the door, and the result handed to
149
+ * every writer beneath it. See the module docblock for the cost argument.
150
+ *
151
+ * @returns the winning touch, or `null` when neither channel has one inside
152
+ * the window — which is the ordinary case and writes nothing.
153
+ */
154
+ export declare function resolveCampaignTouch(options: {
155
+ hostId: string;
156
+ /**
157
+ * The wire form the conversion request carried, when the door reports
158
+ * one. Re-parsed through the allowlist that wrote it rather than trusted:
159
+ * it arrives from the visitor's browser, so it may claim no more than an
160
+ * inbound URL could.
161
+ */
162
+ wire?: unknown;
163
+ /** The address the visitor identified with, raw. */
164
+ email?: unknown;
165
+ /** When they identified themselves. Defaults to now. */
166
+ atMs?: number;
167
+ }, firestore?: any): Promise<ResolvedCampaignTouch | null>;
168
+ /**
169
+ * Credits one conversion to the campaign the visitor last came from.
170
+ *
171
+ * ## Why the record is a document and not a field
172
+ *
173
+ * The revenue join's three reasons, and the first is decisive here too.
174
+ * `create()` fails when the document already exists, which is exact
175
+ * idempotency for free: a retried form POST, a redelivered booking webhook
176
+ * and a re-run capture all land here a second time and must all leave the
177
+ * rollup where they found it. Second, the four kinds live in four different
178
+ * collections — a contact is org-scoped and shared across every site in the
179
+ * org — so there is no one document a field could go on. Third, the
180
+ * submission, the lead and the booking are read by the inbox, the export and
181
+ * the fulfilment path, and none of them wants a field about marketing.
182
+ *
183
+ * ## The rollup exists for the EMAIL channel only
184
+ *
185
+ * A campaign document is a real entity with a real id, so its conversions
186
+ * roll up under it beside the revenue the same join already credits it with.
187
+ * A web campaign is a LABEL the marketer typed into a URL, with no document,
188
+ * no id and no bound on how many distinct values exist — the same unbounded
189
+ * key space the analytics collector caps its per-day label map against. A
190
+ * rollup keyed on it would be a map anybody who can vary a query string can
191
+ * grow. So the web channel's records stand on their own and a report reads
192
+ * them; see the module's report seam note.
193
+ *
194
+ * @returns the record written, or `null` when nothing was credited.
195
+ */
196
+ export declare function attributeCampaignConversion(options: {
197
+ hostId: string;
198
+ kind: CampaignConversionKind;
199
+ /** The submission, lead, contact or booking being credited. */
200
+ refId: string;
201
+ /** The touch {@link resolveCampaignTouch} picked, or `null` for direct. */
202
+ touch: ResolvedCampaignTouch | null | undefined;
203
+ /** When the visitor became identifiable. Defaults to now. */
204
+ convertedAtMs?: number;
205
+ }, firestore?: any): Promise<CampaignConversionRecord | null>;
@@ -0,0 +1,279 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { FieldValue } from "firebase-admin/firestore";
18
+ import { parseCampaignTouch } from "@aglyn/aglyn/app-utils/campaign-touch";
19
+ /*
20
+ * Stamped from the REVENUE join's constants, aliased at the import.
21
+ *
22
+ * They are spelled `EMAIL_` because that is where both halves of that join
23
+ * could reach them, not because the window is an email fact — it is the
24
+ * platform's one answer to how long a touch may be credited. Reading them
25
+ * from there is what makes a lead's record and an order's record carry the
26
+ * same rule rather than two that happen to agree.
27
+ */ import { EMAIL_ATTRIBUTION_MODEL as ATTRIBUTION_MODEL, EMAIL_ATTRIBUTION_WINDOW_DAYS as ATTRIBUTION_WINDOW_DAYS, emailTouchIsInWindow } from "@aglyn/shared-util-email/email-revenue-window";
28
+ import { personKey } from "@aglyn/aglyn/app-utils/person-key";
29
+ import { CAMPAIGN_ATTRIBUTIONS_COLLECTION, eraseCampaignAttributionsForPersonKey } from "./campaign-attribution-store.js";
30
+ import { readEmailCampaignTouch } from "./email-delivery-log.js";
31
+ import { isDocumentId } from "./document-id.js";
32
+ import firebaseAdmin from "./firebase-admin.js";
33
+ const defaultFirestore = ()=>firebaseAdmin.app().firestore();
34
+ /**
35
+ * THE IDENTIFY-MOMENT JOIN — a form, a lead, a contact or a booking, credited
36
+ * to the campaign that led to it.
37
+ *
38
+ * ## Why there is a second join at all, and why it is not a second SCHEME
39
+ *
40
+ * `email-revenue-attribution.ts` credits an ORDER by looking the buyer's
41
+ * address up in the touch map a click wrote. That works because an order
42
+ * names its buyer. Every other outcome a campaign causes is produced by
43
+ * somebody who was anonymous until the instant they produced it: they arrive
44
+ * from a campaign link, browse, and only become identifiable when they submit
45
+ * a form, sign up, book or check out. There is no address to look up until
46
+ * the very moment being attributed.
47
+ *
48
+ * So the touch is carried on the VISITOR (`campaign-touch.ts`) and attached
49
+ * here, at each moment they become somebody. What is NOT rebuilt is the
50
+ * model: the same seven-day window, the same last-click rule, the same
51
+ * `model`/`windowDays` stamped onto every record so a report can say what it
52
+ * counted. The email channel is not re-derived either — it is READ from the
53
+ * touch the delivery webhook already wrote, so a campaign email and a
54
+ * campaign ad are two inputs to one comparison rather than two schemes with
55
+ * two answers.
56
+ *
57
+ * ## Last touch, across both channels
58
+ *
59
+ * At the identify moment there can be two candidate touches:
60
+ *
61
+ * - the WEB touch the visitor's device carried, from an ad, a partner link
62
+ * or a social post; and
63
+ * - the EMAIL touch on `emailDeliveries/{personKey}.campaignTouches[hostId]`,
64
+ * written server-side when they clicked a campaign's mail.
65
+ *
66
+ * Both are window-checked and the LATER one wins. Splitting the outcome
67
+ * between them is the multi-touch model the revenue work rejected for the
68
+ * reason that applies here unchanged: a rule the merchant did not choose
69
+ * produces a figure nobody can check. One outcome, one campaign, and a rule
70
+ * stated on the record.
71
+ *
72
+ * ## No campaign touch means NO RECORD
73
+ *
74
+ * Direct traffic writes nothing at all, and there is deliberately no fallback
75
+ * — no "most recent campaign on this site", no referrer inference, no
76
+ * `utm_source=direct`. A conversion nobody can be credited with is a
77
+ * conversion nobody is credited with, and the absence of a record is how the
78
+ * report says so. The cost of a miss is one uncounted outcome; the cost of a
79
+ * guess is a campaign that reads as effective because it was the last one to
80
+ * run.
81
+ *
82
+ * ## Three reads at most, and usually one
83
+ *
84
+ * Resolving a touch costs ONE keyed document read, and only when the visitor
85
+ * gave an address — no query, no index, nothing that can be truncated. A door
86
+ * resolves once per request and hands the result to every writer beneath it,
87
+ * so a form submission that creates a submission, a contact and a lead pays
88
+ * for the lookup once rather than three times.
89
+ *
90
+ * ## Never throws
91
+ *
92
+ * The same contract as the revenue join and for the same reason: the visitor
93
+ * has already done the thing being attributed and the record of it is already
94
+ * written. A lost attribution understates a campaign; a thrown one loses a
95
+ * lead.
96
+ */ /**
97
+ * The collection and the erasure sweep, re-exported so a caller needs one
98
+ * import for the whole join. They are DEFINED in a leaf module because the
99
+ * sweep runs from `email-delivery-log.ts`, which this file reads — see
100
+ * `campaign-attribution-store.ts` for why that cycle is not merely untidy.
101
+ */ export { CAMPAIGN_ATTRIBUTIONS_COLLECTION, eraseCampaignAttributionsForPersonKey, };
102
+ /** The single conversion rollup document under an email campaign. */ export const CAMPAIGN_CONVERSIONS_REPORT_DOC = 'conversions';
103
+ /**
104
+ * The document id for one conversion.
105
+ *
106
+ * `{kind}:{refId}`, so the same submission credited twice is the same
107
+ * document — which is what makes {@link attributeCampaignConversion}'s
108
+ * `create()` idempotent. Scoped by kind because the ids come from different
109
+ * collections and a booking id has no reason to be distinct from a form
110
+ * submission's.
111
+ */ export function campaignConversionId(kind, refId) {
112
+ return `${kind}:${refId}`;
113
+ }
114
+ /**
115
+ * The campaign touch to credit a conversion happening now, or `null`.
116
+ *
117
+ * Called ONCE per conversion request, at the door, and the result handed to
118
+ * every writer beneath it. See the module docblock for the cost argument.
119
+ *
120
+ * @returns the winning touch, or `null` when neither channel has one inside
121
+ * the window — which is the ordinary case and writes nothing.
122
+ */ export async function resolveCampaignTouch(options, firestore) {
123
+ try {
124
+ var _options_hostId, _options_atMs, _options_email;
125
+ const hostId = String((_options_hostId = options.hostId) != null ? _options_hostId : '');
126
+ if (!isDocumentId(hostId)) return null;
127
+ const convertedAtMs = Number((_options_atMs = options.atMs) != null ? _options_atMs : Date.now());
128
+ if (!Number.isFinite(convertedAtMs) || convertedAtMs <= 0) return null;
129
+ // The window is enforced inside the parser, so an expired or
130
+ // future-dated wire value answers null here exactly as it does in the
131
+ // browser that decided whether to send it.
132
+ const web = parseCampaignTouch(options.wire, convertedAtMs);
133
+ const key = personKey(options.email);
134
+ /*
135
+ * The email channel is only askable of somebody who named an address, and
136
+ * that is the whole reason this join needs the web channel at all. No
137
+ * address means no keyed read: the ordinary anonymous conversion costs
138
+ * nothing here.
139
+ */ const emailTouch = key ? await readEmailCampaignTouch(String((_options_email = options.email) != null ? _options_email : ''), hostId, firestore != null ? firestore : defaultFirestore()) : null;
140
+ const emailInWindow = emailTouch && isDocumentId(emailTouch.campaignId) && emailTouchIsInWindow(emailTouch.clickedAtMs, convertedAtMs) ? emailTouch : null;
141
+ /*
142
+ * LAST TOUCH decides, and a tie goes to email. A tie is only reachable
143
+ * when the two instants are the same millisecond, and in that case the
144
+ * email touch is the better evidence: it was recorded by the provider's
145
+ * own click event on the server, while the web touch is a value the
146
+ * visitor's device supplied.
147
+ */ const emailWins = emailInWindow && (!web || emailInWindow.clickedAtMs >= web.atMs);
148
+ if (emailWins) {
149
+ return _extends({
150
+ channel: 'email',
151
+ campaignId: emailInWindow.campaignId,
152
+ touchedAtMs: emailInWindow.clickedAtMs
153
+ }, key ? {
154
+ personKey: key
155
+ } : {});
156
+ }
157
+ if (!web) return null;
158
+ return _extends({
159
+ channel: 'web'
160
+ }, web.source ? {
161
+ source: web.source
162
+ } : {}, web.medium ? {
163
+ medium: web.medium
164
+ } : {}, web.campaign ? {
165
+ campaign: web.campaign
166
+ } : {}, {
167
+ touchedAtMs: web.atMs
168
+ }, key ? {
169
+ personKey: key
170
+ } : {});
171
+ } catch (error) {
172
+ console.error('resolveCampaignTouch failed', error);
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * Credits one conversion to the campaign the visitor last came from.
178
+ *
179
+ * ## Why the record is a document and not a field
180
+ *
181
+ * The revenue join's three reasons, and the first is decisive here too.
182
+ * `create()` fails when the document already exists, which is exact
183
+ * idempotency for free: a retried form POST, a redelivered booking webhook
184
+ * and a re-run capture all land here a second time and must all leave the
185
+ * rollup where they found it. Second, the four kinds live in four different
186
+ * collections — a contact is org-scoped and shared across every site in the
187
+ * org — so there is no one document a field could go on. Third, the
188
+ * submission, the lead and the booking are read by the inbox, the export and
189
+ * the fulfilment path, and none of them wants a field about marketing.
190
+ *
191
+ * ## The rollup exists for the EMAIL channel only
192
+ *
193
+ * A campaign document is a real entity with a real id, so its conversions
194
+ * roll up under it beside the revenue the same join already credits it with.
195
+ * A web campaign is a LABEL the marketer typed into a URL, with no document,
196
+ * no id and no bound on how many distinct values exist — the same unbounded
197
+ * key space the analytics collector caps its per-day label map against. A
198
+ * rollup keyed on it would be a map anybody who can vary a query string can
199
+ * grow. So the web channel's records stand on their own and a report reads
200
+ * them; see the module's report seam note.
201
+ *
202
+ * @returns the record written, or `null` when nothing was credited.
203
+ */ export async function attributeCampaignConversion(options, firestore) {
204
+ try {
205
+ var _options_hostId, _options_refId, _options_convertedAtMs;
206
+ const { touch } = options;
207
+ if (!touch) return null;
208
+ const hostId = String((_options_hostId = options.hostId) != null ? _options_hostId : '');
209
+ const refId = String((_options_refId = options.refId) != null ? _options_refId : '');
210
+ if (!isDocumentId(hostId) || !isDocumentId(refId)) return null;
211
+ const convertedAtMs = Number((_options_convertedAtMs = options.convertedAtMs) != null ? _options_convertedAtMs : Date.now());
212
+ if (!Number.isFinite(convertedAtMs) || convertedAtMs <= 0) return null;
213
+ // Re-checked at the write rather than trusted from the resolve. The two
214
+ // are separated by the writes that produced the thing being credited, and
215
+ // a touch that has aged out between them is a touch outside the window.
216
+ if (!emailTouchIsInWindow(touch.touchedAtMs, convertedAtMs)) return null;
217
+ const record = _extends({
218
+ kind: options.kind,
219
+ refId,
220
+ channel: touch.channel
221
+ }, touch.campaignId ? {
222
+ campaignId: touch.campaignId
223
+ } : {}, touch.source ? {
224
+ source: touch.source
225
+ } : {}, touch.medium ? {
226
+ medium: touch.medium
227
+ } : {}, touch.campaign ? {
228
+ campaign: touch.campaign
229
+ } : {}, {
230
+ touchedAtMs: touch.touchedAtMs,
231
+ convertedAtMs,
232
+ model: ATTRIBUTION_MODEL,
233
+ windowDays: ATTRIBUTION_WINDOW_DAYS
234
+ }, touch.personKey ? {
235
+ personKey: touch.personKey
236
+ } : {});
237
+ const db = firestore != null ? firestore : defaultFirestore();
238
+ const hostRef = db.collection('hosts').doc(hostId);
239
+ /*
240
+ * `create()`, never `set()` — the revenue join's argument, unchanged. The
241
+ * ALREADY_EXISTS failure IS the idempotency, and the rollup below is
242
+ * reached only when the create succeeded, so the two can never disagree
243
+ * about whether this conversion was counted.
244
+ */ try {
245
+ await hostRef.collection(CAMPAIGN_ATTRIBUTIONS_COLLECTION).doc(campaignConversionId(options.kind, refId)).create(_extends({}, record, {
246
+ createdAt: FieldValue.serverTimestamp()
247
+ }));
248
+ } catch (unused) {
249
+ return null;
250
+ }
251
+ if (touch.channel === 'email' && touch.campaignId) {
252
+ /*
253
+ * A merge-set that CREATES, for the reason `reports/revenue` gives: this
254
+ * writes a document UNDER a campaign, so a campaign that no longer
255
+ * exists gains an orphaned report rather than being resurrected as a
256
+ * husk in the merchant's history — and the campaign was proven to exist
257
+ * when its click wrote the touch.
258
+ *
259
+ * Every figure is an increment, so two conversions settling at once
260
+ * both land.
261
+ */ await hostRef.collection('campaigns').doc(touch.campaignId).collection('reports').doc(CAMPAIGN_CONVERSIONS_REPORT_DOC).set({
262
+ model: ATTRIBUTION_MODEL,
263
+ windowDays: ATTRIBUTION_WINDOW_DAYS,
264
+ byKind: {
265
+ [options.kind]: FieldValue.increment(1)
266
+ },
267
+ updatedAt: FieldValue.serverTimestamp()
268
+ }, {
269
+ merge: true
270
+ });
271
+ }
272
+ return record;
273
+ } catch (error) {
274
+ console.error('attributeCampaignConversion failed', error);
275
+ return null;
276
+ }
277
+ }
278
+
279
+ //# sourceMappingURL=campaign-conversion-attribution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/campaign-conversion-attribution.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n parseCampaignTouch,\n type CampaignTouch,\n} from '@aglyn/aglyn/app-utils/campaign-touch'\n/*\n * Stamped from the REVENUE join's constants, aliased at the import.\n *\n * They are spelled `EMAIL_` because that is where both halves of that join\n * could reach them, not because the window is an email fact — it is the\n * platform's one answer to how long a touch may be credited. Reading them\n * from there is what makes a lead's record and an order's record carry the\n * same rule rather than two that happen to agree.\n */\nimport {\n EMAIL_ATTRIBUTION_MODEL as ATTRIBUTION_MODEL,\n EMAIL_ATTRIBUTION_WINDOW_DAYS as ATTRIBUTION_WINDOW_DAYS,\n emailTouchIsInWindow,\n} from '@aglyn/shared-util-email/email-revenue-window'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport {\n CAMPAIGN_ATTRIBUTIONS_COLLECTION,\n eraseCampaignAttributionsForPersonKey,\n} from './campaign-attribution-store'\nimport { readEmailCampaignTouch } from './email-delivery-log'\nimport { isDocumentId } from './document-id'\nimport firebaseAdmin from './firebase-admin'\n\nconst defaultFirestore = () => firebaseAdmin.app().firestore()\n\n/**\n * THE IDENTIFY-MOMENT JOIN — a form, a lead, a contact or a booking, credited\n * to the campaign that led to it.\n *\n * ## Why there is a second join at all, and why it is not a second SCHEME\n *\n * `email-revenue-attribution.ts` credits an ORDER by looking the buyer's\n * address up in the touch map a click wrote. That works because an order\n * names its buyer. Every other outcome a campaign causes is produced by\n * somebody who was anonymous until the instant they produced it: they arrive\n * from a campaign link, browse, and only become identifiable when they submit\n * a form, sign up, book or check out. There is no address to look up until\n * the very moment being attributed.\n *\n * So the touch is carried on the VISITOR (`campaign-touch.ts`) and attached\n * here, at each moment they become somebody. What is NOT rebuilt is the\n * model: the same seven-day window, the same last-click rule, the same\n * `model`/`windowDays` stamped onto every record so a report can say what it\n * counted. The email channel is not re-derived either — it is READ from the\n * touch the delivery webhook already wrote, so a campaign email and a\n * campaign ad are two inputs to one comparison rather than two schemes with\n * two answers.\n *\n * ## Last touch, across both channels\n *\n * At the identify moment there can be two candidate touches:\n *\n * - the WEB touch the visitor's device carried, from an ad, a partner link\n * or a social post; and\n * - the EMAIL touch on `emailDeliveries/{personKey}.campaignTouches[hostId]`,\n * written server-side when they clicked a campaign's mail.\n *\n * Both are window-checked and the LATER one wins. Splitting the outcome\n * between them is the multi-touch model the revenue work rejected for the\n * reason that applies here unchanged: a rule the merchant did not choose\n * produces a figure nobody can check. One outcome, one campaign, and a rule\n * stated on the record.\n *\n * ## No campaign touch means NO RECORD\n *\n * Direct traffic writes nothing at all, and there is deliberately no fallback\n * — no \"most recent campaign on this site\", no referrer inference, no\n * `utm_source=direct`. A conversion nobody can be credited with is a\n * conversion nobody is credited with, and the absence of a record is how the\n * report says so. The cost of a miss is one uncounted outcome; the cost of a\n * guess is a campaign that reads as effective because it was the last one to\n * run.\n *\n * ## Three reads at most, and usually one\n *\n * Resolving a touch costs ONE keyed document read, and only when the visitor\n * gave an address — no query, no index, nothing that can be truncated. A door\n * resolves once per request and hands the result to every writer beneath it,\n * so a form submission that creates a submission, a contact and a lead pays\n * for the lookup once rather than three times.\n *\n * ## Never throws\n *\n * The same contract as the revenue join and for the same reason: the visitor\n * has already done the thing being attributed and the record of it is already\n * written. A lost attribution understates a campaign; a thrown one loses a\n * lead.\n */\n\n/**\n * The collection and the erasure sweep, re-exported so a caller needs one\n * import for the whole join. They are DEFINED in a leaf module because the\n * sweep runs from `email-delivery-log.ts`, which this file reads — see\n * `campaign-attribution-store.ts` for why that cycle is not merely untidy.\n */\nexport {\n CAMPAIGN_ATTRIBUTIONS_COLLECTION,\n eraseCampaignAttributionsForPersonKey,\n}\n\n/** The single conversion rollup document under an email campaign. */\nexport const CAMPAIGN_CONVERSIONS_REPORT_DOC = 'conversions'\n\n/**\n * Which identify moment a record credits.\n *\n * Kept apart rather than summed, which is the whole reason the kind is on the\n * record. One form submission by a new person creates a submission, a contact\n * and possibly a lead — three true statements about one visitor action — and\n * a report that added them would treble every campaign's conversions. A\n * reader asks for one kind at a time, exactly as the revenue rollup buckets\n * by currency and never totals across them.\n */\nexport type CampaignConversionKind = 'form' | 'lead' | 'contact' | 'booking'\n\n/** Which channel the credited touch arrived through. */\nexport type CampaignTouchChannel = 'email' | 'web'\n\n/** The touch a conversion is credited to, once both channels have been asked. */\nexport interface ResolvedCampaignTouch {\n channel: CampaignTouchChannel\n /** The campaign document, when the touch was a click on our own mail. */\n campaignId?: string\n /** `utm_source`, when the touch was a link on the web. */\n source?: string\n /** `utm_medium`, when the touch was a link on the web. */\n medium?: string\n /** `utm_campaign`, when the touch was a link on the web. */\n campaign?: string\n /** When the visitor followed the campaign link, epoch ms. */\n touchedAtMs: number\n /**\n * `sha256` of the normalized address the touch was resolved against, when\n * the visitor gave one. The handle an erasure erases by — see\n * {@link eraseCampaignAttributionsForPersonKey}.\n */\n personKey?: string\n}\n\n/** What one conversion attribution record holds. */\nexport interface CampaignConversionRecord extends ResolvedCampaignTouch {\n kind: CampaignConversionKind\n /** The submission, lead, contact or booking this credits. */\n refId: string\n /** When the visitor became identifiable. */\n convertedAtMs: number\n /** The model this credit was decided under. */\n model: string\n /** The window, in days, it was decided inside. */\n windowDays: number\n}\n\n/**\n * The document id for one conversion.\n *\n * `{kind}:{refId}`, so the same submission credited twice is the same\n * document — which is what makes {@link attributeCampaignConversion}'s\n * `create()` idempotent. Scoped by kind because the ids come from different\n * collections and a booking id has no reason to be distinct from a form\n * submission's.\n */\nexport function campaignConversionId(\n kind: CampaignConversionKind,\n refId: string,\n): string {\n return `${kind}:${refId}`\n}\n\n/**\n * The campaign touch to credit a conversion happening now, or `null`.\n *\n * Called ONCE per conversion request, at the door, and the result handed to\n * every writer beneath it. See the module docblock for the cost argument.\n *\n * @returns the winning touch, or `null` when neither channel has one inside\n * the window — which is the ordinary case and writes nothing.\n */\nexport async function resolveCampaignTouch(\n options: {\n hostId: string\n /**\n * The wire form the conversion request carried, when the door reports\n * one. Re-parsed through the allowlist that wrote it rather than trusted:\n * it arrives from the visitor's browser, so it may claim no more than an\n * inbound URL could.\n */\n wire?: unknown\n /** The address the visitor identified with, raw. */\n email?: unknown\n /** When they identified themselves. Defaults to now. */\n atMs?: number\n },\n firestore?: any,\n): Promise<ResolvedCampaignTouch | null> {\n try {\n const hostId = String(options.hostId ?? '')\n if (!isDocumentId(hostId)) return null\n const convertedAtMs = Number(options.atMs ?? Date.now())\n if (!Number.isFinite(convertedAtMs) || convertedAtMs <= 0) return null\n\n // The window is enforced inside the parser, so an expired or\n // future-dated wire value answers null here exactly as it does in the\n // browser that decided whether to send it.\n const web: CampaignTouch | null = parseCampaignTouch(\n options.wire,\n convertedAtMs,\n )\n\n const key = personKey(options.email)\n /*\n * The email channel is only askable of somebody who named an address, and\n * that is the whole reason this join needs the web channel at all. No\n * address means no keyed read: the ordinary anonymous conversion costs\n * nothing here.\n */\n const emailTouch = key\n ? await readEmailCampaignTouch(\n String(options.email ?? ''),\n hostId,\n firestore ?? defaultFirestore(),\n )\n : null\n const emailInWindow =\n emailTouch &&\n isDocumentId(emailTouch.campaignId) &&\n emailTouchIsInWindow(emailTouch.clickedAtMs, convertedAtMs)\n ? emailTouch\n : null\n\n /*\n * LAST TOUCH decides, and a tie goes to email. A tie is only reachable\n * when the two instants are the same millisecond, and in that case the\n * email touch is the better evidence: it was recorded by the provider's\n * own click event on the server, while the web touch is a value the\n * visitor's device supplied.\n */\n const emailWins =\n emailInWindow && (!web || emailInWindow.clickedAtMs >= web.atMs)\n if (emailWins) {\n return {\n channel: 'email',\n campaignId: emailInWindow.campaignId,\n touchedAtMs: emailInWindow.clickedAtMs,\n ...(key ? { personKey: key } : {}),\n }\n }\n if (!web) return null\n return {\n channel: 'web',\n ...(web.source ? { source: web.source } : {}),\n ...(web.medium ? { medium: web.medium } : {}),\n ...(web.campaign ? { campaign: web.campaign } : {}),\n touchedAtMs: web.atMs,\n ...(key ? { personKey: key } : {}),\n }\n } catch (error) {\n console.error('resolveCampaignTouch failed', error)\n return null\n }\n}\n\n/**\n * Credits one conversion to the campaign the visitor last came from.\n *\n * ## Why the record is a document and not a field\n *\n * The revenue join's three reasons, and the first is decisive here too.\n * `create()` fails when the document already exists, which is exact\n * idempotency for free: a retried form POST, a redelivered booking webhook\n * and a re-run capture all land here a second time and must all leave the\n * rollup where they found it. Second, the four kinds live in four different\n * collections — a contact is org-scoped and shared across every site in the\n * org — so there is no one document a field could go on. Third, the\n * submission, the lead and the booking are read by the inbox, the export and\n * the fulfilment path, and none of them wants a field about marketing.\n *\n * ## The rollup exists for the EMAIL channel only\n *\n * A campaign document is a real entity with a real id, so its conversions\n * roll up under it beside the revenue the same join already credits it with.\n * A web campaign is a LABEL the marketer typed into a URL, with no document,\n * no id and no bound on how many distinct values exist — the same unbounded\n * key space the analytics collector caps its per-day label map against. A\n * rollup keyed on it would be a map anybody who can vary a query string can\n * grow. So the web channel's records stand on their own and a report reads\n * them; see the module's report seam note.\n *\n * @returns the record written, or `null` when nothing was credited.\n */\nexport async function attributeCampaignConversion(\n options: {\n hostId: string\n kind: CampaignConversionKind\n /** The submission, lead, contact or booking being credited. */\n refId: string\n /** The touch {@link resolveCampaignTouch} picked, or `null` for direct. */\n touch: ResolvedCampaignTouch | null | undefined\n /** When the visitor became identifiable. Defaults to now. */\n convertedAtMs?: number\n },\n firestore?: any,\n): Promise<CampaignConversionRecord | null> {\n try {\n const { touch } = options\n if (!touch) return null\n const hostId = String(options.hostId ?? '')\n const refId = String(options.refId ?? '')\n if (!isDocumentId(hostId) || !isDocumentId(refId)) return null\n const convertedAtMs = Number(options.convertedAtMs ?? Date.now())\n if (!Number.isFinite(convertedAtMs) || convertedAtMs <= 0) return null\n // Re-checked at the write rather than trusted from the resolve. The two\n // are separated by the writes that produced the thing being credited, and\n // a touch that has aged out between them is a touch outside the window.\n if (!emailTouchIsInWindow(touch.touchedAtMs, convertedAtMs)) return null\n\n const record: CampaignConversionRecord = {\n kind: options.kind,\n refId,\n channel: touch.channel,\n ...(touch.campaignId ? { campaignId: touch.campaignId } : {}),\n ...(touch.source ? { source: touch.source } : {}),\n ...(touch.medium ? { medium: touch.medium } : {}),\n ...(touch.campaign ? { campaign: touch.campaign } : {}),\n touchedAtMs: touch.touchedAtMs,\n convertedAtMs,\n model: ATTRIBUTION_MODEL,\n windowDays: ATTRIBUTION_WINDOW_DAYS,\n ...(touch.personKey ? { personKey: touch.personKey } : {}),\n }\n\n const db = firestore ?? defaultFirestore()\n const hostRef = db.collection('hosts').doc(hostId)\n /*\n * `create()`, never `set()` — the revenue join's argument, unchanged. The\n * ALREADY_EXISTS failure IS the idempotency, and the rollup below is\n * reached only when the create succeeded, so the two can never disagree\n * about whether this conversion was counted.\n */\n try {\n await hostRef\n .collection(CAMPAIGN_ATTRIBUTIONS_COLLECTION)\n .doc(campaignConversionId(options.kind, refId))\n .create({ ...record, createdAt: FieldValue.serverTimestamp() })\n } catch {\n return null\n }\n\n if (touch.channel === 'email' && touch.campaignId) {\n /*\n * A merge-set that CREATES, for the reason `reports/revenue` gives: this\n * writes a document UNDER a campaign, so a campaign that no longer\n * exists gains an orphaned report rather than being resurrected as a\n * husk in the merchant's history — and the campaign was proven to exist\n * when its click wrote the touch.\n *\n * Every figure is an increment, so two conversions settling at once\n * both land.\n */\n await hostRef\n .collection('campaigns')\n .doc(touch.campaignId)\n .collection('reports')\n .doc(CAMPAIGN_CONVERSIONS_REPORT_DOC)\n .set(\n {\n model: ATTRIBUTION_MODEL,\n windowDays: ATTRIBUTION_WINDOW_DAYS,\n byKind: { [options.kind]: FieldValue.increment(1) },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n }\n\n return record\n } catch (error) {\n console.error('attributeCampaignConversion failed', error)\n return null\n }\n}\n"],"names":["FieldValue","parseCampaignTouch","EMAIL_ATTRIBUTION_MODEL","ATTRIBUTION_MODEL","EMAIL_ATTRIBUTION_WINDOW_DAYS","ATTRIBUTION_WINDOW_DAYS","emailTouchIsInWindow","personKey","CAMPAIGN_ATTRIBUTIONS_COLLECTION","eraseCampaignAttributionsForPersonKey","readEmailCampaignTouch","isDocumentId","firebaseAdmin","defaultFirestore","app","firestore","CAMPAIGN_CONVERSIONS_REPORT_DOC","campaignConversionId","kind","refId","resolveCampaignTouch","options","hostId","String","convertedAtMs","Number","atMs","Date","now","isFinite","web","wire","key","email","emailTouch","emailInWindow","campaignId","clickedAtMs","emailWins","channel","touchedAtMs","source","medium","campaign","error","console","attributeCampaignConversion","touch","record","model","windowDays","db","hostRef","collection","doc","create","createdAt","serverTimestamp","set","byKind","increment","updatedAt","merge"],"mappings":"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"}