@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,259 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * The do-not-contact list, `contactSuppressions/{nationalKey}` (AGL-1592).
19
+ *
20
+ * WHY THIS EXISTS
21
+ *
22
+ * Privacy Policy v4 §11 tells users they may opt out of marketing calls and
23
+ * texts, and may ask us to delete the phone number we hold. Neither had a
24
+ * mechanism. This is the first half — the durable record that makes "never
25
+ * contact me again" survivable. The second half (`forgetUserPhoneNumber`)
26
+ * lives in `user-profiles.ts`, next to the seed it has to defeat.
27
+ *
28
+ * THE TENSION, AND HOW IT IS RESOLVED
29
+ *
30
+ * "Delete my phone number" and "stop contacting me" are DIFFERENT REQUESTS
31
+ * WITH OPPOSITE DATA OUTCOMES, and §11 currently offers both in one sentence.
32
+ * A suppression list that forgets the identifier cannot suppress anything: the
33
+ * only way to guarantee a number is never dialled is to keep the number and
34
+ * check it before dialling. Forgetting it does not protect the person — it
35
+ * removes the one artefact that could have protected them, and the next list
36
+ * imported from a customer's CRM re-adds them silently.
37
+ *
38
+ * So the two requests are implemented as two operations:
39
+ *
40
+ * - "stop contacting me" → `suppressPhoneContact`. The number is RETAINED,
41
+ * here and only here. Whatever copy the profile
42
+ * holds is left alone; they did not ask us to
43
+ * drop it.
44
+ * - "delete my number" → `forgetUserPhoneNumber` (user-profiles.ts).
45
+ * The profile copy is cleared, the account is
46
+ * marked so an IdP cannot re-assert it, AND a
47
+ * suppression record is written here with
48
+ * `erasePhoneOnFile: true`.
49
+ *
50
+ * That last clause is the whole point: a number we no longer hold anywhere is
51
+ * a number we cannot recognise, and therefore a number we will happily dial
52
+ * the day it arrives from somewhere else. Deletion WITHOUT suppression is the
53
+ * outcome that gets the person called. So the minimal retained record is not
54
+ * a hedge against the deletion request — it is the only way to honour what the
55
+ * person actually wanted.
56
+ *
57
+ * This carve-out is the standard one and is not optional: CCPA §1798.105(d)
58
+ * lets a business keep what it needs to honour an opt-out, and the TSR's
59
+ * entity-specific do-not-call duty is unmeetable without a retained list.
60
+ * §11's current wording does not say so, and should — see the wording proposal
61
+ * recorded on AGL-1592.
62
+ *
63
+ * WHY KEYED ON THE NUMBER AND NOT ON A `uid`
64
+ *
65
+ * The number is what gets dialled. At dial time an outbound programme holds a
66
+ * phone number and often nothing else, so the check has to be answerable from
67
+ * the number alone — one `get()` by document id, no query, no composite index
68
+ * that could go missing and fail the lookup open.
69
+ *
70
+ * It is also the only key that spans accounts. One human can reach us as a
71
+ * self-serve signup AND as an SSO account in their employer's GCIP tenant,
72
+ * with a different `uid` in each and a third if they are ever reprovisioned.
73
+ * A `uid`-keyed record would suppress one of those and leave the same person
74
+ * reachable on the same handset through the others.
75
+ *
76
+ * WHY THE RECORD IS NOT HASHED
77
+ *
78
+ * A phone number is a low-entropy identifier — NANP is ~10^10 values, which a
79
+ * laptop enumerates against SHA-256 in minutes. Hashing it would buy the
80
+ * appearance of de-identification and none of the substance, while making the
81
+ * record unauditable by the person who has to honour it. The collection is
82
+ * Admin-SDK-only in rules (`read, write: if false`), which is strictly less
83
+ * exposed than `users/{uid}.phoneNumber` already is — that one is readable by
84
+ * its owner.
85
+ *
86
+ * WHY A REVOCATION IS A FIELD AND NOT A DELETE
87
+ *
88
+ * Same reasoning as the clickwrap acceptances: the record exists to be
89
+ * evidence. Deleting it on opt-in destroys the proof that the opt-out was
90
+ * honoured for the period it was in force. `revokedAt` is set instead, and
91
+ * every read treats a revoked record as not suppressed.
92
+ */ import { normalizePhone } from "@aglyn/aglyn/server";
93
+ import { FieldValue } from "firebase-admin/firestore";
94
+ import firebaseAdmin from "./firebase-admin.js";
95
+ const firestore = ()=>firebaseAdmin.app().firestore();
96
+ export const CONTACT_SUPPRESSIONS_COLLECTION = 'contactSuppressions';
97
+ /**
98
+ * The channels §11 names. Kept as an explicit list rather than a boolean
99
+ * because they are governed differently — a STOP text is a texting opt-out by
100
+ * statute, and reading it as a calling opt-out too would be us inventing a
101
+ * request the person did not make. Callers that mean both say both.
102
+ */ export const CONTACT_CHANNELS = [
103
+ 'calls',
104
+ 'texts'
105
+ ];
106
+ /**
107
+ * Document id for an E.164 number: the digits, with the `+` dropped.
108
+ *
109
+ * The `+` is a legal Firestore id character, but it is also the one character
110
+ * that means "space" in a form-encoded query string, so a number carried
111
+ * through a URL somewhere down the line would silently become a different
112
+ * number. Dropping it here costs nothing — the full E.164 is stored in the
113
+ * document — and removes the whole class of bug in advance.
114
+ *
115
+ * @returns the id, or `null` when the input is not a number we can be sure of.
116
+ * Never a best-guess id: suppressing the wrong subscriber and
117
+ * believing you suppressed the right one is worse than refusing.
118
+ */ export function contactSuppressionKey(phoneNumber) {
119
+ const e164 = normalizePhone(phoneNumber);
120
+ return e164 ? e164.slice(1) : null;
121
+ }
122
+ /**
123
+ * Record a do-not-contact request. Idempotent by document id, so a retried
124
+ * request or a second STOP cannot produce a second record.
125
+ *
126
+ * Merging rather than replacing, and the channel list UNIONS with whatever is
127
+ * already recorded: someone who opted out of texts in March and calls in June
128
+ * is opted out of both in July. Narrowing a suppression is `releasePhoneContact`
129
+ * and nothing else — no write on this path may ever make a person MORE
130
+ * reachable than they already were.
131
+ *
132
+ * A previously revoked record is un-revoked, because a fresh request is a
133
+ * fresh request.
134
+ *
135
+ * @throws when the number cannot be normalized. Deliberately NOT best-effort:
136
+ * a silently dropped opt-out is the exact failure this issue exists to
137
+ * fix, so the caller has to see it.
138
+ */ export async function suppressPhoneContact(input) {
139
+ var _input_firestore, _ref, _input_uid, _input_recordedByUid, _input_note;
140
+ var _input_channels;
141
+ const e164 = normalizePhone(input.phoneNumber);
142
+ const key = e164 ? e164.slice(1) : null;
143
+ if (!e164 || !key) {
144
+ throw new Error(`[contact-suppression] cannot normalize phone number for suppression`);
145
+ }
146
+ const db = (_input_firestore = input.firestore) != null ? _input_firestore : firestore();
147
+ const ref = db.collection(CONTACT_SUPPRESSIONS_COLLECTION).doc(key);
148
+ const snapshot = await ref.get();
149
+ const requested = (((_input_channels = input.channels) == null ? void 0 : _input_channels.length) ? input.channels : CONTACT_CHANNELS).filter((channel)=>CONTACT_CHANNELS.includes(channel));
150
+ const existing = Array.isArray(snapshot.get('channels')) ? snapshot.get('channels') : [];
151
+ const channels = CONTACT_CHANNELS.filter((channel)=>requested.includes(channel) || existing.includes(channel));
152
+ await ref.set(_extends({
153
+ phoneNumber: e164,
154
+ channels,
155
+ source: input.source,
156
+ uid: (_ref = (_input_uid = input.uid) != null ? _input_uid : snapshot.get('uid')) != null ? _ref : null,
157
+ recordedByUid: (_input_recordedByUid = input.recordedByUid) != null ? _input_recordedByUid : null,
158
+ note: (_input_note = input.note) != null ? _input_note : null,
159
+ // Sticky: an erasure request recorded in March is still an erasure
160
+ // request after a plain "stop texting me" arrives in June. Only an
161
+ // explicit re-supply of the number clears it, and that happens on the
162
+ // profile, not here.
163
+ erasePhoneOnFile: input.erasePhoneOnFile === true || snapshot.get('erasePhoneOnFile') === true,
164
+ revokedAt: null,
165
+ updatedAt: FieldValue.serverTimestamp()
166
+ }, snapshot.exists ? {} : {
167
+ createdAt: FieldValue.serverTimestamp()
168
+ }), {
169
+ merge: true
170
+ });
171
+ return {
172
+ phoneNumber: e164,
173
+ channels,
174
+ created: !snapshot.exists
175
+ };
176
+ }
177
+ /**
178
+ * Opt back in (a START/UNSTOP reply, or a staff correction of a mis-recorded
179
+ * opt-out). The record is kept and marked revoked, never deleted.
180
+ *
181
+ * IMPORTANT — this does NOT create consent. Clearing a suppression only
182
+ * removes a prohibition; it does not manufacture the prior express written
183
+ * consent that TCPA requires before a marketing call or text may be placed at
184
+ * all. That mechanism does not exist yet (AGL-1564, and the privacy benchmark
185
+ * items 9–10), so after this call the correct number of marketing messages to
186
+ * send is still zero.
187
+ *
188
+ * @returns false when there was no record to revoke.
189
+ */ export async function releasePhoneContact(input) {
190
+ var _input_firestore, _input_releasedByUid, _input_note;
191
+ const key = contactSuppressionKey(input.phoneNumber);
192
+ if (!key) return false;
193
+ const db = (_input_firestore = input.firestore) != null ? _input_firestore : firestore();
194
+ const ref = db.collection(CONTACT_SUPPRESSIONS_COLLECTION).doc(key);
195
+ const snapshot = await ref.get();
196
+ if (!snapshot.exists || snapshot.get('revokedAt')) return false;
197
+ await ref.set({
198
+ revokedAt: FieldValue.serverTimestamp(),
199
+ revokedByUid: (_input_releasedByUid = input.releasedByUid) != null ? _input_releasedByUid : null,
200
+ revokedNote: (_input_note = input.note) != null ? _input_note : null
201
+ }, {
202
+ merge: true
203
+ });
204
+ return true;
205
+ }
206
+ /**
207
+ * The dial-time gate. Every outbound marketing call or text MUST pass through
208
+ * this before the number reaches a provider.
209
+ *
210
+ * FAILS CLOSED. A read that throws — rules, quota, a cold Firestore — answers
211
+ * `true`, meaning "treat as suppressed". The alternative is a list outage that
212
+ * silently turns into a round of calls to people who asked us never to call
213
+ * again, which is a statutory violation per call rather than a delayed
214
+ * campaign. Never "fix" a flaky read here by returning false.
215
+ */ export async function isPhoneContactSuppressed(phoneNumber, channel, injectedFirestore) {
216
+ const key = contactSuppressionKey(phoneNumber);
217
+ // An unrecognizable number is one we cannot check against the list, so it is
218
+ // one we must not dial. Same fail-closed rule.
219
+ if (!key) return true;
220
+ try {
221
+ const db = injectedFirestore != null ? injectedFirestore : firestore();
222
+ const snapshot = await db.collection(CONTACT_SUPPRESSIONS_COLLECTION).doc(key).get();
223
+ if (!snapshot.exists) return false;
224
+ if (snapshot.get('revokedAt')) return false;
225
+ if (!channel) return true;
226
+ const channels = snapshot.get('channels');
227
+ return Array.isArray(channels) ? channels.includes(channel) : true;
228
+ } catch (error) {
229
+ console.error('[contact-suppression] lookup failed; failing closed', error);
230
+ return true;
231
+ }
232
+ }
233
+ /**
234
+ * Read one record, for the staff surface and for `seedUserProfile`'s
235
+ * re-assertion guard.
236
+ */ export async function getContactSuppression(phoneNumber, injectedFirestore) {
237
+ const key = contactSuppressionKey(phoneNumber);
238
+ if (!key) return null;
239
+ const db = injectedFirestore != null ? injectedFirestore : firestore();
240
+ const snapshot = await db.collection(CONTACT_SUPPRESSIONS_COLLECTION).doc(key).get();
241
+ if (!snapshot.exists) return null;
242
+ return _extends({
243
+ $id: snapshot.id
244
+ }, snapshot.data());
245
+ }
246
+ /**
247
+ * The staff queue, newest first. Ordered by `updatedAt` so a re-recorded or
248
+ * revoked entry surfaces again — the operator's question is "what changed",
249
+ * not "what was first written".
250
+ */ export async function listContactSuppressions(options) {
251
+ var _ref, _ref1;
252
+ const db = (_ref = options == null ? void 0 : options.firestore) != null ? _ref : firestore();
253
+ const snapshot = await db.collection(CONTACT_SUPPRESSIONS_COLLECTION).orderBy('updatedAt', 'desc').limit(Math.min(Math.max((_ref1 = options == null ? void 0 : options.limit) != null ? _ref1 : 100, 1), 500)).get();
254
+ return snapshot.docs.map((doc)=>_extends({
255
+ $id: doc.id
256
+ }, doc.data()));
257
+ }
258
+
259
+ //# sourceMappingURL=contact-suppression.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/contact-suppression.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 * The do-not-contact list, `contactSuppressions/{nationalKey}` (AGL-1592).\n *\n * WHY THIS EXISTS\n *\n * Privacy Policy v4 §11 tells users they may opt out of marketing calls and\n * texts, and may ask us to delete the phone number we hold. Neither had a\n * mechanism. This is the first half — the durable record that makes \"never\n * contact me again\" survivable. The second half (`forgetUserPhoneNumber`)\n * lives in `user-profiles.ts`, next to the seed it has to defeat.\n *\n * THE TENSION, AND HOW IT IS RESOLVED\n *\n * \"Delete my phone number\" and \"stop contacting me\" are DIFFERENT REQUESTS\n * WITH OPPOSITE DATA OUTCOMES, and §11 currently offers both in one sentence.\n * A suppression list that forgets the identifier cannot suppress anything: the\n * only way to guarantee a number is never dialled is to keep the number and\n * check it before dialling. Forgetting it does not protect the person — it\n * removes the one artefact that could have protected them, and the next list\n * imported from a customer's CRM re-adds them silently.\n *\n * So the two requests are implemented as two operations:\n *\n * - \"stop contacting me\" → `suppressPhoneContact`. The number is RETAINED,\n * here and only here. Whatever copy the profile\n * holds is left alone; they did not ask us to\n * drop it.\n * - \"delete my number\" → `forgetUserPhoneNumber` (user-profiles.ts).\n * The profile copy is cleared, the account is\n * marked so an IdP cannot re-assert it, AND a\n * suppression record is written here with\n * `erasePhoneOnFile: true`.\n *\n * That last clause is the whole point: a number we no longer hold anywhere is\n * a number we cannot recognise, and therefore a number we will happily dial\n * the day it arrives from somewhere else. Deletion WITHOUT suppression is the\n * outcome that gets the person called. So the minimal retained record is not\n * a hedge against the deletion request — it is the only way to honour what the\n * person actually wanted.\n *\n * This carve-out is the standard one and is not optional: CCPA §1798.105(d)\n * lets a business keep what it needs to honour an opt-out, and the TSR's\n * entity-specific do-not-call duty is unmeetable without a retained list.\n * §11's current wording does not say so, and should — see the wording proposal\n * recorded on AGL-1592.\n *\n * WHY KEYED ON THE NUMBER AND NOT ON A `uid`\n *\n * The number is what gets dialled. At dial time an outbound programme holds a\n * phone number and often nothing else, so the check has to be answerable from\n * the number alone — one `get()` by document id, no query, no composite index\n * that could go missing and fail the lookup open.\n *\n * It is also the only key that spans accounts. One human can reach us as a\n * self-serve signup AND as an SSO account in their employer's GCIP tenant,\n * with a different `uid` in each and a third if they are ever reprovisioned.\n * A `uid`-keyed record would suppress one of those and leave the same person\n * reachable on the same handset through the others.\n *\n * WHY THE RECORD IS NOT HASHED\n *\n * A phone number is a low-entropy identifier — NANP is ~10^10 values, which a\n * laptop enumerates against SHA-256 in minutes. Hashing it would buy the\n * appearance of de-identification and none of the substance, while making the\n * record unauditable by the person who has to honour it. The collection is\n * Admin-SDK-only in rules (`read, write: if false`), which is strictly less\n * exposed than `users/{uid}.phoneNumber` already is — that one is readable by\n * its owner.\n *\n * WHY A REVOCATION IS A FIELD AND NOT A DELETE\n *\n * Same reasoning as the clickwrap acceptances: the record exists to be\n * evidence. Deleting it on opt-in destroys the proof that the opt-out was\n * honoured for the period it was in force. `revokedAt` is set instead, and\n * every read treats a revoked record as not suppressed.\n */\n\nimport { normalizePhone } from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport firebaseAdmin from './firebase-admin'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nexport const CONTACT_SUPPRESSIONS_COLLECTION = 'contactSuppressions'\n\n/**\n * The channels §11 names. Kept as an explicit list rather than a boolean\n * because they are governed differently — a STOP text is a texting opt-out by\n * statute, and reading it as a calling opt-out too would be us inventing a\n * request the person did not make. Callers that mean both say both.\n */\nexport const CONTACT_CHANNELS = ['calls', 'texts'] as const\nexport type ContactChannel = (typeof CONTACT_CHANNELS)[number]\n\n/** Where the request came in. Every route §11 offers, plus staff-initiated. */\nexport type ContactSuppressionSource =\n /** Reply STOP (or another CTIA keyword) to a marketing text. */\n | 'sms-keyword'\n /** Emailed privacy@aglyn.com and a human recorded it. */\n | 'email'\n /** Said so on a call and a human recorded it. */\n | 'verbal'\n /** The user asked us to delete the number they had on file. */\n | 'erasure-request'\n /** Recorded by staff for any other reason (a complaint, a bounce, a court). */\n | 'staff'\n\nexport interface SuppressPhoneContactInput {\n /** Anything `normalizePhone` accepts; stored as E.164. */\n phoneNumber: string\n /** Defaults to BOTH channels — the conservative reading of an ambiguous ask. */\n channels?: ContactChannel[]\n source: ContactSuppressionSource\n /** The account the request was matched to, when it was matched to one. */\n uid?: string | null\n /** Staff member who took the email or the call. */\n recordedByUid?: string | null\n /** Free text: what they actually said, the ticket id, the case number. */\n note?: string | null\n /**\n * True only when the person also asked us to stop holding the number.\n * `seedUserProfile` reads this to refuse an IdP re-assertion, so it is the\n * difference between \"do not call me\" and \"do not keep my number\".\n */\n erasePhoneOnFile?: boolean\n /** Injectable for tests; defaults to the admin app's Firestore. */\n firestore?: any\n}\n\nexport interface ContactSuppressionRecord {\n phoneNumber: string\n channels: ContactChannel[]\n source: ContactSuppressionSource\n uid: string | null\n recordedByUid: string | null\n note: string | null\n erasePhoneOnFile: boolean\n /** Non-null once the person opted back in. A revoked record does not suppress. */\n revokedAt: unknown | null\n}\n\n/**\n * Document id for an E.164 number: the digits, with the `+` dropped.\n *\n * The `+` is a legal Firestore id character, but it is also the one character\n * that means \"space\" in a form-encoded query string, so a number carried\n * through a URL somewhere down the line would silently become a different\n * number. Dropping it here costs nothing — the full E.164 is stored in the\n * document — and removes the whole class of bug in advance.\n *\n * @returns the id, or `null` when the input is not a number we can be sure of.\n * Never a best-guess id: suppressing the wrong subscriber and\n * believing you suppressed the right one is worse than refusing.\n */\nexport function contactSuppressionKey(\n phoneNumber: string | null | undefined,\n): string | null {\n const e164 = normalizePhone(phoneNumber)\n return e164 ? e164.slice(1) : null\n}\n\n/**\n * Record a do-not-contact request. Idempotent by document id, so a retried\n * request or a second STOP cannot produce a second record.\n *\n * Merging rather than replacing, and the channel list UNIONS with whatever is\n * already recorded: someone who opted out of texts in March and calls in June\n * is opted out of both in July. Narrowing a suppression is `releasePhoneContact`\n * and nothing else — no write on this path may ever make a person MORE\n * reachable than they already were.\n *\n * A previously revoked record is un-revoked, because a fresh request is a\n * fresh request.\n *\n * @throws when the number cannot be normalized. Deliberately NOT best-effort:\n * a silently dropped opt-out is the exact failure this issue exists to\n * fix, so the caller has to see it.\n */\nexport async function suppressPhoneContact(\n input: SuppressPhoneContactInput,\n): Promise<{ phoneNumber: string; channels: ContactChannel[]; created: boolean }> {\n const e164 = normalizePhone(input.phoneNumber)\n const key = e164 ? e164.slice(1) : null\n if (!e164 || !key) {\n throw new Error(\n `[contact-suppression] cannot normalize phone number for suppression`,\n )\n }\n const db = input.firestore ?? firestore()\n const ref = db.collection(CONTACT_SUPPRESSIONS_COLLECTION).doc(key)\n const snapshot = await ref.get()\n\n const requested = (input.channels?.length ? input.channels : CONTACT_CHANNELS)\n .filter((channel): channel is ContactChannel =>\n (CONTACT_CHANNELS as readonly string[]).includes(channel),\n )\n const existing: ContactChannel[] = Array.isArray(snapshot.get('channels'))\n ? snapshot.get('channels')\n : []\n const channels = CONTACT_CHANNELS.filter(\n (channel) => requested.includes(channel) || existing.includes(channel),\n )\n\n await ref.set(\n {\n phoneNumber: e164,\n channels,\n source: input.source,\n uid: input.uid ?? snapshot.get('uid') ?? null,\n recordedByUid: input.recordedByUid ?? null,\n note: input.note ?? null,\n // Sticky: an erasure request recorded in March is still an erasure\n // request after a plain \"stop texting me\" arrives in June. Only an\n // explicit re-supply of the number clears it, and that happens on the\n // profile, not here.\n erasePhoneOnFile:\n input.erasePhoneOnFile === true || snapshot.get('erasePhoneOnFile') === true,\n revokedAt: null,\n updatedAt: FieldValue.serverTimestamp(),\n ...(snapshot.exists ? {} : { createdAt: FieldValue.serverTimestamp() }),\n },\n { merge: true },\n )\n return { phoneNumber: e164, channels, created: !snapshot.exists }\n}\n\n/**\n * Opt back in (a START/UNSTOP reply, or a staff correction of a mis-recorded\n * opt-out). The record is kept and marked revoked, never deleted.\n *\n * IMPORTANT — this does NOT create consent. Clearing a suppression only\n * removes a prohibition; it does not manufacture the prior express written\n * consent that TCPA requires before a marketing call or text may be placed at\n * all. That mechanism does not exist yet (AGL-1564, and the privacy benchmark\n * items 9–10), so after this call the correct number of marketing messages to\n * send is still zero.\n *\n * @returns false when there was no record to revoke.\n */\nexport async function releasePhoneContact(input: {\n phoneNumber: string\n releasedByUid?: string | null\n note?: string | null\n firestore?: any\n}): Promise<boolean> {\n const key = contactSuppressionKey(input.phoneNumber)\n if (!key) return false\n const db = input.firestore ?? firestore()\n const ref = db.collection(CONTACT_SUPPRESSIONS_COLLECTION).doc(key)\n const snapshot = await ref.get()\n if (!snapshot.exists || snapshot.get('revokedAt')) return false\n await ref.set(\n {\n revokedAt: FieldValue.serverTimestamp(),\n revokedByUid: input.releasedByUid ?? null,\n revokedNote: input.note ?? null,\n },\n { merge: true },\n )\n return true\n}\n\n/**\n * The dial-time gate. Every outbound marketing call or text MUST pass through\n * this before the number reaches a provider.\n *\n * FAILS CLOSED. A read that throws — rules, quota, a cold Firestore — answers\n * `true`, meaning \"treat as suppressed\". The alternative is a list outage that\n * silently turns into a round of calls to people who asked us never to call\n * again, which is a statutory violation per call rather than a delayed\n * campaign. Never \"fix\" a flaky read here by returning false.\n */\nexport async function isPhoneContactSuppressed(\n phoneNumber: string | null | undefined,\n channel?: ContactChannel,\n injectedFirestore?: any,\n): Promise<boolean> {\n const key = contactSuppressionKey(phoneNumber)\n // An unrecognizable number is one we cannot check against the list, so it is\n // one we must not dial. Same fail-closed rule.\n if (!key) return true\n try {\n const db = injectedFirestore ?? firestore()\n const snapshot = await db\n .collection(CONTACT_SUPPRESSIONS_COLLECTION)\n .doc(key)\n .get()\n if (!snapshot.exists) return false\n if (snapshot.get('revokedAt')) return false\n if (!channel) return true\n const channels = snapshot.get('channels')\n return Array.isArray(channels) ? channels.includes(channel) : true\n } catch (error) {\n console.error('[contact-suppression] lookup failed; failing closed', error)\n return true\n }\n}\n\n/**\n * Read one record, for the staff surface and for `seedUserProfile`'s\n * re-assertion guard.\n */\nexport async function getContactSuppression(\n phoneNumber: string | null | undefined,\n injectedFirestore?: any,\n): Promise<(ContactSuppressionRecord & { $id: string }) | null> {\n const key = contactSuppressionKey(phoneNumber)\n if (!key) return null\n const db = injectedFirestore ?? firestore()\n const snapshot = await db\n .collection(CONTACT_SUPPRESSIONS_COLLECTION)\n .doc(key)\n .get()\n if (!snapshot.exists) return null\n return { $id: snapshot.id, ...(snapshot.data() as ContactSuppressionRecord) }\n}\n\n/**\n * The staff queue, newest first. Ordered by `updatedAt` so a re-recorded or\n * revoked entry surfaces again — the operator's question is \"what changed\",\n * not \"what was first written\".\n */\nexport async function listContactSuppressions(options?: {\n limit?: number\n firestore?: any\n}): Promise<Array<ContactSuppressionRecord & { $id: string }>> {\n const db = options?.firestore ?? firestore()\n const snapshot = await db\n .collection(CONTACT_SUPPRESSIONS_COLLECTION)\n .orderBy('updatedAt', 'desc')\n .limit(Math.min(Math.max(options?.limit ?? 100, 1), 500))\n .get()\n return snapshot.docs.map((doc: any) => ({\n $id: doc.id,\n ...(doc.data() as ContactSuppressionRecord),\n }))\n}\n"],"names":["normalizePhone","FieldValue","firebaseAdmin","firestore","app","CONTACT_SUPPRESSIONS_COLLECTION","CONTACT_CHANNELS","contactSuppressionKey","phoneNumber","e164","slice","suppressPhoneContact","input","key","Error","db","ref","collection","doc","snapshot","get","requested","channels","length","filter","channel","includes","existing","Array","isArray","set","source","uid","recordedByUid","note","erasePhoneOnFile","revokedAt","updatedAt","serverTimestamp","exists","createdAt","merge","created","releasePhoneContact","revokedByUid","releasedByUid","revokedNote","isPhoneContactSuppressed","injectedFirestore","error","console","getContactSuppression","$id","id","data","listContactSuppressions","options","orderBy","limit","Math","min","max","docs","map"],"mappings":"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"}
@@ -0,0 +1,86 @@
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 { type CrmActivityLink } from '@aglyn/aglyn/server';
18
+ export interface CrmBookingRecordInput {
19
+ hostId: string;
20
+ /**
21
+ * The owning org, as `getOrgForHost` returns it — the plugin set, the
22
+ * consent groups and the scope default are all read off it.
23
+ */
24
+ org: Record<string, unknown> | null | undefined;
25
+ orgId: string;
26
+ /** The booking as its row was written. */
27
+ booking: {
28
+ id: string;
29
+ serviceId?: string;
30
+ serviceName?: string;
31
+ email?: string;
32
+ startsAtMs: number;
33
+ endsAtMs: number;
34
+ /** The `crm` query value the link carried, kept on the row. */
35
+ crmRef?: string | null;
36
+ };
37
+ /** The service as stored: its two CRM switches and its timezone. */
38
+ service: {
39
+ name?: string;
40
+ timezone?: string;
41
+ crmFollowUpTask?: boolean;
42
+ crmMeetingActivity?: boolean;
43
+ } | null | undefined;
44
+ /**
45
+ * The host document's plugin fields, when the caller already holds them;
46
+ * read here otherwise. The deny-list is per site, and a site that has
47
+ * switched the CRM off must file nothing.
48
+ */
49
+ host?: {
50
+ disabledPlugins?: string[];
51
+ enabledPlugins?: string[];
52
+ } | null;
53
+ }
54
+ export type CrmBookingRecordOutcome = {
55
+ filed: false;
56
+ reason:
57
+ /** The CRM plugin does not run on this site. */
58
+ 'crm-off'
59
+ /**
60
+ * The org's plan does not carry the CRM suite, whose activities and
61
+ * tasks these are (AGL-2787).
62
+ */
63
+ | 'not-entitled'
64
+ /** Neither the reference nor the address named a record. */
65
+ | 'no-record'
66
+ /** A meeting for this booking is already on the timeline. */
67
+ | 'already-filed'
68
+ /** The service asks for neither a meeting nor a follow-up. */
69
+ | 'nothing-to-file'
70
+ /** A read or a write threw; logged, and nothing is promised. */
71
+ | 'failed';
72
+ } | {
73
+ filed: true;
74
+ /** How the record was found. */
75
+ matchedBy: 'crmRef' | 'email';
76
+ link: CrmActivityLink;
77
+ /** The meeting's id, or `null` when the service files no meeting. */
78
+ activityId: string | null;
79
+ /** The follow-up's id, or `null` when the service asks for none. */
80
+ taskId: string | null;
81
+ };
82
+ /**
83
+ * Files what a confirmed booking owes the CRM: the meeting, and the
84
+ * follow-up when the service asks for one. See the module comment.
85
+ */
86
+ export declare function recordCrmBooking(firestore: FirebaseFirestore.Firestore, input: CrmBookingRecordInput): Promise<CrmBookingRecordOutcome>;
@@ -0,0 +1,251 @@
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 { checkEntitlement, consentGroupForHost, CRM_COLLECTIONS, crmActivityLogHasRoom, crmBookingFollowUpDueMs, crmBookingFollowUpTitle, crmBookingMeetingBody, crmScopeTokens, isHostPluginEnabled, parseCrmBookingRef, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
18
+ import { FieldValue } from "firebase-admin/firestore";
19
+ import { findContactByEmail } from "./contact-email-index.js";
20
+ import { countCrmActivitiesForRecord } from "./crm-records.js";
21
+ /**
22
+ * WHAT A BOOKING FILES ON THE CRM RECORD (AGL-2660).
23
+ *
24
+ * A confirmed booking is a meeting the record has on its calendar, and the
25
+ * timeline should say so without a rep logging it by hand. So once a
26
+ * booking's row is written — at creation for a free service, on payment
27
+ * for a paid one — the record is found and a `meeting` activity filed under
28
+ * it, with a follow-up task beside it when the service asks for one.
29
+ *
30
+ * ## Which record
31
+ *
32
+ * The reference the booking link carried wins: `crmRef` names the contact,
33
+ * lead or deal the rep dropped the link from, and a booking made through
34
+ * that link belongs to that record even when the booker typed a different
35
+ * address. Without one — a booking taken off the widget cold — the booker's
36
+ * address is looked up through the org's address index, narrowed to this
37
+ * site, exactly as the automation steps resolve an event's contact. A
38
+ * reference to a record the site cannot see, or that no longer exists,
39
+ * falls through to the address rather than filing under nobody.
40
+ *
41
+ * ## Where it lives
42
+ *
43
+ * In the admin library rather than the Bookings plugin, because the row is
44
+ * a CRM record: its scope, its ceiling and its collections are the CRM's,
45
+ * and a plugin that stamped its own would be a second creator to keep in
46
+ * step with `crmScopeTokens`. The Bookings plugin passes what it already
47
+ * holds and reads the outcome; the CRM plugin is never imported.
48
+ *
49
+ * Firestore is a parameter, as in `crm-records.ts`: the callers hold one,
50
+ * and a module that reached for the Admin app itself would drag it into
51
+ * every spec that only wants the arithmetic.
52
+ *
53
+ * ## Failure posture
54
+ *
55
+ * **Never throws.** A booking that could not be filed on the CRM is still
56
+ * a booking — the guest has the slot and the confirmation — so a failure
57
+ * here is logged and reported in the outcome, never raised into the
58
+ * request that took the booking or the webhook that confirmed it.
59
+ */ /** The CRM plugin's id, as `enabledPlugins` and `disabledPlugins` name it. */ const CRM_PLUGIN_ID = 'crm';
60
+ /**
61
+ * The record a booking belongs to — the reference first, the address second.
62
+ */ async function resolveBookingRecord(firestore, input, groupId) {
63
+ var _hit_data;
64
+ const { hostId, orgId } = input;
65
+ const orgRef = firestore.collection('orgs').doc(orgId);
66
+ const contactsRef = orgRef.collection('contacts');
67
+ const ref = parseCrmBookingRef(input.booking.crmRef);
68
+ if ((ref == null ? void 0 : ref.kind) === 'contact') {
69
+ const contact = await contactsRef.doc(ref.id).get();
70
+ if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {
71
+ var _contact_data;
72
+ const facet = readContactFacet((_contact_data = contact.data()) != null ? _contact_data : {}, groupId);
73
+ return _extends({
74
+ link: _extends({
75
+ contactId: contact.id
76
+ }, facet.companyId ? {
77
+ companyId: facet.companyId
78
+ } : {}),
79
+ matchedBy: 'crmRef'
80
+ }, facet.ownerUid ? {
81
+ ownerUid: facet.ownerUid
82
+ } : {});
83
+ }
84
+ }
85
+ if ((ref == null ? void 0 : ref.kind) === 'deal') {
86
+ const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(ref.id).get();
87
+ if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {
88
+ var _deal_get, _deal_get1, _deal_get2;
89
+ const contactId = String((_deal_get = deal.get('contactId')) != null ? _deal_get : '');
90
+ const companyId = String((_deal_get1 = deal.get('companyId')) != null ? _deal_get1 : '');
91
+ const ownerUid = String((_deal_get2 = deal.get('ownerUid')) != null ? _deal_get2 : '');
92
+ return _extends({
93
+ link: _extends({
94
+ dealId: deal.id
95
+ }, contactId ? {
96
+ contactId
97
+ } : {}, companyId ? {
98
+ companyId
99
+ } : {}),
100
+ matchedBy: 'crmRef'
101
+ }, ownerUid ? {
102
+ ownerUid
103
+ } : {});
104
+ }
105
+ }
106
+ if ((ref == null ? void 0 : ref.kind) === 'lead') {
107
+ // A lead is host-scoped by path and carries no `visibleTo` of its own.
108
+ const lead = await firestore.collection('hosts').doc(hostId).collection('leads').doc(ref.id).get();
109
+ if (lead.exists) {
110
+ var _lead_get;
111
+ const ownerUid = String((_lead_get = lead.get('ownerUid')) != null ? _lead_get : '');
112
+ return _extends({
113
+ link: {
114
+ leadId: lead.id
115
+ },
116
+ matchedBy: 'crmRef'
117
+ }, ownerUid ? {
118
+ ownerUid
119
+ } : {});
120
+ }
121
+ }
122
+ const hit = await findContactByEmail(contactsRef, input.booking.email, {
123
+ hostId
124
+ });
125
+ if (!hit) return null;
126
+ const facet = readContactFacet((_hit_data = hit.data()) != null ? _hit_data : {}, groupId);
127
+ return _extends({
128
+ link: _extends({
129
+ contactId: hit.id
130
+ }, facet.companyId ? {
131
+ companyId: facet.companyId
132
+ } : {}),
133
+ matchedBy: 'email'
134
+ }, facet.ownerUid ? {
135
+ ownerUid: facet.ownerUid
136
+ } : {});
137
+ }
138
+ /**
139
+ * Files what a confirmed booking owes the CRM: the meeting, and the
140
+ * follow-up when the service asks for one. See the module comment.
141
+ */ export async function recordCrmBooking(firestore, input) {
142
+ const { hostId, orgId, booking, service } = input;
143
+ try {
144
+ var _data, _input_org, _input_org1, _input_org2, _ref, _ref1;
145
+ if (!hostId || !orgId || !(booking == null ? void 0 : booking.id)) return {
146
+ filed: false,
147
+ reason: 'failed'
148
+ };
149
+ const wantsMeeting = (service == null ? void 0 : service.crmMeetingActivity) !== false;
150
+ const wantsTask = (service == null ? void 0 : service.crmFollowUpTask) === true;
151
+ if (!wantsMeeting && !wantsTask) return {
152
+ filed: false,
153
+ reason: 'nothing-to-file'
154
+ };
155
+ const host = input.host === undefined ? (_data = (await firestore.collection('hosts').doc(hostId).get()).data()) != null ? _data : null : input.host;
156
+ if (!isHostPluginEnabled((_input_org = input.org) != null ? _input_org : null, host, CRM_PLUGIN_ID)) {
157
+ return {
158
+ filed: false,
159
+ reason: 'crm-off'
160
+ };
161
+ }
162
+ if (!checkEntitlement(input.org, 'crm')) {
163
+ return {
164
+ filed: false,
165
+ reason: 'not-entitled'
166
+ };
167
+ }
168
+ const orgRef = firestore.collection('orgs').doc(orgId);
169
+ const activities = orgRef.collection(CRM_COLLECTIONS.activities);
170
+ // One meeting per booking, whatever path re-enters: the paid webhook is
171
+ // redelivered for days after any 500, and its own guard is the status,
172
+ // which this row is not part of.
173
+ const already = await activities.where('bookingId', '==', booking.id).limit(1).get();
174
+ if (!already.empty) return {
175
+ filed: false,
176
+ reason: 'already-filed'
177
+ };
178
+ const group = consentGroupForHost((_input_org1 = input.org) != null ? _input_org1 : null, hostId);
179
+ const record = await resolveBookingRecord(firestore, input, group.groupId);
180
+ if (!record) return {
181
+ filed: false,
182
+ reason: 'no-record'
183
+ };
184
+ const visibleTo = crmScopeTokens((_input_org2 = input.org) != null ? _input_org2 : null, group);
185
+ const serviceName = String((_ref = (_ref1 = service == null ? void 0 : service.name) != null ? _ref1 : booking.serviceName) != null ? _ref : '').trim();
186
+ const timezone = (service == null ? void 0 : service.timezone) || undefined;
187
+ let activityId = null;
188
+ if (wantsMeeting) {
189
+ // The per-record ceiling (AGL-2611): a full log refuses the meeting
190
+ // but not the follow-up — a task is owed either way.
191
+ const logged = await countCrmActivitiesForRecord(orgRef, record.link);
192
+ if (crmActivityLogHasRoom(logged)) {
193
+ const activity = _extends({
194
+ kind: 'meeting',
195
+ body: crmBookingMeetingBody({
196
+ serviceName,
197
+ startsAtMs: booking.startsAtMs,
198
+ timezone
199
+ }),
200
+ // When it HAPPENS, which is the slot — not when it was booked.
201
+ atMs: booking.startsAtMs,
202
+ byUid: ''
203
+ }, record.link, {
204
+ bookingId: booking.id,
205
+ hostId,
206
+ visibleTo,
207
+ createdAt: FieldValue.serverTimestamp(),
208
+ updatedAt: FieldValue.serverTimestamp()
209
+ });
210
+ const written = await activities.add(activity);
211
+ activityId = written.id;
212
+ }
213
+ }
214
+ let taskId = null;
215
+ if (wantsTask) {
216
+ const task = _extends({
217
+ title: crmBookingFollowUpTitle(serviceName),
218
+ kind: 'todo',
219
+ priority: 'normal',
220
+ status: 'open',
221
+ dueAtMs: crmBookingFollowUpDueMs(booking.endsAtMs, timezone)
222
+ }, record.ownerUid ? {
223
+ assigneeUid: record.ownerUid
224
+ } : {}, {
225
+ createdByUid: ''
226
+ }, record.link, {
227
+ hostId,
228
+ visibleTo,
229
+ createdAt: FieldValue.serverTimestamp(),
230
+ updatedAt: FieldValue.serverTimestamp()
231
+ });
232
+ const written = await orgRef.collection(CRM_COLLECTIONS.tasks).add(task);
233
+ taskId = written.id;
234
+ }
235
+ return {
236
+ filed: true,
237
+ matchedBy: record.matchedBy,
238
+ link: record.link,
239
+ activityId,
240
+ taskId
241
+ };
242
+ } catch (error) {
243
+ console.error('[crm] booking could not be filed', hostId, booking == null ? void 0 : booking.id, error);
244
+ return {
245
+ filed: false,
246
+ reason: 'failed'
247
+ };
248
+ }
249
+ }
250
+
251
+ //# sourceMappingURL=crm-booking-activity.js.map