@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,183 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { filterPluginsByReleaseFlags, isReleaseFlagKey, isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides, parseReleaseFlagValue, RELEASE_FLAGS, resolveEffectivePlan } from "@aglyn/aglyn/server";
17
+ import { findUserByUidAcrossPools } from "./auth-pools.js";
18
+ import { firebaseAdmin } from "./firebase-admin.js";
19
+ /**
20
+ * Server-side release-flag verdicts (AGL-422): the tenant loader and the
21
+ * API dispatchers subtract flagged-off plugins, so the server needs the
22
+ * same Remote Config values the console client activates. The admin SDK
23
+ * template read is cached with a short TTL — flag flips propagate within
24
+ * a minute without a per-request round trip.
25
+ *
26
+ * Fail-open to the REGISTRY DEFAULTS (not "all on"): environments without
27
+ * Remote Config (emulator e2e, local dev) behave exactly like an
28
+ * unpublished template does on the client, so verdicts agree across
29
+ * surfaces.
30
+ */ const TTL_MS = 60000;
31
+ let cache;
32
+ let pending;
33
+ const registryDefaults = ()=>Object.fromEntries(RELEASE_FLAGS.map((definition)=>[
34
+ definition.key,
35
+ {
36
+ enabled: definition.defaultEnabled
37
+ }
38
+ ]));
39
+ export async function getServerReleaseFlagValues() {
40
+ if (cache && Date.now() - cache.at < TTL_MS) return cache.values;
41
+ if (!pending) {
42
+ pending = (async ()=>{
43
+ const values = registryDefaults();
44
+ try {
45
+ const template = await firebaseAdmin.app().remoteConfig().getTemplate();
46
+ for (const definition of RELEASE_FLAGS){
47
+ const parameter = template.parameters[definition.key];
48
+ const defaultValue = parameter == null ? void 0 : parameter.defaultValue;
49
+ const raw = defaultValue && 'value' in defaultValue ? defaultValue.value : undefined;
50
+ values[definition.key] = parseReleaseFlagValue(raw, definition.defaultEnabled);
51
+ }
52
+ } catch (unused) {
53
+ // No Remote Config here (emulator/local) — registry defaults gate.
54
+ }
55
+ cache = {
56
+ at: Date.now(),
57
+ values
58
+ };
59
+ return values;
60
+ })().finally(()=>{
61
+ pending = undefined;
62
+ });
63
+ }
64
+ return pending;
65
+ }
66
+ const orgCache = new Map();
67
+ /**
68
+ * Both halves of the per-org answer from ONE cached document read.
69
+ *
70
+ * Sharing the read is the point: `plan` and `releaseFlags` live on the same
71
+ * org document, this runs on every published page render and every API
72
+ * dispatch, and fetching the tier separately would have doubled the hot-path
73
+ * Firestore traffic to learn something already in hand.
74
+ */ export async function getOrgReleaseFlagTargeting(orgId) {
75
+ if (!orgId) return {
76
+ overrides: {},
77
+ plan: null
78
+ };
79
+ const hit = orgCache.get(orgId);
80
+ if (hit && Date.now() - hit.at < TTL_MS) return hit.targeting;
81
+ const targeting = {
82
+ overrides: {},
83
+ plan: null
84
+ };
85
+ try {
86
+ const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get();
87
+ const data = snapshot.data();
88
+ targeting.overrides = parseOrgReleaseFlagOverrides(data == null ? void 0 : data['releaseFlags']);
89
+ // Only when a document actually came back. `snapshot.data()` is undefined
90
+ // for a missing doc, and `resolveEffectivePlan` would read that as Free.
91
+ if (data) targeting.plan = resolveEffectivePlan(data);
92
+ } catch (unused) {
93
+ // No Firestore here, or the doc is unreadable — inherit, never fail.
94
+ }
95
+ orgCache.set(orgId, {
96
+ at: Date.now(),
97
+ targeting
98
+ });
99
+ return targeting;
100
+ }
101
+ export async function getOrgReleaseFlagOverrides(orgId) {
102
+ return (await getOrgReleaseFlagTargeting(orgId)).overrides;
103
+ }
104
+ /**
105
+ * The verdict for ONE flag and ONE org, overrides applied (AGL-1635).
106
+ *
107
+ * The surfaces that gate a single flag server-side — the tenant admin bar
108
+ * slot, edit-context redemption, edit-access minting — resolve through here
109
+ * rather than reading `getServerReleaseFlagValues()` and calling
110
+ * `isReleaseFlagOn` themselves, so a per-org grant reaches all of them at
111
+ * once. A gate that kept the old two-step would silently ignore overrides,
112
+ * which is the failure this issue exists to fix.
113
+ */ export async function isServerReleaseFlagOnForOrg(flagKey, orgId) {
114
+ const [values, targeting] = await Promise.all([
115
+ getServerReleaseFlagValues(),
116
+ getOrgReleaseFlagTargeting(orgId)
117
+ ]);
118
+ return isReleaseFlagOnForOrg(flagKey, values[flagKey], orgId != null ? orgId : null, targeting.overrides, targeting.plan);
119
+ }
120
+ /** Test seam: drops both caches so a spec can change a verdict. */ export function __resetReleaseFlagCaches() {
121
+ cache = undefined;
122
+ orgCache.clear();
123
+ }
124
+ /**
125
+ * The server half of the plugin release gate (AGL-422): subtracts
126
+ * flagged-off first-party plugins from an effective set. The staff bypass is
127
+ * only paid for when something was actually subtracted AND the request
128
+ * carries a bearer token — the common all-flags-on path never verifies.
129
+ *
130
+ * `orgId` is the ONLY subject: it selects the per-org overrides (AGL-1635)
131
+ * and it is the rollout bucket. AGL-1635 took a separate `subjectId` here
132
+ * because callers fell back to a hostId when the org lookup missed, and a
133
+ * hostId must never be read as an org id. AGL-1656 removed that fallback
134
+ * instead of accommodating it — a hostId and an orgId hash to different
135
+ * buckets, so a mid-rollout flag could land the published site and the
136
+ * console on opposite sides of the same percentage for the same workspace.
137
+ * With one subject the two cannot disagree by construction, and there is no
138
+ * longer a parameter through which a non-org subject could be reintroduced.
139
+ */ export async function filterEnabledPluginsByReleaseFlags(pluginIds, options) {
140
+ var _options_authorization;
141
+ const [values, targeting] = await Promise.all([
142
+ getServerReleaseFlagValues(),
143
+ getOrgReleaseFlagTargeting(options.orgId)
144
+ ]);
145
+ const isFlagOn = (flagKey)=>{
146
+ var _options_orgId;
147
+ return isReleaseFlagKey(flagKey) ? isReleaseFlagOnForOrg(flagKey, values[flagKey], (_options_orgId = options.orgId) != null ? _options_orgId : null, targeting.overrides, targeting.plan) : true;
148
+ };
149
+ const filtered = filterPluginsByReleaseFlags(pluginIds, isFlagOn);
150
+ if (filtered.length === pluginIds.length) return filtered;
151
+ const authorization = (_options_authorization = options.authorization) != null ? _options_authorization : '';
152
+ if (authorization.startsWith('Bearer ')) {
153
+ try {
154
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length));
155
+ if (decoded['staff'] === true) return [
156
+ ...pluginIds
157
+ ];
158
+ } catch (unused) {
159
+ // Invalid token — gate as anonymous.
160
+ }
161
+ return filtered;
162
+ }
163
+ if (options.subjectUid) {
164
+ // The same claim the token path reads, looked up by account: a staff
165
+ // member's own provider redirect carries no token, and previewing a dark
166
+ // plugin has to survive the round trip their session started. Paid only
167
+ // when a flag actually subtracted something and a route vouched for the
168
+ // account. Across every auth pool (AGL-1122): a staff member who signs in
169
+ // through SSO is a tenant user the project pool does not hold.
170
+ try {
171
+ var _account_record_customClaims;
172
+ const account = await findUserByUidAcrossPools(options.subjectUid);
173
+ if ((account == null ? void 0 : (_account_record_customClaims = account.record.customClaims) == null ? void 0 : _account_record_customClaims['staff']) === true) return [
174
+ ...pluginIds
175
+ ];
176
+ } catch (unused) {
177
+ // Auth did not answer — gate as anonymous.
178
+ }
179
+ }
180
+ return filtered;
181
+ }
182
+
183
+ //# sourceMappingURL=release-flags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/release-flags.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 {\n filterPluginsByReleaseFlags,\n isReleaseFlagKey,\n isReleaseFlagOnForOrg,\n parseOrgReleaseFlagOverrides,\n parseReleaseFlagValue,\n RELEASE_FLAGS,\n resolveEffectivePlan,\n type OrgPlan,\n type OrgReleaseFlagOverrides,\n type ReleaseFlagKey,\n type ReleaseFlagValue,\n} from '@aglyn/aglyn/server'\nimport { findUserByUidAcrossPools } from './auth-pools'\nimport { firebaseAdmin } from './firebase-admin'\n\n/**\n * Server-side release-flag verdicts (AGL-422): the tenant loader and the\n * API dispatchers subtract flagged-off plugins, so the server needs the\n * same Remote Config values the console client activates. The admin SDK\n * template read is cached with a short TTL — flag flips propagate within\n * a minute without a per-request round trip.\n *\n * Fail-open to the REGISTRY DEFAULTS (not \"all on\"): environments without\n * Remote Config (emulator e2e, local dev) behave exactly like an\n * unpublished template does on the client, so verdicts agree across\n * surfaces.\n */\n\nconst TTL_MS = 60_000\n\nlet cache:\n | { at: number; values: Record<ReleaseFlagKey, ReleaseFlagValue> }\n | undefined\nlet pending:\n | Promise<Record<ReleaseFlagKey, ReleaseFlagValue>>\n | undefined\n\nconst registryDefaults = (): Record<ReleaseFlagKey, ReleaseFlagValue> =>\n Object.fromEntries(\n RELEASE_FLAGS.map((definition) => [\n definition.key,\n { enabled: definition.defaultEnabled },\n ]),\n ) as Record<ReleaseFlagKey, ReleaseFlagValue>\n\nexport async function getServerReleaseFlagValues(): Promise<\n Record<ReleaseFlagKey, ReleaseFlagValue>\n> {\n if (cache && Date.now() - cache.at < TTL_MS) return cache.values\n if (!pending) {\n pending = (async () => {\n const values = registryDefaults()\n try {\n const template = await firebaseAdmin.app().remoteConfig().getTemplate()\n for (const definition of RELEASE_FLAGS) {\n const parameter = template.parameters[definition.key]\n const defaultValue = parameter?.defaultValue\n const raw =\n defaultValue && 'value' in defaultValue\n ? defaultValue.value\n : undefined\n values[definition.key] = parseReleaseFlagValue(\n raw,\n definition.defaultEnabled,\n )\n }\n } catch {\n // No Remote Config here (emulator/local) — registry defaults gate.\n }\n cache = { at: Date.now(), values }\n return values\n })().finally(() => {\n pending = undefined\n })\n }\n return pending\n}\n\n/**\n * Per-org release-flag overrides (AGL-1635), read off `orgs/{orgId}`.\n *\n * Cached on the same TTL as the template read above, and for the same\n * reason: these are consulted on every published page render and every API\n * dispatch, so an uncached read would add a Firestore round trip to the hot\n * path. A grant therefore takes up to a minute to land, matching how long a\n * flag flip already takes — one propagation story, not two.\n *\n * Keyed by orgId because a subject may be a uid or a hostId, and only an\n * org can carry overrides. Failure is silent and yields `{}`: a missing org\n * doc or a denied read must gate exactly as \"no override set\", never as an\n * error that takes a published page down.\n */\n/**\n * What one org read answers about release flags: the staff overrides, and\n * the org's EFFECTIVE plan for tier targeting (AGL-2486).\n *\n * `plan` is null when the read did not produce an org document — missing,\n * denied, or no Firestore at all. Deliberately not `'free'`: an org whose\n * doc could not be read has an UNKNOWN tier, and\n * `resolveEffectivePlan(undefined)` answers `'free'`, which would hand a\n * Free-targeted flag to every org the read failed for. The two cases have to\n * stay distinguishable at the boundary, because the evaluator treats an\n * unknown plan as a refusal and a known `'free'` as a match.\n */\ninterface OrgReleaseFlagTargeting {\n overrides: OrgReleaseFlagOverrides\n plan: OrgPlan | null\n}\n\nconst orgCache = new Map<string, { at: number; targeting: OrgReleaseFlagTargeting }>()\n\n/**\n * Both halves of the per-org answer from ONE cached document read.\n *\n * Sharing the read is the point: `plan` and `releaseFlags` live on the same\n * org document, this runs on every published page render and every API\n * dispatch, and fetching the tier separately would have doubled the hot-path\n * Firestore traffic to learn something already in hand.\n */\nexport async function getOrgReleaseFlagTargeting(\n orgId: string | null | undefined,\n): Promise<OrgReleaseFlagTargeting> {\n if (!orgId) return { overrides: {}, plan: null }\n const hit = orgCache.get(orgId)\n if (hit && Date.now() - hit.at < TTL_MS) return hit.targeting\n const targeting: OrgReleaseFlagTargeting = { overrides: {}, plan: null }\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .get()\n const data = snapshot.data()\n targeting.overrides = parseOrgReleaseFlagOverrides(data?.['releaseFlags'])\n // Only when a document actually came back. `snapshot.data()` is undefined\n // for a missing doc, and `resolveEffectivePlan` would read that as Free.\n if (data) targeting.plan = resolveEffectivePlan(data as any)\n } catch {\n // No Firestore here, or the doc is unreadable — inherit, never fail.\n }\n orgCache.set(orgId, { at: Date.now(), targeting })\n return targeting\n}\n\nexport async function getOrgReleaseFlagOverrides(\n orgId: string | null | undefined,\n): Promise<OrgReleaseFlagOverrides> {\n return (await getOrgReleaseFlagTargeting(orgId)).overrides\n}\n\n/**\n * The verdict for ONE flag and ONE org, overrides applied (AGL-1635).\n *\n * The surfaces that gate a single flag server-side — the tenant admin bar\n * slot, edit-context redemption, edit-access minting — resolve through here\n * rather than reading `getServerReleaseFlagValues()` and calling\n * `isReleaseFlagOn` themselves, so a per-org grant reaches all of them at\n * once. A gate that kept the old two-step would silently ignore overrides,\n * which is the failure this issue exists to fix.\n */\nexport async function isServerReleaseFlagOnForOrg(\n flagKey: ReleaseFlagKey,\n orgId: string | null | undefined,\n): Promise<boolean> {\n const [values, targeting] = await Promise.all([\n getServerReleaseFlagValues(),\n getOrgReleaseFlagTargeting(orgId),\n ])\n return isReleaseFlagOnForOrg(\n flagKey,\n values[flagKey],\n orgId ?? null,\n targeting.overrides,\n targeting.plan,\n )\n}\n\n/** Test seam: drops both caches so a spec can change a verdict. */\nexport function __resetReleaseFlagCaches(): void {\n cache = undefined\n orgCache.clear()\n}\n\n/**\n * The server half of the plugin release gate (AGL-422): subtracts\n * flagged-off first-party plugins from an effective set. The staff bypass is\n * only paid for when something was actually subtracted AND the request\n * carries a bearer token — the common all-flags-on path never verifies.\n *\n * `orgId` is the ONLY subject: it selects the per-org overrides (AGL-1635)\n * and it is the rollout bucket. AGL-1635 took a separate `subjectId` here\n * because callers fell back to a hostId when the org lookup missed, and a\n * hostId must never be read as an org id. AGL-1656 removed that fallback\n * instead of accommodating it — a hostId and an orgId hash to different\n * buckets, so a mid-rollout flag could land the published site and the\n * console on opposite sides of the same percentage for the same workspace.\n * With one subject the two cannot disagree by construction, and there is no\n * longer a parameter through which a non-org subject could be reintroduced.\n */\nexport async function filterEnabledPluginsByReleaseFlags(\n pluginIds: readonly string[],\n options: {\n /**\n * The org this request belongs to: rollout bucket AND override key.\n * Null when no org could be resolved — a subject-less request gets the\n * fully-enabled flags only, never a partial rollout.\n */\n orgId?: string | null\n /** Raw Authorization header, when the caller has one (API dispatch). */\n authorization?: string | null\n /**\n * The account a TOKENLESS request acts for, as its route proved it\n * (AGL-2978) — see `PluginApiRequestSubject.uid`. Consulted for the staff\n * preview only, and only when the request carries no bearer token: a\n * request with a token is the token's, whatever else it names.\n */\n subjectUid?: string | null\n },\n): Promise<string[]> {\n const [values, targeting] = await Promise.all([\n getServerReleaseFlagValues(),\n getOrgReleaseFlagTargeting(options.orgId),\n ])\n const isFlagOn = (flagKey: string): boolean =>\n isReleaseFlagKey(flagKey)\n ? isReleaseFlagOnForOrg(\n flagKey,\n values[flagKey],\n options.orgId ?? null,\n targeting.overrides,\n targeting.plan,\n )\n : true\n const filtered = filterPluginsByReleaseFlags(pluginIds, isFlagOn)\n if (filtered.length === pluginIds.length) return filtered\n\n const authorization = options.authorization ?? ''\n if (authorization.startsWith('Bearer ')) {\n try {\n const decoded = await firebaseAdmin\n .app()\n .auth()\n .verifyIdToken(authorization.slice('Bearer '.length))\n if (decoded['staff'] === true) return [...pluginIds]\n } catch {\n // Invalid token — gate as anonymous.\n }\n return filtered\n }\n if (options.subjectUid) {\n // The same claim the token path reads, looked up by account: a staff\n // member's own provider redirect carries no token, and previewing a dark\n // plugin has to survive the round trip their session started. Paid only\n // when a flag actually subtracted something and a route vouched for the\n // account. Across every auth pool (AGL-1122): a staff member who signs in\n // through SSO is a tenant user the project pool does not hold.\n try {\n const account = await findUserByUidAcrossPools(options.subjectUid)\n if (account?.record.customClaims?.['staff'] === true) return [...pluginIds]\n } catch {\n // Auth did not answer — gate as anonymous.\n }\n }\n return filtered\n}\n"],"names":["filterPluginsByReleaseFlags","isReleaseFlagKey","isReleaseFlagOnForOrg","parseOrgReleaseFlagOverrides","parseReleaseFlagValue","RELEASE_FLAGS","resolveEffectivePlan","findUserByUidAcrossPools","firebaseAdmin","TTL_MS","cache","pending","registryDefaults","Object","fromEntries","map","definition","key","enabled","defaultEnabled","getServerReleaseFlagValues","Date","now","at","values","template","app","remoteConfig","getTemplate","parameter","parameters","defaultValue","raw","value","undefined","finally","orgCache","Map","getOrgReleaseFlagTargeting","orgId","overrides","plan","hit","get","targeting","snapshot","firestore","collection","doc","data","set","getOrgReleaseFlagOverrides","isServerReleaseFlagOnForOrg","flagKey","Promise","all","__resetReleaseFlagCaches","clear","filterEnabledPluginsByReleaseFlags","pluginIds","options","isFlagOn","filtered","length","authorization","startsWith","decoded","auth","verifyIdToken","slice","subjectUid","account","record","customClaims"],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * How long `adminAudit` rows stay queryable before `/api/admin/audit-archive`
19
+ * moves them to Storage. A replay window older than this cannot be answered
20
+ * from Firestore at all, which is the one failure mode that must not look
21
+ * like "nothing to replay".
22
+ */
23
+ export declare const AUDIT_HOT_WINDOW_MS: number;
24
+ /** What happened to one erasure that had to be re-applied. */
25
+ export interface ReplayedErasure {
26
+ kind: 'org' | 'user';
27
+ /** The org id or uid the original erasure destroyed. */
28
+ id: string;
29
+ /**
30
+ * - `replayed` — it had come back and has been erased again.
31
+ * - `absent` — nothing was resurrected; the restore did not reach it.
32
+ * - `blocked` — it came back and could NOT be erased again. The reason is
33
+ * on `detail`, and this is the outcome a human has to act on.
34
+ */
35
+ outcome: 'replayed' | 'absent' | 'blocked';
36
+ detail?: string;
37
+ }
38
+ export interface ReplayErasuresResult {
39
+ /** Snapshot instant the restore was taken from — the replay's lower bound. */
40
+ sinceMs: number;
41
+ /** True when nothing was written: a plan, not a replay. */
42
+ dryRun: boolean;
43
+ /**
44
+ * Set when the answer cannot be trusted. Today the only value is
45
+ * `'audit-window'`: `sinceMs` predates the hot `adminAudit` window, so rows
46
+ * naming erasures inside the restored span may already be in the Storage
47
+ * archive and this run cannot see them. **An empty list under this warning
48
+ * is not evidence that there was nothing to replay.**
49
+ */
50
+ incomplete?: 'audit-window';
51
+ /** Audit rows examined. */
52
+ examined: number;
53
+ entries: ReplayedErasure[];
54
+ /** Convenience for the exit code — any `blocked` entry, or `incomplete`. */
55
+ ok: boolean;
56
+ }
57
+ /**
58
+ * Re-apply every erasure a restore resurrected (AGL-1975).
59
+ *
60
+ * Live DPA §11 promises, in terms: *"a deletion instruction survives any
61
+ * restoration — data deleted at Customer's instruction and later restored from
62
+ * a backup will be deleted again."* Nothing implemented it. This does.
63
+ *
64
+ * **The reason it is needed is specific, not theoretical.** A Firestore import
65
+ * is **merge-by-id, not replace** (`docs/DISASTER_RECOVERY.md` Procedures C
66
+ * and D both say so), so importing a pre-erasure export into `(default)`
67
+ * silently reinstates every document an erasure deleted — the whole org tree,
68
+ * the profile, the support threads — alongside the recovery it was actually
69
+ * run for. It happens during an incident, when nobody is reading a DPA, and
70
+ * nothing anywhere says it happened.
71
+ *
72
+ * **It calls `eraseOrg`/`eraseUser` and implements no cascade of its own.**
73
+ * That is the AGL-1481 rule and this is exactly the shape it was written for:
74
+ * a replay tool with its own copy of the sweep list would be a second
75
+ * implementation of a cascade delete, and the last one drifted within a week —
76
+ * so a replay would faithfully re-erase the six collections somebody
77
+ * remembered and quietly leave the four they did not.
78
+ *
79
+ * ## The two things that make a naive replay fail
80
+ *
81
+ * 1. **`eraseOrg` refuses without a live request.** It re-reads
82
+ * `erasureRequestedAt` and the 7-day hold, deliberately, so a stale or
83
+ * cancelled request can never delete. A restored org may carry no request
84
+ * at all: if the customer asked AFTER the snapshot was taken, the
85
+ * resurrected document predates the ask. The audit row is what closes
86
+ * that — `after.requestedAt` records which request the original run
87
+ * fulfilled — so the request is reinstated at its original instant before
88
+ * the erasure is re-run. The hold is satisfied by construction: that
89
+ * instant is in the past by at least the hold, or the first erasure would
90
+ * not have executed either.
91
+ * 2. **`eraseUser` refuses an org owner.** A restore can bring back both a
92
+ * person and the workspace they owned, and `userErasureBlockers` then
93
+ * correctly refuses to cascade. That is reported as `blocked`, never
94
+ * forced: cascading here would delete a workspace as a side effect of
95
+ * replaying a personal-account erasure, which is precisely the consent
96
+ * boundary `eraseUser` exists to hold. Erase the org first — usually it
97
+ * has its own audit row in the same list — and run this again.
98
+ *
99
+ * ## Why the audit window is a first-class result and not a log line
100
+ *
101
+ * The rows this reads are hot for 90 days before the archiver moves them to
102
+ * Storage. That comfortably covers the 7-day PITR window and the ≤7-day usable
103
+ * managed backup. It does **not** cover a 90-day-old GCS export, where the
104
+ * erasure record and the export can age out within days of each other. A
105
+ * replay over a window that old returns an empty list for two
106
+ * indistinguishable reasons — nothing was erased, or nothing is left to read —
107
+ * so it says `incomplete: 'audit-window'` and reports `ok: false`. An empty
108
+ * list is not the same as a clean bill, and this is the one place that
109
+ * distinction decides whether a published promise was kept.
110
+ *
111
+ * The query is a range on `at` alone, with the action filtered in memory
112
+ * rather than in the query. `where('action','in',[…]).where('at','>=',…)`
113
+ * needs a composite index that does not exist, and an erasure replay is not
114
+ * where anybody should discover a `FAILED_PRECONDITION` — a single-field range
115
+ * is auto-indexed everywhere, including a freshly restored database whose
116
+ * composite indexes are still building.
117
+ */
118
+ export declare function replayErasuresSince(options: {
119
+ /** The instant the restored snapshot was taken. */
120
+ sinceMs: number;
121
+ /** Report what WOULD be re-erased and change nothing. Default true. */
122
+ dryRun?: boolean;
123
+ /** Who is answerable, for the new audit rows. */
124
+ actorUid?: string;
125
+ now?: number;
126
+ }): Promise<ReplayErasuresResult>;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { FieldValue, Timestamp } from "firebase-admin/firestore";
17
+ import { firebaseAdmin } from "./firebase-admin.js";
18
+ import { eraseOrg, eraseUser } from "./erase.js";
19
+ /**
20
+ * How long `adminAudit` rows stay queryable before `/api/admin/audit-archive`
21
+ * moves them to Storage. A replay window older than this cannot be answered
22
+ * from Firestore at all, which is the one failure mode that must not look
23
+ * like "nothing to replay".
24
+ */ export const AUDIT_HOT_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
25
+ /**
26
+ * Re-apply every erasure a restore resurrected (AGL-1975).
27
+ *
28
+ * Live DPA §11 promises, in terms: *"a deletion instruction survives any
29
+ * restoration — data deleted at Customer's instruction and later restored from
30
+ * a backup will be deleted again."* Nothing implemented it. This does.
31
+ *
32
+ * **The reason it is needed is specific, not theoretical.** A Firestore import
33
+ * is **merge-by-id, not replace** (`docs/DISASTER_RECOVERY.md` Procedures C
34
+ * and D both say so), so importing a pre-erasure export into `(default)`
35
+ * silently reinstates every document an erasure deleted — the whole org tree,
36
+ * the profile, the support threads — alongside the recovery it was actually
37
+ * run for. It happens during an incident, when nobody is reading a DPA, and
38
+ * nothing anywhere says it happened.
39
+ *
40
+ * **It calls `eraseOrg`/`eraseUser` and implements no cascade of its own.**
41
+ * That is the AGL-1481 rule and this is exactly the shape it was written for:
42
+ * a replay tool with its own copy of the sweep list would be a second
43
+ * implementation of a cascade delete, and the last one drifted within a week —
44
+ * so a replay would faithfully re-erase the six collections somebody
45
+ * remembered and quietly leave the four they did not.
46
+ *
47
+ * ## The two things that make a naive replay fail
48
+ *
49
+ * 1. **`eraseOrg` refuses without a live request.** It re-reads
50
+ * `erasureRequestedAt` and the 7-day hold, deliberately, so a stale or
51
+ * cancelled request can never delete. A restored org may carry no request
52
+ * at all: if the customer asked AFTER the snapshot was taken, the
53
+ * resurrected document predates the ask. The audit row is what closes
54
+ * that — `after.requestedAt` records which request the original run
55
+ * fulfilled — so the request is reinstated at its original instant before
56
+ * the erasure is re-run. The hold is satisfied by construction: that
57
+ * instant is in the past by at least the hold, or the first erasure would
58
+ * not have executed either.
59
+ * 2. **`eraseUser` refuses an org owner.** A restore can bring back both a
60
+ * person and the workspace they owned, and `userErasureBlockers` then
61
+ * correctly refuses to cascade. That is reported as `blocked`, never
62
+ * forced: cascading here would delete a workspace as a side effect of
63
+ * replaying a personal-account erasure, which is precisely the consent
64
+ * boundary `eraseUser` exists to hold. Erase the org first — usually it
65
+ * has its own audit row in the same list — and run this again.
66
+ *
67
+ * ## Why the audit window is a first-class result and not a log line
68
+ *
69
+ * The rows this reads are hot for 90 days before the archiver moves them to
70
+ * Storage. That comfortably covers the 7-day PITR window and the ≤7-day usable
71
+ * managed backup. It does **not** cover a 90-day-old GCS export, where the
72
+ * erasure record and the export can age out within days of each other. A
73
+ * replay over a window that old returns an empty list for two
74
+ * indistinguishable reasons — nothing was erased, or nothing is left to read —
75
+ * so it says `incomplete: 'audit-window'` and reports `ok: false`. An empty
76
+ * list is not the same as a clean bill, and this is the one place that
77
+ * distinction decides whether a published promise was kept.
78
+ *
79
+ * The query is a range on `at` alone, with the action filtered in memory
80
+ * rather than in the query. `where('action','in',[…]).where('at','>=',…)`
81
+ * needs a composite index that does not exist, and an erasure replay is not
82
+ * where anybody should discover a `FAILED_PRECONDITION` — a single-field range
83
+ * is auto-indexed everywhere, including a freshly restored database whose
84
+ * composite indexes are still building.
85
+ */ export async function replayErasuresSince(options) {
86
+ const { sinceMs, dryRun = true, actorUid = 'script:replay-erasures', now = Date.now() } = options;
87
+ const firestore = firebaseAdmin.app().firestore();
88
+ const incomplete = now - sinceMs > AUDIT_HOT_WINDOW_MS ? 'audit-window' : undefined;
89
+ const rows = await firestore.collection('adminAudit').where('at', '>=', Timestamp.fromMillis(sinceMs)).orderBy('at', 'asc').get();
90
+ const entries = [];
91
+ for (const row of rows.docs){
92
+ var _row_get, _target_split_, _result_blockers;
93
+ const action = row.get('action');
94
+ if (action !== 'org.erased' && action !== 'user.erased') continue;
95
+ const target = String((_row_get = row.get('target')) != null ? _row_get : '');
96
+ const kind = action === 'org.erased' ? 'org' : 'user';
97
+ const id = (_target_split_ = target.split('/')[1]) != null ? _target_split_ : '';
98
+ if (!id) continue;
99
+ if (kind === 'org') {
100
+ var _ref;
101
+ var _row_get1;
102
+ const orgRef = firestore.collection('orgs').doc(id);
103
+ const org = await orgRef.get();
104
+ if (!org.exists) {
105
+ entries.push({
106
+ kind,
107
+ id,
108
+ outcome: 'absent'
109
+ });
110
+ continue;
111
+ }
112
+ // Which request the original run fulfilled. Without it a restored org
113
+ // that was never asked to be erased before the snapshot is refused with
114
+ // `no-request` — the erasure would silently not replay.
115
+ const requestedAt = Number((_ref = (_row_get1 = row.get('after')) == null ? void 0 : _row_get1.requestedAt) != null ? _ref : 0);
116
+ if (dryRun) {
117
+ entries.push({
118
+ kind,
119
+ id,
120
+ outcome: 'replayed',
121
+ detail: 'would re-erase (plan)'
122
+ });
123
+ continue;
124
+ }
125
+ if (requestedAt) {
126
+ await orgRef.set({
127
+ erasureRequestedAt: Timestamp.fromMillis(requestedAt)
128
+ }, {
129
+ merge: true
130
+ });
131
+ }
132
+ const result = await eraseOrg(id, {
133
+ actorUid
134
+ });
135
+ entries.push(result.ok ? {
136
+ kind,
137
+ id,
138
+ outcome: 'replayed'
139
+ } : {
140
+ kind,
141
+ id,
142
+ outcome: 'blocked',
143
+ detail: result.skippedReason
144
+ });
145
+ continue;
146
+ }
147
+ // A person. `profiles/{uid}` is checked as well as `users/{uid}`: the
148
+ // public identity is a separate top-level document (AGL-1970), so a
149
+ // restore can bring it back on its own.
150
+ const [user, profile] = await Promise.all([
151
+ firestore.collection('users').doc(id).get(),
152
+ firestore.collection('profiles').doc(id).get()
153
+ ]);
154
+ if (!user.exists && !profile.exists) {
155
+ entries.push({
156
+ kind,
157
+ id,
158
+ outcome: 'absent'
159
+ });
160
+ continue;
161
+ }
162
+ if (dryRun) {
163
+ entries.push({
164
+ kind,
165
+ id,
166
+ outcome: 'replayed',
167
+ detail: 'would re-erase (plan)'
168
+ });
169
+ continue;
170
+ }
171
+ const result = await eraseUser(id);
172
+ entries.push(result.ok ? {
173
+ kind,
174
+ id,
175
+ outcome: 'replayed'
176
+ } : {
177
+ kind,
178
+ id,
179
+ outcome: 'blocked',
180
+ // `owns-orgs` is the one a human has to act on: erase the
181
+ // workspace first, then run this again.
182
+ detail: result.skippedReason === 'owns-orgs' ? `owns-orgs (${((_result_blockers = result.blockers) != null ? _result_blockers : []).map((blocker)=>blocker.orgId).join(', ')})` : result.skippedReason
183
+ });
184
+ }
185
+ const blocked = entries.some((entry)=>entry.outcome === 'blocked');
186
+ // A record that the promise was kept — or that it could not be. Written on
187
+ // a real run only: a plan did not replay anything, and an audit row saying
188
+ // it did is the mistake this shape has to make impossible.
189
+ if (!dryRun) {
190
+ await firestore.collection('adminAudit').add({
191
+ actorUid,
192
+ action: 'erasures.replayed',
193
+ target: `restore/${new Date(sinceMs).toISOString()}`,
194
+ before: {
195
+ examined: rows.size
196
+ },
197
+ after: {
198
+ sinceMs,
199
+ incomplete: incomplete != null ? incomplete : null,
200
+ replayed: entries.filter((e)=>e.outcome === 'replayed').length,
201
+ absent: entries.filter((e)=>e.outcome === 'absent').length,
202
+ blocked: entries.filter((e)=>e.outcome === 'blocked').length
203
+ },
204
+ at: FieldValue.serverTimestamp()
205
+ }).catch(()=>undefined);
206
+ }
207
+ return {
208
+ sinceMs,
209
+ dryRun,
210
+ incomplete,
211
+ examined: rows.size,
212
+ entries,
213
+ ok: !blocked && !incomplete
214
+ };
215
+ }
216
+
217
+ //# sourceMappingURL=replay-erasures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/replay-erasures.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, Timestamp } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from './firebase-admin'\nimport { eraseOrg, eraseUser } from './erase'\n\n/**\n * How long `adminAudit` rows stay queryable before `/api/admin/audit-archive`\n * moves them to Storage. A replay window older than this cannot be answered\n * from Firestore at all, which is the one failure mode that must not look\n * like \"nothing to replay\".\n */\nexport const AUDIT_HOT_WINDOW_MS = 90 * 24 * 60 * 60 * 1000\n\n/** What happened to one erasure that had to be re-applied. */\nexport interface ReplayedErasure {\n kind: 'org' | 'user'\n /** The org id or uid the original erasure destroyed. */\n id: string\n /**\n * - `replayed` — it had come back and has been erased again.\n * - `absent` — nothing was resurrected; the restore did not reach it.\n * - `blocked` — it came back and could NOT be erased again. The reason is\n * on `detail`, and this is the outcome a human has to act on.\n */\n outcome: 'replayed' | 'absent' | 'blocked'\n detail?: string\n}\n\nexport interface ReplayErasuresResult {\n /** Snapshot instant the restore was taken from — the replay's lower bound. */\n sinceMs: number\n /** True when nothing was written: a plan, not a replay. */\n dryRun: boolean\n /**\n * Set when the answer cannot be trusted. Today the only value is\n * `'audit-window'`: `sinceMs` predates the hot `adminAudit` window, so rows\n * naming erasures inside the restored span may already be in the Storage\n * archive and this run cannot see them. **An empty list under this warning\n * is not evidence that there was nothing to replay.**\n */\n incomplete?: 'audit-window'\n /** Audit rows examined. */\n examined: number\n entries: ReplayedErasure[]\n /** Convenience for the exit code — any `blocked` entry, or `incomplete`. */\n ok: boolean\n}\n\n/**\n * Re-apply every erasure a restore resurrected (AGL-1975).\n *\n * Live DPA §11 promises, in terms: *\"a deletion instruction survives any\n * restoration — data deleted at Customer's instruction and later restored from\n * a backup will be deleted again.\"* Nothing implemented it. This does.\n *\n * **The reason it is needed is specific, not theoretical.** A Firestore import\n * is **merge-by-id, not replace** (`docs/DISASTER_RECOVERY.md` Procedures C\n * and D both say so), so importing a pre-erasure export into `(default)`\n * silently reinstates every document an erasure deleted — the whole org tree,\n * the profile, the support threads — alongside the recovery it was actually\n * run for. It happens during an incident, when nobody is reading a DPA, and\n * nothing anywhere says it happened.\n *\n * **It calls `eraseOrg`/`eraseUser` and implements no cascade of its own.**\n * That is the AGL-1481 rule and this is exactly the shape it was written for:\n * a replay tool with its own copy of the sweep list would be a second\n * implementation of a cascade delete, and the last one drifted within a week —\n * so a replay would faithfully re-erase the six collections somebody\n * remembered and quietly leave the four they did not.\n *\n * ## The two things that make a naive replay fail\n *\n * 1. **`eraseOrg` refuses without a live request.** It re-reads\n * `erasureRequestedAt` and the 7-day hold, deliberately, so a stale or\n * cancelled request can never delete. A restored org may carry no request\n * at all: if the customer asked AFTER the snapshot was taken, the\n * resurrected document predates the ask. The audit row is what closes\n * that — `after.requestedAt` records which request the original run\n * fulfilled — so the request is reinstated at its original instant before\n * the erasure is re-run. The hold is satisfied by construction: that\n * instant is in the past by at least the hold, or the first erasure would\n * not have executed either.\n * 2. **`eraseUser` refuses an org owner.** A restore can bring back both a\n * person and the workspace they owned, and `userErasureBlockers` then\n * correctly refuses to cascade. That is reported as `blocked`, never\n * forced: cascading here would delete a workspace as a side effect of\n * replaying a personal-account erasure, which is precisely the consent\n * boundary `eraseUser` exists to hold. Erase the org first — usually it\n * has its own audit row in the same list — and run this again.\n *\n * ## Why the audit window is a first-class result and not a log line\n *\n * The rows this reads are hot for 90 days before the archiver moves them to\n * Storage. That comfortably covers the 7-day PITR window and the ≤7-day usable\n * managed backup. It does **not** cover a 90-day-old GCS export, where the\n * erasure record and the export can age out within days of each other. A\n * replay over a window that old returns an empty list for two\n * indistinguishable reasons — nothing was erased, or nothing is left to read —\n * so it says `incomplete: 'audit-window'` and reports `ok: false`. An empty\n * list is not the same as a clean bill, and this is the one place that\n * distinction decides whether a published promise was kept.\n *\n * The query is a range on `at` alone, with the action filtered in memory\n * rather than in the query. `where('action','in',[…]).where('at','>=',…)`\n * needs a composite index that does not exist, and an erasure replay is not\n * where anybody should discover a `FAILED_PRECONDITION` — a single-field range\n * is auto-indexed everywhere, including a freshly restored database whose\n * composite indexes are still building.\n */\nexport async function replayErasuresSince(options: {\n /** The instant the restored snapshot was taken. */\n sinceMs: number\n /** Report what WOULD be re-erased and change nothing. Default true. */\n dryRun?: boolean\n /** Who is answerable, for the new audit rows. */\n actorUid?: string\n now?: number\n}): Promise<ReplayErasuresResult> {\n const {\n sinceMs,\n dryRun = true,\n actorUid = 'script:replay-erasures',\n now = Date.now(),\n } = options\n const firestore = firebaseAdmin.app().firestore()\n\n const incomplete =\n now - sinceMs > AUDIT_HOT_WINDOW_MS ? ('audit-window' as const) : undefined\n\n const rows = await firestore\n .collection('adminAudit')\n .where('at', '>=', Timestamp.fromMillis(sinceMs))\n .orderBy('at', 'asc')\n .get()\n\n const entries: ReplayedErasure[] = []\n for (const row of rows.docs) {\n const action = row.get('action') as string | undefined\n if (action !== 'org.erased' && action !== 'user.erased') continue\n const target = String(row.get('target') ?? '')\n const kind = action === 'org.erased' ? 'org' : 'user'\n const id = target.split('/')[1] ?? ''\n if (!id) continue\n\n if (kind === 'org') {\n const orgRef = firestore.collection('orgs').doc(id)\n const org = await orgRef.get()\n if (!org.exists) {\n entries.push({ kind, id, outcome: 'absent' })\n continue\n }\n // Which request the original run fulfilled. Without it a restored org\n // that was never asked to be erased before the snapshot is refused with\n // `no-request` — the erasure would silently not replay.\n const requestedAt = Number(row.get('after')?.requestedAt ?? 0)\n if (dryRun) {\n entries.push({\n kind,\n id,\n outcome: 'replayed',\n detail: 'would re-erase (plan)',\n })\n continue\n }\n if (requestedAt) {\n await orgRef.set(\n { erasureRequestedAt: Timestamp.fromMillis(requestedAt) },\n { merge: true },\n )\n }\n const result = await eraseOrg(id, { actorUid })\n entries.push(\n result.ok\n ? { kind, id, outcome: 'replayed' }\n : { kind, id, outcome: 'blocked', detail: result.skippedReason },\n )\n continue\n }\n\n // A person. `profiles/{uid}` is checked as well as `users/{uid}`: the\n // public identity is a separate top-level document (AGL-1970), so a\n // restore can bring it back on its own.\n const [user, profile] = await Promise.all([\n firestore.collection('users').doc(id).get(),\n firestore.collection('profiles').doc(id).get(),\n ])\n if (!user.exists && !profile.exists) {\n entries.push({ kind, id, outcome: 'absent' })\n continue\n }\n if (dryRun) {\n entries.push({\n kind,\n id,\n outcome: 'replayed',\n detail: 'would re-erase (plan)',\n })\n continue\n }\n const result = await eraseUser(id)\n entries.push(\n result.ok\n ? { kind, id, outcome: 'replayed' }\n : {\n kind,\n id,\n outcome: 'blocked',\n // `owns-orgs` is the one a human has to act on: erase the\n // workspace first, then run this again.\n detail:\n result.skippedReason === 'owns-orgs'\n ? `owns-orgs (${(result.blockers ?? [])\n .map((blocker) => blocker.orgId)\n .join(', ')})`\n : result.skippedReason,\n },\n )\n }\n\n const blocked = entries.some((entry) => entry.outcome === 'blocked')\n\n // A record that the promise was kept — or that it could not be. Written on\n // a real run only: a plan did not replay anything, and an audit row saying\n // it did is the mistake this shape has to make impossible.\n if (!dryRun) {\n await firestore\n .collection('adminAudit')\n .add({\n actorUid,\n action: 'erasures.replayed',\n target: `restore/${new Date(sinceMs).toISOString()}`,\n before: { examined: rows.size },\n after: {\n sinceMs,\n incomplete: incomplete ?? null,\n replayed: entries.filter((e) => e.outcome === 'replayed').length,\n absent: entries.filter((e) => e.outcome === 'absent').length,\n blocked: entries.filter((e) => e.outcome === 'blocked').length,\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n }\n\n return {\n sinceMs,\n dryRun,\n incomplete,\n examined: rows.size,\n entries,\n ok: !blocked && !incomplete,\n }\n}\n"],"names":["FieldValue","Timestamp","firebaseAdmin","eraseOrg","eraseUser","AUDIT_HOT_WINDOW_MS","replayErasuresSince","options","sinceMs","dryRun","actorUid","now","Date","firestore","app","incomplete","undefined","rows","collection","where","fromMillis","orderBy","get","entries","row","docs","target","result","action","String","kind","id","split","orgRef","doc","org","exists","push","outcome","requestedAt","Number","detail","set","erasureRequestedAt","merge","ok","skippedReason","user","profile","Promise","all","blockers","map","blocker","orgId","join","blocked","some","entry","add","toISOString","before","examined","size","after","replayed","filter","e","length","absent","at","serverTimestamp","catch"],"mappings":"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"}