@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,174 @@
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 shared secret, or empty when the deployment has none.
19
+ *
20
+ * `CRON_SECRET` is the documented fallback, matching what the campaign sender
21
+ * and the unsubscribe handler already resolve. Read per call rather than at
22
+ * module load: these run in serverless handlers where the module may be
23
+ * evaluated during a build, long before the runtime env exists.
24
+ */
25
+ export declare function unsubscribeLinkSecret(): string;
26
+ /**
27
+ * The signed subject for an unsubscribe link.
28
+ *
29
+ * ## Two signed forms, and why the campaign id is additive
30
+ *
31
+ * Every email already sitting in an inbox carries a two-part signature over
32
+ * `hostId:email`, and those links must go on working forever — an unsubscribe
33
+ * link that stops honoring itself is the one bug in this area with a legal
34
+ * edge on it. So the campaign is appended only when it is present, and the
35
+ * verifier chooses which form to check by whether the link carries a `cid`.
36
+ *
37
+ * SIGNED rather than passed alongside. An unsigned `cid` would be an
38
+ * attribution anybody holding one valid link could point at any campaign they
39
+ * liked — a small forgery, and a completely gratuitous one, since the
40
+ * campaign is already known at the moment the link is minted.
41
+ */
42
+ export declare function unsubscribeSignatureSubject(hostId: string, email: string, campaignId?: string, topicId?: string): string;
43
+ /** HMAC for unsubscribe links; env-gated on the shared secret. */
44
+ export declare function unsubscribeSignature(hostId: string, email: string, secret: string, campaignId?: string, topicId?: string): string;
45
+ /**
46
+ * Whether a signature is this link's.
47
+ *
48
+ * Which form is checked is decided by the LINK, not by the signature: a link
49
+ * carrying no `cid` is checked against the two-part form and a link carrying
50
+ * one against the three-part form. There is no fallback between them, and
51
+ * that is what stops this being a downgrade — an attacker cannot take a
52
+ * three-part link, drop the `cid` and have it verify, because the two-part
53
+ * check over the same `hostId:email` produces a different digest. Nor can
54
+ * they bolt a `cid` onto a two-part link: the three-part check then fails.
55
+ *
56
+ * `timingSafeEqual` needs equal lengths, so the length is compared first — it
57
+ * is not a secret, both digests are fixed-width hex, and the call throws on a
58
+ * mismatch rather than returning false.
59
+ */
60
+ export declare function unsubscribeSignatureMatches(args: {
61
+ hostId: string;
62
+ email: string;
63
+ campaignId?: string;
64
+ signature: string;
65
+ secret: string;
66
+ }): boolean;
67
+ /**
68
+ * The purpose component that distinguishes a CONFIRMATION subject.
69
+ *
70
+ * See {@link confirmSignatureSubject}. Exported because the plugin's verifier
71
+ * imports it rather than restating the literal.
72
+ */
73
+ export declare const CONFIRM_SUBJECT_PREFIX = "confirm";
74
+ /**
75
+ * The signed subject for a double opt-in confirmation link.
76
+ *
77
+ * ## Why it is not one of the three forms above
78
+ *
79
+ * A confirmation names a host, an address and a topic, and it has NO
80
+ * campaign — nobody is unsubscribing from a message, they are joining a
81
+ * stream. The forms above refuse exactly that combination: a topic with no
82
+ * campaign would leave an empty middle component, and `host:email::t` is the
83
+ * same string as a three-part subject whose campaign id is `:t`.
84
+ *
85
+ * So it gets a leading PURPOSE component. That is a fourth form of the same
86
+ * scheme, not a second scheme: the digest, the secret and the comparison are
87
+ * unchanged.
88
+ *
89
+ * ## The one collision, and the guard for it
90
+ *
91
+ * `confirm:H:E:T` is byte-identical to the four-part unsubscribe subject
92
+ * `A:B:C:D` when the site's id is literally `confirm`, which would let one
93
+ * signature verify as both. Ids are not ours to constrain after the fact, so
94
+ * the subject is refused for that host rather than the collision being
95
+ * reasoned about: the cost is one unusable document id, and the alternative
96
+ * is a signature that means two things.
97
+ *
98
+ * @returns the subject, or `''` for a combination it cannot sign
99
+ * unambiguously. Empty rather than a partial subject, so
100
+ * {@link confirmSignature} yields no signature rather than one over
101
+ * the empty string — which would verify for every other caller that
102
+ * also produced one.
103
+ */
104
+ export declare function confirmSignatureSubject(hostId: string, email: string, topicId: string): string;
105
+ /** HMAC for a confirmation link; empty for an unsignable subject. */
106
+ export declare function confirmSignature(hostId: string, email: string, topicId: string, secret: string): string;
107
+ /**
108
+ * The absolute confirmation URL for one address and one topic.
109
+ *
110
+ * Minted where the signup happens, because the message carrying it is sent
111
+ * from there — and that message is TRANSACTIONAL, not marketing: the person
112
+ * just asked for this, so asking them to confirm it is the transaction they
113
+ * started. It carries no unsubscribe header for the same reason a receipt
114
+ * does not.
115
+ *
116
+ * Empty when there is no secret or no origin, for the reason
117
+ * {@link buildUnsubscribeUrl} gives: a link pointing at nothing is worse than
118
+ * no link, because the recipient believes they have confirmed.
119
+ */
120
+ export declare function buildConfirmUrl(input: {
121
+ siteBase: string;
122
+ hostId: string;
123
+ email: string;
124
+ topicId: string;
125
+ /** Defaults to {@link unsubscribeLinkSecret} — the same signing secret. */
126
+ secret?: string;
127
+ }): string;
128
+ /**
129
+ * The absolute unsubscribe URL for one recipient of one site's mail.
130
+ *
131
+ * @returns the URL, or empty string when there is no secret to sign with or
132
+ * no origin to resolve against. Empty rather than a half-built URL:
133
+ * a caller can tell it does not have a link, and a link pointing at
134
+ * nothing is worse than an absent header, because the recipient
135
+ * believes they have unsubscribed.
136
+ */
137
+ export declare function buildUnsubscribeUrl(input: {
138
+ siteBase: string;
139
+ hostId: string;
140
+ email: string;
141
+ /** The campaign this link rides in, when there is one. */
142
+ campaignId?: string;
143
+ /** The topic the message belonged to, when it belonged to one. */
144
+ topicId?: string;
145
+ /**
146
+ * Which of the two URLs over this one signature to build.
147
+ *
148
+ * `one-click` is what the `List-Unsubscribe` header names: a mailbox
149
+ * provider POSTs it with no human present and expects the act to have
150
+ * happened when it reads the 200, so it points at the route whose POST
151
+ * writes immediately and must never point at a page somebody has to submit.
152
+ * `preferences` is the link a PERSON clicks in the footer, where the topic
153
+ * this message belonged to is one of the things they can stop instead of
154
+ * all of it. Defaults to `one-click`, which is what a sender with no topic
155
+ * — every non-campaign marketing path — wants.
156
+ */
157
+ surface?: 'one-click' | 'preferences';
158
+ /** Defaults to {@link unsubscribeLinkSecret}. */
159
+ secret?: string;
160
+ }): string;
161
+ /**
162
+ * The signature of a payload for one purpose, base64url — or `''` when there
163
+ * is no secret, no purpose or no payload to sign, so a caller can tell it has
164
+ * no link rather than one that verifies for anybody.
165
+ */
166
+ export declare function signedLinkSignature(purpose: string, payload: string, secret?: string): string;
167
+ /** Whether a signature is this payload's, for this purpose, in constant time. */
168
+ export declare function signedLinkSignatureMatches(args: {
169
+ purpose: string;
170
+ payload: string;
171
+ signature: string;
172
+ /** Defaults to {@link unsubscribeLinkSecret}. */
173
+ secret?: string;
174
+ }): boolean;
@@ -0,0 +1,236 @@
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 UNSUBSCRIBE LINK — one signer, one URL shape, one verifier.
18
+ *
19
+ * A signed `/api/email/unsubscribe` URL was minted in exactly one place, the
20
+ * campaign sender, and verified in another, the unsubscribe handler. Two
21
+ * copies of an HMAC subject is a shape that only stays correct while nobody
22
+ * adds a third caller — and the marketing gate is the third caller, so the
23
+ * subject moved here and both existing sides now read it from one place.
24
+ *
25
+ * ## The address is lowercased in the subject AND in the URL
26
+ *
27
+ * This is the property that makes one derivation work rather than two that
28
+ * agree by luck. The campaign sender lowercases every address far upstream,
29
+ * so its links have always carried a lowercase address and its signature has
30
+ * always covered one; the handler's verifier does not lowercase, and did not
31
+ * need to. Marketing mail reaches addresses that no upstream step
32
+ * normalized — a checkout's `customerEmail`, a form payload's `email` — so a
33
+ * link minted over `Bob@Example.com` would sign the lowercase form and put
34
+ * the mixed-case form on the URL, and the verifier would answer 403 to a
35
+ * recipient pressing Unsubscribe.
36
+ *
37
+ * So {@link buildUnsubscribeUrl} writes the lowercased address into the query
38
+ * as well as into the subject, and the two agree by construction rather than
39
+ * by every caller remembering.
40
+ */ import { createHmac, timingSafeEqual } from "crypto";
41
+ /**
42
+ * The shared secret, or empty when the deployment has none.
43
+ *
44
+ * `CRON_SECRET` is the documented fallback, matching what the campaign sender
45
+ * and the unsubscribe handler already resolve. Read per call rather than at
46
+ * module load: these run in serverless handlers where the module may be
47
+ * evaluated during a build, long before the runtime env exists.
48
+ */ export function unsubscribeLinkSecret() {
49
+ return process.env.EMAIL_UNSUBSCRIBE_SECRET || process.env.CRON_SECRET || '';
50
+ }
51
+ /**
52
+ * The signed subject for an unsubscribe link.
53
+ *
54
+ * ## Two signed forms, and why the campaign id is additive
55
+ *
56
+ * Every email already sitting in an inbox carries a two-part signature over
57
+ * `hostId:email`, and those links must go on working forever — an unsubscribe
58
+ * link that stops honoring itself is the one bug in this area with a legal
59
+ * edge on it. So the campaign is appended only when it is present, and the
60
+ * verifier chooses which form to check by whether the link carries a `cid`.
61
+ *
62
+ * SIGNED rather than passed alongside. An unsigned `cid` would be an
63
+ * attribution anybody holding one valid link could point at any campaign they
64
+ * liked — a small forgery, and a completely gratuitous one, since the
65
+ * campaign is already known at the moment the link is minted.
66
+ */ export function unsubscribeSignatureSubject(hostId, email, campaignId, topicId) {
67
+ const address = String(email != null ? email : '').trim().toLowerCase();
68
+ /*
69
+ * A colon in either id is refused outright, because the forms are joined
70
+ * with one. A four-part `host:email:c:t` is byte-identical to a three-part
71
+ * subject whose campaign id is `c:t`, so one signature would verify two
72
+ * different parameter tuples and a topic link could be re-presented as a
73
+ * campaign link with the topic spliced in. It costs nothing real: ids come
74
+ * from `createResourceUid()`, whose alphabet has no colon.
75
+ *
76
+ * A topic with no campaign DROPS the topic rather than producing a
77
+ * four-part subject with an empty middle, which would make
78
+ * `host:email::t` and a campaign id of `:t` the same string. The sender
79
+ * always has a campaign, so this is a guard rather than a path.
80
+ */ const campaign = String(campaignId != null ? campaignId : '');
81
+ const topic = String(topicId != null ? topicId : '');
82
+ if (campaign.includes(':') || topic.includes(':')) return '';
83
+ if (topic && campaign) return `${hostId}:${address}:${campaign}:${topic}`;
84
+ if (campaign) return `${hostId}:${address}:${campaign}`;
85
+ return `${hostId}:${address}`;
86
+ }
87
+ /** HMAC for unsubscribe links; env-gated on the shared secret. */ export function unsubscribeSignature(hostId, email, secret, campaignId, topicId) {
88
+ const subject = unsubscribeSignatureSubject(hostId, email, campaignId, topicId);
89
+ // An unsignable subject yields no signature rather than a signature over the
90
+ // empty string, which would verify for every caller that also passed one.
91
+ if (!subject) return '';
92
+ return createHmac('sha256', secret).update(subject).digest('hex');
93
+ }
94
+ /**
95
+ * Whether a signature is this link's.
96
+ *
97
+ * Which form is checked is decided by the LINK, not by the signature: a link
98
+ * carrying no `cid` is checked against the two-part form and a link carrying
99
+ * one against the three-part form. There is no fallback between them, and
100
+ * that is what stops this being a downgrade — an attacker cannot take a
101
+ * three-part link, drop the `cid` and have it verify, because the two-part
102
+ * check over the same `hostId:email` produces a different digest. Nor can
103
+ * they bolt a `cid` onto a two-part link: the three-part check then fails.
104
+ *
105
+ * `timingSafeEqual` needs equal lengths, so the length is compared first — it
106
+ * is not a secret, both digests are fixed-width hex, and the call throws on a
107
+ * mismatch rather than returning false.
108
+ */ export function unsubscribeSignatureMatches(args) {
109
+ const expected = unsubscribeSignature(args.hostId, args.email, args.secret, args.campaignId || undefined);
110
+ return expected.length === args.signature.length && timingSafeEqual(new Uint8Array(Buffer.from(expected)), new Uint8Array(Buffer.from(args.signature)));
111
+ }
112
+ /**
113
+ * The purpose component that distinguishes a CONFIRMATION subject.
114
+ *
115
+ * See {@link confirmSignatureSubject}. Exported because the plugin's verifier
116
+ * imports it rather than restating the literal.
117
+ */ export const CONFIRM_SUBJECT_PREFIX = 'confirm';
118
+ /**
119
+ * The signed subject for a double opt-in confirmation link.
120
+ *
121
+ * ## Why it is not one of the three forms above
122
+ *
123
+ * A confirmation names a host, an address and a topic, and it has NO
124
+ * campaign — nobody is unsubscribing from a message, they are joining a
125
+ * stream. The forms above refuse exactly that combination: a topic with no
126
+ * campaign would leave an empty middle component, and `host:email::t` is the
127
+ * same string as a three-part subject whose campaign id is `:t`.
128
+ *
129
+ * So it gets a leading PURPOSE component. That is a fourth form of the same
130
+ * scheme, not a second scheme: the digest, the secret and the comparison are
131
+ * unchanged.
132
+ *
133
+ * ## The one collision, and the guard for it
134
+ *
135
+ * `confirm:H:E:T` is byte-identical to the four-part unsubscribe subject
136
+ * `A:B:C:D` when the site's id is literally `confirm`, which would let one
137
+ * signature verify as both. Ids are not ours to constrain after the fact, so
138
+ * the subject is refused for that host rather than the collision being
139
+ * reasoned about: the cost is one unusable document id, and the alternative
140
+ * is a signature that means two things.
141
+ *
142
+ * @returns the subject, or `''` for a combination it cannot sign
143
+ * unambiguously. Empty rather than a partial subject, so
144
+ * {@link confirmSignature} yields no signature rather than one over
145
+ * the empty string — which would verify for every other caller that
146
+ * also produced one.
147
+ */ export function confirmSignatureSubject(hostId, email, topicId) {
148
+ const address = String(email != null ? email : '').trim().toLowerCase();
149
+ const host = String(hostId != null ? hostId : '');
150
+ const topic = String(topicId != null ? topicId : '');
151
+ if (!host || !address || !topic) return '';
152
+ if (host === CONFIRM_SUBJECT_PREFIX) return '';
153
+ if (host.includes(':') || topic.includes(':')) return '';
154
+ return `${CONFIRM_SUBJECT_PREFIX}:${host}:${address}:${topic}`;
155
+ }
156
+ /** HMAC for a confirmation link; empty for an unsignable subject. */ export function confirmSignature(hostId, email, topicId, secret) {
157
+ const subject = confirmSignatureSubject(hostId, email, topicId);
158
+ if (!subject || !secret) return '';
159
+ return createHmac('sha256', secret).update(subject).digest('hex');
160
+ }
161
+ /**
162
+ * The absolute confirmation URL for one address and one topic.
163
+ *
164
+ * Minted where the signup happens, because the message carrying it is sent
165
+ * from there — and that message is TRANSACTIONAL, not marketing: the person
166
+ * just asked for this, so asking them to confirm it is the transaction they
167
+ * started. It carries no unsubscribe header for the same reason a receipt
168
+ * does not.
169
+ *
170
+ * Empty when there is no secret or no origin, for the reason
171
+ * {@link buildUnsubscribeUrl} gives: a link pointing at nothing is worse than
172
+ * no link, because the recipient believes they have confirmed.
173
+ */ export function buildConfirmUrl(input) {
174
+ var _input_secret, _input_siteBase, _input_email;
175
+ const secret = (_input_secret = input.secret) != null ? _input_secret : unsubscribeLinkSecret();
176
+ const siteBase = String((_input_siteBase = input.siteBase) != null ? _input_siteBase : '').replace(/\/+$/, '');
177
+ const address = String((_input_email = input.email) != null ? _input_email : '').trim().toLowerCase();
178
+ const signature = confirmSignature(input.hostId, address, input.topicId, secret);
179
+ if (!siteBase || !signature) return '';
180
+ return `${siteBase}/api/email/confirm` + `?hostId=${encodeURIComponent(input.hostId)}` + `&email=${encodeURIComponent(address)}` + `&tid=${encodeURIComponent(input.topicId)}` + `&sig=${signature}`;
181
+ }
182
+ /**
183
+ * The absolute unsubscribe URL for one recipient of one site's mail.
184
+ *
185
+ * @returns the URL, or empty string when there is no secret to sign with or
186
+ * no origin to resolve against. Empty rather than a half-built URL:
187
+ * a caller can tell it does not have a link, and a link pointing at
188
+ * nothing is worse than an absent header, because the recipient
189
+ * believes they have unsubscribed.
190
+ */ export function buildUnsubscribeUrl(input) {
191
+ var _input_secret, _input_siteBase, _input_email;
192
+ const secret = (_input_secret = input.secret) != null ? _input_secret : unsubscribeLinkSecret();
193
+ const siteBase = String((_input_siteBase = input.siteBase) != null ? _input_siteBase : '').replace(/\/+$/, '');
194
+ const address = String((_input_email = input.email) != null ? _input_email : '').trim().toLowerCase();
195
+ if (!secret || !siteBase || !input.hostId || !address) return '';
196
+ const signature = unsubscribeSignature(input.hostId, address, secret, input.campaignId, input.topicId);
197
+ if (!signature) return '';
198
+ const route = input.surface === 'preferences' ? '/api/email/preferences' : '/api/email/unsubscribe';
199
+ return `${siteBase}${route}` + `?hostId=${encodeURIComponent(input.hostId)}` + `&email=${encodeURIComponent(address)}` + `&sig=${signature}` + (input.campaignId ? `&cid=${encodeURIComponent(input.campaignId)}` : '') + (input.topicId ? `&tid=${encodeURIComponent(input.topicId)}` : '');
200
+ }
201
+ /*==========================================
202
+ * A SIGNED LINK OF A SENDER'S OWN (AGL-2981).
203
+ *
204
+ * The forms above are the platform's own unsubscribe and confirmation
205
+ * links, each a subject over a site and an address. A sender that mails
206
+ * from somewhere else — a plugin sending from a person's own mailbox, whose
207
+ * link carries its own facts — signs its link here instead, over a payload
208
+ * of its own and under a PURPOSE of its own.
209
+ *
210
+ * The purpose is not a prefix of the subject, which is how the confirmation
211
+ * form had to guard a collision by hand. It selects the KEY: every purpose's
212
+ * links are signed with an HMAC key derived from the shared secret and the
213
+ * purpose, so a signature minted for one purpose verifies for no other and
214
+ * for none of the platform's own forms above, whatever the payloads spell.
215
+ * The secret, the digest and the constant-time comparison are the same.
216
+ *==========================================*/ /** The key one purpose's links are signed with: derived from the secret, bound to the purpose. */ function signedLinkKey(purpose, secret) {
217
+ return createHmac('sha256', secret).update(`aglyn-signed-link:${purpose}`).digest();
218
+ }
219
+ /**
220
+ * The signature of a payload for one purpose, base64url — or `''` when there
221
+ * is no secret, no purpose or no payload to sign, so a caller can tell it has
222
+ * no link rather than one that verifies for anybody.
223
+ */ export function signedLinkSignature(purpose, payload, secret = unsubscribeLinkSecret()) {
224
+ const name = String(purpose != null ? purpose : '').trim();
225
+ const body = String(payload != null ? payload : '');
226
+ if (!secret || !name || !body) return '';
227
+ return createHmac('sha256', signedLinkKey(name, secret)).update(body).digest('base64url');
228
+ }
229
+ /** Whether a signature is this payload's, for this purpose, in constant time. */ export function signedLinkSignatureMatches(args) {
230
+ var _args_secret, _args_signature;
231
+ const expected = signedLinkSignature(args.purpose, args.payload, (_args_secret = args.secret) != null ? _args_secret : unsubscribeLinkSecret());
232
+ const presented = String((_args_signature = args.signature) != null ? _args_signature : '');
233
+ return expected.length > 0 && expected.length === presented.length && timingSafeEqual(new Uint8Array(Buffer.from(expected)), new Uint8Array(Buffer.from(presented)));
234
+ }
235
+
236
+ //# sourceMappingURL=email-unsubscribe-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/email-unsubscribe-link.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 UNSUBSCRIBE LINK — one signer, one URL shape, one verifier.\n *\n * A signed `/api/email/unsubscribe` URL was minted in exactly one place, the\n * campaign sender, and verified in another, the unsubscribe handler. Two\n * copies of an HMAC subject is a shape that only stays correct while nobody\n * adds a third caller — and the marketing gate is the third caller, so the\n * subject moved here and both existing sides now read it from one place.\n *\n * ## The address is lowercased in the subject AND in the URL\n *\n * This is the property that makes one derivation work rather than two that\n * agree by luck. The campaign sender lowercases every address far upstream,\n * so its links have always carried a lowercase address and its signature has\n * always covered one; the handler's verifier does not lowercase, and did not\n * need to. Marketing mail reaches addresses that no upstream step\n * normalized — a checkout's `customerEmail`, a form payload's `email` — so a\n * link minted over `Bob@Example.com` would sign the lowercase form and put\n * the mixed-case form on the URL, and the verifier would answer 403 to a\n * recipient pressing Unsubscribe.\n *\n * So {@link buildUnsubscribeUrl} writes the lowercased address into the query\n * as well as into the subject, and the two agree by construction rather than\n * by every caller remembering.\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto'\n\n/**\n * The shared secret, or empty when the deployment has none.\n *\n * `CRON_SECRET` is the documented fallback, matching what the campaign sender\n * and the unsubscribe handler already resolve. Read per call rather than at\n * module load: these run in serverless handlers where the module may be\n * evaluated during a build, long before the runtime env exists.\n */\nexport function unsubscribeLinkSecret(): string {\n return process.env.EMAIL_UNSUBSCRIBE_SECRET || process.env.CRON_SECRET || ''\n}\n\n/**\n * The signed subject for an unsubscribe link.\n *\n * ## Two signed forms, and why the campaign id is additive\n *\n * Every email already sitting in an inbox carries a two-part signature over\n * `hostId:email`, and those links must go on working forever — an unsubscribe\n * link that stops honoring itself is the one bug in this area with a legal\n * edge on it. So the campaign is appended only when it is present, and the\n * verifier chooses which form to check by whether the link carries a `cid`.\n *\n * SIGNED rather than passed alongside. An unsigned `cid` would be an\n * attribution anybody holding one valid link could point at any campaign they\n * liked — a small forgery, and a completely gratuitous one, since the\n * campaign is already known at the moment the link is minted.\n */\nexport function unsubscribeSignatureSubject(\n hostId: string,\n email: string,\n campaignId?: string,\n topicId?: string,\n): string {\n const address = String(email ?? '')\n .trim()\n .toLowerCase()\n /*\n * A colon in either id is refused outright, because the forms are joined\n * with one. A four-part `host:email:c:t` is byte-identical to a three-part\n * subject whose campaign id is `c:t`, so one signature would verify two\n * different parameter tuples and a topic link could be re-presented as a\n * campaign link with the topic spliced in. It costs nothing real: ids come\n * from `createResourceUid()`, whose alphabet has no colon.\n *\n * A topic with no campaign DROPS the topic rather than producing a\n * four-part subject with an empty middle, which would make\n * `host:email::t` and a campaign id of `:t` the same string. The sender\n * always has a campaign, so this is a guard rather than a path.\n */\n const campaign = String(campaignId ?? '')\n const topic = String(topicId ?? '')\n if (campaign.includes(':') || topic.includes(':')) return ''\n if (topic && campaign) return `${hostId}:${address}:${campaign}:${topic}`\n if (campaign) return `${hostId}:${address}:${campaign}`\n return `${hostId}:${address}`\n}\n\n/** HMAC for unsubscribe links; env-gated on the shared secret. */\nexport function unsubscribeSignature(\n hostId: string,\n email: string,\n secret: string,\n campaignId?: string,\n topicId?: string,\n): string {\n const subject = unsubscribeSignatureSubject(hostId, email, campaignId, topicId)\n // An unsignable subject yields no signature rather than a signature over the\n // empty string, which would verify for every caller that also passed one.\n if (!subject) return ''\n return createHmac('sha256', secret).update(subject).digest('hex')\n}\n\n/**\n * Whether a signature is this link's.\n *\n * Which form is checked is decided by the LINK, not by the signature: a link\n * carrying no `cid` is checked against the two-part form and a link carrying\n * one against the three-part form. There is no fallback between them, and\n * that is what stops this being a downgrade — an attacker cannot take a\n * three-part link, drop the `cid` and have it verify, because the two-part\n * check over the same `hostId:email` produces a different digest. Nor can\n * they bolt a `cid` onto a two-part link: the three-part check then fails.\n *\n * `timingSafeEqual` needs equal lengths, so the length is compared first — it\n * is not a secret, both digests are fixed-width hex, and the call throws on a\n * mismatch rather than returning false.\n */\nexport function unsubscribeSignatureMatches(args: {\n hostId: string\n email: string\n campaignId?: string\n signature: string\n secret: string\n}): boolean {\n const expected = unsubscribeSignature(\n args.hostId,\n args.email,\n args.secret,\n args.campaignId || undefined,\n )\n return (\n expected.length === args.signature.length &&\n timingSafeEqual(\n new Uint8Array(Buffer.from(expected)),\n new Uint8Array(Buffer.from(args.signature)),\n )\n )\n}\n\n/**\n * The purpose component that distinguishes a CONFIRMATION subject.\n *\n * See {@link confirmSignatureSubject}. Exported because the plugin's verifier\n * imports it rather than restating the literal.\n */\nexport const CONFIRM_SUBJECT_PREFIX = 'confirm'\n\n/**\n * The signed subject for a double opt-in confirmation link.\n *\n * ## Why it is not one of the three forms above\n *\n * A confirmation names a host, an address and a topic, and it has NO\n * campaign — nobody is unsubscribing from a message, they are joining a\n * stream. The forms above refuse exactly that combination: a topic with no\n * campaign would leave an empty middle component, and `host:email::t` is the\n * same string as a three-part subject whose campaign id is `:t`.\n *\n * So it gets a leading PURPOSE component. That is a fourth form of the same\n * scheme, not a second scheme: the digest, the secret and the comparison are\n * unchanged.\n *\n * ## The one collision, and the guard for it\n *\n * `confirm:H:E:T` is byte-identical to the four-part unsubscribe subject\n * `A:B:C:D` when the site's id is literally `confirm`, which would let one\n * signature verify as both. Ids are not ours to constrain after the fact, so\n * the subject is refused for that host rather than the collision being\n * reasoned about: the cost is one unusable document id, and the alternative\n * is a signature that means two things.\n *\n * @returns the subject, or `''` for a combination it cannot sign\n * unambiguously. Empty rather than a partial subject, so\n * {@link confirmSignature} yields no signature rather than one over\n * the empty string — which would verify for every other caller that\n * also produced one.\n */\nexport function confirmSignatureSubject(\n hostId: string,\n email: string,\n topicId: string,\n): string {\n const address = String(email ?? '')\n .trim()\n .toLowerCase()\n const host = String(hostId ?? '')\n const topic = String(topicId ?? '')\n if (!host || !address || !topic) return ''\n if (host === CONFIRM_SUBJECT_PREFIX) return ''\n if (host.includes(':') || topic.includes(':')) return ''\n return `${CONFIRM_SUBJECT_PREFIX}:${host}:${address}:${topic}`\n}\n\n/** HMAC for a confirmation link; empty for an unsignable subject. */\nexport function confirmSignature(\n hostId: string,\n email: string,\n topicId: string,\n secret: string,\n): string {\n const subject = confirmSignatureSubject(hostId, email, topicId)\n if (!subject || !secret) return ''\n return createHmac('sha256', secret).update(subject).digest('hex')\n}\n\n/**\n * The absolute confirmation URL for one address and one topic.\n *\n * Minted where the signup happens, because the message carrying it is sent\n * from there — and that message is TRANSACTIONAL, not marketing: the person\n * just asked for this, so asking them to confirm it is the transaction they\n * started. It carries no unsubscribe header for the same reason a receipt\n * does not.\n *\n * Empty when there is no secret or no origin, for the reason\n * {@link buildUnsubscribeUrl} gives: a link pointing at nothing is worse than\n * no link, because the recipient believes they have confirmed.\n */\nexport function buildConfirmUrl(input: {\n siteBase: string\n hostId: string\n email: string\n topicId: string\n /** Defaults to {@link unsubscribeLinkSecret} — the same signing secret. */\n secret?: string\n}): string {\n const secret = input.secret ?? unsubscribeLinkSecret()\n const siteBase = String(input.siteBase ?? '').replace(/\\/+$/, '')\n const address = String(input.email ?? '')\n .trim()\n .toLowerCase()\n const signature = confirmSignature(\n input.hostId,\n address,\n input.topicId,\n secret,\n )\n if (!siteBase || !signature) return ''\n return (\n `${siteBase}/api/email/confirm` +\n `?hostId=${encodeURIComponent(input.hostId)}` +\n `&email=${encodeURIComponent(address)}` +\n `&tid=${encodeURIComponent(input.topicId)}` +\n `&sig=${signature}`\n )\n}\n\n/**\n * The absolute unsubscribe URL for one recipient of one site's mail.\n *\n * @returns the URL, or empty string when there is no secret to sign with or\n * no origin to resolve against. Empty rather than a half-built URL:\n * a caller can tell it does not have a link, and a link pointing at\n * nothing is worse than an absent header, because the recipient\n * believes they have unsubscribed.\n */\nexport function buildUnsubscribeUrl(input: {\n siteBase: string\n hostId: string\n email: string\n /** The campaign this link rides in, when there is one. */\n campaignId?: string\n /** The topic the message belonged to, when it belonged to one. */\n topicId?: string\n /**\n * Which of the two URLs over this one signature to build.\n *\n * `one-click` is what the `List-Unsubscribe` header names: a mailbox\n * provider POSTs it with no human present and expects the act to have\n * happened when it reads the 200, so it points at the route whose POST\n * writes immediately and must never point at a page somebody has to submit.\n * `preferences` is the link a PERSON clicks in the footer, where the topic\n * this message belonged to is one of the things they can stop instead of\n * all of it. Defaults to `one-click`, which is what a sender with no topic\n * — every non-campaign marketing path — wants.\n */\n surface?: 'one-click' | 'preferences'\n /** Defaults to {@link unsubscribeLinkSecret}. */\n secret?: string\n}): string {\n const secret = input.secret ?? unsubscribeLinkSecret()\n const siteBase = String(input.siteBase ?? '').replace(/\\/+$/, '')\n const address = String(input.email ?? '')\n .trim()\n .toLowerCase()\n if (!secret || !siteBase || !input.hostId || !address) return ''\n const signature = unsubscribeSignature(\n input.hostId,\n address,\n secret,\n input.campaignId,\n input.topicId,\n )\n if (!signature) return ''\n const route =\n input.surface === 'preferences'\n ? '/api/email/preferences'\n : '/api/email/unsubscribe'\n return (\n `${siteBase}${route}` +\n `?hostId=${encodeURIComponent(input.hostId)}` +\n `&email=${encodeURIComponent(address)}` +\n `&sig=${signature}` +\n (input.campaignId ? `&cid=${encodeURIComponent(input.campaignId)}` : '') +\n (input.topicId ? `&tid=${encodeURIComponent(input.topicId)}` : '')\n )\n}\n\n/*==========================================\n * A SIGNED LINK OF A SENDER'S OWN (AGL-2981).\n *\n * The forms above are the platform's own unsubscribe and confirmation\n * links, each a subject over a site and an address. A sender that mails\n * from somewhere else — a plugin sending from a person's own mailbox, whose\n * link carries its own facts — signs its link here instead, over a payload\n * of its own and under a PURPOSE of its own.\n *\n * The purpose is not a prefix of the subject, which is how the confirmation\n * form had to guard a collision by hand. It selects the KEY: every purpose's\n * links are signed with an HMAC key derived from the shared secret and the\n * purpose, so a signature minted for one purpose verifies for no other and\n * for none of the platform's own forms above, whatever the payloads spell.\n * The secret, the digest and the constant-time comparison are the same.\n *==========================================*/\n\n/** The key one purpose's links are signed with: derived from the secret, bound to the purpose. */\nfunction signedLinkKey(purpose: string, secret: string): Buffer {\n return createHmac('sha256', secret).update(`aglyn-signed-link:${purpose}`).digest()\n}\n\n/**\n * The signature of a payload for one purpose, base64url — or `''` when there\n * is no secret, no purpose or no payload to sign, so a caller can tell it has\n * no link rather than one that verifies for anybody.\n */\nexport function signedLinkSignature(\n purpose: string,\n payload: string,\n secret: string = unsubscribeLinkSecret(),\n): string {\n const name = String(purpose ?? '').trim()\n const body = String(payload ?? '')\n if (!secret || !name || !body) return ''\n return createHmac('sha256', signedLinkKey(name, secret)).update(body).digest('base64url')\n}\n\n/** Whether a signature is this payload's, for this purpose, in constant time. */\nexport function signedLinkSignatureMatches(args: {\n purpose: string\n payload: string\n signature: string\n /** Defaults to {@link unsubscribeLinkSecret}. */\n secret?: string\n}): boolean {\n const expected = signedLinkSignature(args.purpose, args.payload, args.secret ?? unsubscribeLinkSecret())\n const presented = String(args.signature ?? '')\n return (\n expected.length > 0 &&\n expected.length === presented.length &&\n timingSafeEqual(new Uint8Array(Buffer.from(expected)), new Uint8Array(Buffer.from(presented)))\n )\n}\n"],"names":["createHmac","timingSafeEqual","unsubscribeLinkSecret","process","env","EMAIL_UNSUBSCRIBE_SECRET","CRON_SECRET","unsubscribeSignatureSubject","hostId","email","campaignId","topicId","address","String","trim","toLowerCase","campaign","topic","includes","unsubscribeSignature","secret","subject","update","digest","unsubscribeSignatureMatches","args","expected","undefined","length","signature","Uint8Array","Buffer","from","CONFIRM_SUBJECT_PREFIX","confirmSignatureSubject","host","confirmSignature","buildConfirmUrl","input","siteBase","replace","encodeURIComponent","buildUnsubscribeUrl","route","surface","signedLinkKey","purpose","signedLinkSignature","payload","name","body","signedLinkSignatureMatches","presented"],"mappings":"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"}
@@ -0,0 +1,116 @@
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 PluginPersonErasureReport } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
18
+ export interface ErasePersonOptions {
19
+ orgId: string;
20
+ /** Any spelling; normalized before anything is looked up. */
21
+ email: unknown;
22
+ /** Injectable for tests; defaults to the admin app's Firestore. */
23
+ firestore?: any;
24
+ /** Injectable for tests; defaults to `Date.now()`. */
25
+ now?: number;
26
+ }
27
+ /**
28
+ * Counts, never identities: this is what the request document and the
29
+ * audit row record, and both are read by people who must not learn the
30
+ * address from them.
31
+ */
32
+ export interface ErasePersonCounts {
33
+ /** Sites in the workspace the sweep walked. */
34
+ hosts: number;
35
+ /** Suppression rows written, one per site. */
36
+ hostsSuppressed: number;
37
+ /** Contact documents deleted — the row every site shared. */
38
+ contacts: number;
39
+ /** Companies whose contact count moved down. */
40
+ companyLinks: number;
41
+ /** Deals whose `contactId` was removed. */
42
+ deals: number;
43
+ tasks: number;
44
+ activities: number;
45
+ /** `hosts/{hostId}/leads/{personKey}` rows deleted. */
46
+ leads: number;
47
+ /** Audience-list member rows deleted, across every list. */
48
+ listMemberships: number;
49
+ /** Orders with the buyer's identity removed; the record stays. */
50
+ orders: number;
51
+ /** Bookings with the person's identity removed; the record stays. */
52
+ bookings: number;
53
+ /** Delivery-log messages deleted under the address. */
54
+ emailDeliveries: number;
55
+ /**
56
+ * Each plugin's share (AGL-2981), by plugin id: its eraser's own counts,
57
+ * or `null` for an eraser that failed — its data may remain.
58
+ */
59
+ plugins: Record<string, PluginPersonErasureReport | null>;
60
+ }
61
+ export type ErasePersonResult = ({
62
+ ok: true;
63
+ } & ErasePersonCounts) | {
64
+ ok: false;
65
+ skippedReason: 'invalid-email';
66
+ };
67
+ /**
68
+ * Remove one person from one workspace (AGL-2623).
69
+ *
70
+ * `planContactDetach` is deliberately not consulted: the CRM's delete is a
71
+ * detach that leaves the row for the other holders, and this is the act
72
+ * that must not. The contact document goes whole — every site's facet,
73
+ * every consent entry, every attribution — and with it everything the
74
+ * workspace keeps beside the person.
75
+ *
76
+ * ## Order
77
+ *
78
+ * 1. **Suppress first.** One row per site on the per-site suppression
79
+ * list, before any delete: a form filled in while the sweep runs must
80
+ * already find the door closed, or the sweep deletes a row that the
81
+ * capture re-creates a moment later.
82
+ * 2. The plugins' share: every eraser registered on
83
+ * `plugin-person-erasure`, handed the address, its key and the ids of
84
+ * the contacts about to go, while those documents still exist.
85
+ * 3. The contact and its satellites — company counts, deals unlinked,
86
+ * tasks and activities deleted — by the contact's id, then the
87
+ * document itself.
88
+ * 4. Leads, list memberships, orders and bookings by the address, on
89
+ * every site of the workspace.
90
+ * 5. The delivery log, last: it is filed under the address alone, and the
91
+ * tombstone it leaves is what keeps a later import from refilling it.
92
+ *
93
+ * ## What is anonymized rather than deleted
94
+ *
95
+ * An order is the merchant's record of a sale and a booking of an
96
+ * appointment; the amounts, the line items and the tax are theirs to keep
97
+ * and the law expects them kept. The person is taken OFF those records —
98
+ * name, email, phone, addresses — and a stamp says when. A deal is the
99
+ * team's own pipeline record and is unlinked. Everything else that names
100
+ * the person is about the person and is deleted.
101
+ *
102
+ * ## What is not reached, and why
103
+ *
104
+ * A form submission keeps the address inside `fields`, under whatever the
105
+ * form called it — there is no key to query by and a scan of every
106
+ * submission on every site is unbounded. A site member's login is their
107
+ * own account. A subscription carries the address it bills, and a live one
108
+ * cannot be anonymized without breaking its receipts. Each is named to the
109
+ * admin by the dialog so they can finish by hand.
110
+ *
111
+ * Every sweep is best-effort against the others: a failure in one is
112
+ * logged and counted as zero, and the request's counts say what happened.
113
+ * The caller decides whether zero contacts on a person it could see is a
114
+ * failure to retry.
115
+ */
116
+ export declare function erasePerson(options: ErasePersonOptions): Promise<ErasePersonResult>;