@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 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/erase.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from './firebase-admin'\nimport { eraseMediaDeliveryScope } from './media-delivery'\nimport { deleteHostProjectionForAllMembers } from './host-memberships'\nimport { detachWorkspaceDomain } from './workspace-domains'\nimport {\n CONSOLE_DOMAINS_COLLECTION,\n releaseConsoleDomain,\n} from './console-domains'\nimport { authForPool, findUserByUidAcrossPools } from './auth-pools'\nimport { eraseEmailDeliveriesForAddresses } from './email-delivery-log'\nimport {\n type AccountAddressSet,\n type AccountAddressSource,\n resolveAccountAddresses,\n} from './account-addresses'\nimport { EMAIL_IDENTITY_INDEX_COLLECTION } from './account-emails'\nimport { removeOrgMember } from './organizations'\nimport {\n runPluginUserErasers,\n type PluginUserErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport {\n runPluginOrgErasers,\n type PluginOrgErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport { isBillingSubscription } from '@aglyn/aglyn/server'\nimport { readOrgBilling } from './org-billing'\nimport {\n disposeHostSendingDomain,\n readSendingDomainTeardownByLabel,\n type SendingDomainDisposition,\n type TeardownSendingDomainDriver,\n} from './sending-domain-debt'\n\n/** The reversible hold before a requested erasure is executed (AGL-485). */\nexport const ERASURE_HOLD_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * The admin app is initialized without a default storageBucket, so every\n * bucket access must name it explicitly (same as the media routes). Falls\n * through to the admin default if the env is somehow unset.\n */\nfunction storageBucket() {\n const name = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET\n return firebaseAdmin.app().storage().bucket(name || undefined)\n}\n\n/**\n * `supplierDeliveries` — the dropship outbox, keyed by `hostId` as a FIELD.\n *\n * The literal rather than an import: the collection belongs to\n * `libs/plugins/commerce/src/lib/server/supplier-outbox.ts`\n * (`SUPPLIER_DELIVERY_COLLECTION`), which is tagged `aglyn:addons`, and this\n * library is `scope:data` — the nx boundary forbids the edge, and reaching\n * for a dynamic import to dodge it would poison the lint graph instead. The\n * emulator spec asserts the two spellings still agree by reading that file,\n * so a rename fails a test rather than silently un-wiring this sweep.\n */\nconst SUPPLIER_DELIVERY_COLLECTION = 'supplierDeliveries'\n\n/**\n * `outreachMailboxCredentials` — the OAuth grant behind each mailbox a rep\n * connects to Outreach, keyed by the mailbox id and carrying `orgId` as a\n * FIELD.\n *\n * The literal rather than an import, for `SUPPLIER_DELIVERY_COLLECTION`'s\n * reason: the collection belongs to `libs/plugins/outreach`\n * (`OUTREACH_COLLECTIONS.mailboxCredentials` in\n * `src/lib/model/outreach.types.ts`), and this `scope:data` library may not\n * import a plugin. The plugin's own spec asserts the two spellings agree, so\n * a rename fails a test rather than silently un-wiring the sweep.\n *\n * A static sweep here, not a hook the plugin registers, because an erasure\n * cannot rely on the plugin being present. An org that switched Outreach off\n * still holds the tokens its reps granted, and the process running the\n * erasure — the cron, the operator script, a replay — may never have loaded\n * the plugin's bundle at all. A credential's lifetime cannot depend on which\n * bundles an erasing process happened to load.\n */\nconst OUTREACH_MAILBOX_CREDENTIALS_COLLECTION = 'outreachMailboxCredentials'\n\n/**\n * Destroy the site's DEAD-LETTERED supplier deliveries (AGL-1448).\n *\n * `supplierDeliveries/{id}` is TOP-LEVEL and carries `hostId` as a field, so\n * neither `recursiveDelete(hosts/{hostId})` here nor\n * `recursiveDelete(orgs/{orgId})` in `eraseOrg` can see it. That placement is\n * deliberate and correct — a subcollection would be editor-writable under the\n * permissive host catch-all, and the row carries the exact JSON body we sign\n * and POST to the supplier — so the fix is a sweep, not a move.\n *\n * **The gap is the dead letter, and the module that created it named it.**\n * `supplier-outbox.ts` DELETES a row the moment delivery succeeds, and its\n * docblock says why in terms: the body holds the buyer's email and name, and\n * a top-level document is outside the recursive delete an erasure runs. The\n * happy path was solved by that deletion. The failure path was not: after\n * `SUPPLIER_DELIVERY_MAX_ATTEMPTS` the row is left standing as `failed`,\n * because a human is supposed to act on it and the merchant has been told.\n * Nothing ever collects it afterwards — no TTL policy keys on it, no cron\n * reaps it, and the erasure could not reach it. So the one class of row that\n * survives is the one holding a THIRD PARTY's name, email and shipping\n * address, against a site whose owner has since asked to be erased and whom\n * nobody can ask to act on the dead letter any more.\n *\n * Bounded by the `hostId` field, never a collection sweep: this collection\n * holds every other merchant's orders. Chunked, because a batch caps at 500\n * and nothing bounds a dead-letter backlog below that.\n *\n * In `eraseHost` rather than `eraseOrg` on purpose — it is host-scoped data,\n * so the self-serve \"delete this site\" path gets the same cleanup as a full\n * org erasure instead of leaving the residue behind for the smaller action.\n *\n * Best-effort, like every other trailing cleanup in `eraseHost`: a site\n * delete must not fail because an outbox query did.\n */\nasync function eraseHostSupplierDeliveries(hostId: string): Promise<number> {\n if (!hostId) return 0\n try {\n const firestore = firebaseAdmin.app().firestore()\n const rows = await firestore\n .collection(SUPPLIER_DELIVERY_COLLECTION)\n .where('hostId', '==', hostId)\n .get()\n for (let index = 0; index < rows.docs.length; index += 400) {\n const batch = firestore.batch()\n for (const doc of rows.docs.slice(index, index + 400)) {\n batch.delete(doc.ref)\n }\n await batch.commit()\n }\n return rows.size\n } catch (error) {\n console.error(`eraseHost: supplier outbox cleanup failed for ${hostId}`, error)\n return 0\n }\n}\n\n/**\n * Permanently erase a single site and everything it owns (AGL-488).\n * Unlike deleting the host doc alone (which orphans data), this cleans up\n * every trailing reference:\n * - Storage objects under `hosts/{hostId}/` (media, CDN variants),\n * - the `hostIndex/{hostId}` routing entry (subdomain/cname resolution),\n * - the owning org's `hosts` routing-map entry, and\n * - the host's Firestore tree (screens, layouts, versions, counters, …)\n * via `recursiveDelete`.\n *\n * Shared by the self-serve delete-site route and the org-erasure pipeline\n * (AGL-487). Fail-soft on Storage/index cleanup so a partial failure never\n * blocks the Firestore delete; safe to re-run.\n */\nexport interface EraseHostOptions {\n /**\n * The console-side sending-domain teardown, injected because this library\n * may not hold the credentials it needs — see `sending-domain-debt.ts`.\n *\n * Absent, the site's domain is not released inline: the debt is recorded on\n * the surviving label claim and `/api/admin/reap-sending-domains` settles\n * it. That is the only correct default here. A library the tenant runtime\n * imports cannot reach a full-access mail credential, and an erasure must\n * not fail or stall because a vendor is unreachable.\n */\n tearDownSendingDomain?: TeardownSendingDomainDriver | null\n}\n\nexport interface EraseHostResult {\n /** What became of the site's dedicated sending domain, if it had one. */\n sendingDomain: SendingDomainDisposition\n}\n\nexport async function eraseHost(\n hostId: string,\n options: EraseHostOptions = {},\n): Promise<EraseHostResult> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n const orgId = hostSnapshot.get('orgId') as string | undefined\n\n /*==========================================\n * THE SENDING DOMAIN, READ FIRST AND RELEASED LAST.\n *\n * Read here because the pinned label lives on the host document and the\n * `recursiveDelete` at the bottom of this function destroys it. Read\n * afterwards there would be nothing left to say WHICH provider domain holds\n * a plan-capped account slot and WHICH records in our zone carry a signing\n * key for a name a future site can claim.\n *\n * Released after everything else, so a teardown that dies partway leaves the\n * claim standing and the domain findable rather than orphaning two vendors'\n * resources with nothing pointing at them.\n *=========================================*/\n const sendingLabel = String(hostSnapshot.get('sendingLabel') ?? '').trim()\n const sendingTeardown = sendingLabel\n ? await readSendingDomainTeardownByLabel(sendingLabel).catch(() => null)\n : null\n\n // Storage first (best-effort — the object tree is derived, regenerable).\n try {\n await storageBucket().deleteFiles({ prefix: `hosts/${hostId}/` })\n } catch (error) {\n console.error(`eraseHost: storage cleanup failed for ${hostId}`, error)\n }\n // And the delivery provider's copies of the site's video (AGL-2824), held\n // under the same library prefix. Nothing runs when no provider is\n // configured to store; a failure is logged, like the bucket's.\n await eraseMediaDeliveryScope({ collection: 'hosts', scopeId: hostId })\n\n // Dead-lettered supplier deliveries (AGL-1448). See the note on\n // SUPPLIER_DELIVERY_COLLECTION below for why this is here and not implied\n // by the recursiveDelete at the end of this function.\n await eraseHostSupplierDeliveries(hostId)\n\n // Routing: the middleware resolves a request to a host via hostIndex and\n // the owning org's hosts map — drop both so the subdomain/cname 404s.\n await firestore\n .collection('hostIndex')\n .doc(hostId)\n .delete()\n .catch(() => undefined)\n if (orgId) {\n // The routing entry and the site's POOLED SEATS go in the SAME write —\n // POS registers (AGL-1775) and collaborators (AGL-2439), which are the\n // same mechanism on two keys and must be released together or a deleted\n // site strands one pool while returning the other.\n //\n // `registerAllocations[hostId]` is capacity the org has\n // paid for and assigned here; a deleted site must return it to the pool\n // or the org keeps paying $89/mo for a seat pinned to a site that no\n // longer exists and cannot be reassigned from any surface. Releasing it\n // by deleting the key rather than by decrementing a counter means the\n // pool is `purchased - sum(allocations)` by arithmetic and has nothing to\n // drift out of step with.\n await firestore\n .collection('orgs')\n .doc(orgId)\n .set(\n {\n hosts: { [hostId]: FieldValue.delete() },\n registerAllocations: { [hostId]: FieldValue.delete() },\n collaboratorAllocations: { [hostId]: FieldValue.delete() },\n },\n { merge: true },\n )\n .catch(() => undefined)\n // Drop every member's reverse-index row for this host (AGL-844); the\n // members still exist here (recursiveDelete of the org, if any, is later).\n await deleteHostProjectionForAllMembers(orgId, hostId).catch(() => undefined)\n }\n\n // The host document tree (screens/layouts/versions/counters/products/…).\n await firestore.recursiveDelete(hostRef)\n\n // Never throws, and never leaves the slot unaccounted for: whatever the\n // vendors refuse is recorded on the label claim for the reaper to finish.\n return {\n sendingDomain: await disposeHostSendingDomain({\n teardown: sendingTeardown ? { ...sendingTeardown, hostId } : null,\n tearDown: options?.tearDownSendingDomain,\n }),\n }\n}\n\n/**\n * Delete a single resource document *and everything under it* (AGL-945).\n *\n * Firestore does not cascade: deleting `orgs/{o}/datasets/{d}` leaves every\n * `records/{r}` beneath it alive but unreachable from the console — still\n * billed, still matched by the rules (which match subcollection paths\n * independently of the parent), and resurrected wholesale if the parent id\n * is ever reused. Only the Admin SDK has `recursiveDelete`, so the console\n * cards route their deletes through /api/resources/erase, which lands here.\n *\n * Authorization belongs to the caller — this only walks the tree.\n */\nexport async function eraseSubtree(\n path: readonly [string, string, string, string],\n): Promise<void> {\n const firestore = firebaseAdmin.app().firestore()\n const [scope, scopeId, kind, id] = path\n await firestore.recursiveDelete(\n firestore.collection(scope).doc(scopeId).collection(kind).doc(id),\n )\n}\n\n/** Best-effort Stripe customer deletion — PII lives at the processor too. */\nasync function deleteStripeCustomer(customerId?: string): Promise<void> {\n const key = process.env.STRIPE_SECRET_KEY\n if (!key || !customerId) return\n try {\n await fetch(`https://api.stripe.com/v1/customers/${customerId}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${key}` },\n })\n } catch (error) {\n console.error(`eraseOrg: Stripe customer ${customerId} delete failed`, error)\n }\n}\n\n/**\n * Delete every API credential the org owns (AGL-1444).\n *\n * `apiKeys` is a TOP-LEVEL collection keyed by the SHA-256 of the token and\n * carrying `orgId` as a FIELD, so `recursiveDelete(orgRef)` cannot see it —\n * a path-scoped cascade is structurally blind to anything not under the path.\n * The credential therefore outlived the workspace: `verifyApiKey` kept\n * resolving the token to a live principal naming an org that no longer\n * existed. `authenticateApiV1` happens to refuse it (it reads the org doc and\n * 401s), but that is a gate one layer above the credential, and a key whose\n * revocation depends on every future caller repeating a lookup is not\n * revoked. The document is also a small record of the erased workspace in its\n * own right — a human-authored label, the creating uid, the granted scopes.\n *\n * Bounded by the `orgId` field, never a collection sweep: this collection\n * holds every other customer's integration credentials too.\n *\n * Returns the number destroyed, for the audit row — an erasure trail that\n * understates what it removed is the one record that has to be right.\n */\nasync function eraseOrgApiKeys(orgId: string, dryRun = false): Promise<number> {\n return deleteDocsByOrgId('apiKeys', orgId, dryRun)\n}\n\n/**\n * Delete every Outreach mailbox credential the org holds (AGL-2974).\n *\n * `outreachMailboxCredentials` is a TOP-LEVEL collection keyed by the mailbox\n * id and carrying `orgId` as a FIELD, so `recursiveDelete(orgRef)` cannot see\n * it — the blindness `eraseOrgApiKeys` answers, on a more dangerous document.\n * Left standing, the OAuth grant a rep gave for their own mailbox would\n * outlive the workspace it was given to: tokens that can still send as that\n * person, held for an organization that no longer exists and that nobody can\n * ask to disconnect them.\n *\n * This destroys the platform's copy of the grant; it does not call the\n * provider's revocation endpoint, which needs the plugin's provider adapter.\n * With the tokens gone the grant has nothing left to act through.\n *\n * Bounded by the `orgId` field, never a collection sweep: this collection\n * holds every other workspace's connected mailboxes too.\n *\n * Returns the number destroyed, for the audit row — an erasure trail that\n * understates what it removed is the one record that has to be right.\n */\nasync function eraseOrgOutreachMailboxCredentials(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n return deleteDocsByOrgId(\n OUTREACH_MAILBOX_CREDENTIALS_COLLECTION,\n orgId,\n dryRun,\n )\n}\n\n/**\n * Delete every document in a TOP-LEVEL collection that names this org in its\n * `orgId` FIELD (AGL-1444/AGL-1448).\n *\n * The shared mechanism behind every collection a path-scoped cascade cannot\n * see. Two properties are the whole point, and neither is optional:\n *\n * - **Bounded by the field, never a collection sweep.** Every one of these\n * collections holds other customers' credentials, routing and billing\n * correlations. A `collection(name).get()` here would erase the estate.\n * - **Chunked**, because a batch caps at 500 writes and nothing bounds an\n * org's row count below that.\n *\n * Returns the number destroyed, for the audit row — an erasure trail that\n * understates what it removed is the one record that has to be right.\n *\n * `dryRun` runs the query and skips the commit (AGL-1481), so a plan counts\n * rows through the SAME function that deletes them. A separate counting pass\n * would be a second enumeration of the sweep list, which is precisely the\n * divergence AGL-1481 exists to remove.\n *\n * **`platformRevenue` must NEVER be added to this sweep (AGL-1811).** It is\n * org-keyed by field — exactly the shape this mechanism eats — and that is\n * the trap: those rows are per-transaction TAX FILING RECORDS (gross, tax,\n * jurisdiction) with a statutory retention obligation, and the quarterly\n * Texas return is their sum. GDPR Art. 17(3)(b) exempts them: erasure does\n * not extend to records retained for compliance with a legal obligation. An\n * erased org's rows deliberately outlive it — the\n * `erase-org-tax-retention.emulator.spec` pins survival, so an over-eager\n * future sweep fails a test instead of un-filing a tax period.\n *\n * **`storefrontTaxCollected` must NEVER be added either (AGL-1904).** Same\n * shape, same trap, same reason: it carries `orgId` as a field, and its rows\n * are the record of sales tax charged to shoppers on that org's storefront —\n * including the tax a `mode: 'stripe'` store collects under AGLYN's own\n * Texas registration, which Aglyn must be able to account for long after the\n * merchant has gone.\n */\nasync function deleteDocsByOrgId(\n collection: string,\n orgId: string,\n dryRun = false,\n): Promise<number> {\n if (!orgId) return 0\n const firestore = firebaseAdmin.app().firestore()\n const rows = await firestore\n .collection(collection)\n .where('orgId', '==', orgId)\n .get()\n if (dryRun) return rows.size\n for (let index = 0; index < rows.docs.length; index += 400) {\n const batch = firestore.batch()\n for (const doc of rows.docs.slice(index, index + 400)) batch.delete(doc.ref)\n await batch.commit()\n }\n return rows.size\n}\n\n/**\n * Drop the org's public SSO routing documents (AGL-1448).\n *\n * `ssoDomains/{domain}` is keyed by the DOMAIN and carries `orgId` as a field,\n * so `recursiveDelete(orgRef)` never reaches it. The org's own claims under\n * `orgs/{orgId}/ssoDomains/{domain}` do die with the tree; the public routing\n * doc — the one that decides where a sign-in goes — does not.\n *\n * **`unpublishSsoDomains` is not the fix, though it looks like it.** That\n * function is the reversible half of the console's SSO toggle: it sets\n * `active: false` and leaves the document standing precisely so re-enabling\n * restores the same pool and the same uids. Applied to an erasure it would\n * leave behind a record naming the deleted org, its GCIP tenant and provider\n * ids, its IdP display name and the customer's email domain — and, worse, a\n * live reservation: `issueDomainClaim` refuses a domain whose `ssoDomains` doc\n * belongs to another org REGARDLESS of `active`, so a ghost would hold a real\n * customer's domain against them with no way to appeal. Absence is the only\n * state that both stops the routing and releases the name.\n *\n * Deleting is safe on the read side: `/api/auth/sso-lookup` already treats a\n * missing doc and an inactive one identically (`{ ssoEnabled: false }`).\n */\nasync function eraseOrgSsoDomains(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n return deleteDocsByOrgId('ssoDomains', orgId, dryRun)\n}\n\n/**\n * Release every custom console domain the org holds (AGL-1448).\n *\n * `releaseConsoleDomain` already does exactly the right thing and was called\n * from nothing but its own spec. It is wired rather than reimplemented, and\n * the ordering it enforces is the reason: Vercel detach FIRST, documents\n * second, and the claim KEPT if a detach fails. `resolveConsoleDomain`'s\n * `unknown` verdict — the one that lets localhost, previews and self-hosted\n * installs work — is only safe while every name the console project still\n * answers for has a document (AGL-1430). Deleting the claim here directly\n * would break that correspondence and hand the name to the next org that\n * claims it, with Vercel's `already-exists` reading as health.\n *\n * It takes one domain, so the org-bounded query is the part that was missing.\n * Every name in a claim set — the primary and its `www` twin — carries the\n * same `orgId` and the same `primaryHost`, so releasing once per distinct\n * primary covers the set exactly, without a second pass over the twins.\n *\n * Best-effort, like every other external cleanup here: an erasure must not\n * fail because Vercel did. A name that could not be detached keeps its claim\n * and is reported in the audit row as un-released.\n */\nasync function releaseOrgConsoleDomains(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n const firestore = firebaseAdmin.app().firestore()\n const claims = await firestore\n .collection(CONSOLE_DOMAINS_COLLECTION)\n .where('orgId', '==', orgId)\n .get()\n const primaries = new Set(\n claims.docs.map((doc) => (doc.get('primaryHost') as string) || doc.id),\n )\n // A plan counts the claim sets it WOULD release. `releaseConsoleDomain`\n // detaches at Vercel, so it must not run for a dry run (AGL-1481).\n if (dryRun) return primaries.size\n let released = 0\n for (const domain of primaries) {\n const result = await releaseConsoleDomain({ orgId, domain }).catch(\n (error) => {\n console.error(`eraseOrg: console domain ${domain} release failed`, error)\n return { released: false }\n },\n )\n if (result.released) released += 1\n }\n return released\n}\n\n/**\n * Drop the local Stripe reverse index (AGL-1448, AGL-1028).\n *\n * `stripeCustomers/{stripeCustomerId} -> { orgId }` exists because AGL-1028\n * moved `stripeCustomerId` off the org doc into a manager-gated subcollection,\n * which broke the webhook's `where('stripeCustomerId', '==', …)` lookup. It is\n * denied to every client for one reason, stated in the rules: readable, it\n * maps a billing identity back to a workspace.\n *\n * The erasure had this exactly inverted. It deleted the customer AT STRIPE and\n * kept the local index — so the artefact that survived was the correlation the\n * whole issue existed to prevent, now pointing at an org that no longer exists\n * to be gated on. This deletes the local rows. The Stripe-side delete is\n * unchanged and still runs: PII lives at the processor too, and one is not a\n * substitute for the other.\n *\n * Bounded by the `orgId` field rather than by the org's CURRENT customer id,\n * because nothing cleans up the index when a customer id changes — an org that\n * was re-created in Stripe has more than one row pointing at it.\n */\nasync function eraseOrgStripeIndex(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n return deleteDocsByOrgId('stripeCustomers', orgId, dryRun)\n}\n\n/**\n * Drop the org's REST API replay keys (AGL-1448).\n *\n * `apiIdempotency/{sha256(orgId:key)}` dedupes `POST`s by `Idempotency-Key`\n * (AGL-618). Low severity — a replay key names an org and a record id and\n * authorises nothing — but it does NOT age out on its own, which is the part\n * worth writing down because the assumption runs the other way: the only TTL\n * policy on the database is `rateLimits.expiresAt`\n * (`docs/FIRESTORE_MANUAL_CONFIG.md`), and these documents carry no expiry\n * field for a policy to key on. Left alone they accumulate against a dead org\n * indefinitely.\n *\n * Taken with the credentials rather than at the end: the record ids they carry\n * point into the org tree that step 4 destroys.\n */\nasync function eraseOrgIdempotencyKeys(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n return deleteDocsByOrgId('apiIdempotency', orgId, dryRun)\n}\n\n/**\n * Release every slug the org has EVER held (AGL-1448).\n *\n * The erasure deleted `orgSlugs/{org.slug}` — the current name only. But\n * `changeOrgSlug` leaves a tombstone at the previous slug (`{ orgId, movedTo,\n * renamedAt }`) so old workspace URLs keep redirecting (AGL-585/AGL-236), and\n * nothing ever collected them. Every historical name an org held therefore\n * survived its erasure, in the one collection the rules make\n * `allow read: if true` — public because it doubles as the pre-auth health\n * probe. An erased workspace's naming history stayed world-readable, each\n * tombstone still naming the dead org id.\n *\n * Bounded by the `orgId` field, which is exactly the right boundary here and\n * not merely the safe one: claiming a tombstoned slug FULL-REPLACES the\n * document with the new owner's `{ orgId }`, so a name this org renamed away\n * from and somebody else has since taken does not match — and must not be\n * swept, because it is now a live tenant's reservation.\n *\n * The current slug is unioned in rather than assumed present: an org whose\n * reservation was lost to a legacy write would otherwise keep its workspace\n * subdomain attached to the project. ⛔ ONLY WHEN NOBODY HOLDS IT (AGL-2585).\n * A workspace address taken by an unverified signup is a reservation that\n * lapses, and a live workspace may since have claimed the name — at which\n * point `orgSlugs/{slug}` names THAT org, and the erasure of this one would\n * otherwise delete a working tenant's reservation and detach their subdomain.\n * The `orgId` query above already refuses that document; this is the one line\n * that used to add it back by assumption.\n *\n * Each name's `{slug}.aglyn.com` is detached too. `changeOrgSlug` deliberately\n * KEEPS the old subdomain attached so the tombstone's 308 has a hostname to\n * run on; once the tombstone goes, the attachment is a name resolving to a\n * console for an org that does not exist. Best-effort, like every other\n * external cleanup here: an erasure must not fail because a DNS API did.\n */\nasync function eraseOrgSlugs(\n orgId: string,\n currentSlug?: string,\n dryRun = false,\n): Promise<number> {\n const firestore = firebaseAdmin.app().firestore()\n const held = await firestore\n .collection('orgSlugs')\n .where('orgId', '==', orgId)\n .get()\n const slugs = new Set(held.docs.map((doc) => doc.id))\n if (currentSlug && !slugs.has(currentSlug)) {\n // Absent means the reservation was lost and only the DNS attachment is\n // left to clean up. Present but not matched by the query above means it\n // belongs to another org now, and is not this erasure's to touch.\n const reservation = await firestore\n .collection('orgSlugs')\n .doc(currentSlug)\n .get()\n .catch(() => null)\n if (reservation && !reservation.exists) slugs.add(currentSlug)\n }\n // `detachWorkspaceDomain` calls a live DNS API — never on a plan (AGL-1481).\n if (dryRun) return slugs.size\n for (const slug of slugs) {\n await firestore.collection('orgSlugs').doc(slug).delete().catch(() => undefined)\n await detachWorkspaceDomain(slug).catch(() => undefined)\n }\n return slugs.size\n}\n\n/**\n * Sweep up ORPHANED `hostIndex` rows the per-host loop cannot enumerate\n * (AGL-1448).\n *\n * `hostIndex/{hostId}` is `{ orgId, subdomain }` and `allow read: if\n * isSignedIn()`. It is deleted today inside `eraseHost`, which `eraseOrg`\n * calls once per document in `hosts where orgId == org` — so the row is\n * reached DERIVATIVELY, through a second collection, and only for as long as\n * the two agree.\n *\n * **The erasure is itself one of the ways they stop agreeing.** `eraseHost`'s\n * `hostIndex` delete is `.catch(() => undefined)` — fail-soft on purpose, so\n * a transient failure never blocks the Firestore delete — and the very next\n * statement `recursiveDelete`s `hosts/{hostId}`. The instant that lands, the\n * orphaned index row is unreachable from the org side forever: nothing points\n * at it, so neither this run nor any retry can ever enumerate it again. A\n * fail-soft that manufactures a PERMANENT orphan is not fail-soft, and the\n * residue is a signed-in-readable row naming a dead org and the subdomain its\n * customer chose.\n *\n * Bounded by the `orgId` field — this is the table the middleware resolves\n * every request on, so an unbounded sweep here does not lose an index, it\n * takes the estate offline.\n *\n * A BACKSTOP, not a replacement: the per-host loop still does the real work\n * (Storage, the org routing map, the member projections, the host tree). This\n * only collects what that loop structurally cannot see.\n */\nasync function eraseOrgHostIndex(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n return deleteDocsByOrgId('hostIndex', orgId, dryRun)\n}\n\n/**\n * Sweep up `hostMemberships` projections belonging to FORMER members\n * (AGL-1448).\n *\n * `users/{uid}/hostMemberships/{hostId}` carries `{ orgId, subdomain,\n * displayName, nameLower, role }` — the site switcher's row. It lives under a\n * USER, so `recursiveDelete(orgs/{orgId})` cannot reach it, and `eraseOrg`'s\n * members loop only walks `orgs/{orgId}/members`.\n *\n * `deleteHostProjectionForAllMembers(orgId, hostId)` clears the row for\n * everyone in that member list at the moment the erasure runs. **A former\n * member is not in it.** Their row is written by the projection fan-out and\n * removed by `removeOrgMember`, which is fail-soft in the same way as\n * everything else on that path; one missed fan-out leaves a row no later\n * erasure can find, because the enumeration starts from a membership that no\n * longer exists. The erased workspace's site NAME and subdomain then live on\n * in the user document of somebody who left the company.\n *\n * ## This is a COLLECTION-GROUP query, and that owes an index\n *\n * `hostMemberships` has two composite indexes, both `queryScope: COLLECTION`.\n * A collection-group equality on `orgId` needs a `COLLECTION_GROUP`\n * single-field index, which Firestore does NOT create automatically — the\n * automatic single-field indexes are collection-scoped except for\n * array-contains. `cloud/firebase-firestore.indexes.json` therefore gains a\n * `fieldOverrides` entry alongside the existing `members.uid` and\n * `messages.authorId` ones, and **it must be deployed by hand.**\n *\n * Until it is, the query throws `FAILED_PRECONDITION`. AGL-1793 is this\n * codebase's record of exactly that failure mode — both commerce crons\n * silently dead in production for want of a `COLLECTION_GROUP_ASC` override —\n * so this refuses to be the thing that kills an erasure. It is caught and\n * reported as `null`, which is deliberately NOT `0`: a zero would say \"there\n * was nothing to sweep\" and a missing index would wear that as a clean bill\n * of health. `null` in the audit row says \"attempted, could not run\", and the\n * spec asserts a NUMBER so the wired-and-indexed path is what is pinned.\n */\nasync function eraseOrgHostMemberships(\n orgId: string,\n dryRun = false,\n): Promise<number | null> {\n if (!orgId) return 0\n try {\n const firestore = firebaseAdmin.app().firestore()\n const rows = await firestore\n .collectionGroup('hostMemberships')\n .where('orgId', '==', orgId)\n .get()\n if (dryRun) return rows.size\n for (let index = 0; index < rows.docs.length; index += 400) {\n const batch = firestore.batch()\n for (const doc of rows.docs.slice(index, index + 400)) {\n batch.delete(doc.ref)\n }\n await batch.commit()\n }\n return rows.size\n } catch (error) {\n console.error(\n `eraseOrg: hostMemberships projection sweep failed for ${orgId} — a missing COLLECTION_GROUP index on hostMemberships.orgId is the likeliest cause`,\n error,\n )\n return null\n }\n}\n\n/**\n * What an erasure did with `publisherProfiles/{orgId}` (AGL-1970).\n *\n * Three outcomes rather than a boolean, because the third one is a decision\n * the audit row has to be able to state: a listing the erased org published\n * can outlive it, and a listing whose publisher document is simply gone is\n * unattributable in a way nobody chose.\n */\nexport type PublisherProfileDisposition = 'absent' | 'deleted' | 'tombstoned'\n\n/**\n * Drop the org's PUBLIC marketplace identity — the profile and every handle\n * it reserved (AGL-1970).\n *\n * Both survived every erasure until now, and both are `allow read: if true`\n * (`cloud/firebase-firestore.rules`, `match /publisherProfiles/{orgId}`). This\n * is the AGL-1448 shape with one twist that is the whole reason it was missed:\n * `publisherProfiles` is keyed by the org id **as the document id**, so it is\n * invisible to `deleteDocsByOrgId` *and* to a reader scanning this file for\n * the field-keyed sweep list. Nothing prompts you to notice it.\n *\n * What survived is not cosmetic. `stripeAccountId` is a **payout\n * destination** — written server-side only after Connect onboarding/KYC and\n * trusted by every checkout path — so an erased org stayed world-readable with\n * a live payment-account identifier attached to a dead identity. That is the\n * `stripeCustomers` correlation AGL-1448 removed, pointing the other way.\n *\n * `publisherHandles` is the easy half and goes unconditionally: it carries\n * `orgId` as a FIELD, so `deleteDocsByOrgId` fits exactly, and it is a **live\n * reservation** — `claimPublisherHandle` refuses a handle another org's row\n * names, so a ghost holds a marketplace name against a real customer with no\n * way to appeal. That is the `ssoDomains` argument verbatim, and absence is\n * again the only state that both stops the read and releases the name. Rename\n * tombstones (`{ orgId, movedTo }`) carry the same `orgId` and go with it, and\n * the field bound is what makes that safe: re-claiming a tombstone FULL-\n * REPLACES it with the new owner's `{ orgId }`, so a handle this org renamed\n * away from and somebody else has since taken does not match the query.\n *\n * **The profile is the half with a genuine tension, and it is resolved here\n * rather than deferred.** `marketplaceListings` outlives an erasure — that is\n * AGL-1448's parked Tier 3 product decision, an erased org's listing being\n * something buyers paid for — so deleting the publisher document outright can\n * leave a listing attributed to nothing. So:\n *\n * - **No surviving listing** → `recursiveDelete` the profile. `deleted`.\n * `recursiveDelete` and not `delete`: `publish-plugin` keeps its daily\n * publish-rate window at `publisherProfiles/{orgId}/meta/publishWindow`,\n * and a document delete would orphan it under a path with no owner.\n * - **A listing survives** → the same `recursiveDelete`, then a minimal\n * `{ erased: true, erasedAt }` in its place. `tombstoned`. The tombstone\n * carries no `handle`, no `displayName`, no `bio`/`avatarUrl`/`website`,\n * no `publisherAgreement` and — the point — no `stripeAccountId`. It is\n * \"an internal record that the erasure happened\", which is exactly what\n * the Privacy Policy §5 sentence reserves; every byte the sentence calls\n * content is gone either way. Readers already handle it: with no `handle`,\n * `resolvePublisherProfile` returns `null`, which is the pre-existing\n * \"this org has no profile\" path.\n *\n * The count of surviving listings is returned either way, so an erasure that\n * left something standing SAYS SO in its audit row instead of reporting a\n * clean success — the Tier 3 decision is still open, and an erasure is the\n * one place its cost is measurable.\n */\nasync function eraseOrgPublisherIdentity(\n orgId: string,\n dryRun = false,\n): Promise<{\n handles: number\n profile: PublisherProfileDisposition\n listingsRetained: number\n}> {\n const firestore = firebaseAdmin.app().firestore()\n const handles = await deleteDocsByOrgId('publisherHandles', orgId, dryRun)\n const profileRef = firestore.collection('publisherProfiles').doc(orgId)\n const [profileSnapshot, listings] = await Promise.all([\n profileRef.get(),\n // `profileId` is the listing's publishing-org id (AGL-652) — the same\n // value as this profile's document id, under the older field name.\n firestore.collection('marketplaceListings').where('profileId', '==', orgId).get(),\n ])\n const listingsRetained = listings.size\n if (!profileSnapshot.exists) {\n return { handles, profile: 'absent', listingsRetained }\n }\n if (dryRun) {\n return {\n handles,\n profile: listingsRetained ? 'tombstoned' : 'deleted',\n listingsRetained,\n }\n }\n await firestore.recursiveDelete(profileRef)\n if (!listingsRetained) return { handles, profile: 'deleted', listingsRetained }\n await profileRef.set({ erased: true, erasedAt: FieldValue.serverTimestamp() })\n return { handles, profile: 'tombstoned', listingsRetained }\n}\n\n/**\n * Destroy every support ticket the org opened, and the messages under each\n * (AGL-1971).\n *\n * `supportTickets/{id}` is top-level with `orgId` as a field — the exact shape\n * `deleteDocsByOrgId` exists for — and it was in neither sweep list. It is\n * also the highest-PII-density collection outside the org tree: `messages`\n * rows carry `authorId`, `authorEmail` and up to 5000 characters of prose a\n * customer wrote to us, which is where somebody pastes the invoice, the member\n * list, or the very data they are asking us to delete.\n *\n * **`deleteDocsByOrgId` is deliberately NOT used here**, though the query is\n * identical. It deletes documents, not subtrees, and a batch delete of the\n * ticket documents would leave every `messages` row alive under a parent that\n * no longer exists — the prose and the email surviving the delete that was\n * supposed to remove them, now unreachable by any query that starts from\n * `orgId`. `recursiveDelete` per ticket is the only correct shape.\n *\n * **No retention exception, and the alternative was considered.** A closed\n * support thread is arguable dispute evidence, which is the class of argument\n * that keeps `platformRevenue` alive (AGL-1811). It does not reach here: that\n * exemption is GDPR Art. 17(3)(b), *records retained to comply with a legal\n * obligation*, and no law requires us to keep a support thread — a Texas tax\n * return is filed from `platformRevenue`; nothing is filed from a ticket.\n * Live DPA §11 commits the other way in terms (\"delete or return Customer\n * Personal Data, except to the extent retention is required by law\"). What\n * remains as evidence is the `adminAudit` row: the erasure happened, when, at\n * whose instruction, and how many tickets it destroyed — ids and counts,\n * never content (AGL-1443).\n *\n * Sequential rather than parallel: `recursiveDelete` is itself a bulk writer,\n * and fanning N of them out at once is how a 60-second cron loses an erasure.\n */\nasync function eraseOrgSupportTickets(\n orgId: string,\n dryRun = false,\n): Promise<number> {\n if (!orgId) return 0\n const firestore = firebaseAdmin.app().firestore()\n const tickets = await firestore\n .collection('supportTickets')\n .where('orgId', '==', orgId)\n .get()\n if (dryRun) return tickets.size\n for (const ticket of tickets.docs) {\n await firestore.recursiveDelete(ticket.ref)\n }\n return tickets.size\n}\n\n/**\n * The steps of `eraseOrg` after the hold check, named so a failed attempt can\n * say WHERE it stopped (AGL-1455).\n *\n * `export` used to lead this list, and its removal is the AGL-1443 change: an\n * erasure no longer writes anything before it starts deleting, so there is no\n * longer a step whose meaning is \"nothing is gone yet\". `credentials` is now\n * the first thing that can fail, and it destroys as it goes.\n */\ntype EraseStep =\n | 'plugins'\n | 'credentials'\n | 'support'\n | 'hosts'\n | 'org-storage'\n | 'stripe'\n | 'members'\n | 'org-tree'\n | 'slugs'\n\n/**\n * The two things an OPERATOR needs that a served caller does not (AGL-1481).\n *\n * `tools/scripts/erase-tenant.mjs` is the manual path staff reach for when the\n * cron is stuck. It used to be a second implementation of this function, and it\n * drifted: by the time AGL-1481 was filed it was missing four sweeps added that\n * week and still wrote a complete dump of the workspace to the operator's\n * working directory. The fix is that it CALLS this function — which means the\n * capabilities it had and this function lacked have to live here instead.\n *\n * Neither is a hold bypass, deliberately. The script refused to run before the\n * 7-day hold elapsed and this function refuses too; an `ignoreHold` flag would\n * have been the easy way to keep the two paths identical and is the one\n * difference worth preserving as a refusal on both.\n */\nexport interface EraseOrgOptions {\n /**\n * Count what an erasure WOULD destroy and destroy nothing. Every sweep runs\n * its query and skips its write, so the plan is produced by the same list of\n * sweeps that the erasure is — a separate counting pass would be exactly the\n * second enumeration this issue removed. Nothing external is touched (no\n * Storage, no Stripe, no Vercel, no DNS) and no audit row is written.\n *\n * Returns `ok: false` with `skippedReason: 'dry-run'`: `ok` means the org was\n * erased, and a plan did not erase it. A caller that treats a plan as an\n * erasure is the one mistake this shape has to make impossible.\n */\n dryRun?: boolean\n /**\n * Who is answerable for this erasure, for the audit row. Defaults to the\n * cron. A staff member running the script by hand is not the scheduler, and\n * an erasure trail that says otherwise names the wrong actor for the single\n * most irreversible action the platform performs.\n */\n actorUid?: string\n /**\n * The console-side sending-domain teardown, passed through to every host\n * this erasure destroys. See {@link EraseHostOptions.tearDownSendingDomain}.\n *\n * Never consulted on a dry run: a plan touches no vendor.\n */\n tearDownSendingDomain?: TeardownSendingDomainDriver | null\n /**\n * Erase a workspace that carries NO erasure request at all (AGL-2585).\n *\n * ⛔ EXACTLY ONE CALLER, and the option exists so that caller has to say so\n * out loud. `/api/admin/reap-unverified-orgs` collects workspaces created by\n * a signup whose address was never confirmed — there is nobody to ask for an\n * erasure, because nobody has been shown to exist. Every other path reaches\n * this function through `erasureRequestedAt`, which is a person's decision\n * with a seven-day hold on it, and none of them passes this.\n *\n * What it turns off is narrow and named: the `no-request` refusal and the\n * hold, and nothing else. Every sweep, every audit row and `dryRun` behave\n * identically — the caller supplies the proof that this workspace is\n * nobody's, and this records WHAT that proof was on the audit row.\n *\n * It does not skip the request when one exists. An org somebody has already\n * asked to erase is still that person's decision, and the reaper refuses\n * such an org before it ever gets here.\n */\n withoutRequest?: { reason: string } | null\n}\n\nexport interface EraseOrgResult {\n ok: boolean\n /**\n * Set when the org was NOT erased (flag missing, hold not elapsed, gone) —\n * and `'dry-run'` when nothing was erased because nothing was meant to be.\n */\n skippedReason?: string\n hosts?: number\n /** Member back-references cleaned up — reported so a plan can show them. */\n members?: number\n /** API credentials destroyed (AGL-1444) — outside the org path. */\n apiKeys?: number\n /**\n * What each plugin's workspace eraser did (AGL-2978), keyed by plugin id:\n * counts and flags, such as grants revoked at a provider. The erasers run\n * before anything below is deleted. **`null` means that plugin's eraser\n * failed** and its share may not be done; it is not zero. A plan carries\n * each eraser's count of what it would do.\n */\n plugins?: Record<string, PluginOrgErasureReport | null>\n /** Outreach mailbox grants destroyed (AGL-2974) — outside the org path. */\n outreachMailboxCredentials?: number\n /** Public SSO routing docs destroyed (AGL-1448) — outside the org path. */\n ssoDomains?: number\n /** Custom console domains released (AGL-1448) — outside the org path. */\n consoleDomains?: number\n /** REST API replay keys destroyed (AGL-1448) — no TTL reaps these. */\n apiIdempotency?: number\n /** Local Stripe reverse-index rows destroyed (AGL-1448/AGL-1028). */\n stripeIndex?: number\n /** Slugs released — the current one AND every tombstone (AGL-1448). */\n slugs?: number\n /**\n * ORPHANED `hostIndex` rows collected (AGL-1448) — ones with no surviving\n * `hosts` document for the per-host loop to reach them through.\n */\n hostIndex?: number\n /**\n * `hostMemberships` projections collected (AGL-1448), including FORMER\n * members' rows that no membership list names.\n *\n * `null` means the sweep could not RUN — almost always the missing\n * `COLLECTION_GROUP` index on `hostMemberships.orgId`. Deliberately not\n * `0`: an erasure that could not reach a collection must not report the\n * same number as one that reached it and found nothing.\n */\n hostMemberships?: number | null\n /** Marketplace handle reservations released (AGL-1970) — id-keyed, public. */\n publisherHandles?: number\n /** What became of the org's public publisher profile (AGL-1970). */\n publisherProfile?: PublisherProfileDisposition\n /**\n * Marketplace listings that OUTLIVE this erasure and still name the org\n * (AGL-1970/AGL-1448 Tier 3). Reported rather than deleted: an erasure that\n * leaves something standing has to say so, and this is the number that makes\n * the parked listing-survival decision cost something measurable.\n */\n listingsRetained?: number\n /**\n * Support threads destroyed, each with its `messages` subtree (AGL-1971) —\n * the highest-PII-density collection outside the org path.\n */\n supportTickets?: number\n /**\n * What became of the erased sites' dedicated sending domains.\n *\n * `deferred` is the number an operator reads: those provider slots and zone\n * records are still held, the debt is on each label claim, and\n * `/api/admin/reap-sending-domains` is what settles them. The erasure itself\n * is complete either way — a vendor outage may delay a slot's release and\n * may not delay a deletion somebody has a legal right to.\n */\n sendingDomains?: {\n released: number\n deferred: number\n /** Shared pool members refused. Always 0; a non-zero is a real alarm. */\n protected: number\n }\n}\n\n/**\n * What an in-flight erasure has destroyed so far — counts only.\n *\n * `members` is excluded: the inventory already reports it as `before.members`\n * on both audit rows, and a second copy under `after` would say the same\n * number twice.\n */\ntype EraseProgress = Omit<EraseOrgResult, 'ok' | 'skippedReason' | 'members'>\n\n/**\n * A stable label for an error, carrying no free text (AGL-1455).\n *\n * The class name and the SDK's error code are enough to tell a Firestore\n * permission denial from a deadline exceeded from an out-of-memory kill. The\n * MESSAGE is deliberately not stored: a Firestore or Storage error quotes the\n * path — and sometimes the payload — it choked on, and this row lands in a\n * collection that must not become a second copy of what the erasure failed to\n * delete (AGL-1443). The full error still goes to `console.error`, which is\n * the log, not the record.\n */\nfunction errorLabel(error: unknown): string {\n const source = (error ?? {}) as { name?: unknown; code?: unknown }\n const parts = [source.name, source.code].filter(\n (part) => typeof part === 'string' || typeof part === 'number',\n )\n return parts.length ? parts.join(':') : 'unknown'\n}\n\n/**\n * Record a failed erasure attempt where somebody will actually find it\n * (AGL-1455).\n *\n * Aborting when the export cannot be written is correct and stays — the bug\n * was that the abort was invisible. `skippedReason` reached exactly one\n * place, the cron endpoint's HTTP response body, and nobody reads a\n * scheduler's 200. Meanwhile `erasureRequestedAt` stays set, so the same org\n * is retried on the next run and fails the same way indefinitely, and the\n * customer who requested the erasure is told nothing.\n *\n * An `adminAudit` row is the cheapest durable form and the one the success\n * path already uses, so the two read as a pair: same `actorUid`, same\n * `target`, same `before` inventory, and `after` carrying what actually\n * happened. Staff can find every stuck erasure by filtering the audit log on\n * the action, which is exactly the search that was impossible before.\n *\n * `after.exportWritten`/`after.exportPath` used to be the fields that\n * mattered here: they named the complete dump of a still-existing workspace\n * that a post-export failure left in the bucket. AGL-1443 removed the write,\n * so that state is no longer reachable and the fields no longer exist. What\n * replaces them is `after.requestedAt` — WHICH erasure request is stuck,\n * which the object's name used to be the only record of.\n *\n * Ids, counts, a step name and a timestamp. Never customer data.\n *\n * Best-effort and never throws: this runs while an erasure is already\n * failing, and losing the original error to a bookkeeping write would be\n * worse than losing the row.\n */\nasync function recordErasureFailure(entry: {\n orgId: string\n step: EraseStep\n error: unknown\n /** Whoever asked for the erasure — the cron, or a named operator. */\n actorUid: string\n /** Millis of the `erasureRequestedAt` this attempt was fulfilling. */\n requestedAt: number\n before: { hosts: number; members: number }\n after: Record<string, unknown>\n}): Promise<void> {\n await firebaseAdmin\n .app()\n .firestore()\n .collection('adminAudit')\n .add({\n actorUid: entry.actorUid,\n action: 'org.erase-failed',\n target: `orgs/${entry.orgId}`,\n before: entry.before,\n after: {\n failedStep: entry.step,\n error: errorLabel(entry.error),\n requestedAt: entry.requestedAt,\n ...entry.after,\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch((auditError) => {\n console.error(\n `eraseOrg: could not record the failed attempt for ${entry.orgId}`,\n auditError,\n )\n })\n}\n\n/**\n * Permanently erase an organization once its 7-day hold has elapsed\n * (AGL-485/487). Runs from the automated cron and the manual staff path.\n *\n * **Both of those are literally this function (AGL-1481).** The manual path,\n * `tools/scripts/erase-tenant.mjs`, used to be a second implementation, and a\n * second implementation of a cascade delete is a divergence with a schedule:\n * within a week of `eraseOrgApiKeys`, the SSO/console-domain release and the\n * org-keyed index sweeps landing here, the script was missing all four and\n * reporting success without them — leaving a live API credential, live domain\n * reservations, `orgSlugs` tombstones and the `stripeCustomers` reverse index\n * behind an erasure a human had been told was complete. The script now calls\n * this, and `EraseOrgOptions` is where the two capabilities it had and this\n * did not (a plan, and a named actor) live instead.\n *\n * Order matters and every step is defensive:\n * 1. Re-read the org and re-verify erasureRequestedAt + hold — never\n * delete on a stale/cancelled request.\n * 2. Nothing is written. This step used to persist a final JSON export of\n * the org and host trees to `erasures/{orgId}/…` and abort if it could\n * not — see the note below for why it is gone (AGL-1443).\n * 3. Revoke the org's API credentials (AGL-1444), its public routing — SSO\n * domains and custom console domains (AGL-1448) — and its public\n * marketplace identity: the publisher profile and every handle it\n * reserved (AGL-1970); and destroy the OAuth grants behind its reps'\n * Outreach mailboxes (AGL-2974). All of them are top-level collections\n * keyed by something other than a path under the org, so the org-tree\n * delete cannot reach them; doing them before the content delete also\n * closes the mid-erasure window, in which a credential, a domain or a\n * public publisher page still resolves to a half-deleted workspace.\n * 4. Delete each host (eraseHost), org-level Storage, the Stripe customer\n * AND its local reverse index, member back-references, then the org tree\n * and every slug the org ever held — tombstones included (AGL-1448).\n * 5. Audit.\n *\n * Every ending is audited, not just the successful one (AGL-1455). A throw\n * from any step writes an `org.erase-failed` row before re-throwing, because\n * `erasureRequestedAt` stays set on a failure: without a record the org is\n * retried and fails identically on every subsequent run, forever, and nothing\n * anywhere says so.\n *\n * **The erasure writes no copy of the workspace (AGL-1443).** It used to\n * persist `erasures/{orgId}/{requestedMs}.json` — and \"export\" understated\n * it: `exportDocTree` recursed `listCollections()` with no bound and copied\n * `snapshot.data()` wholesale, so the object was a complete verbatim copy of\n * the org tree and every host tree, carrying `webhooks.secret` (a plaintext\n * HMAC key), `orders.paymentLinkUrl` (a live bearer URL that lets its holder\n * pay), `screens.protection.passwordHash` and `ssoDomains.token`. It landed\n * on a prefix this function's own storage sweep does not cover, in a bucket\n * with no lifecycle rule, so the most sensitive object the platform can\n * produce outlived the request that deleted everything else — indefinitely,\n * unread, and belonging to a customer who had been told their workspace was\n * gone. Four facts decided it, and none of them is about tidiness:\n *\n * - **Nothing read it.** One producer, zero consumers in the repo; the\n * owner's confirmation email never mentioned it.\n * - **A governed full copy already exists.** Firestore backups run weekly\n * with 14-week retention (AGL-871), so the dump added no recoverability —\n * only a second, ungoverned place the data lived.\n * - **The minimum that proves an erasure was already being written.** The\n * `adminAudit` row below is ids and counts, in a collection with a\n * retention policy. The one thing the object's NAME carried that the row\n * did not — which request this run fulfilled — is now `after.requestedAt`.\n * - **The DPA commits to \"a limited period, after which deleted or\n * de-identified\"**, which indefinite retention of a full dump sits\n * outside of.\n *\n * Two consequences are deliberate, not side effects. The export-write abort\n * is gone with the write: there is nothing left to fail before the first\n * delete, so an erasure can no longer be blocked by Storage. And the export\n * was the only unbounded work in this function — it built the whole workspace\n * in memory inside a 60-second cron, so the larger the workspace the likelier\n * its erasure silently never happened (AGL-1455 half 1). That defect is\n * removed by deletion rather than by a streaming exporter.\n */\nexport async function eraseOrg(\n orgId: string,\n options: EraseOrgOptions = {},\n): Promise<EraseOrgResult> {\n const {\n dryRun = false,\n actorUid = 'cron:run-erasures',\n tearDownSendingDomain = null,\n withoutRequest = null,\n } = options\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) return { ok: false, skippedReason: 'not-found' }\n\n const requestedMs = orgSnapshot.get('erasureRequestedAt')?.toMillis?.() ?? null\n // The request and its hold, unless the caller carries its own proof\n // (AGL-2585). Both refusals are skipped together: a workspace with no\n // request has no stamp for a hold to run from, so keeping the second while\n // dropping the first would be a hold measured against `null`.\n if (!withoutRequest) {\n if (!requestedMs) return { ok: false, skippedReason: 'no-request' }\n if (Date.now() - requestedMs < ERASURE_HOLD_MS) {\n return { ok: false, skippedReason: 'hold-active' }\n }\n }\n\n const hosts = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .get()\n const members = await orgRef.collection('members').get()\n const slug = orgSnapshot.get('slug') as string | undefined\n // AGL-1028: moved to `orgs/{orgId}/billing/stripe`; the helper falls back\n // to the org doc for orgs the backfill has not reached.\n const stripeCustomerId = (await readOrgBilling(orgId)).stripeCustomerId as\n | string\n | undefined\n\n // The inventory both audit rows report, taken before anything is touched.\n const before = { hosts: hosts.size, members: members.size }\n\n // No export. The write that used to stand here is gone (AGL-1443) — see\n // the note above this function. Nothing is persisted anywhere before the\n // deleting starts, and `requestedMs` now travels in the audit row rather\n // than in an object's name.\n\n // None of what follows was guarded (AGL-1455) — `eraseOrgApiKeys`,\n // `eraseOrgSsoDomains`, `eraseOrgIdempotencyKeys` and `recursiveDelete` all\n // throw. A throw here leaves a workspace half destroyed and still standing,\n // recorded nowhere. Each step now names itself, and the error is re-thrown\n // unchanged — this adds a record, it does not swallow the failure. What it\n // can no longer leave behind is the state that used to be the worst one: a\n // surviving workspace with its own complete dump already in the bucket.\n const progress: EraseProgress = {}\n let step: EraseStep = 'plugins'\n try {\n // The plugins' share first (AGL-2978), while the org, its roster and\n // every record swept below still exist: an eraser that revokes a grant at\n // its provider opens the stored grant to do it. Each eraser is isolated,\n // so a failing one is recorded as `null` and stops nothing; on a plan\n // each one counts and touches no provider.\n progress.plugins = await runPluginOrgErasers({ orgId, dryRun })\n\n step = 'credentials'\n // Credentials and routing BEFORE content (AGL-1444/AGL-1448). The org doc\n // survives until the recursiveDelete at the end, so a key presented — or a\n // sign-in routed, or a console domain resolved — mid-erasure would still\n // pass the org gate and reach a half-deleted workspace. Revoking first\n // closes that window as well as the permanent one.\n progress.apiKeys = await eraseOrgApiKeys(orgId, dryRun)\n progress.outreachMailboxCredentials = await eraseOrgOutreachMailboxCredentials(\n orgId,\n dryRun,\n )\n progress.ssoDomains = await eraseOrgSsoDomains(orgId, dryRun)\n progress.consoleDomains = await releaseOrgConsoleDomains(orgId, dryRun)\n progress.apiIdempotency = await eraseOrgIdempotencyKeys(orgId, dryRun)\n\n // The org's PUBLIC marketplace identity (AGL-1970). Here rather than with\n // the Stripe step, though it carries a payout id, because `publisherHandles`\n // is a live name reservation and belongs with the other reservations this\n // step releases — and because the profile is world-readable, so the sooner\n // it stops resolving the smaller the mid-erasure window.\n const publisher = await eraseOrgPublisherIdentity(orgId, dryRun)\n progress.publisherHandles = publisher.handles\n progress.publisherProfile = publisher.profile\n progress.listingsRetained = publisher.listingsRetained\n\n // Support threads (AGL-1971). Its own step because it is the only sweep\n // here that walks a subtree per row, so it is the one most likely to be\n // where a large workspace's erasure runs out of time — and a failure that\n // cannot say WHICH step stopped it is the AGL-1455 defect again.\n step = 'support'\n progress.supportTickets = await eraseOrgSupportTickets(orgId, dryRun)\n\n /*\n * Hosts (Storage + routing + Firestore trees + the sending domain).\n *\n * Each site's dedicated domain is released here rather than by a sweep\n * that never existed: a workspace erasure used to leave one plan-capped\n * provider slot and three zone records per site standing forever, with\n * the DKIM key still live under a label a future site could claim.\n *\n * The driver is passed in and may be absent, and either way the erasure\n * finishes. What the vendors refuse is recorded as a debt on the label\n * claim, which survives the `recursiveDelete` of both the host and the\n * org, and the reaper settles it.\n */\n step = 'hosts'\n progress.hosts = 0\n progress.sendingDomains = { released: 0, deferred: 0, protected: 0 }\n for (const host of hosts.docs) {\n if (!dryRun) {\n const erased = await eraseHost(host.id, { tearDownSendingDomain })\n if (erased.sendingDomain === 'released') {\n progress.sendingDomains.released += 1\n }\n if (erased.sendingDomain === 'deferred') {\n progress.sendingDomains.deferred += 1\n }\n if (erased.sendingDomain === 'protected') {\n progress.sendingDomains.protected += 1\n }\n }\n progress.hosts += 1\n }\n\n // The backstop for what the loop above cannot enumerate (AGL-1448): a\n // `hostIndex` row whose `hosts` document is already gone. AFTER the loop\n // deliberately — the loop's own fail-soft delete is one of the things\n // that creates them, so sweeping first would leave this run's orphans\n // behind for a retry that can no longer see them.\n progress.hostIndex = await eraseOrgHostIndex(orgId, dryRun)\n\n // Org-level Storage (media/dataset assets outside the host prefix).\n step = 'org-storage'\n if (!dryRun) {\n try {\n await storageBucket().deleteFiles({ prefix: `orgs/${orgId}/` })\n } catch (error) {\n console.error(`eraseOrg: org storage cleanup failed for ${orgId}`, error)\n }\n // The org library's copies at the delivery provider (AGL-2824); each\n // site's went with its `eraseHost` above.\n await eraseMediaDeliveryScope({ collection: 'orgs', scopeId: orgId })\n }\n\n // Stripe: the customer at the processor, AND the local reverse index that\n // correlates that identity back to this workspace (AGL-1448). Deleting\n // only the first left exactly the record AGL-1028 denied to every client.\n step = 'stripe'\n // `readOrgBilling` hands back the customer for THIS deployment's Stripe\n // mode (AGL-2486), which is the only one `deleteStripeCustomer` could act\n // on anyway — it spends `STRIPE_SECRET_KEY`, and a live key cannot delete a\n // test customer. Known residue, deliberately not chased here: an org that\n // was also exercised in TEST mode leaves its test customer standing at\n // Stripe after a live erasure. That is synthetic data created by us, not\n // subject data supplied by the workspace, and reaching it would mean\n // erasure spending a second API key in a mode this deployment is not in.\n if (!dryRun) await deleteStripeCustomer(stripeCustomerId)\n progress.stripeIndex = await eraseOrgStripeIndex(orgId, dryRun)\n\n // Members' reverse index into this org.\n step = 'members'\n if (!dryRun) {\n for (const member of members.docs) {\n await firestore\n .collection('users')\n .doc(member.id)\n .collection('orgs')\n .doc(orgId)\n .delete()\n .catch(() => undefined)\n }\n }\n\n // The backstop for the members loop (AGL-1448): a `hostMemberships`\n // projection belonging to somebody who has LEFT the org, whom\n // `deleteHostProjectionForAllMembers` cannot enumerate because the\n // membership it would start from is already gone. Collection-group —\n // see the note on the function, and the index it owes.\n progress.hostMemberships = await eraseOrgHostMemberships(orgId, dryRun)\n\n // The org subtree, then every slug reservation it has ever held — the\n // current one and each rename tombstone (AGL-1448), with the matching\n // `{slug}.aglyn.com` detached (AGL-1136).\n step = 'org-tree'\n if (!dryRun) await firestore.recursiveDelete(orgRef)\n step = 'slugs'\n progress.slugs = await eraseOrgSlugs(orgId, slug, dryRun)\n } catch (error) {\n console.error(`eraseOrg: ${step} failed for ${orgId}`, error)\n await recordErasureFailure({\n orgId,\n step,\n error,\n actorUid,\n requestedAt: requestedMs,\n before,\n // How far it got, so the next reader knows what is already destroyed.\n after: { ...progress },\n })\n throw error\n }\n\n // A plan (AGL-1481). Every sweep above ran its query and skipped its write,\n // so these are the counts the real run would report — reached through the\n // same code, which is the whole point of the operator script calling this\n // function instead of listing the sweeps a second time. No audit row: an\n // erasure that did not happen is not a record of one.\n if (dryRun) {\n return {\n ok: false,\n skippedReason: 'dry-run',\n ...progress,\n hosts: hosts.size,\n members: members.size,\n }\n }\n\n // The proof of erasure, and now the only record of it: actor, action,\n // target, the request it fulfilled, the inventory found and what each sweep\n // destroyed. Ids and counts — never the content (AGL-1443).\n await firestore\n .collection('adminAudit')\n .add({\n actorUid,\n action: 'org.erased',\n target: `orgs/${orgId}`,\n before,\n after: {\n requestedAt: requestedMs,\n // What stood in for a request, when nothing did (AGL-2585). Absent on\n // every ordinary erasure, so its presence is the whole flag.\n ...(withoutRequest ? { withoutRequest: withoutRequest.reason } : {}),\n ...progress,\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n return { ok: true, ...progress, hosts: hosts.size, members: members.size }\n}\n\n/**\n * Strip an erased person from the support threads they wrote in, WITHOUT\n * destroying the threads (AGL-1971).\n *\n * The asymmetry with `eraseOrgSupportTickets` is the whole point and is worth\n * stating rather than assuming. A ticket belongs to an ORG, not to the person\n * who happened to open it: it is the workspace's support history, other people\n * on the roster can read it, and it is evidence in a dispute the workspace may\n * still be having with us. Erasing one member must not delete it — and cannot,\n * honestly: for that thread Aglyn is the processor and the ORG is the\n * controller, so deleting it on a member's instruction is acting without the\n * controller's instruction, which `docs/PRIVACY_REQUESTS.md` §6 refuses in the\n * mirror-image case. `eraseUser` also already refuses to cascade into\n * `eraseOrg` for exactly this reason.\n *\n * So the narrow correct action is redaction: `authorId` and `authorEmail` go\n * to `null` and `authorErased: true` marks why, on that person's messages\n * only. What remains is the message BODY, deliberately — it is the workspace's\n * record of a conversation with us, and it is the workspace's to delete. An\n * erasure of the ORG destroys it, which is the path that reaches it.\n *\n * **A collection-group query, not a walk over their current memberships.** The\n * membership walk is the tempting version and it is wrong in a specific way:\n * `eraseUser` only knows the orgs the person belongs to NOW, so every ticket\n * they wrote in a workspace they have since left would keep their email — the\n * residual is invisible and grows with tenure. `messages` is used as a\n * subcollection name by `supportTickets` and by nothing else in the repo, so\n * the group is exactly the right set. It needs the COLLECTION_GROUP index\n * override on `messages.authorId` in `cloud/firebase-firestore.indexes.json`,\n * added with this change.\n *\n * Returns the number of messages redacted, or **`null` when the query could\n * not run** — a missing index in an environment where it was never deployed.\n * `null` is not zero and the caller must not flatten it: it means this erasure\n * did not reach the support messages at all, and an erasure that quietly\n * reports success for a step it skipped is the failure this whole file is\n * organised against. It is reported in the result and in the audit row rather\n * than thrown, because the account deletion itself must still complete.\n */\nasync function redactUserSupportMessages(uid: string): Promise<number | null> {\n if (!uid) return 0\n const firestore = firebaseAdmin.app().firestore()\n let rows: FirebaseFirestore.QuerySnapshot\n try {\n rows = await firestore\n .collectionGroup('messages')\n .where('authorId', '==', uid)\n .get()\n } catch (error) {\n console.error(\n `eraseUser: support message redaction could not run for ${uid}`,\n error,\n )\n return null\n }\n for (let index = 0; index < rows.docs.length; index += 400) {\n const batch = firestore.batch()\n for (const doc of rows.docs.slice(index, index + 400)) {\n batch.set(\n doc.ref,\n { authorId: null, authorEmail: null, authorErased: true },\n { merge: true },\n )\n }\n await batch.commit()\n }\n return rows.size\n}\n\n/** An org that stops a person's account from being erased. */\nexport interface UserErasureBlocker {\n orgId: string\n orgName: string\n /** Whether the org still bills — the reason a human must intervene. */\n hasLiveSubscription: boolean\n /** Other people who would be stranded if this org lost its owner. */\n otherMembers: number\n}\n\nexport interface UserErasureCandidateOrg {\n orgId: string\n orgName: string\n ownerUid: string | null\n hasLiveSubscription: boolean\n memberCount: number\n}\n\n/**\n * Which orgs block erasing `uid` (AGL-1140).\n *\n * Pure so the policy can be tested without Firestore, because the policy is\n * the part worth arguing about — the deletion itself is mechanical.\n *\n * **Owning an org blocks erasure, always.** Not only when it bills, and not\n * only when other members would be stranded. The alternative — cascading\n * into `eraseOrg` — deletes a workspace, its sites and its data as a side\n * effect of someone closing a personal account, and no consent given to the\n * second act was given to the first. Blocking is recoverable; a cascade is\n * not.\n *\n * A live subscription and stranded members are reported rather than gating,\n * so the message can say WHY this org needs attention instead of a bare\n * refusal — \"transfer ownership\" is useless advice if you do not know which\n * of eleven workspaces is the problem.\n */\nexport function userErasureBlockers(\n uid: string,\n orgs: readonly UserErasureCandidateOrg[],\n): UserErasureBlocker[] {\n return orgs\n .filter((org) => org.ownerUid === uid)\n .map((org) => ({\n orgId: org.orgId,\n orgName: org.orgName,\n hasLiveSubscription: org.hasLiveSubscription,\n // The owner themselves is a member; anyone beyond that is stranded.\n otherMembers: Math.max(0, org.memberCount - 1),\n }))\n}\n\n/**\n * An address this account holds that a SECOND account also holds, which stops\n * the erasure until a human decides what it is.\n *\n * ⛔ Carries no address and no other uid. The whole question is about a second\n * customer, and a refusal that names them — or hands back an address in the\n * clear, which `emailDeliveries` is hashed precisely to avoid keeping —\n * discloses the person the refusal exists to protect. The staff account page\n * already lists this account's own addresses and marks the shared ones, so\n * the operator has the specifics without this record carrying them.\n */\nexport interface SharedAddressBlocker {\n /** `sha256(address)` — matches the delivery log's own key. Not readable. */\n key: string\n /** Where this account got it: `primary`, `provider`, `stored`. */\n sources: AccountAddressSource[]\n}\n\n/**\n * Which of an account's addresses a second account is also known to hold.\n *\n * Pure, and separate from the sweep, for the same reason `userErasureBlockers`\n * is: the policy is the arguable part. The mechanical part is deleting rows.\n *\n * ⚠️ `shared` is evidence in ONE direction. True proves a second holder;\n * false only means none was found, because no lookup exists for an account\n * holding an address through a federated provider. So this finds every case\n * we can SEE and cannot promise there is no other — which is exactly why the\n * sweep still tombstones every address it erases.\n */\nexport function sharedAddressBlockers(\n set: Pick<AccountAddressSet, 'addresses'>,\n): SharedAddressBlocker[] {\n return set.addresses\n .filter((entry) => entry.shared === true)\n .map((entry) => ({ key: entry.key, sources: [...entry.sources] }))\n}\n\nexport interface EraseUserResult {\n ok: boolean\n /** Set when the account was NOT erased. */\n skippedReason?: 'not-found' | 'owns-orgs' | 'shared-address'\n /** Orgs that must be handed over or deleted first. */\n blockers?: UserErasureBlocker[]\n /**\n * Addresses a second account also holds. Set with\n * `skippedReason: 'shared-address'`, and NOTHING was erased.\n */\n sharedAddresses?: SharedAddressBlocker[]\n /** What was actually removed, for the audit row and the caller's message. */\n deleted?: {\n subcollections: string[]\n authRecord: boolean\n photo: boolean\n /** `profiles/{uid}` — the world-readable public identity (AGL-1970). */\n profile: boolean\n /**\n * Support messages stripped of `authorId`/`authorEmail` (AGL-1971). The\n * threads themselves belong to the org and survive. **`null` means the\n * redaction could not run** — not that there was nothing to redact.\n */\n supportMessagesRedacted: number | null\n /**\n * What each plugin's eraser removed (AGL-2939), keyed by plugin id — the\n * data a plugin keeps about the person where neither the recursive\n * delete of the user document nor an org's cascade reaches, such as\n * documents under each org keyed by the uid. **`null` means that\n * plugin's eraser failed** and its data may remain; it is not zero.\n */\n plugins: Record<string, PluginUserErasureReport | null>\n /**\n * Messages removed from `emailDeliveries/{sha256(address)}/messages` —\n * the per-recipient delivery log. Zero for an account with no address on\n * file, or one we never mailed.\n *\n * Across EVERY address the account held, not just the current primary.\n */\n emailDeliveries: number\n /**\n * `emailIdentityIndex/{address}` rows released — the address-keyed\n * uniqueness claims, which no `recursiveDelete` of the user can reach.\n */\n emailIdentityIndex: number\n /**\n * How wide the address sweep actually was.\n *\n * ⚠️ `incomplete` is the field a compliance answer turns on: true means a\n * source could not be read and an address the account held may have been\n * missed, so this was not a complete erasure and must not be reported as\n * one. Counts only — never the addresses, which is the data being erased.\n */\n addressSweep: {\n /** Addresses found across the Auth record, its providers and the store. */\n resolved: number\n /** Addresses the delivery sweep actually visited. */\n erased: number\n /**\n * Addresses left INTACT because a second account also holds them.\n *\n * Always 0 on a successful erasure: a contested address refuses the\n * whole run before anything is destroyed. It is reported so the audit\n * row states that rather than leaving a reader to infer it.\n */\n contested: number\n incomplete: boolean\n }\n }\n}\n\n/**\n * Permanently erase a person's account (AGL-1140).\n *\n * Nothing did this before: `eraseHost`, `eraseSubtree` and `eraseOrg` existed\n * and no path anywhere deleted a `users/{uid}`. That was survivable while the\n * doc held a name and a phone; AGL-1133 added a postal address, and \"we have\n * no mechanism to delete it\" is a bad answer to an erasure request.\n *\n * `eraseOrg` is deliberately NOT the model here. An org is a workspace with\n * one owner; a person belongs to many orgs, so erasing them must not take\n * anything with it that outlives them.\n *\n * Order matters:\n * 1. Refuse if they own an org — see `userErasureBlockers`.\n * 1b. Refuse if a second account holds one of their addresses — see\n * `sharedAddressBlockers`. Both refusals come before any delete, so a\n * blocked erasure leaves the account exactly as it was.\n * 2. Remove their membership from every org they belong to, so no roster\n * keeps their email and no host projection keeps granting them access.\n * This runs BEFORE the profile delete: a half-erased account that still\n * appears on a roster is worse than one not yet started.\n * 3. Delete the `users/{uid}` subtree, the avatar, the world-readable\n * `profiles/{uid}` public identity (AGL-1970), redact their name and\n * email off every support message they wrote (AGL-1971), then the auth\n * record last — once that is gone there is no uid to retry with.\n */\nexport async function eraseUser(uid: string): Promise<EraseUserResult> {\n const firestore = firebaseAdmin.app().firestore()\n const userRef = firestore.collection('users').doc(uid)\n const snapshot = await userRef.get()\n const membership = await userRef.collection('orgs').get()\n // An absent profile doc is not an absent account — the doc is only born on\n // first save (AGL-1127), so the auth record and the org memberships can\n // outlive it. Treat \"nothing anywhere\" as not-found, not \"no doc\".\n if (!snapshot.exists && membership.empty) {\n const record = await findUserByUidAcrossPools(uid).catch(() => null)\n if (!record) return { ok: false, skippedReason: 'not-found' }\n }\n\n const candidates: UserErasureCandidateOrg[] = []\n for (const row of membership.docs) {\n const orgRef = firestore.collection('orgs').doc(row.id)\n const [org, members] = await Promise.all([\n orgRef.get(),\n orgRef.collection('members').count().get(),\n ])\n if (!org.exists) continue\n candidates.push({\n orgId: row.id,\n orgName: String(org.get('name') ?? row.id),\n ownerUid: (org.get('ownerUid') as string | undefined) ?? null,\n hasLiveSubscription: isBillingSubscription(org.data() as never),\n memberCount: Number(members.data().count ?? 0),\n })\n }\n const blockers = userErasureBlockers(uid, candidates)\n if (blockers.length) {\n return { ok: false, skippedReason: 'owns-orgs', blockers }\n }\n\n // Inventory BEFORE anything is deleted. Taken after the membership sweep\n // below, this under-reported: `removeOrgMember` empties `users/{uid}/orgs`,\n // and Firestore does not list a collection with no documents in it — so a\n // live run erased three subcollections and the audit record named two.\n // An erasure audit trail that understates what it destroyed is the one\n // record that has to be right. Measured 2026-08-01 (AGL-1140).\n const subcollections = (await userRef.listCollections()).map((c) => c.id)\n\n /*\n * EVERY ADDRESS, RESOLVED BEFORE ANY OF THEM IS DESTROYED.\n *\n * Two of the three sources are erased by this function itself:\n * `users/{uid}/emails` goes with the `recursiveDelete` below, and the Auth\n * record — the only place the primary lives — goes at the very end. Reading\n * them afterwards is reading nothing, and an erasure that resolves an empty\n * address set sweeps nothing while reporting success. That is the exact\n * shape of a spec passing because its fixture never reached the code, and\n * here it would be a live erasure reporting a completeness it does not have.\n *\n * `detectShared` because the SHARED case changes what the caller is told,\n * not what is destroyed — see `eraseEmailDeliveriesForAddresses`.\n */\n const pooledForAddresses = await findUserByUidAcrossPools(uid).catch(() => null)\n const addressSet: AccountAddressSet = await resolveAccountAddresses({\n uid,\n record: pooledForAddresses?.record ?? null,\n detectShared: true,\n firestore,\n }).catch(() => ({\n uid,\n primary: null,\n addresses: [],\n incomplete: true,\n }))\n\n /*\n * A SECOND ACCOUNT HOLDS ONE OF THESE ADDRESSES — REFUSE, HERE.\n *\n * The delivery log is filed under the address, so an address two accounts\n * hold has one set of rows answering \"what did you send me\" for two account\n * records. Whether that is one human with two accounts (the ordinary live\n * shape — a federated provider address that is another account's primary)\n * or a genuinely shared role mailbox like `billing@` decides whether\n * erasing those rows honours a request or destroys a stranger's history,\n * and NOTHING readable here distinguishes them. The data records that two\n * accounts name one address; it records nothing about the humans.\n *\n * Both automatic answers are unacceptable, and they fail differently:\n * erasing the second party's mail cannot be undone, while leaving it and\n * reporting the erasure complete is the gap this whole area was opened to\n * close. So neither is chosen — the erasure stops and says why.\n *\n * BEFORE the membership sweep, deliberately. A refusal raised after the\n * account is half-deleted is not a refusal, and the uid is the only handle\n * a human has to act on this: erase the auth record and there is no longer\n * anything to look at, no way to see which address was contested, and no\n * way to run the erasure once the question is settled. Refusing while the\n * account is whole is the only version of this that stays reversible.\n *\n * The remedy is ordinary staff work, not a special path: decide whether the\n * accounts are one person, detach the address from this one or merge them,\n * then run the erasure again. `/admin/users/[uid]` already lists this\n * account's addresses and marks the shared ones.\n */\n const contested = sharedAddressBlockers(addressSet)\n if (contested.length) {\n return {\n ok: false,\n skippedReason: 'shared-address',\n sharedAddresses: contested,\n }\n }\n\n // Memberships first — a roster row carries their email and a host\n // projection carries their access, and both outlive the profile doc.\n for (const candidate of candidates) {\n await removeOrgMember(candidate.orgId, uid).catch((error) => {\n console.error(`eraseUser: membership cleanup failed for ${candidate.orgId}`, error)\n })\n }\n\n // What the plugins keep about the person (AGL-2939). A document under an\n // org keyed by the uid is the shape no recursive delete on this path can\n // see — the same shape as the delivery log below. Each plugin's eraser\n // gets every workspace the person was in, because the memberships above\n // are already gone; one that fails is recorded as `null` and the erasure\n // goes on.\n const plugins = await runPluginUserErasers({\n uid,\n orgIds: candidates.map((candidate) => candidate.orgId),\n })\n\n // The avatar outlives the doc otherwise.\n let photo = false\n try {\n await storageBucket().deleteFiles({ prefix: `users/${uid}/` })\n photo = true\n } catch (error) {\n console.error(`eraseUser: storage cleanup failed for ${uid}`, error)\n }\n\n await firestore.recursiveDelete(userRef)\n\n // `profiles/{uid}` — the person's PUBLIC identity (AGL-1970).\n //\n // A separate top-level collection keyed by the uid AS THE DOCUMENT ID, so\n // `recursiveDelete(users/{uid})` above never reaches it and no reader\n // scanning this function for a sweep list is prompted to notice it. It is\n // `allow read: if true` in the rules and carries `handle`, `displayName`,\n // and `stripeAccountId`/`stripeChargesEnabled` — a Stripe Connect payout\n // destination written server-side after KYC. An erased person therefore\n // stayed publicly listed with a live payment-account identifier attached,\n // which Privacy Policy §5 (\"a genuine recursive delete… we keep no copy of\n // the erased content\") says in terms does not happen.\n //\n // No tombstone, unlike the org's publisher profile: the only reader of the\n // display name is the support forum's author rendering, and a forum post\n // losing its author name is the intended outcome of erasing its author.\n //\n // `recursiveDelete` rather than `delete` — nothing writes a subcollection\n // under a profile today, and a document delete would silently orphan the\n // first thing that does.\n let profile = false\n try {\n const profileRef = firestore.collection('profiles').doc(uid)\n profile = (await profileRef.get()).exists\n await firestore.recursiveDelete(profileRef)\n } catch (error) {\n console.error(`eraseUser: public profile delete failed for ${uid}`, error)\n }\n\n // Their name and email off every support message they wrote, in any\n // workspace, past or present (AGL-1971). The threads are the orgs' and\n // survive — see `redactUserSupportMessages`.\n const supportMessagesRedacted = await redactUserSupportMessages(uid)\n\n // Auth record LAST: once it is gone there is no uid to retry with, and\n // `authForPool` is required because a project-level delete cannot see an\n // SSO account at all (AGL-1122).\n //\n // The lookup is hoisted above the delete because the DELIVERY LOG is filed\n // under the ADDRESS, not the uid, and the auth record is the last place the\n // address exists. Erase it while there is still something to erase it by.\n let authRecord = false\n let emailDeliveries = 0\n /** Addresses actually swept — not the same as the ones we hold, on failure. */\n let erasedAddressCount = 0\n /** Addresses left intact because a second account also holds them. */\n let contestedAddressCount = 0\n /** Identity-index rows released, one per address that had one. */\n let emailIdentityIndex = 0\n try {\n const record = await findUserByUidAcrossPools(uid)\n if (record) {\n /*\n * `emailDeliveries/{sha256(address)}/messages` — every subject we ever\n * sent this person, with the times they opened and clicked. Personal\n * data by any reading of it, and reachable from neither\n * `recursiveDelete(users/{uid})` nor `profiles/{uid}` because it is\n * keyed by address in a third top-level collection. The same shape as\n * the `profiles/{uid}` omission this function already documents above:\n * a store nothing in the sweep list would prompt a reader to notice.\n *\n * EVERY ADDRESS, not the current primary. Passing one address left a\n * prior or provider-supplied address holding its full history —\n * recipient, subjects, open and click times — after the request had\n * been honoured and reported complete.\n *\n * Best-effort, like every other step here — a log that survives must\n * not stop the auth record from going.\n */\n const sweep = await eraseEmailDeliveriesForAddresses(\n addressSet.addresses,\n firestore,\n ).catch(() => null)\n emailDeliveries = sweep?.removed ?? 0\n erasedAddressCount = sweep?.addresses.length ?? 0\n contestedAddressCount = sweep?.contestedAddresses.length ?? 0\n\n /*\n * `emailIdentityIndex/{address}` — the SAME omission one collection\n * over.\n *\n * A top-level collection keyed by the address IN THE CLEAR, holding\n * `{uid, address, claimedAt}`. `recursiveDelete(users/{uid})` cannot\n * see it for exactly the reason it cannot see the delivery log, so an\n * erased account left a readable address still pointing at its uid.\n *\n * Released rather than tombstoned, unlike the delivery log: this is a\n * uniqueness claim, and `removeAccountEmail` already argues that\n * holding one after the address is gone burns the address for whoever\n * might legitimately want it next.\n *\n * ⛔ Only rows this uid actually owns. The index is the guard against\n * two accounts holding one address, and deleting another account's\n * claim would hand their sign-in identifier to the next taker.\n */\n for (const entry of addressSet.addresses) {\n try {\n const ref = firestore\n .collection(EMAIL_IDENTITY_INDEX_COLLECTION)\n .doc(entry.address)\n const indexed = await ref.get()\n if (indexed.exists && indexed.get('uid') === uid) {\n await ref.delete()\n emailIdentityIndex += 1\n }\n } catch (error) {\n console.error(`eraseUser: identity index release failed for ${uid}`, error)\n }\n }\n await authForPool(record.tenantId).deleteUser(uid)\n authRecord = true\n }\n } catch (error) {\n console.error(`eraseUser: auth record delete failed for ${uid}`, error)\n }\n\n /*\n * WHAT THE ADDRESS SWEEP ACTUALLY COVERED.\n *\n * Counts, never the addresses themselves: this collection is readable by\n * any staff role and `maskEmailAddress` exists because an audit row holding\n * a readable address made `adminAudit` the leakier of the two stores for\n * the same data. An erasure record listing the addresses it destroyed would\n * be the sharpest version of that — it would preserve, in the clear, the\n * exact set the request existed to remove.\n *\n * `addressesIncomplete` is the one that matters to a reader. It says a\n * source could not be read, so the sweep may have missed an address the\n * account held — the difference between \"erased everywhere\" and \"erased\n * everywhere we could see\", which is the difference a regulator asks about.\n */\n const addressSweep = {\n resolved: addressSet.addresses.length,\n erased: erasedAddressCount,\n contested: contestedAddressCount,\n incomplete: addressSet.incomplete,\n }\n\n await firestore\n .collection('adminAudit')\n .add({\n actorUid: 'system:erase-user',\n action: 'user.erased',\n target: `users/${uid}`,\n before: { orgs: candidates.length },\n after: {\n subcollections,\n authRecord,\n photo,\n profile,\n supportMessagesRedacted,\n plugins,\n emailDeliveries,\n emailIdentityIndex,\n addressSweep,\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n return {\n ok: true,\n deleted: {\n subcollections,\n authRecord,\n photo,\n profile,\n supportMessagesRedacted,\n plugins,\n emailDeliveries,\n emailIdentityIndex,\n addressSweep,\n },\n }\n}\n"],"names":["FieldValue","firebaseAdmin","eraseMediaDeliveryScope","deleteHostProjectionForAllMembers","detachWorkspaceDomain","CONSOLE_DOMAINS_COLLECTION","releaseConsoleDomain","authForPool","findUserByUidAcrossPools","eraseEmailDeliveriesForAddresses","resolveAccountAddresses","EMAIL_IDENTITY_INDEX_COLLECTION","removeOrgMember","runPluginUserErasers","runPluginOrgErasers","isBillingSubscription","readOrgBilling","disposeHostSendingDomain","readSendingDomainTeardownByLabel","ERASURE_HOLD_MS","storageBucket","name","process","env","NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET","app","storage","bucket","undefined","SUPPLIER_DELIVERY_COLLECTION","OUTREACH_MAILBOX_CREDENTIALS_COLLECTION","eraseHostSupplierDeliveries","hostId","firestore","rows","collection","where","get","index","docs","length","batch","doc","slice","delete","ref","commit","size","error","console","eraseHost","options","hostSnapshot","hostRef","orgId","sendingLabel","String","trim","sendingTeardown","catch","deleteFiles","prefix","scopeId","set","hosts","registerAllocations","collaboratorAllocations","merge","recursiveDelete","sendingDomain","teardown","tearDown","tearDownSendingDomain","eraseSubtree","path","scope","kind","id","deleteStripeCustomer","customerId","key","STRIPE_SECRET_KEY","fetch","method","headers","Authorization","eraseOrgApiKeys","dryRun","deleteDocsByOrgId","eraseOrgOutreachMailboxCredentials","eraseOrgSsoDomains","releaseOrgConsoleDomains","claims","primaries","Set","map","released","domain","result","eraseOrgStripeIndex","eraseOrgIdempotencyKeys","eraseOrgSlugs","currentSlug","held","slugs","has","reservation","exists","add","slug","eraseOrgHostIndex","eraseOrgHostMemberships","collectionGroup","eraseOrgPublisherIdentity","handles","profileRef","profileSnapshot","listings","Promise","all","listingsRetained","profile","erased","erasedAt","serverTimestamp","eraseOrgSupportTickets","tickets","ticket","errorLabel","source","parts","code","filter","part","join","recordErasureFailure","entry","actorUid","action","target","before","after","failedStep","step","requestedAt","at","auditError","eraseOrg","orgSnapshot","withoutRequest","orgRef","ok","skippedReason","requestedMs","toMillis","Date","now","members","stripeCustomerId","progress","plugins","apiKeys","outreachMailboxCredentials","ssoDomains","consoleDomains","apiIdempotency","publisher","publisherHandles","publisherProfile","supportTickets","sendingDomains","deferred","protected","host","hostIndex","stripeIndex","member","hostMemberships","reason","redactUserSupportMessages","uid","authorId","authorEmail","authorErased","userErasureBlockers","orgs","org","ownerUid","orgName","hasLiveSubscription","otherMembers","Math","max","memberCount","sharedAddressBlockers","addresses","shared","sources","eraseUser","userRef","snapshot","membership","empty","record","candidates","row","count","push","data","Number","blockers","subcollections","listCollections","c","pooledForAddresses","addressSet","detectShared","primary","incomplete","contested","sharedAddresses","candidate","orgIds","photo","supportMessagesRedacted","authRecord","emailDeliveries","erasedAddressCount","contestedAddressCount","emailIdentityIndex","sweep","removed","contestedAddresses","address","indexed","tenantId","deleteUser","addressSweep","resolved","deleted"],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 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 * as Aglyn from '@aglyn/aglyn/server';
18
+ import { type DecodedIdToken } from 'firebase-admin/auth';
19
+ import { type FirestoreDataConverter } from 'firebase-admin/firestore';
20
+ export declare const hostConverter: FirestoreDataConverter<Aglyn.AglynHost>;
21
+ export declare const screenConverter: FirestoreDataConverter<Aglyn.AglynScreen>;
22
+ export declare const screenVersionConverter: FirestoreDataConverter<Aglyn.AglynScreenVersion>;
23
+ export declare const layoutConverter: FirestoreDataConverter<Aglyn.AglynLayout>;
24
+ export declare const layoutVersionConverter: FirestoreDataConverter<Aglyn.AglynLayoutVersion>;
25
+ declare function firestoreNamespace(): FirebaseFirestore.Firestore;
26
+ declare namespace firestoreNamespace {
27
+ var FieldValue: typeof FirebaseFirestore.FieldValue;
28
+ var Timestamp: typeof FirebaseFirestore.Timestamp;
29
+ var FieldPath: typeof FirebaseFirestore.FieldPath;
30
+ }
31
+ declare const firebaseAdmin: {
32
+ app: (name?: string) => {
33
+ firestore: () => FirebaseFirestore.Firestore;
34
+ auth: () => import("firebase-admin/auth").Auth;
35
+ storage: () => import("firebase-admin/storage").Storage;
36
+ remoteConfig: () => import("firebase-admin/remote-config").RemoteConfig;
37
+ };
38
+ firestore: typeof firestoreNamespace;
39
+ database: () => import("firebase-admin/database").Database;
40
+ };
41
+ /**
42
+ * Email-verification gate (AGL-479). Email/password accounts must verify
43
+ * their address before any console access; OAuth accounts arrive with
44
+ * `email_verified: true`, so they pass untouched. `verifyConsoleIdToken`
45
+ * verifies the ID token exactly like `auth().verifyIdToken` and additionally
46
+ * throws `EmailNotVerifiedError` when the address is unverified — verified
47
+ * callers see identical behavior. Route handlers pair the raw verify with
48
+ * `emailUnverifiedResponse()` (a 403) so the denial stays distinct from an
49
+ * invalid-token 401. Fails closed: a token with no `email_verified` claim
50
+ * (e.g. some custom-token sign-ins) is treated as unverified.
51
+ *
52
+ * Exception (AGL-480): staff impersonation sessions carry an `impersonatedBy`
53
+ * claim (minted by /api/admin/impersonate). Staff have already authenticated
54
+ * and the act is audited, so the impersonated account's own verification
55
+ * state must not gate the support session — otherwise staff can't reach a
56
+ * brand-new, still-unverified owner, the exact account most likely to need
57
+ * help. `isImpersonationSession` gates that exemption.
58
+ */
59
+ export declare class EmailNotVerifiedError extends Error {
60
+ readonly code = "auth/email-not-verified";
61
+ constructor();
62
+ }
63
+ export declare function isEmailVerified(decoded: DecodedIdToken): boolean;
64
+ /** Staff impersonation session (AGL-357/AGL-480): token minted with an
65
+ * `impersonatedBy` claim. Exempt from the email-verification gate. */
66
+ export declare function isImpersonationSession(decoded: DecodedIdToken): boolean;
67
+ export declare function verifyConsoleIdToken(idToken: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
68
+ /** 403 sent to callers whose email address is not yet verified (AGL-479). */
69
+ export declare function emailUnverifiedResponse(): Response;
70
+ export { firebaseAdmin };
71
+ export default firebaseAdmin;