@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,211 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The only writer of `orgs/{orgId}/lists/{listId}/members` (AGL-2499).
19
+ *
20
+ * Two enrollment routes reach this collection — the commerce newsletter
21
+ * handler and the workflow `enrollList` step — and they used to derive the
22
+ * document id two incompatible ways: a full `sha256(email)` on one side, a
23
+ * `hmac('aglyn-list-member', email)` truncated to 20 hex on the other. The
24
+ * same person subscribing by both routes became two members of one list.
25
+ *
26
+ * The id is now `personKey` on both, which is the derivation both
27
+ * `docs/specs/email-overhaul.md` §3d and `docs/specs/reusable-forms.md` §4
28
+ * specify, and it normalizes before hashing so casing cannot fork it either.
29
+ *
30
+ * ## Why a helper and not two corrected call sites
31
+ *
32
+ * Two call sites that merely agree today are what produced the split: nothing
33
+ * about either one said the other existed. A third route — the reusable-forms
34
+ * capture path is already specified — would have been written the same way.
35
+ * Enrolling goes through this function so that the id has one definition and
36
+ * no caller is offered the chance to derive its own.
37
+ */
38
+ import type { DocumentReference } from 'firebase-admin/firestore';
39
+ import { type ConsentGroup } from '@aglyn/aglyn/server';
40
+ /**
41
+ * Every document id one address may be filed under on a list's `members`:
42
+ * the canonical key first, then the two ids earlier writers minted. A
43
+ * reader that must find the person wherever they were enrolled — the
44
+ * erasure sweep (AGL-2623) — asks for all of them with one `getAll`, the
45
+ * same set `enrollListMember` reads before it writes, so a member enrolled
46
+ * under an old id is neither missed nor duplicated.
47
+ */
48
+ export declare function listMemberDocIds(email: unknown): string[];
49
+ /**
50
+ * What makes an enrollment mailable, and who is answerable for it.
51
+ *
52
+ * Two values and no third. They are not interchangeable and the difference is
53
+ * the whole reason the basis is stored rather than implied:
54
+ *
55
+ * - `contact-opt-in` — the person ticked a box. The enrollment carries a
56
+ * decision they made, and `atMs` is when THEY made it, not when the row was
57
+ * written. Nobody is answerable because nobody asserted anything.
58
+ * - `operator-attested` — a console account stated that they have this
59
+ * person's permission. That statement is itself the basis, so it is only
60
+ * worth storing with the account that made it and the moment they did.
61
+ *
62
+ * Rendering the second as though it were the first is what a support or
63
+ * compliance question cannot recover from, which is why the basis rides
64
+ * beside `marketingConsent` on every row that has one.
65
+ */
66
+ export type ListMemberConsentBasis = 'contact-opt-in' | 'operator-attested';
67
+ /** A basis, with the attribution that makes it answerable. */
68
+ export interface ListMemberConsent {
69
+ basis: ListMemberConsentBasis;
70
+ /**
71
+ * When the basis was recorded.
72
+ *
73
+ * For a pass-through this is the ORIGINAL opt-in moment carried over from
74
+ * the person's own record, because the question a consent record answers is
75
+ * when the person said yes — not when somebody copied their answer onto a
76
+ * list.
77
+ */
78
+ atMs: number;
79
+ /** The console account that attested. Meaningless for a pass-through. */
80
+ byUid?: string | null;
81
+ /**
82
+ * Why, in the operator's own terms, for whoever audits this later.
83
+ *
84
+ * Written for an attestation and ignored for a pass-through, which needs no
85
+ * explanation beyond the person's own record. An import fills it from what
86
+ * the FILE declared — the opt-in source and date the merchant supplied per
87
+ * row — because "a declared basis per address, not one checkbox over the
88
+ * file" is only worth asking for if the declaration survives onto the row
89
+ * it was made about.
90
+ */
91
+ reason?: string;
92
+ }
93
+ export interface EnrollListMemberInput {
94
+ /** `orgs/{orgId}/lists/{listId}` — the caller has already proved it exists. */
95
+ listRef: DocumentReference;
96
+ /**
97
+ * The consent group the enrollment is being made IN THE NAME OF.
98
+ *
99
+ * Lists live on the org and every site in it can mail one, so a membership
100
+ * carries a basis that no path could attribute to a controller. A GROUP
101
+ * rather than a site, because a business running three sites under one name
102
+ * legitimately enrolls somebody into all three at once — and an agency's
103
+ * client, which declared no group, gets exactly itself.
104
+ *
105
+ * Required and undefaulted: a caller that has not resolved a group has not
106
+ * decided who this person agreed to hear from.
107
+ */
108
+ group: ConsentGroup;
109
+ email: string;
110
+ name?: string;
111
+ /** Free-form provenance: `'newsletter'`, `'action:{actionId}'`. */
112
+ source: string;
113
+ /**
114
+ * Explicit marketing opt-in, with a consent timestamp — the same shape
115
+ * `upsertHostContact` and `addHostLead` already carry.
116
+ *
117
+ * A list membership had NO consent field of any kind, so `audience: 'list'`
118
+ * had nothing for the send-time join to read
119
+ * (`docs/specs/email-overhaul.md` §1d/§3f). This is the field that closes
120
+ * it, and it is the caller's captured checkbox: enrolling is an act, and
121
+ * `false`/omitted therefore writes nothing rather than a basis.
122
+ */
123
+ marketingConsent?: boolean;
124
+ /**
125
+ * The basis in full, for a caller that knows more than "a box was ticked".
126
+ *
127
+ * `marketingConsent: true` is the shorthand a capture surface uses: it means
128
+ * `contact-opt-in`, recorded now, asserted by nobody. Anything else — a
129
+ * pass-through carrying the person's ORIGINAL opt-in date, or an
130
+ * attestation carrying the account that made it — has to say so, and this
131
+ * is where it says it. The two inputs are normalized to one value before
132
+ * anything is written, so there is a single writer of the consent fields.
133
+ */
134
+ consent?: ListMemberConsent;
135
+ /**
136
+ * How the person got here: enrolled by hand or by an automation
137
+ * (`'manual'`), or selected by a dynamic list's rule (`'rule'`).
138
+ *
139
+ * The materializer needs to tell its own rows apart from a manual one on
140
+ * the same list, because a person who stops matching the rule leaves and a
141
+ * person somebody added by hand does not.
142
+ */
143
+ via?: 'manual' | 'rule';
144
+ }
145
+ export interface EnrolledListMember {
146
+ /** The document actually written — a legacy id when one was adopted. */
147
+ memberId: string;
148
+ /** True when an existing legacy-keyed row was written instead of a new one. */
149
+ adopted: boolean;
150
+ /** False when the row already existed under some id. */
151
+ created: boolean;
152
+ }
153
+ /** Why nothing was written. */
154
+ export type EnrollListMemberRefusal =
155
+ /** Not an address this collection can key. Nothing to write, nothing to fix. */
156
+ 'unusable-address'
157
+ /**
158
+ * The person's membership records a REFUSAL. Not overridable by any caller
159
+ * and not reported as a failure to fix, because the fix would be to ignore
160
+ * it.
161
+ */
162
+ | 'declined';
163
+ /**
164
+ * Enrolled, or refused and why.
165
+ *
166
+ * A union rather than `EnrolledListMember | null`, because the two refusals
167
+ * are not the same event and a caller that cannot tell them apart is a caller
168
+ * that reports a person's recorded refusal as a malformed address.
169
+ */
170
+ export type EnrollListMemberResult = ({
171
+ enrolled: true;
172
+ } & EnrolledListMember) | {
173
+ enrolled: false;
174
+ refusal: EnrollListMemberRefusal;
175
+ };
176
+ /**
177
+ * Enrolls one address into a list, at one document per person.
178
+ *
179
+ * ## The legacy lookup is the migration
180
+ *
181
+ * Changing the derivation without one would strand every row written under the
182
+ * old ids: the next enrollment of an address already on the list would key a
183
+ * *new* document beside the old one, so a defect that produced duplicates only
184
+ * when two routes met would start producing them on a single route. Rather
185
+ * than rewrite those ids — a bulk operation that has to delete the row it
186
+ * replaces, and deleting an enrollment destroys the consent record that says
187
+ * the person asked to be there — the write resolves the person's existing
188
+ * document and keeps using it.
189
+ *
190
+ * The cost is one `getAll` of three refs, on a human-triggered signup, in
191
+ * place of the blind `set` this replaced. It is one round trip, not three.
192
+ *
193
+ * `addedAt` is stamped only when the document is created, so re-enrolling
194
+ * keeps the date the person actually joined — the same "earliest wins" collapse
195
+ * `docs/specs/email-overhaul.md` §3d asks a backfill to preserve.
196
+ *
197
+ * ## A recorded refusal stops every route through this function
198
+ *
199
+ * This is the only writer of the collection, so the refusal check belongs
200
+ * here and not at the four call sites: a guard on the button that adds
201
+ * somebody by hand leaves the newsletter handler, the workflow `enrollList`
202
+ * step and the dynamic-list materializer free to put the same person back.
203
+ * The membership snapshot is already in hand from the id resolution above, so
204
+ * the check costs no read.
205
+ *
206
+ * It answers the question about the LIST ROW. A refusal recorded on the
207
+ * person's CRM record is a different document this function does not fetch —
208
+ * a caller that can see one is expected to consult it, and the send-time
209
+ * consent join reads the row this function writes either way.
210
+ */
211
+ export declare function enrollListMember(input: EnrollListMemberInput): Promise<EnrollListMemberResult>;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The only writer of `orgs/{orgId}/lists/{listId}/members` (AGL-2499).
18
+ *
19
+ * Two enrollment routes reach this collection — the commerce newsletter
20
+ * handler and the workflow `enrollList` step — and they used to derive the
21
+ * document id two incompatible ways: a full `sha256(email)` on one side, a
22
+ * `hmac('aglyn-list-member', email)` truncated to 20 hex on the other. The
23
+ * same person subscribing by both routes became two members of one list.
24
+ *
25
+ * The id is now `personKey` on both, which is the derivation both
26
+ * `docs/specs/email-overhaul.md` §3d and `docs/specs/reusable-forms.md` §4
27
+ * specify, and it normalizes before hashing so casing cannot fork it either.
28
+ *
29
+ * ## Why a helper and not two corrected call sites
30
+ *
31
+ * Two call sites that merely agree today are what produced the split: nothing
32
+ * about either one said the other existed. A third route — the reusable-forms
33
+ * capture path is already specified — would have been written the same way.
34
+ * Enrolling goes through this function so that the id has one definition and
35
+ * no caller is offered the chance to derive its own.
36
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
37
+ import { FieldValue } from "firebase-admin/firestore";
38
+ import { normalizeContactEmail, personKey, CAPTURED_BY_HOST_FIELD, marketingConsentFieldsForGroup, readMarketingBasis } from "@aglyn/aglyn/server";
39
+ import { createHash, createHmac } from "node:crypto";
40
+ /**
41
+ * The two ids this collection was written under before `personKey`.
42
+ *
43
+ * Read-only, and never written for a new row: they exist so an address
44
+ * already enrolled under a legacy id is *found* rather than duplicated. See
45
+ * `enrollListMember` for why that lookup is the migration.
46
+ *
47
+ * Both take the normalized address, which is what the two original call sites
48
+ * happened to pass — each lowercased at its own entry point before hashing, so
49
+ * every row already on the collection was keyed from a lowercased address even
50
+ * though neither derivation enforced it.
51
+ */ function legacyListMemberIds(normalizedEmail) {
52
+ return [
53
+ createHash('sha256').update(normalizedEmail).digest('hex'),
54
+ createHmac('sha256', 'aglyn-list-member').update(normalizedEmail).digest('hex').slice(0, 20)
55
+ ];
56
+ }
57
+ /**
58
+ * Every document id one address may be filed under on a list's `members`:
59
+ * the canonical key first, then the two ids earlier writers minted. A
60
+ * reader that must find the person wherever they were enrolled — the
61
+ * erasure sweep (AGL-2623) — asks for all of them with one `getAll`, the
62
+ * same set `enrollListMember` reads before it writes, so a member enrolled
63
+ * under an old id is neither missed nor duplicated.
64
+ */ export function listMemberDocIds(email) {
65
+ const normalized = normalizeContactEmail(email);
66
+ if (!normalized) return [];
67
+ const key = personKey(normalized);
68
+ if (!key) return [];
69
+ return [
70
+ key,
71
+ ...legacyListMemberIds(normalized).filter((id)=>id !== key)
72
+ ];
73
+ }
74
+ /**
75
+ * Enrolls one address into a list, at one document per person.
76
+ *
77
+ * ## The legacy lookup is the migration
78
+ *
79
+ * Changing the derivation without one would strand every row written under the
80
+ * old ids: the next enrollment of an address already on the list would key a
81
+ * *new* document beside the old one, so a defect that produced duplicates only
82
+ * when two routes met would start producing them on a single route. Rather
83
+ * than rewrite those ids — a bulk operation that has to delete the row it
84
+ * replaces, and deleting an enrollment destroys the consent record that says
85
+ * the person asked to be there — the write resolves the person's existing
86
+ * document and keeps using it.
87
+ *
88
+ * The cost is one `getAll` of three refs, on a human-triggered signup, in
89
+ * place of the blind `set` this replaced. It is one round trip, not three.
90
+ *
91
+ * `addedAt` is stamped only when the document is created, so re-enrolling
92
+ * keeps the date the person actually joined — the same "earliest wins" collapse
93
+ * `docs/specs/email-overhaul.md` §3d asks a backfill to preserve.
94
+ *
95
+ * ## A recorded refusal stops every route through this function
96
+ *
97
+ * This is the only writer of the collection, so the refusal check belongs
98
+ * here and not at the four call sites: a guard on the button that adds
99
+ * somebody by hand leaves the newsletter handler, the workflow `enrollList`
100
+ * step and the dynamic-list materializer free to put the same person back.
101
+ * The membership snapshot is already in hand from the id resolution above, so
102
+ * the check costs no read.
103
+ *
104
+ * It answers the question about the LIST ROW. A refusal recorded on the
105
+ * person's CRM record is a different document this function does not fetch —
106
+ * a caller that can see one is expected to consult it, and the send-time
107
+ * consent join reads the row this function writes either way.
108
+ */ export async function enrollListMember(input) {
109
+ var _ref, _input_consent, _consent_byUid, _consent_reason;
110
+ const email = normalizeContactEmail(input.email);
111
+ if (!email) return {
112
+ enrolled: false,
113
+ refusal: 'unusable-address'
114
+ };
115
+ const key = personKey(email);
116
+ if (!key) return {
117
+ enrolled: false,
118
+ refusal: 'unusable-address'
119
+ };
120
+ const members = input.listRef.collection('members');
121
+ const canonicalRef = members.doc(key);
122
+ const legacyRefs = legacyListMemberIds(email)// A legacy derivation that happens to agree with `personKey` — `sha256` of
123
+ // the same normalized address does — must not be fetched twice: `getAll`
124
+ // rejects duplicate references.
125
+ .filter((id)=>id !== key).map((id)=>members.doc(id));
126
+ const snapshots = await input.listRef.firestore.getAll(canonicalRef, ...legacyRefs);
127
+ const existing = snapshots.find((snapshot)=>snapshot.exists);
128
+ const target = (_ref = existing == null ? void 0 : existing.ref) != null ? _ref : canonicalRef;
129
+ if (existing && readMarketingBasis(existing.data(), input.group).basis === 'declined') {
130
+ return {
131
+ enrolled: false,
132
+ refusal: 'declined'
133
+ };
134
+ }
135
+ /*
136
+ * ONE value from the two ways a caller may state a basis.
137
+ *
138
+ * `marketingConsent: true` is a capture surface saying "a box was ticked
139
+ * just now", which is `contact-opt-in` recorded at this moment with nobody
140
+ * asserting it. Normalizing here rather than branching at the write is what
141
+ * keeps the consent fields to a single writer — two branches stamping
142
+ * overlapping subsets of them is how a row comes to carry a basis with no
143
+ * timestamp, or a timestamp with no basis.
144
+ */ const consent = (_input_consent = input.consent) != null ? _input_consent : input.marketingConsent ? {
145
+ basis: 'contact-opt-in',
146
+ atMs: Date.now()
147
+ } : null;
148
+ await target.set(_extends({
149
+ email
150
+ }, input.name ? {
151
+ name: input.name
152
+ } : {}, {
153
+ source: input.source
154
+ }, input.via ? {
155
+ via: input.via
156
+ } : {}, consent ? marketingConsentFieldsForGroup(input.group, consent.atMs, {
157
+ marketingConsentBasis: consent.basis,
158
+ marketingConsentByUid: (_consent_byUid = consent.byUid) != null ? _consent_byUid : null,
159
+ marketingConsentReason: (_consent_reason = consent.reason) != null ? _consent_reason : ''
160
+ }) : {}, existing ? {} : {
161
+ addedAt: FieldValue.serverTimestamp(),
162
+ [CAPTURED_BY_HOST_FIELD]: [
163
+ input.group.hostId
164
+ ]
165
+ }), {
166
+ merge: true
167
+ });
168
+ return {
169
+ enrolled: true,
170
+ memberId: target.id,
171
+ adopted: target.id !== key,
172
+ created: !existing
173
+ };
174
+ }
175
+
176
+ //# sourceMappingURL=list-members.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/list-members.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 only writer of `orgs/{orgId}/lists/{listId}/members` (AGL-2499).\n *\n * Two enrollment routes reach this collection — the commerce newsletter\n * handler and the workflow `enrollList` step — and they used to derive the\n * document id two incompatible ways: a full `sha256(email)` on one side, a\n * `hmac('aglyn-list-member', email)` truncated to 20 hex on the other. The\n * same person subscribing by both routes became two members of one list.\n *\n * The id is now `personKey` on both, which is the derivation both\n * `docs/specs/email-overhaul.md` §3d and `docs/specs/reusable-forms.md` §4\n * specify, and it normalizes before hashing so casing cannot fork it either.\n *\n * ## Why a helper and not two corrected call sites\n *\n * Two call sites that merely agree today are what produced the split: nothing\n * about either one said the other existed. A third route — the reusable-forms\n * capture path is already specified — would have been written the same way.\n * Enrolling goes through this function so that the id has one definition and\n * no caller is offered the chance to derive its own.\n */\n\nimport type { DocumentReference } from 'firebase-admin/firestore'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n normalizeContactEmail,\n personKey,\n CAPTURED_BY_HOST_FIELD,\n marketingConsentFieldsForGroup,\n readMarketingBasis,\n type ConsentGroup,\n} from '@aglyn/aglyn/server'\nimport { createHash, createHmac } from 'node:crypto'\n\n/**\n * The two ids this collection was written under before `personKey`.\n *\n * Read-only, and never written for a new row: they exist so an address\n * already enrolled under a legacy id is *found* rather than duplicated. See\n * `enrollListMember` for why that lookup is the migration.\n *\n * Both take the normalized address, which is what the two original call sites\n * happened to pass — each lowercased at its own entry point before hashing, so\n * every row already on the collection was keyed from a lowercased address even\n * though neither derivation enforced it.\n */\nfunction legacyListMemberIds(normalizedEmail: string): string[] {\n return [\n createHash('sha256').update(normalizedEmail).digest('hex'),\n createHmac('sha256', 'aglyn-list-member')\n .update(normalizedEmail)\n .digest('hex')\n .slice(0, 20),\n ]\n}\n\n/**\n * Every document id one address may be filed under on a list's `members`:\n * the canonical key first, then the two ids earlier writers minted. A\n * reader that must find the person wherever they were enrolled — the\n * erasure sweep (AGL-2623) — asks for all of them with one `getAll`, the\n * same set `enrollListMember` reads before it writes, so a member enrolled\n * under an old id is neither missed nor duplicated.\n */\nexport function listMemberDocIds(email: unknown): string[] {\n const normalized = normalizeContactEmail(email)\n if (!normalized) return []\n const key = personKey(normalized)\n if (!key) return []\n return [key, ...legacyListMemberIds(normalized).filter((id) => id !== key)]\n}\n\n/**\n * What makes an enrollment mailable, and who is answerable for it.\n *\n * Two values and no third. They are not interchangeable and the difference is\n * the whole reason the basis is stored rather than implied:\n *\n * - `contact-opt-in` — the person ticked a box. The enrollment carries a\n * decision they made, and `atMs` is when THEY made it, not when the row was\n * written. Nobody is answerable because nobody asserted anything.\n * - `operator-attested` — a console account stated that they have this\n * person's permission. That statement is itself the basis, so it is only\n * worth storing with the account that made it and the moment they did.\n *\n * Rendering the second as though it were the first is what a support or\n * compliance question cannot recover from, which is why the basis rides\n * beside `marketingConsent` on every row that has one.\n */\nexport type ListMemberConsentBasis = 'contact-opt-in' | 'operator-attested'\n\n/** A basis, with the attribution that makes it answerable. */\nexport interface ListMemberConsent {\n basis: ListMemberConsentBasis\n /**\n * When the basis was recorded.\n *\n * For a pass-through this is the ORIGINAL opt-in moment carried over from\n * the person's own record, because the question a consent record answers is\n * when the person said yes — not when somebody copied their answer onto a\n * list.\n */\n atMs: number\n /** The console account that attested. Meaningless for a pass-through. */\n byUid?: string | null\n /**\n * Why, in the operator's own terms, for whoever audits this later.\n *\n * Written for an attestation and ignored for a pass-through, which needs no\n * explanation beyond the person's own record. An import fills it from what\n * the FILE declared — the opt-in source and date the merchant supplied per\n * row — because \"a declared basis per address, not one checkbox over the\n * file\" is only worth asking for if the declaration survives onto the row\n * it was made about.\n */\n reason?: string\n}\n\nexport interface EnrollListMemberInput {\n /** `orgs/{orgId}/lists/{listId}` — the caller has already proved it exists. */\n listRef: DocumentReference\n /**\n * The consent group the enrollment is being made IN THE NAME OF.\n *\n * Lists live on the org and every site in it can mail one, so a membership\n * carries a basis that no path could attribute to a controller. A GROUP\n * rather than a site, because a business running three sites under one name\n * legitimately enrolls somebody into all three at once — and an agency's\n * client, which declared no group, gets exactly itself.\n *\n * Required and undefaulted: a caller that has not resolved a group has not\n * decided who this person agreed to hear from.\n */\n group: ConsentGroup\n email: string\n name?: string\n /** Free-form provenance: `'newsletter'`, `'action:{actionId}'`. */\n source: string\n /**\n * Explicit marketing opt-in, with a consent timestamp — the same shape\n * `upsertHostContact` and `addHostLead` already carry.\n *\n * A list membership had NO consent field of any kind, so `audience: 'list'`\n * had nothing for the send-time join to read\n * (`docs/specs/email-overhaul.md` §1d/§3f). This is the field that closes\n * it, and it is the caller's captured checkbox: enrolling is an act, and\n * `false`/omitted therefore writes nothing rather than a basis.\n */\n marketingConsent?: boolean\n /**\n * The basis in full, for a caller that knows more than \"a box was ticked\".\n *\n * `marketingConsent: true` is the shorthand a capture surface uses: it means\n * `contact-opt-in`, recorded now, asserted by nobody. Anything else — a\n * pass-through carrying the person's ORIGINAL opt-in date, or an\n * attestation carrying the account that made it — has to say so, and this\n * is where it says it. The two inputs are normalized to one value before\n * anything is written, so there is a single writer of the consent fields.\n */\n consent?: ListMemberConsent\n /**\n * How the person got here: enrolled by hand or by an automation\n * (`'manual'`), or selected by a dynamic list's rule (`'rule'`).\n *\n * The materializer needs to tell its own rows apart from a manual one on\n * the same list, because a person who stops matching the rule leaves and a\n * person somebody added by hand does not.\n */\n via?: 'manual' | 'rule'\n}\n\nexport interface EnrolledListMember {\n /** The document actually written — a legacy id when one was adopted. */\n memberId: string\n /** True when an existing legacy-keyed row was written instead of a new one. */\n adopted: boolean\n /** False when the row already existed under some id. */\n created: boolean\n}\n\n/** Why nothing was written. */\nexport type EnrollListMemberRefusal =\n /** Not an address this collection can key. Nothing to write, nothing to fix. */\n | 'unusable-address'\n /**\n * The person's membership records a REFUSAL. Not overridable by any caller\n * and not reported as a failure to fix, because the fix would be to ignore\n * it.\n */\n | 'declined'\n\n/**\n * Enrolled, or refused and why.\n *\n * A union rather than `EnrolledListMember | null`, because the two refusals\n * are not the same event and a caller that cannot tell them apart is a caller\n * that reports a person's recorded refusal as a malformed address.\n */\nexport type EnrollListMemberResult =\n | ({ enrolled: true } & EnrolledListMember)\n | { enrolled: false; refusal: EnrollListMemberRefusal }\n\n/**\n * Enrolls one address into a list, at one document per person.\n *\n * ## The legacy lookup is the migration\n *\n * Changing the derivation without one would strand every row written under the\n * old ids: the next enrollment of an address already on the list would key a\n * *new* document beside the old one, so a defect that produced duplicates only\n * when two routes met would start producing them on a single route. Rather\n * than rewrite those ids — a bulk operation that has to delete the row it\n * replaces, and deleting an enrollment destroys the consent record that says\n * the person asked to be there — the write resolves the person's existing\n * document and keeps using it.\n *\n * The cost is one `getAll` of three refs, on a human-triggered signup, in\n * place of the blind `set` this replaced. It is one round trip, not three.\n *\n * `addedAt` is stamped only when the document is created, so re-enrolling\n * keeps the date the person actually joined — the same \"earliest wins\" collapse\n * `docs/specs/email-overhaul.md` §3d asks a backfill to preserve.\n *\n * ## A recorded refusal stops every route through this function\n *\n * This is the only writer of the collection, so the refusal check belongs\n * here and not at the four call sites: a guard on the button that adds\n * somebody by hand leaves the newsletter handler, the workflow `enrollList`\n * step and the dynamic-list materializer free to put the same person back.\n * The membership snapshot is already in hand from the id resolution above, so\n * the check costs no read.\n *\n * It answers the question about the LIST ROW. A refusal recorded on the\n * person's CRM record is a different document this function does not fetch —\n * a caller that can see one is expected to consult it, and the send-time\n * consent join reads the row this function writes either way.\n */\nexport async function enrollListMember(\n input: EnrollListMemberInput,\n): Promise<EnrollListMemberResult> {\n const email = normalizeContactEmail(input.email)\n if (!email) return { enrolled: false, refusal: 'unusable-address' }\n const key = personKey(email)\n if (!key) return { enrolled: false, refusal: 'unusable-address' }\n\n const members = input.listRef.collection('members')\n const canonicalRef = members.doc(key)\n const legacyRefs = legacyListMemberIds(email)\n // A legacy derivation that happens to agree with `personKey` — `sha256` of\n // the same normalized address does — must not be fetched twice: `getAll`\n // rejects duplicate references.\n .filter((id) => id !== key)\n .map((id) => members.doc(id))\n\n const snapshots = await input.listRef.firestore.getAll(\n canonicalRef,\n ...legacyRefs,\n )\n const existing = snapshots.find((snapshot) => snapshot.exists)\n const target = existing?.ref ?? canonicalRef\n\n if (\n existing &&\n readMarketingBasis(existing.data() as Record<string, unknown>, input.group)\n .basis === 'declined'\n ) {\n return { enrolled: false, refusal: 'declined' }\n }\n\n /*\n * ONE value from the two ways a caller may state a basis.\n *\n * `marketingConsent: true` is a capture surface saying \"a box was ticked\n * just now\", which is `contact-opt-in` recorded at this moment with nobody\n * asserting it. Normalizing here rather than branching at the write is what\n * keeps the consent fields to a single writer — two branches stamping\n * overlapping subsets of them is how a row comes to carry a basis with no\n * timestamp, or a timestamp with no basis.\n */\n const consent: ListMemberConsent | null =\n input.consent ??\n (input.marketingConsent\n ? { basis: 'contact-opt-in', atMs: Date.now() }\n : null)\n\n await target.set(\n {\n email,\n ...(input.name ? { name: input.name } : {}),\n source: input.source,\n ...(input.via ? { via: input.via } : {}),\n /*\n * Written only when the caller has a basis, and never unwritten.\n *\n * A merge that stamped `marketingConsent: false` on the omitted case\n * would erase a basis this person gave on an earlier enrollment, and a\n * withdrawn consent is not the same event as a re-enrollment that\n * happened to carry no checkbox. Withdrawal has its own path — the\n * unsubscribe link and the suppression list.\n *\n * `marketingConsentAtMs` moves with each fresh basis on purpose: the\n * question a consent record answers is when the person last said yes.\n *\n * The `marketingConsent` family rather than the `consent*` names\n * `docs/specs/email-overhaul.md` §3d sketched: `readMarketingBasis` is\n * the shipped reader and it reads these, so a second vocabulary here\n * would be a basis the send-time join cannot see. The two new fields\n * are attribution ON that field — they say why the person is mailable,\n * never whether — so nothing about the send decision moves into them.\n *\n * Written UNDER the enrolling GROUP, through the one writer helper. A\n * list lives on the org and every site in it can mail one, so a basis\n * written at the top of this row would enroll a person into one\n * client's newsletter and make them mailable by all of them. A group of\n * one — the default — is that client and nobody else.\n *\n * `marketingConsentByUid` is written on EVERY basis, `null` for a\n * pass-through. Leaving it absent would let a real opt-in that\n * supersedes an earlier attestation inherit the attesting account, and\n * a row reading \"this account vouched for them\" when the person ticked\n * a box themselves is a false attribution in the one direction a\n * compliance answer cannot afford.\n *\n * `marketingConsentReason` follows the same rule for the same reason:\n * always written, `''` for a pass-through, so a later real opt-in\n * cannot inherit the sentence an earlier import wrote about where the\n * merchant said the address came from.\n */\n ...(consent\n ? marketingConsentFieldsForGroup(input.group, consent.atMs, {\n marketingConsentBasis: consent.basis,\n marketingConsentByUid: consent.byUid ?? null,\n marketingConsentReason: consent.reason ?? '',\n })\n : {}),\n ...(existing\n ? {}\n : {\n addedAt: FieldValue.serverTimestamp(),\n [CAPTURED_BY_HOST_FIELD]: [input.group.hostId],\n }),\n },\n { merge: true },\n )\n\n return {\n enrolled: true,\n memberId: target.id,\n adopted: target.id !== key,\n created: !existing,\n }\n}\n"],"names":["FieldValue","normalizeContactEmail","personKey","CAPTURED_BY_HOST_FIELD","marketingConsentFieldsForGroup","readMarketingBasis","createHash","createHmac","legacyListMemberIds","normalizedEmail","update","digest","slice","listMemberDocIds","email","normalized","key","filter","id","enrollListMember","input","consent","enrolled","refusal","members","listRef","collection","canonicalRef","doc","legacyRefs","map","snapshots","firestore","getAll","existing","find","snapshot","exists","target","ref","data","group","basis","marketingConsent","atMs","Date","now","set","name","source","via","marketingConsentBasis","marketingConsentByUid","byUid","marketingConsentReason","reason","addedAt","serverTimestamp","hostId","merge","memberId","adopted","created"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC;AAGD,SAASA,UAAU,QAAQ,2BAA0B;AACrD,SACEC,qBAAqB,EACrBC,SAAS,EACTC,sBAAsB,EACtBC,8BAA8B,EAC9BC,kBAAkB,QAEb,sBAAqB;AAC5B,SAASC,UAAU,EAAEC,UAAU,QAAQ,cAAa;AAEpD;;;;;;;;;;;CAWC,GACD,SAASC,oBAAoBC,eAAuB;IAClD,OAAO;QACLH,WAAW,UAAUI,MAAM,CAACD,iBAAiBE,MAAM,CAAC;QACpDJ,WAAW,UAAU,qBAClBG,MAAM,CAACD,iBACPE,MAAM,CAAC,OACPC,KAAK,CAAC,GAAG;KACb;AACH;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAc;IAC7C,MAAMC,aAAad,sBAAsBa;IACzC,IAAI,CAACC,YAAY,OAAO,EAAE;IAC1B,MAAMC,MAAMd,UAAUa;IACtB,IAAI,CAACC,KAAK,OAAO,EAAE;IACnB,OAAO;QAACA;WAAQR,oBAAoBO,YAAYE,MAAM,CAAC,CAACC,KAAOA,OAAOF;KAAK;AAC7E;AAoIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,eAAeG,iBACpBC,KAA4B;cA0C1BA,gBAmD+BC,gBACCA;IA5FlC,MAAMP,QAAQb,sBAAsBmB,MAAMN,KAAK;IAC/C,IAAI,CAACA,OAAO,OAAO;QAAEQ,UAAU;QAAOC,SAAS;IAAmB;IAClE,MAAMP,MAAMd,UAAUY;IACtB,IAAI,CAACE,KAAK,OAAO;QAAEM,UAAU;QAAOC,SAAS;IAAmB;IAEhE,MAAMC,UAAUJ,MAAMK,OAAO,CAACC,UAAU,CAAC;IACzC,MAAMC,eAAeH,QAAQI,GAAG,CAACZ;IACjC,MAAMa,aAAarB,oBAAoBM,MACrC,2EAA2E;IAC3E,yEAAyE;IACzE,gCAAgC;KAC/BG,MAAM,CAAC,CAACC,KAAOA,OAAOF,KACtBc,GAAG,CAAC,CAACZ,KAAOM,QAAQI,GAAG,CAACV;IAE3B,MAAMa,YAAY,MAAMX,MAAMK,OAAO,CAACO,SAAS,CAACC,MAAM,CACpDN,iBACGE;IAEL,MAAMK,WAAWH,UAAUI,IAAI,CAAC,CAACC,WAAaA,SAASC,MAAM;IAC7D,MAAMC,iBAASJ,4BAAAA,SAAUK,GAAG,mBAAIZ;IAEhC,IACEO,YACA7B,mBAAmB6B,SAASM,IAAI,IAA+BpB,MAAMqB,KAAK,EACvEC,KAAK,KAAK,YACb;QACA,OAAO;YAAEpB,UAAU;YAAOC,SAAS;QAAW;IAChD;IAEA;;;;;;;;;GASC,GACD,MAAMF,WACJD,iBAAAA,MAAMC,OAAO,YAAbD,iBACCA,MAAMuB,gBAAgB,GACnB;QAAED,OAAO;QAAkBE,MAAMC,KAAKC,GAAG;IAAG,IAC5C;IAEN,MAAMR,OAAOS,GAAG,CACd;QACEjC;OACIM,MAAM4B,IAAI,GAAG;QAAEA,MAAM5B,MAAM4B,IAAI;IAAC,IAAI,CAAC;QACzCC,QAAQ7B,MAAM6B,MAAM;OAChB7B,MAAM8B,GAAG,GAAG;QAAEA,KAAK9B,MAAM8B,GAAG;IAAC,IAAI,CAAC,GAsClC7B,UACAjB,+BAA+BgB,MAAMqB,KAAK,EAAEpB,QAAQuB,IAAI,EAAE;QACxDO,uBAAuB9B,QAAQqB,KAAK;QACpCU,qBAAqB,GAAE/B,iBAAAA,QAAQgC,KAAK,YAAbhC,iBAAiB;QACxCiC,sBAAsB,GAAEjC,kBAAAA,QAAQkC,MAAM,YAAdlC,kBAAkB;IAC5C,KACA,CAAC,GACDa,WACA,CAAC,IACD;QACEsB,SAASxD,WAAWyD,eAAe;QACnC,CAACtD,uBAAuB,EAAE;YAACiB,MAAMqB,KAAK,CAACiB,MAAM;SAAC;IAChD,IAEN;QAAEC,OAAO;IAAK;IAGhB,OAAO;QACLrC,UAAU;QACVsC,UAAUtB,OAAOpB,EAAE;QACnB2C,SAASvB,OAAOpB,EAAE,KAAKF;QACvB8C,SAAS,CAAC5B;IACZ;AACF"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Which live pages a change reaches, for a site's screens, layouts and
19
+ * components.
20
+ *
21
+ * The corpus read and the two closures over it, held here rather than in the
22
+ * console because the console is no longer the only side that needs them. A
23
+ * dataset record is written from the tenant as well — a form submission, an
24
+ * automation step — and those writes make the same pages stale as a console
25
+ * edit does. Two implementations of "which screens render this" is the shape
26
+ * that put AGL-1223 in the tree: one reader handled a node tree's second
27
+ * storage form and the other did not, so half the corpus answered "used
28
+ * nowhere" and a publish dropped no cache.
29
+ *
30
+ * Nothing here is dataset-specific, and nothing is console-specific. The
31
+ * console's `scan-artifact-usage` re-exports these and keeps the parts that
32
+ * are about the console's own "what would I break" copy.
33
+ */
34
+ import { type ReusableComponentProp } from '@aglyn/aglyn/server';
35
+ /** A screen/layout/component reduced to what a usage scan needs. */
36
+ export interface UsageCandidate {
37
+ id: string;
38
+ displayName?: string;
39
+ /** Legacy field some older documents used instead of `displayName`. */
40
+ name?: string;
41
+ deletedAt?: unknown;
42
+ /**
43
+ * Node tree to search. For screens and layouts this is the PUBLISHED
44
+ * version's nodes (what visitors see); for components it is the definition
45
+ * tree off the component document, which is what the runtime reads.
46
+ */
47
+ nodes?: Record<string, any> | null;
48
+ /** Published version, carried through so the caller can deep-link. */
49
+ versionId?: string;
50
+ /** Screens only: the layout they render inside. */
51
+ layoutId?: string;
52
+ /** Screens only: the screen they nest under, which is part of their path. */
53
+ parentId?: string;
54
+ /**
55
+ * Components only: the properties the definition declares (AGL-1247). A
56
+ * Link property's default renders as a link wherever an instance leaves the
57
+ * property unset, and it is stored here, not in `nodes` (AGL-2846).
58
+ */
59
+ props?: ReadonlyArray<ReusableComponentProp | null | undefined> | null;
60
+ }
61
+ /** One document that depends on the artifact a scan was asked about. */
62
+ export interface UsageDependent {
63
+ type: 'screen' | 'layout' | 'component' | 'collection';
64
+ id: string;
65
+ name: string;
66
+ via: Array<'id' | 'name'>;
67
+ versionId?: string;
68
+ /**
69
+ * HOW the dependent references the artifact — screens and collection
70
+ * listings (AGL-703, AGL-2806).
71
+ *
72
+ * A component or a layout has exactly one kind of dependent and the noun
73
+ * says everything: an instance, or a binding. A screen has three, and they
74
+ * break in three different ways — a link goes dead, a child moves, a
75
+ * collection loses the page it renders through. Copy that could not tell
76
+ * them apart would have to describe the worst case every time. A collection
77
+ * listing's dependents are all links, and say so.
78
+ */
79
+ relation?: 'link' | 'child' | 'template';
80
+ }
81
+ /** The three corpora every closure below walks. */
82
+ export interface UsageSources {
83
+ screens: UsageCandidate[];
84
+ layouts: UsageCandidate[];
85
+ components: UsageCandidate[];
86
+ }
87
+ /** `displayName`, falling back to a legacy `name`, then the raw id. */
88
+ export declare function usageCandidateLabel(candidate: UsageCandidate): string;
89
+ /** A soft-deleted document renders nothing, so it depends on nothing. */
90
+ export declare const isLiveUsageCandidate: (candidate: UsageCandidate) => boolean;
91
+ /**
92
+ * Everything that references a reusable component (AGL-703).
93
+ *
94
+ * Three places, because the renderer expands instances in three places:
95
+ * published screen versions, published layout versions, and OTHER component
96
+ * definitions — `composeReusableComponentNodes` grafts nested instances, so
97
+ * a component used only inside another component is genuinely used. Omitting
98
+ * that third scan would report "used nowhere" for it and invite a confident
99
+ * deletion, which is worse than showing nothing at all.
100
+ */
101
+ export declare function scanComponentUsage(componentId: string, sources: UsageSources): UsageDependent[];
102
+ /**
103
+ * Everything rendering inside a layout (AGL-703).
104
+ *
105
+ * Two kinds of dependent, both expressed by the same `layoutId` pointer:
106
+ *
107
+ * - **screens**, which name the layout they render inside;
108
+ * - **other layouts**, since a layout can itself sit inside one. A nested
109
+ * layout is a real dependent — deleting its parent unwraps every screen
110
+ * underneath it — so leaving layouts out would report a parent layout as
111
+ * used only by the screens that name it directly, and none of the ones
112
+ * that reach it through a child.
113
+ *
114
+ * A layout never counts as its own dependent; `canNestLayout` refuses that,
115
+ * and this refuses to report it even if stored data holds one.
116
+ */
117
+ export declare function scanLayoutUsage(layoutId: string, screens: UsageCandidate[], layouts?: UsageCandidate[]): UsageDependent[];
118
+ /**
119
+ * Every live screen rendered inside `layoutId`, at ANY nesting depth
120
+ * (AGL-1150).
121
+ *
122
+ * `scanLayoutUsage` answers one level. Layouts nest — a screen points at a
123
+ * layout, which can point at a parent layout, and `compose-screen-nodes` walks
124
+ * that whole chain when composing a page. So publishing a layout changes every
125
+ * screen below it, not just the ones bound to it directly, and a cache drop
126
+ * that only handles the direct level leaves the rest showing stale chrome for
127
+ * the full revalidate window.
128
+ *
129
+ * Pure, and separate from the Firestore read, so the nesting behaviour is
130
+ * testable without a database.
131
+ *
132
+ * Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written
133
+ * straight to Firestore is not bound by that, and a cycle here would hang a
134
+ * publish request rather than surface anything.
135
+ */
136
+ export declare function screenIdsUsingLayoutDeep(layoutId: string, screens: UsageCandidate[], layouts?: UsageCandidate[]): string[];
137
+ /**
138
+ * Every live screen whose rendered output contains `componentId`, however
139
+ * indirectly (AGL-1161).
140
+ *
141
+ * `scanComponentUsage` answers one level and returns three kinds of dependent.
142
+ * Only one of them is a screen, and the other two both reach screens by routes
143
+ * a single-level scan cannot see:
144
+ *
145
+ * - a **component** dependent nests the target inside itself, and that outer
146
+ * component may itself only be used inside a third — so component→component
147
+ * edges have to be followed to a fixed point;
148
+ * - a **layout** dependent puts the component in page chrome, which every
149
+ * screen under that layout renders. Layouts nest, so that is
150
+ * `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.
151
+ *
152
+ * Miss either and a publish reports success while some pages keep serving the
153
+ * old component for the full revalidate window — the failure this whole arc
154
+ * exists to remove, and the one that is hardest to notice because the pages
155
+ * that ARE dropped update instantly.
156
+ *
157
+ * Pure, and separate from the Firestore read, so the closure is testable
158
+ * without a database — the same split `screenIdsUsingLayoutDeep` uses.
159
+ *
160
+ * Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a
161
+ * document written straight to Firestore is not bound by what the editor
162
+ * allows, and a cycle here would hang a publish rather than surface anything.
163
+ */
164
+ export declare function screenIdsUsingComponentDeep(componentId: string, sources: UsageSources): string[];
165
+ export interface UsageCandidateRead {
166
+ candidates: UsageCandidate[];
167
+ /**
168
+ * The collection held more documents than `limit` allowed, so the scan below
169
+ * it is INCOMPLETE.
170
+ *
171
+ * Returned rather than logged, because the two callers owe the user
172
+ * different things: an advisory "what would I break" can show a partial
173
+ * answer and say so, while a cache drop that silently scans a prefix reports
174
+ * a successful publish and leaves real pages stale.
175
+ */
176
+ truncated: boolean;
177
+ }
178
+ /**
179
+ * One collection's documents, with published nodes attached when the scan
180
+ * needs to search them.
181
+ *
182
+ * `limit` is a real bound, not a guess: it is fetched with one extra document
183
+ * so exceeding it is DETECTED rather than assumed away. A caller that ignores
184
+ * `truncated` is choosing to be wrong quietly.
185
+ */
186
+ export declare function readUsageCandidates(hostRef: FirebaseFirestore.DocumentReference, collectionName: 'screens' | 'layouts' | 'components', options: {
187
+ withNodes: boolean;
188
+ limit: number;
189
+ }): Promise<UsageCandidateRead>;
190
+ /**
191
+ * Every screen, layout and component of a site, with their node trees — the
192
+ * corpus every tree-searching closure above walks.
193
+ *
194
+ * Each collection ONCE, in memory: a query per level would multiply round
195
+ * trips by the nesting depth of the graph. Shared so two scans cannot read
196
+ * different corpora under different bounds, and a change to one cannot quietly
197
+ * narrow the other.
198
+ */
199
+ export declare function readUsageSources(hostRef: FirebaseFirestore.DocumentReference, limit: number): Promise<{
200
+ candidates: UsageSources;
201
+ truncated: boolean;
202
+ }>;