@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,231 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { consumeRateLimit } from "./rate-limit-store.js";
18
+ /**
19
+ * Throttle for the UNAUTHENTICATED site-member password-recovery endpoint
20
+ * (AGL-1966).
21
+ *
22
+ * ## The shape being closed
23
+ *
24
+ * Two unauthenticated tenant plugin endpoints compose into a mail relay on
25
+ * `aglyn.com`'s sending domain: `membership/register` puts an arbitrary
26
+ * address into `hosts/{hostId}/siteMembers` (member accounts are unlimited on
27
+ * every plan, AGL-889, so nothing there refuses), and `membership/recover`
28
+ * mails a reset link to any address that has such a row. Register N addresses,
29
+ * call recover N times, and every one is a real message we sent, carrying an
30
+ * attacker-chosen site name in the subject.
31
+ *
32
+ * The only thing in the way was the dispatcher's `visitorWriteRateLimitRefusal`
33
+ * — 120 writes/60s per (site, IP), a bucket register and recover SHARE. That
34
+ * ceiling is sized for a shopper editing cart quantities; measured as mail it
35
+ * is ~86k messages a day from one address.
36
+ *
37
+ * The damage is not the Resend bill. It is the sending reputation of
38
+ * `aglyn.com`, which is shared by every customer's transactional mail, and
39
+ * which cannot be bought back after the fact.
40
+ *
41
+ * ## Why this is a separate module from `password-reset-throttle.ts`
42
+ *
43
+ * That one guards ADMIN-initiated resets, and says so at length: every caller
44
+ * is already authenticated as staff, an org admin or a site admin, "so this is
45
+ * **not** the enumeration defense that guards the public `membership/recover`
46
+ * endpoint — nobody reaches here without already knowing the account exists."
47
+ * This is that defense. The caps differ (there is no actor to key on here, and
48
+ * an IP is not an actor), and the refusal contract differs — see below, it is
49
+ * the part that is easy to get wrong.
50
+ *
51
+ * It lives at this layer, beside its sibling, for the reason that one gives:
52
+ * the commerce plugin cannot import the console's API helpers, and a number
53
+ * duplicated in two places drifts.
54
+ *
55
+ * ## Two kinds of cap, because they have two different refusals
56
+ *
57
+ * This is the load-bearing distinction in the module, and it exists entirely
58
+ * to keep the endpoint from becoming an existence oracle.
59
+ *
60
+ * **Attempt caps** ({@link consumeMembershipRecoverAttempt}) — per recipient
61
+ * and per IP. Consumed BEFORE the member lookup, so a request for an address
62
+ * that is a member and a request for one that is not spend exactly the same
63
+ * budget and get exactly the same answer. Their refusal may therefore be a
64
+ * visible `429`: it says nothing about whether the address exists, because the
65
+ * counter that produced it counts both alike. Consuming these *after* the
66
+ * lookup, or only on the send path, would be the bug — the 429 would then
67
+ * appear only for real members and the endpoint's whole silent-success
68
+ * contract would be undone by its own rate limiter.
69
+ *
70
+ * **The send cap** ({@link consumeMembershipRecoverSend}) — per host, per day.
71
+ * Consumed only when a message is actually about to go out, because it is a
72
+ * ceiling on OUTBOUND MAIL rather than on requests. That means it is only ever
73
+ * reached on the member-exists branch, so its refusal must NOT be visible: the
74
+ * caller takes the same silent `200 {ok:true}` exit as an unknown address. The
75
+ * cost of that is a real member who gets no mail and no error during an
76
+ * attack on their site — collateral that is accepted deliberately, and sized
77
+ * for below.
78
+ *
79
+ * ## Backed by the durable limiter, not a `Map`
80
+ *
81
+ * `membership-recover.ts` previously carried a per-instance `Map` keyed on
82
+ * IP+email. On Vercel that is close to no limit at all — it resets on every
83
+ * cold start and each concurrent instance keeps its own, so the effective cap
84
+ * is `limit × instances` and an attacker widens it by going wider (AGL-794).
85
+ * A mail send is exactly the "consequence, not volume" case that
86
+ * `RATE_LIMITING.md` says to spend a Firestore transaction on.
87
+ *
88
+ * ### Contention fails CLOSED here, and that is correct (AGL-2404)
89
+ *
90
+ * `consumeRateLimit` refuses with `contended: true` when one key is hot, and
91
+ * only degrades when the store itself is unreachable. Both postures land well
92
+ * on this surface. The attempt keys are per recipient and per IP, so a
93
+ * legitimate visitor essentially never races themselves — concurrency on one
94
+ * of these keys is the attack. And the send key is per host per day, which is
95
+ * one document: a busy site's genuine recover traffic is a handful of requests
96
+ * an hour, so contention there is likewise a flood rather than a Tuesday.
97
+ */ /**
98
+ * Reset mails per recipient address, per hour, across every site.
99
+ *
100
+ * Matches `RESET_SENDS_PER_RECIPIENT` deliberately: a reset link is valid for
101
+ * an hour anyway, so a second and third are only useful if the first was lost,
102
+ * and one number for "how many resets may one mailbox receive in an hour" is
103
+ * easier to defend than two.
104
+ *
105
+ * NOT keyed by host. A per-(host, address) cap would let an attacker who
106
+ * controls several sites — which a free org can create — multiply one
107
+ * mailbox's mail by the number of sites, and the person being carpet-bombed
108
+ * does not care which of our customers' sites the mail claims to come from.
109
+ */ export const RECOVER_SENDS_PER_RECIPIENT = 3;
110
+ /**
111
+ * Recovery attempts per client IP, per hour, across every site and address.
112
+ *
113
+ * Sized against the human behaviour, not the attack: a person who has
114
+ * forgotten a password tries it on one or two sites, mistypes the address
115
+ * once, and stops. Ten is already several times that. A household or office
116
+ * NAT sharing one address would need ten genuine forgotten passwords in one
117
+ * hour to notice this.
118
+ *
119
+ * This is the cap that actually prices enumeration. The per-recipient cap
120
+ * alone does nothing against the register-then-recover composition, because
121
+ * every address in that attack is fresh and none of them has a count.
122
+ */ export const RECOVER_ATTEMPTS_PER_IP = 10;
123
+ /** Both attempt caps use a one-hour fixed window, like their sibling. */ export const RECOVER_ATTEMPT_WINDOW_MS = 60 * 60 * 1000;
124
+ /**
125
+ * Recovery mails one SITE may emit per day.
126
+ *
127
+ * An abuse ceiling, not a usage limit, and it is set where it is because of
128
+ * the collateral described above: it must sit far enough above any real site's
129
+ * genuine volume that reaching it is evidence of an attack. A storefront with
130
+ * ten thousand members sees single-digit resets a day; two hundred is more
131
+ * than an order of magnitude of headroom.
132
+ *
133
+ * What it buys, given the dispatcher's 120/min already upstream: it converts
134
+ * "~86k messages a day per site from one IP" into 200, and — because it is
135
+ * keyed on the site rather than on the caller — it holds no matter how many
136
+ * IPs the caller brings. That is the property the compound visitor-write key
137
+ * explicitly cannot have, and the reason this cap is per host alone.
138
+ */ export const RECOVER_SENDS_PER_HOST_PER_DAY = 200;
139
+ /** Fixed 24h window for the send cap. */ export const RECOVER_SEND_WINDOW_MS = 24 * 60 * 60 * 1000;
140
+ /**
141
+ * How new a member row may be and still be worth a reset mail.
142
+ *
143
+ * The register-then-recover composition is the whole attack, and no real
144
+ * person needs a password reset in the first ten minutes of having chosen the
145
+ * password. This is the cheapest control on the list and the only one that
146
+ * refuses the composition itself rather than rationing it.
147
+ *
148
+ * Enforced by the handler (it needs the member document); the constant lives
149
+ * here so the policy is in one place.
150
+ */ export const RECOVER_MIN_MEMBER_AGE_MS = 10 * 60 * 1000;
151
+ function refusal(limited, resetMs, at, rate) {
152
+ return {
153
+ allowed: false,
154
+ retryAfterSeconds: Math.max(1, Math.ceil((resetMs - at) / 1000)),
155
+ limited,
156
+ degraded: rate.degraded,
157
+ contended: rate.contended
158
+ };
159
+ }
160
+ /**
161
+ * Counts one recovery ATTEMPT against the per-recipient and per-IP caps.
162
+ *
163
+ * Call before deciding whether the address is a member — see the module note
164
+ * on why the ordering is the control and not an implementation detail.
165
+ *
166
+ * The recipient cap is checked first and short-circuits, matching
167
+ * `consumePasswordResetSend`: one address being hammered must not also spend
168
+ * the IP budget that a person sharing that NAT is relying on.
169
+ *
170
+ * The email is passed through to the key in the clear on purpose —
171
+ * `consumeRateLimit` hashes every key into the document id precisely so that
172
+ * caller-held identifiers do not sit in plaintext ids or index exports.
173
+ */ export async function consumeMembershipRecoverAttempt(options) {
174
+ const { email, ip, now, firestore } = options;
175
+ const shared = _extends({
176
+ windowMs: RECOVER_ATTEMPT_WINDOW_MS
177
+ }, now === undefined ? {} : {
178
+ now
179
+ }, firestore === undefined ? {} : {
180
+ firestore
181
+ });
182
+ const at = now != null ? now : Date.now();
183
+ const recipient = await consumeRateLimit(`recover:to:${email}`, _extends({}, shared, {
184
+ limit: RECOVER_SENDS_PER_RECIPIENT
185
+ }));
186
+ if (!recipient.allowed) {
187
+ return refusal('recipient', recipient.resetMs, at, recipient);
188
+ }
189
+ const source = ip ? await consumeRateLimit(`recover:ip:${ip}`, _extends({}, shared, {
190
+ limit: RECOVER_ATTEMPTS_PER_IP
191
+ })) : null;
192
+ if (source && !source.allowed) {
193
+ return refusal('ip', source.resetMs, at, source);
194
+ }
195
+ return {
196
+ allowed: true,
197
+ retryAfterSeconds: 0,
198
+ limited: null,
199
+ degraded: recipient.degraded || Boolean(source == null ? void 0 : source.degraded),
200
+ contended: false
201
+ };
202
+ }
203
+ /**
204
+ * Counts one recovery mail against the per-site daily ceiling.
205
+ *
206
+ * Call immediately before sending, and treat a refusal as the silent-success
207
+ * exit — never as a visible error. A visible one would fire only for addresses
208
+ * that really are members, which is the oracle the endpoint exists to avoid.
209
+ */ export async function consumeMembershipRecoverSend(options) {
210
+ const { hostId, now, firestore } = options;
211
+ const at = now != null ? now : Date.now();
212
+ const rate = await consumeRateLimit(`recover:host:${hostId}`, _extends({
213
+ limit: RECOVER_SENDS_PER_HOST_PER_DAY,
214
+ windowMs: RECOVER_SEND_WINDOW_MS
215
+ }, now === undefined ? {} : {
216
+ now
217
+ }, firestore === undefined ? {} : {
218
+ firestore
219
+ }));
220
+ if (!rate.allowed) return refusal('host', rate.resetMs, at, rate);
221
+ return {
222
+ allowed: true,
223
+ retryAfterSeconds: 0,
224
+ limited: null,
225
+ degraded: rate.degraded,
226
+ contended: false
227
+ };
228
+ }
229
+ export default consumeMembershipRecoverAttempt;
230
+
231
+ //# sourceMappingURL=membership-recover-throttle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/membership-recover-throttle.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 { consumeRateLimit } from './rate-limit-store'\n\n/**\n * Throttle for the UNAUTHENTICATED site-member password-recovery endpoint\n * (AGL-1966).\n *\n * ## The shape being closed\n *\n * Two unauthenticated tenant plugin endpoints compose into a mail relay on\n * `aglyn.com`'s sending domain: `membership/register` puts an arbitrary\n * address into `hosts/{hostId}/siteMembers` (member accounts are unlimited on\n * every plan, AGL-889, so nothing there refuses), and `membership/recover`\n * mails a reset link to any address that has such a row. Register N addresses,\n * call recover N times, and every one is a real message we sent, carrying an\n * attacker-chosen site name in the subject.\n *\n * The only thing in the way was the dispatcher's `visitorWriteRateLimitRefusal`\n * — 120 writes/60s per (site, IP), a bucket register and recover SHARE. That\n * ceiling is sized for a shopper editing cart quantities; measured as mail it\n * is ~86k messages a day from one address.\n *\n * The damage is not the Resend bill. It is the sending reputation of\n * `aglyn.com`, which is shared by every customer's transactional mail, and\n * which cannot be bought back after the fact.\n *\n * ## Why this is a separate module from `password-reset-throttle.ts`\n *\n * That one guards ADMIN-initiated resets, and says so at length: every caller\n * is already authenticated as staff, an org admin or a site admin, \"so this is\n * **not** the enumeration defense that guards the public `membership/recover`\n * endpoint — nobody reaches here without already knowing the account exists.\"\n * This is that defense. The caps differ (there is no actor to key on here, and\n * an IP is not an actor), and the refusal contract differs — see below, it is\n * the part that is easy to get wrong.\n *\n * It lives at this layer, beside its sibling, for the reason that one gives:\n * the commerce plugin cannot import the console's API helpers, and a number\n * duplicated in two places drifts.\n *\n * ## Two kinds of cap, because they have two different refusals\n *\n * This is the load-bearing distinction in the module, and it exists entirely\n * to keep the endpoint from becoming an existence oracle.\n *\n * **Attempt caps** ({@link consumeMembershipRecoverAttempt}) — per recipient\n * and per IP. Consumed BEFORE the member lookup, so a request for an address\n * that is a member and a request for one that is not spend exactly the same\n * budget and get exactly the same answer. Their refusal may therefore be a\n * visible `429`: it says nothing about whether the address exists, because the\n * counter that produced it counts both alike. Consuming these *after* the\n * lookup, or only on the send path, would be the bug — the 429 would then\n * appear only for real members and the endpoint's whole silent-success\n * contract would be undone by its own rate limiter.\n *\n * **The send cap** ({@link consumeMembershipRecoverSend}) — per host, per day.\n * Consumed only when a message is actually about to go out, because it is a\n * ceiling on OUTBOUND MAIL rather than on requests. That means it is only ever\n * reached on the member-exists branch, so its refusal must NOT be visible: the\n * caller takes the same silent `200 {ok:true}` exit as an unknown address. The\n * cost of that is a real member who gets no mail and no error during an\n * attack on their site — collateral that is accepted deliberately, and sized\n * for below.\n *\n * ## Backed by the durable limiter, not a `Map`\n *\n * `membership-recover.ts` previously carried a per-instance `Map` keyed on\n * IP+email. On Vercel that is close to no limit at all — it resets on every\n * cold start and each concurrent instance keeps its own, so the effective cap\n * is `limit × instances` and an attacker widens it by going wider (AGL-794).\n * A mail send is exactly the \"consequence, not volume\" case that\n * `RATE_LIMITING.md` says to spend a Firestore transaction on.\n *\n * ### Contention fails CLOSED here, and that is correct (AGL-2404)\n *\n * `consumeRateLimit` refuses with `contended: true` when one key is hot, and\n * only degrades when the store itself is unreachable. Both postures land well\n * on this surface. The attempt keys are per recipient and per IP, so a\n * legitimate visitor essentially never races themselves — concurrency on one\n * of these keys is the attack. And the send key is per host per day, which is\n * one document: a busy site's genuine recover traffic is a handful of requests\n * an hour, so contention there is likewise a flood rather than a Tuesday.\n */\n\n/**\n * Reset mails per recipient address, per hour, across every site.\n *\n * Matches `RESET_SENDS_PER_RECIPIENT` deliberately: a reset link is valid for\n * an hour anyway, so a second and third are only useful if the first was lost,\n * and one number for \"how many resets may one mailbox receive in an hour\" is\n * easier to defend than two.\n *\n * NOT keyed by host. A per-(host, address) cap would let an attacker who\n * controls several sites — which a free org can create — multiply one\n * mailbox's mail by the number of sites, and the person being carpet-bombed\n * does not care which of our customers' sites the mail claims to come from.\n */\nexport const RECOVER_SENDS_PER_RECIPIENT = 3\n\n/**\n * Recovery attempts per client IP, per hour, across every site and address.\n *\n * Sized against the human behaviour, not the attack: a person who has\n * forgotten a password tries it on one or two sites, mistypes the address\n * once, and stops. Ten is already several times that. A household or office\n * NAT sharing one address would need ten genuine forgotten passwords in one\n * hour to notice this.\n *\n * This is the cap that actually prices enumeration. The per-recipient cap\n * alone does nothing against the register-then-recover composition, because\n * every address in that attack is fresh and none of them has a count.\n */\nexport const RECOVER_ATTEMPTS_PER_IP = 10\n\n/** Both attempt caps use a one-hour fixed window, like their sibling. */\nexport const RECOVER_ATTEMPT_WINDOW_MS = 60 * 60 * 1000\n\n/**\n * Recovery mails one SITE may emit per day.\n *\n * An abuse ceiling, not a usage limit, and it is set where it is because of\n * the collateral described above: it must sit far enough above any real site's\n * genuine volume that reaching it is evidence of an attack. A storefront with\n * ten thousand members sees single-digit resets a day; two hundred is more\n * than an order of magnitude of headroom.\n *\n * What it buys, given the dispatcher's 120/min already upstream: it converts\n * \"~86k messages a day per site from one IP\" into 200, and — because it is\n * keyed on the site rather than on the caller — it holds no matter how many\n * IPs the caller brings. That is the property the compound visitor-write key\n * explicitly cannot have, and the reason this cap is per host alone.\n */\nexport const RECOVER_SENDS_PER_HOST_PER_DAY = 200\n\n/** Fixed 24h window for the send cap. */\nexport const RECOVER_SEND_WINDOW_MS = 24 * 60 * 60 * 1000\n\n/**\n * How new a member row may be and still be worth a reset mail.\n *\n * The register-then-recover composition is the whole attack, and no real\n * person needs a password reset in the first ten minutes of having chosen the\n * password. This is the cheapest control on the list and the only one that\n * refuses the composition itself rather than rationing it.\n *\n * Enforced by the handler (it needs the member document); the constant lives\n * here so the policy is in one place.\n */\nexport const RECOVER_MIN_MEMBER_AGE_MS = 10 * 60 * 1000\n\nexport interface MembershipRecoverThrottleResult {\n allowed: boolean\n /** Seconds until the exhausted window rolls over; 0 when allowed. */\n retryAfterSeconds: number\n /** Which cap refused, for logs. Never told to the caller. */\n limited: 'recipient' | 'ip' | 'host' | null\n /** True when the durable store was unreachable and only a local cap applied. */\n degraded: boolean\n /** True when the key was contended and the request was refused on that basis. */\n contended: boolean\n}\n\nexport interface MembershipRecoverAttemptOptions {\n /** The address the recovery was requested for, normalized by the caller. */\n email: string\n /**\n * The caller's address from `readClientIp`, or `null` when nothing readable\n * named one — in which case the per-source cap is SKIPPED rather than shared.\n * One `recover:ip:unknown` budget would spend every site's recovery\n * allowance on behalf of whoever tried first; the per-recipient cap above is\n * the half that stops mailbombing one person and needs no address.\n */\n ip: string | null\n now?: number\n /** Injectable for tests; defaults to the Admin SDK's Firestore. */\n firestore?: unknown\n}\n\nfunction refusal(\n limited: 'recipient' | 'ip' | 'host',\n resetMs: number,\n at: number,\n rate: { degraded: boolean; contended: boolean },\n): MembershipRecoverThrottleResult {\n return {\n allowed: false,\n retryAfterSeconds: Math.max(1, Math.ceil((resetMs - at) / 1000)),\n limited,\n degraded: rate.degraded,\n contended: rate.contended,\n }\n}\n\n/**\n * Counts one recovery ATTEMPT against the per-recipient and per-IP caps.\n *\n * Call before deciding whether the address is a member — see the module note\n * on why the ordering is the control and not an implementation detail.\n *\n * The recipient cap is checked first and short-circuits, matching\n * `consumePasswordResetSend`: one address being hammered must not also spend\n * the IP budget that a person sharing that NAT is relying on.\n *\n * The email is passed through to the key in the clear on purpose —\n * `consumeRateLimit` hashes every key into the document id precisely so that\n * caller-held identifiers do not sit in plaintext ids or index exports.\n */\nexport async function consumeMembershipRecoverAttempt(\n options: MembershipRecoverAttemptOptions,\n): Promise<MembershipRecoverThrottleResult> {\n const { email, ip, now, firestore } = options\n const shared = {\n windowMs: RECOVER_ATTEMPT_WINDOW_MS,\n ...(now === undefined ? {} : { now }),\n ...(firestore === undefined ? {} : { firestore }),\n }\n const at = now ?? Date.now()\n\n const recipient = await consumeRateLimit(`recover:to:${email}`, {\n ...shared,\n limit: RECOVER_SENDS_PER_RECIPIENT,\n })\n if (!recipient.allowed) {\n return refusal('recipient', recipient.resetMs, at, recipient)\n }\n\n const source = ip\n ? await consumeRateLimit(`recover:ip:${ip}`, {\n ...shared,\n limit: RECOVER_ATTEMPTS_PER_IP,\n })\n : null\n if (source && !source.allowed) {\n return refusal('ip', source.resetMs, at, source)\n }\n\n return {\n allowed: true,\n retryAfterSeconds: 0,\n limited: null,\n degraded: recipient.degraded || Boolean(source?.degraded),\n contended: false,\n }\n}\n\nexport interface MembershipRecoverSendOptions {\n hostId: string\n now?: number\n firestore?: unknown\n}\n\n/**\n * Counts one recovery mail against the per-site daily ceiling.\n *\n * Call immediately before sending, and treat a refusal as the silent-success\n * exit — never as a visible error. A visible one would fire only for addresses\n * that really are members, which is the oracle the endpoint exists to avoid.\n */\nexport async function consumeMembershipRecoverSend(\n options: MembershipRecoverSendOptions,\n): Promise<MembershipRecoverThrottleResult> {\n const { hostId, now, firestore } = options\n const at = now ?? Date.now()\n const rate = await consumeRateLimit(`recover:host:${hostId}`, {\n limit: RECOVER_SENDS_PER_HOST_PER_DAY,\n windowMs: RECOVER_SEND_WINDOW_MS,\n ...(now === undefined ? {} : { now }),\n ...(firestore === undefined ? {} : { firestore }),\n })\n if (!rate.allowed) return refusal('host', rate.resetMs, at, rate)\n return {\n allowed: true,\n retryAfterSeconds: 0,\n limited: null,\n degraded: rate.degraded,\n contended: false,\n }\n}\n\nexport default consumeMembershipRecoverAttempt\n"],"names":["consumeRateLimit","RECOVER_SENDS_PER_RECIPIENT","RECOVER_ATTEMPTS_PER_IP","RECOVER_ATTEMPT_WINDOW_MS","RECOVER_SENDS_PER_HOST_PER_DAY","RECOVER_SEND_WINDOW_MS","RECOVER_MIN_MEMBER_AGE_MS","refusal","limited","resetMs","at","rate","allowed","retryAfterSeconds","Math","max","ceil","degraded","contended","consumeMembershipRecoverAttempt","options","email","ip","now","firestore","shared","windowMs","undefined","Date","recipient","limit","source","Boolean","consumeMembershipRecoverSend","hostId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,wBAAoB;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EC,GAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,8BAA8B,EAAC;AAE5C;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,0BAA0B,GAAE;AAEzC,uEAAuE,GACvE,OAAO,MAAMC,4BAA4B,KAAK,KAAK,KAAI;AAEvD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,iCAAiC,IAAG;AAEjD,uCAAuC,GACvC,OAAO,MAAMC,yBAAyB,KAAK,KAAK,KAAK,KAAI;AAEzD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,4BAA4B,KAAK,KAAK,KAAI;AA8BvD,SAASC,QACPC,OAAoC,EACpCC,OAAe,EACfC,EAAU,EACVC,IAA+C;IAE/C,OAAO;QACLC,SAAS;QACTC,mBAAmBC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAAC,AAACP,CAAAA,UAAUC,EAAC,IAAK;QAC1DF;QACAS,UAAUN,KAAKM,QAAQ;QACvBC,WAAWP,KAAKO,SAAS;IAC3B;AACF;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,gCACpBC,OAAwC;IAExC,MAAM,EAAEC,KAAK,EAAEC,EAAE,EAAEC,GAAG,EAAEC,SAAS,EAAE,GAAGJ;IACtC,MAAMK,SAAS;QACbC,UAAUvB;OACNoB,QAAQI,YAAY,CAAC,IAAI;QAAEJ;IAAI,GAC/BC,cAAcG,YAAY,CAAC,IAAI;QAAEH;IAAU;IAEjD,MAAMd,KAAKa,cAAAA,MAAOK,KAAKL,GAAG;IAE1B,MAAMM,YAAY,MAAM7B,iBAAiB,CAAC,WAAW,EAAEqB,OAAO,EAAE,aAC3DI;QACHK,OAAO7B;;IAET,IAAI,CAAC4B,UAAUjB,OAAO,EAAE;QACtB,OAAOL,QAAQ,aAAasB,UAAUpB,OAAO,EAAEC,IAAImB;IACrD;IAEA,MAAME,SAAST,KACX,MAAMtB,iBAAiB,CAAC,WAAW,EAAEsB,IAAI,EAAE,aACtCG;QACHK,OAAO5B;UAET;IACJ,IAAI6B,UAAU,CAACA,OAAOnB,OAAO,EAAE;QAC7B,OAAOL,QAAQ,MAAMwB,OAAOtB,OAAO,EAAEC,IAAIqB;IAC3C;IAEA,OAAO;QACLnB,SAAS;QACTC,mBAAmB;QACnBL,SAAS;QACTS,UAAUY,UAAUZ,QAAQ,IAAIe,QAAQD,0BAAAA,OAAQd,QAAQ;QACxDC,WAAW;IACb;AACF;AAQA;;;;;;CAMC,GACD,OAAO,eAAee,6BACpBb,OAAqC;IAErC,MAAM,EAAEc,MAAM,EAAEX,GAAG,EAAEC,SAAS,EAAE,GAAGJ;IACnC,MAAMV,KAAKa,cAAAA,MAAOK,KAAKL,GAAG;IAC1B,MAAMZ,OAAO,MAAMX,iBAAiB,CAAC,aAAa,EAAEkC,QAAQ,EAAE;QAC5DJ,OAAO1B;QACPsB,UAAUrB;OACNkB,QAAQI,YAAY,CAAC,IAAI;QAAEJ;IAAI,GAC/BC,cAAcG,YAAY,CAAC,IAAI;QAAEH;IAAU;IAEjD,IAAI,CAACb,KAAKC,OAAO,EAAE,OAAOL,QAAQ,QAAQI,KAAKF,OAAO,EAAEC,IAAIC;IAC5D,OAAO;QACLC,SAAS;QACTC,mBAAmB;QACnBL,SAAS;QACTS,UAAUN,KAAKM,QAAQ;QACvBC,WAAW;IACb;AACF;AAEA,eAAeC,gCAA+B"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type AglynNotification } from '@aglyn/aglyn/server';
18
+ export type NotificationPayload = Omit<AglynNotification, '$id' | 'createdAt' | 'readAt'>;
19
+ /**
20
+ * Notification fan-out (AGL-259): batch-writes one doc per recipient at
21
+ * `users/{uid}/notifications`. Never throws — a notification miss must
22
+ * not break the mutation that emitted it.
23
+ */
24
+ export declare function notifyUsers(uids: Iterable<string>, payload: NotificationPayload): Promise<void>;
25
+ /**
26
+ * Notifies every staff-claim holder (AGL-850) — the support-desk audience.
27
+ * Staff are not org members, so `notifyOrgAdmins`/`notifyHostManagers` never
28
+ * reach them; this enumerates the `staff` custom claim instead. The docs land
29
+ * at `users/{uid}/notifications`, which the console notifications menu already
30
+ * renders, so no separate staff inbox is needed. Never throws.
31
+ */
32
+ export declare function notifyStaff(payload: NotificationPayload): Promise<void>;
33
+ /** Notifies the org's owner + admins (billing, membership, org events). */
34
+ export declare function notifyOrgAdmins(orgId: string, payload: NotificationPayload): Promise<void>;
35
+ /**
36
+ * Notifies everyone who manages a host (admin/editor in the host doc's
37
+ * `memberRoles` projection) — the audience for form submissions and
38
+ * bookings on that site.
39
+ *
40
+ * The OWNING ORG travels with the notification (AGL-1773). Host links are
41
+ * stored in the legacy `/{hostDocId}/rest` shape and rewritten to
42
+ * `/{orgSlug}/hosts/{subdomain}/rest` when they are FOLLOWED
43
+ * (`normalizeNotificationLink`, AGL-644) — a rewrite that needs an org slug.
44
+ * With no `orgId` on the doc the console had nothing to key on and fell back
45
+ * to whichever org the reader happens to have OPEN, so a manager who belongs
46
+ * to more than one workspace was routed to `/{other-org}/hosts/{subdomain}/…`
47
+ * and got a designed 404: `HostGuard` only resolves subdomains belonging to
48
+ * the current org. `notifyOrgAdmins` has always stamped it; the host fan-out
49
+ * — every form submission, booking, order and stock alert — never did.
50
+ *
51
+ * The host doc already carries `orgId` (AGL-233), so this costs no extra
52
+ * read. Spread conditionally: the field is optional on the host model and
53
+ * Firestore rejects `undefined`.
54
+ */
55
+ export declare function notifyHostManagers(hostId: string, payload: NotificationPayload): Promise<void>;
@@ -0,0 +1,134 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { notificationMuted } from "@aglyn/aglyn/server";
18
+ import { FieldValue } from "firebase-admin/firestore";
19
+ import { listStaffUidsAcrossPools } from "./auth-pools.js";
20
+ import firebaseAdmin from "./firebase-admin.js";
21
+ import { listOrgMembers } from "./organizations.js";
22
+ const firestore = ()=>firebaseAdmin.app().firestore();
23
+ /**
24
+ * Notification fan-out (AGL-259): batch-writes one doc per recipient at
25
+ * `users/{uid}/notifications`. Never throws — a notification miss must
26
+ * not break the mutation that emitted it.
27
+ */ export async function notifyUsers(uids, payload) {
28
+ try {
29
+ const db = firestore();
30
+ const targets = [
31
+ ...new Set(uids)
32
+ ].filter(Boolean).slice(0, 400);
33
+ if (!targets.length) return;
34
+ // Per-user category mutes (AGL-267): one getAll over the user docs.
35
+ const userDocs = await db.getAll(...targets.map((uid)=>db.collection('users').doc(uid)));
36
+ const batch = db.batch();
37
+ let count = 0;
38
+ for (const userDoc of userDocs){
39
+ const prefs = userDoc.get('notificationPrefs');
40
+ if (notificationMuted(prefs, payload.type)) continue;
41
+ batch.set(db.collection('users').doc(userDoc.id).collection('notifications').doc(), _extends({}, payload, {
42
+ createdAt: FieldValue.serverTimestamp()
43
+ }));
44
+ count += 1;
45
+ }
46
+ if (count > 0) await batch.commit();
47
+ } catch (error) {
48
+ console.error('notification fan-out failed', error);
49
+ }
50
+ }
51
+ // Staff are identified by a Firebase Auth custom claim (`staff`), which is not
52
+ // a Firestore query — so the roster comes from paginating the auth users. A
53
+ // burst of ticket activity shouldn't rescan every time, so the (small) result
54
+ // is cached briefly. Fails soft to an empty list.
55
+ let staffUidCache = null;
56
+ const STAFF_CACHE_MS = 60000;
57
+ async function listStaffUids() {
58
+ const now = Date.now();
59
+ if (staffUidCache && now - staffUidCache.at < STAFF_CACHE_MS) {
60
+ return staffUidCache.uids;
61
+ }
62
+ // Across ALL auth pools (AGL-1122). This scanned only the project pool, so
63
+ // a staff member who signs in through enterprise SSO — whose account lives
64
+ // in a GCIP tenant pool — was never in the fan-out. Nothing errored; the
65
+ // notification simply never arrived, which is the worst shape a miss can
66
+ // take on an alerting path.
67
+ const uids = await listStaffUidsAcrossPools();
68
+ staffUidCache = {
69
+ uids,
70
+ at: now
71
+ };
72
+ return uids;
73
+ }
74
+ /**
75
+ * Notifies every staff-claim holder (AGL-850) — the support-desk audience.
76
+ * Staff are not org members, so `notifyOrgAdmins`/`notifyHostManagers` never
77
+ * reach them; this enumerates the `staff` custom claim instead. The docs land
78
+ * at `users/{uid}/notifications`, which the console notifications menu already
79
+ * renders, so no separate staff inbox is needed. Never throws.
80
+ */ export async function notifyStaff(payload) {
81
+ try {
82
+ await notifyUsers(await listStaffUids(), payload);
83
+ } catch (error) {
84
+ console.error('staff notification failed', error);
85
+ }
86
+ }
87
+ /** Notifies the org's owner + admins (billing, membership, org events). */ export async function notifyOrgAdmins(orgId, payload) {
88
+ try {
89
+ const members = await listOrgMembers(orgId);
90
+ const admins = members.filter((member)=>member.role === 'owner' || member.role === 'admin').map((member)=>member.$id);
91
+ await notifyUsers(admins, _extends({}, payload, {
92
+ orgId
93
+ }));
94
+ } catch (error) {
95
+ console.error('org admin notification failed', error);
96
+ }
97
+ }
98
+ /**
99
+ * Notifies everyone who manages a host (admin/editor in the host doc's
100
+ * `memberRoles` projection) — the audience for form submissions and
101
+ * bookings on that site.
102
+ *
103
+ * The OWNING ORG travels with the notification (AGL-1773). Host links are
104
+ * stored in the legacy `/{hostDocId}/rest` shape and rewritten to
105
+ * `/{orgSlug}/hosts/{subdomain}/rest` when they are FOLLOWED
106
+ * (`normalizeNotificationLink`, AGL-644) — a rewrite that needs an org slug.
107
+ * With no `orgId` on the doc the console had nothing to key on and fell back
108
+ * to whichever org the reader happens to have OPEN, so a manager who belongs
109
+ * to more than one workspace was routed to `/{other-org}/hosts/{subdomain}/…`
110
+ * and got a designed 404: `HostGuard` only resolves subdomains belonging to
111
+ * the current org. `notifyOrgAdmins` has always stamped it; the host fan-out
112
+ * — every form submission, booking, order and stock alert — never did.
113
+ *
114
+ * The host doc already carries `orgId` (AGL-233), so this costs no extra
115
+ * read. Spread conditionally: the field is optional on the host model and
116
+ * Firestore rejects `undefined`.
117
+ */ export async function notifyHostManagers(hostId, payload) {
118
+ try {
119
+ var _host_get, _payload_orgId;
120
+ const host = await firestore().collection('hosts').doc(hostId).get();
121
+ const memberRoles = (_host_get = host.get('memberRoles')) != null ? _host_get : {};
122
+ const managers = Object.entries(memberRoles).filter(([, role])=>role === 'admin' || role === 'editor').map(([uid])=>uid);
123
+ const orgId = (_payload_orgId = payload.orgId) != null ? _payload_orgId : host.get('orgId');
124
+ await notifyUsers(managers, _extends({}, payload, {
125
+ hostId
126
+ }, orgId ? {
127
+ orgId
128
+ } : {}));
129
+ } catch (error) {
130
+ console.error('host manager notification failed', error);
131
+ }
132
+ }
133
+
134
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/notifications.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 { notificationMuted, type AglynNotification } from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { listStaffUidsAcrossPools } from './auth-pools'\nimport firebaseAdmin from './firebase-admin'\nimport { listOrgMembers } from './organizations'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nexport type NotificationPayload = Omit<\n AglynNotification,\n '$id' | 'createdAt' | 'readAt'\n>\n\n/**\n * Notification fan-out (AGL-259): batch-writes one doc per recipient at\n * `users/{uid}/notifications`. Never throws — a notification miss must\n * not break the mutation that emitted it.\n */\nexport async function notifyUsers(\n uids: Iterable<string>,\n payload: NotificationPayload,\n): Promise<void> {\n try {\n const db = firestore()\n const targets = [...new Set(uids)].filter(Boolean).slice(0, 400)\n if (!targets.length) return\n // Per-user category mutes (AGL-267): one getAll over the user docs.\n const userDocs = await db.getAll(\n ...targets.map((uid) => db.collection('users').doc(uid)),\n )\n const batch = db.batch()\n let count = 0\n for (const userDoc of userDocs) {\n const prefs = userDoc.get('notificationPrefs') as\n | Record<string, boolean>\n | undefined\n if (notificationMuted(prefs, payload.type)) continue\n batch.set(\n db\n .collection('users')\n .doc(userDoc.id)\n .collection('notifications')\n .doc(),\n { ...payload, createdAt: FieldValue.serverTimestamp() },\n )\n count += 1\n }\n if (count > 0) await batch.commit()\n } catch (error) {\n console.error('notification fan-out failed', error)\n }\n}\n\n// Staff are identified by a Firebase Auth custom claim (`staff`), which is not\n// a Firestore query — so the roster comes from paginating the auth users. A\n// burst of ticket activity shouldn't rescan every time, so the (small) result\n// is cached briefly. Fails soft to an empty list.\nlet staffUidCache: { uids: string[]; at: number } | null = null\nconst STAFF_CACHE_MS = 60_000\n\nasync function listStaffUids(): Promise<string[]> {\n const now = Date.now()\n if (staffUidCache && now - staffUidCache.at < STAFF_CACHE_MS) {\n return staffUidCache.uids\n }\n // Across ALL auth pools (AGL-1122). This scanned only the project pool, so\n // a staff member who signs in through enterprise SSO — whose account lives\n // in a GCIP tenant pool — was never in the fan-out. Nothing errored; the\n // notification simply never arrived, which is the worst shape a miss can\n // take on an alerting path.\n const uids = await listStaffUidsAcrossPools()\n staffUidCache = { uids, at: now }\n return uids\n}\n\n/**\n * Notifies every staff-claim holder (AGL-850) — the support-desk audience.\n * Staff are not org members, so `notifyOrgAdmins`/`notifyHostManagers` never\n * reach them; this enumerates the `staff` custom claim instead. The docs land\n * at `users/{uid}/notifications`, which the console notifications menu already\n * renders, so no separate staff inbox is needed. Never throws.\n */\nexport async function notifyStaff(payload: NotificationPayload): Promise<void> {\n try {\n await notifyUsers(await listStaffUids(), payload)\n } catch (error) {\n console.error('staff notification failed', error)\n }\n}\n\n/** Notifies the org's owner + admins (billing, membership, org events). */\nexport async function notifyOrgAdmins(\n orgId: string,\n payload: NotificationPayload,\n): Promise<void> {\n try {\n const members = await listOrgMembers(orgId)\n const admins = members\n .filter((member) => member.role === 'owner' || member.role === 'admin')\n .map((member) => member.$id)\n await notifyUsers(admins, { ...payload, orgId })\n } catch (error) {\n console.error('org admin notification failed', error)\n }\n}\n\n/**\n * Notifies everyone who manages a host (admin/editor in the host doc's\n * `memberRoles` projection) — the audience for form submissions and\n * bookings on that site.\n *\n * The OWNING ORG travels with the notification (AGL-1773). Host links are\n * stored in the legacy `/{hostDocId}/rest` shape and rewritten to\n * `/{orgSlug}/hosts/{subdomain}/rest` when they are FOLLOWED\n * (`normalizeNotificationLink`, AGL-644) — a rewrite that needs an org slug.\n * With no `orgId` on the doc the console had nothing to key on and fell back\n * to whichever org the reader happens to have OPEN, so a manager who belongs\n * to more than one workspace was routed to `/{other-org}/hosts/{subdomain}/…`\n * and got a designed 404: `HostGuard` only resolves subdomains belonging to\n * the current org. `notifyOrgAdmins` has always stamped it; the host fan-out\n * — every form submission, booking, order and stock alert — never did.\n *\n * The host doc already carries `orgId` (AGL-233), so this costs no extra\n * read. Spread conditionally: the field is optional on the host model and\n * Firestore rejects `undefined`.\n */\nexport async function notifyHostManagers(\n hostId: string,\n payload: NotificationPayload,\n): Promise<void> {\n try {\n const host = await firestore().collection('hosts').doc(hostId).get()\n const memberRoles =\n (host.get('memberRoles') as Record<string, string> | undefined) ?? {}\n const managers = Object.entries(memberRoles)\n .filter(([, role]) => role === 'admin' || role === 'editor')\n .map(([uid]) => uid)\n const orgId = payload.orgId ?? (host.get('orgId') as string | undefined)\n await notifyUsers(managers, {\n ...payload,\n hostId,\n ...(orgId ? { orgId } : {}),\n })\n } catch (error) {\n console.error('host manager notification failed', error)\n }\n}\n"],"names":["notificationMuted","FieldValue","listStaffUidsAcrossPools","firebaseAdmin","listOrgMembers","firestore","app","notifyUsers","uids","payload","db","targets","Set","filter","Boolean","slice","length","userDocs","getAll","map","uid","collection","doc","batch","count","userDoc","prefs","get","type","set","id","createdAt","serverTimestamp","commit","error","console","staffUidCache","STAFF_CACHE_MS","listStaffUids","now","Date","at","notifyStaff","notifyOrgAdmins","orgId","members","admins","member","role","$id","notifyHostManagers","hostId","host","memberRoles","managers","Object","entries"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAgC,sBAAqB;AAC/E,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,wBAAwB,QAAQ,kBAAc;AACvD,OAAOC,mBAAmB,sBAAkB;AAC5C,SAASC,cAAc,QAAQ,qBAAiB;AAEhD,MAAMC,YAAY,IAAMF,cAAcG,GAAG,GAAGD,SAAS;AAOrD;;;;CAIC,GACD,OAAO,eAAeE,YACpBC,IAAsB,EACtBC,OAA4B;IAE5B,IAAI;QACF,MAAMC,KAAKL;QACX,MAAMM,UAAU;eAAI,IAAIC,IAAIJ;SAAM,CAACK,MAAM,CAACC,SAASC,KAAK,CAAC,GAAG;QAC5D,IAAI,CAACJ,QAAQK,MAAM,EAAE;QACrB,oEAAoE;QACpE,MAAMC,WAAW,MAAMP,GAAGQ,MAAM,IAC3BP,QAAQQ,GAAG,CAAC,CAACC,MAAQV,GAAGW,UAAU,CAAC,SAASC,GAAG,CAACF;QAErD,MAAMG,QAAQb,GAAGa,KAAK;QACtB,IAAIC,QAAQ;QACZ,KAAK,MAAMC,WAAWR,SAAU;YAC9B,MAAMS,QAAQD,QAAQE,GAAG,CAAC;YAG1B,IAAI3B,kBAAkB0B,OAAOjB,QAAQmB,IAAI,GAAG;YAC5CL,MAAMM,GAAG,CACPnB,GACGW,UAAU,CAAC,SACXC,GAAG,CAACG,QAAQK,EAAE,EACdT,UAAU,CAAC,iBACXC,GAAG,IACN,aAAKb;gBAASsB,WAAW9B,WAAW+B,eAAe;;YAErDR,SAAS;QACX;QACA,IAAIA,QAAQ,GAAG,MAAMD,MAAMU,MAAM;IACnC,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,+BAA+BA;IAC/C;AACF;AAEA,+EAA+E;AAC/E,4EAA4E;AAC5E,8EAA8E;AAC9E,kDAAkD;AAClD,IAAIE,gBAAuD;AAC3D,MAAMC,iBAAiB;AAEvB,eAAeC;IACb,MAAMC,MAAMC,KAAKD,GAAG;IACpB,IAAIH,iBAAiBG,MAAMH,cAAcK,EAAE,GAAGJ,gBAAgB;QAC5D,OAAOD,cAAc5B,IAAI;IAC3B;IACA,2EAA2E;IAC3E,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,4BAA4B;IAC5B,MAAMA,OAAO,MAAMN;IACnBkC,gBAAgB;QAAE5B;QAAMiC,IAAIF;IAAI;IAChC,OAAO/B;AACT;AAEA;;;;;;CAMC,GACD,OAAO,eAAekC,YAAYjC,OAA4B;IAC5D,IAAI;QACF,MAAMF,YAAY,MAAM+B,iBAAiB7B;IAC3C,EAAE,OAAOyB,OAAO;QACdC,QAAQD,KAAK,CAAC,6BAA6BA;IAC7C;AACF;AAEA,yEAAyE,GACzE,OAAO,eAAeS,gBACpBC,KAAa,EACbnC,OAA4B;IAE5B,IAAI;QACF,MAAMoC,UAAU,MAAMzC,eAAewC;QACrC,MAAME,SAASD,QACZhC,MAAM,CAAC,CAACkC,SAAWA,OAAOC,IAAI,KAAK,WAAWD,OAAOC,IAAI,KAAK,SAC9D7B,GAAG,CAAC,CAAC4B,SAAWA,OAAOE,GAAG;QAC7B,MAAM1C,YAAYuC,QAAQ,aAAKrC;YAASmC;;IAC1C,EAAE,OAAOV,OAAO;QACdC,QAAQD,KAAK,CAAC,iCAAiCA;IACjD;AACF;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,eAAegB,mBACpBC,MAAc,EACd1C,OAA4B;IAE5B,IAAI;YAGC2C,WAIW3C;QANd,MAAM2C,OAAO,MAAM/C,YAAYgB,UAAU,CAAC,SAASC,GAAG,CAAC6B,QAAQxB,GAAG;QAClE,MAAM0B,eACHD,YAAAA,KAAKzB,GAAG,CAAC,0BAATyB,YAAkE,CAAC;QACtE,MAAME,WAAWC,OAAOC,OAAO,CAACH,aAC7BxC,MAAM,CAAC,CAAC,GAAGmC,KAAK,GAAKA,SAAS,WAAWA,SAAS,UAClD7B,GAAG,CAAC,CAAC,CAACC,IAAI,GAAKA;QAClB,MAAMwB,SAAQnC,iBAAAA,QAAQmC,KAAK,YAAbnC,iBAAkB2C,KAAKzB,GAAG,CAAC;QACzC,MAAMpB,YAAY+C,UAAU,aACvB7C;YACH0C;WACIP,QAAQ;YAAEA;QAAM,IAAI,CAAC;IAE7B,EAAE,OAAOV,OAAO;QACdC,QAAQD,KAAK,CAAC,oCAAoCA;IACpD;AACF"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { hasInlineOrgBilling, type OrgBillingDoc, type OrgInlineBilling } from '@aglyn/aglyn/server';
18
+ /** `orgs/{orgId}/billing/stripe`. */
19
+ export declare function orgBillingRef(orgId: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
20
+ /**
21
+ * Reads an org's commercial keys, falling back to the org doc.
22
+ *
23
+ * The fallback is what makes the migration safe to land in pieces: a reader
24
+ * repointed here keeps working against an org that has not been backfilled
25
+ * yet, and keeps working if the backfill missed one. It stops being reachable
26
+ * only when the inline fields are finally deleted from the org doc — which is
27
+ * the LAST step, taken once production has been observed reading from the new
28
+ * location.
29
+ *
30
+ * Without it, the failure mode is silent and expensive: a missing billing doc
31
+ * reads as `undefined`, which every caller downstream interprets as "no
32
+ * subscription", i.e. a paying workspace rendered as Free.
33
+ */
34
+ export declare function readOrgBilling(orgId: string): Promise<OrgBillingDoc>;
35
+ /**
36
+ * Which Stripe MODES this org has a stored customer for (AGL-2486, follow-up).
37
+ *
38
+ * `readOrgBilling` answering `stripeCustomerId: null` has two completely
39
+ * different meanings — "this workspace has never been billed" and "this
40
+ * workspace's customer lives in the other Stripe world and THIS deployment
41
+ * cannot see it" — and every surface downstream rendered them as the same
42
+ * sentence, "No invoices yet." That is the swallowed-query-as-measured-zero
43
+ * shape: nothing looks wrong, and the wrong reading is the reassuring one.
44
+ *
45
+ * The fix is NOT to fall back to the other mode's id — that fallback is the
46
+ * original bug, and it stays deleted. It is to let a caller say which of the
47
+ * two silences it is looking at.
48
+ *
49
+ * Returns BOOLEANS, never ids. A test-mode surface asking this question must
50
+ * not come away holding a live `cus_…`: it would have no legitimate use for
51
+ * one, and putting it in a JSON response would publish a live customer id to
52
+ * every browser on a test deployment.
53
+ */
54
+ export declare function readOrgBillingCustomerModes(orgId: string): Promise<{
55
+ live: boolean;
56
+ test: boolean;
57
+ }>;
58
+ /**
59
+ * Writes the commercial keys to the billing doc, and onto the org doc the
60
+ * entitlement inputs that stay there.
61
+ *
62
+ * Three writes, one batch:
63
+ *
64
+ * 1. the billing doc itself, `merge: true` to match what the org-doc write did;
65
+ * 2. the org doc, merged, with what entitlement resolution reads there:
66
+ * `billingStatus`, so the AGL-275 dunning banner keeps working for
67
+ * non-managers who can no longer read `subscription`; and `seatAddons`,
68
+ * which never moved (`ORG_BILLING_MOVED_KEYS`), whenever the patch carries
69
+ * the add-on quantities;
70
+ * 3. the `stripeCustomers` reverse index, so the webhook can still resolve an
71
+ * org from a Stripe customer id without a query.
72
+ *
73
+ * `writeInline` mirrored the keys back onto the org doc as well. It was on
74
+ * during the migration so a rollback lost nothing, and is now OFF: writing them
75
+ * inline is what the whole issue exists to stop, and leaving it on would let the
76
+ * next Stripe webhook silently re-add the fields minutes after they were
77
+ * deleted, re-opening the exposure with nothing to show for it.
78
+ *
79
+ * The option survives for a deliberate one-off rollback, and nothing passes it.
80
+ *
81
+ * The READ fallback in `readOrgBilling` deliberately stays. It costs one get on
82
+ * a doc that no longer exists, and it is the only thing standing between an org
83
+ * the backfill somehow missed and a paying workspace rendering as Free.
84
+ */
85
+ export declare function writeOrgBilling(orgId: string, patch: Partial<OrgBillingDoc> & Pick<OrgInlineBilling, 'seatAddons'>, options?: {
86
+ writeInline?: boolean;
87
+ }): Promise<void>;
88
+ /**
89
+ * Resolves an org id from a Stripe customer id.
90
+ *
91
+ * Prefers the reverse index; falls back to the legacy `where('stripeCustomerId')`
92
+ * query on `orgs` for customers whose index entry has not been written yet.
93
+ * The fallback disappears with the inline fields.
94
+ */
95
+ export declare function findOrgIdByStripeCustomer(customerId: string): Promise<string | null>;
96
+ /** Re-exported so callers do not need a second import to test migration state. */
97
+ export { hasInlineOrgBilling };