@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,551 @@
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
+ * Cross-pool Firebase Auth lookups (AGL-1122).
18
+ *
19
+ * Enterprise SSO (AGL-1101) signs users into a per-org **GCIP tenant**, and a
20
+ * tenant has its OWN user pool. A project-level `getUserByEmail` /
21
+ * `listUsers` does not see those users at all — measured on aglyn-main:
22
+ * the owner of aglyn-org returned `auth/user-not-found` at project level and
23
+ * was present only under tenant `aglyn-org-y5v14`.
24
+ *
25
+ * That silently broke five separate things: staff could not find, view,
26
+ * disable or grant staff to an SSO user; adding one to an org or a site by
27
+ * email 404'd into "send them an invite" for an account that already exists;
28
+ * and `notifyStaff` skipped SSO staff entirely. Fixing those one route at a
29
+ * time would guarantee the next new route reintroduces the bug, so every
30
+ * caller goes through here instead.
31
+ *
32
+ * Custom claims are per-pool too: `setCustomUserClaims` on the project pool
33
+ * cannot grant staff to a tenant user. `authForPool` hands back the right
34
+ * `BaseAuth` so a mutation lands in the pool the user actually lives in.
35
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
36
+ import firebaseAdmin from "./firebase-admin.js";
37
+ const auth = ()=>firebaseAdmin.app().auth();
38
+ /**
39
+ * Does this record show that a human ever authenticated as it? (AGL-2005)
40
+ *
41
+ * An address or a provider entry is the evidence. `signInWithCustomToken`
42
+ * mints an account out of nothing when the uid is absent from the pool the
43
+ * token was minted in, and the record it manufactures has neither: measured
44
+ * on production, a project-pool twin of a tenant uid had `email: null` and
45
+ * `providerData: []` while the real SSO record carried both an address and
46
+ * `saml.aglyn-workspace`.
47
+ *
48
+ * `displayName` and custom claims deliberately do NOT count. Both can be
49
+ * written onto any record after the fact — `updateProfile` and `grantStaff`
50
+ * in the staff console do exactly that — so a shadow that a staff action had
51
+ * already landed on would start qualifying as an identity and go on winning
52
+ * the lookups it should be losing. The bar has to be a signal the forgery
53
+ * cannot acquire by being acted upon.
54
+ */ export function isIdentifiedUserRecord(record) {
55
+ var _ref;
56
+ var _record_providerData;
57
+ if (!record) return false;
58
+ return Boolean(record.email) || ((_ref = (_record_providerData = record.providerData) == null ? void 0 : _record_providerData.length) != null ? _ref : 0) > 0;
59
+ }
60
+ /**
61
+ * How strongly a record identifies its human, for choosing between two that
62
+ * share a uid (AGL-2005). Ordering only — the numbers have no other meaning.
63
+ *
64
+ * Email and provider data lead because they are what `isIdentifiedUserRecord`
65
+ * trusts; the rest break ties between records that both qualify.
66
+ */ export function identityStrength(record) {
67
+ var _ref, _record_customClaims;
68
+ var _record_providerData;
69
+ if (!record) return 0;
70
+ let score = 0;
71
+ if (record.email) score += 8;
72
+ if (((_ref = (_record_providerData = record.providerData) == null ? void 0 : _record_providerData.length) != null ? _ref : 0) > 0) score += 4;
73
+ if (record.phoneNumber) score += 2;
74
+ if (record.displayName) score += 1;
75
+ if (Object.keys((_record_customClaims = record.customClaims) != null ? _record_customClaims : {}).length > 0) score += 1;
76
+ return score;
77
+ }
78
+ /**
79
+ * Collapse rows sharing a uid to ONE row apiece (AGL-2005).
80
+ *
81
+ * **decided:** "We still have two users list in this list with the
82
+ * same uid but one without an email attached, this needs fixed we should only
83
+ * see one user, even if they are sso."
84
+ *
85
+ * AGL-1962 deliberately refused to merge, on the grounds that collapsing two
86
+ * accounts could hide an identity split. That reasoning still stands for two
87
+ * DISTINCT accounts — which is why this keys on **uid** and never on email.
88
+ * Two people who happen to share an address are two accounts with two uids
89
+ * and stay two rows; merging them on email would delete a real user from the
90
+ * staff console, a worse bug than the one being fixed and an invisible one.
91
+ *
92
+ * A repeated uid is different in kind: a uid is unique WITHIN a pool, so the
93
+ * same uid in two pools is not two people, it is one person plus an artifact
94
+ * of a cross-pool custom-token mint. Those merge.
95
+ *
96
+ * Two rules make the merge safe:
97
+ *
98
+ * - **The identified record wins.** Ranked by `identityStrength`, so an
99
+ * emailless, providerless artifact can never be the row staff see — the
100
+ * exact inversion that let the shadow win every uid lookup.
101
+ * - **The collision is not discarded.** `markCrossPoolUidCollisions` runs
102
+ * here rather than being left to the caller, so the surviving row always
103
+ * carries `uidAlsoInPools` naming the pools that were folded into it. One
104
+ * row, and it still says it was two.
105
+ *
106
+ * The survivor takes the FIRST occurrence's position so list ordering does
107
+ * not shuffle, and ties keep the earlier row so the result is deterministic.
108
+ */ export function collapseCrossPoolUidRows(users) {
109
+ // Marked here, not by the caller. A caller that forgot would silently turn
110
+ // this into the cover-up the merge is explicitly not allowed to be.
111
+ const marked = markCrossPoolUidCollisions(users);
112
+ const winnerByUid = new Map();
113
+ for (const user of marked){
114
+ const incumbent = winnerByUid.get(user.record.uid);
115
+ if (!incumbent || identityStrength(user.record) > identityStrength(incumbent.record)) {
116
+ winnerByUid.set(user.record.uid, user);
117
+ }
118
+ }
119
+ const emitted = new Set();
120
+ const collapsed = [];
121
+ for (const user of marked){
122
+ var _winnerByUid_get;
123
+ const uid = user.record.uid;
124
+ if (emitted.has(uid)) continue;
125
+ emitted.add(uid);
126
+ collapsed.push((_winnerByUid_get = winnerByUid.get(uid)) != null ? _winnerByUid_get : user);
127
+ }
128
+ return collapsed;
129
+ }
130
+ /**
131
+ * Flag every uid that appears in more than one pool, in place of hiding it.
132
+ *
133
+ * Pure and total, so a spec can drive it without Firebase. Callers must pass
134
+ * the rows they intend to show together — a collision it cannot see is one
135
+ * it cannot report.
136
+ */ export function markCrossPoolUidCollisions(users) {
137
+ const poolsByUid = new Map();
138
+ for (const user of users){
139
+ var _poolsByUid_get;
140
+ const pools = (_poolsByUid_get = poolsByUid.get(user.record.uid)) != null ? _poolsByUid_get : [];
141
+ pools.push(user.tenantId);
142
+ poolsByUid.set(user.record.uid, pools);
143
+ }
144
+ return users.map((user)=>{
145
+ var _poolsByUid_get;
146
+ const pools = (_poolsByUid_get = poolsByUid.get(user.record.uid)) != null ? _poolsByUid_get : [];
147
+ if (pools.length < 2) return user;
148
+ // Everyone holding this uid EXCEPT this row's own pool. Compared by
149
+ // index, not by value: two rows from the same pool would be a Firebase
150
+ // impossibility, but filtering by value would silently drop a real one.
151
+ const others = pools.filter((_, index)=>index !== pools.indexOf(user.tenantId));
152
+ return _extends({}, user, {
153
+ uidAlsoInPools: others
154
+ });
155
+ });
156
+ }
157
+ /**
158
+ * Every GCIP tenant on the project. Cached briefly: this is asked once per
159
+ * lookup and the set changes only when an enterprise org is onboarded.
160
+ * Fails soft to an empty list — a tenant-listing outage must degrade to
161
+ * today's project-only behaviour rather than break sign-in-adjacent routes.
162
+ */ let tenantCache = null;
163
+ const TENANT_CACHE_MS = 60000;
164
+ export async function listAuthTenantIds() {
165
+ const now = Date.now();
166
+ if (tenantCache && now - tenantCache.at < TENANT_CACHE_MS) {
167
+ return tenantCache.ids;
168
+ }
169
+ try {
170
+ const ids = [];
171
+ let pageToken;
172
+ do {
173
+ const page = await auth().tenantManager().listTenants(100, pageToken);
174
+ ids.push(...page.tenants.map((tenant)=>tenant.tenantId));
175
+ pageToken = page.pageToken;
176
+ }while (pageToken)
177
+ tenantCache = {
178
+ ids,
179
+ at: now
180
+ };
181
+ return ids;
182
+ } catch (error) {
183
+ var _ref;
184
+ console.error('tenant listing failed; falling back to project pool', error);
185
+ return (_ref = tenantCache == null ? void 0 : tenantCache.ids) != null ? _ref : [];
186
+ }
187
+ }
188
+ /** Test seam — drops the tenant cache so a spec can vary the tenant set. */ export function resetAuthTenantCache() {
189
+ tenantCache = null;
190
+ }
191
+ /** The `BaseAuth` for a pool: the project pool for null, else the tenant's. */ export function authForPool(tenantId) {
192
+ return tenantId ? auth().tenantManager().authForTenant(tenantId) : auth();
193
+ }
194
+ /**
195
+ * Find a user by email across the project pool and every GCIP tenant.
196
+ *
197
+ * The project pool is tried first — it holds every non-SSO user, which is
198
+ * almost all of them, so the common case stays a single call. Returns null
199
+ * when no pool has the address, which is the only honest answer for "this
200
+ * person has no Aglyn account".
201
+ */ export async function findUserByEmailAcrossPools(email) {
202
+ const address = email.trim().toLowerCase();
203
+ if (!address) return null;
204
+ try {
205
+ return {
206
+ record: await auth().getUserByEmail(address),
207
+ tenantId: null
208
+ };
209
+ } catch (unused) {
210
+ // Not in the project pool — fall through to the tenants.
211
+ }
212
+ for (const tenantId of (await listAuthTenantIds())){
213
+ try {
214
+ const record = await authForPool(tenantId).getUserByEmail(address);
215
+ return {
216
+ record,
217
+ tenantId
218
+ };
219
+ } catch (unused) {
220
+ // Not in this tenant; keep looking.
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ /**
226
+ * Find a user by uid across the project pool and every GCIP tenant. Same
227
+ * shape as the email lookup — a uid is only unique WITHIN a pool, so the
228
+ * caller needs to know which pool answered before it mutates anything.
229
+ *
230
+ * **First pool to answer is NOT good enough** (AGL-2005). This used to return
231
+ * the project pool's record the moment it existed, which is how a forged
232
+ * emailless twin won every uid lookup for a real SSO user: `grantStaff`,
233
+ * `disable`, `setRole`, `updateProfile`, `revokeRefreshTokens`, impersonate,
234
+ * usage-email, resolve-people, org-lockdown and erasure all landed on a
235
+ * record nobody can sign in as. Two of those were caught having happened —
236
+ * a `revokeRefreshTokens` dated 2026-08-14 sat on the twin while the real
237
+ * account's `tokensValidAfterTime` never moved, so "sign out everywhere" did
238
+ * nothing at all.
239
+ *
240
+ * So a pool's answer is accepted only when the record it returns actually
241
+ * identifies someone, and the winner is chosen by the SAME rule the staff
242
+ * list collapses rows with. That equivalence is the point: the row staff
243
+ * click is the record the action mutates, by construction rather than by
244
+ * coincidence.
245
+ *
246
+ * The fast path survives intact — a project account with an address or a
247
+ * provider is returned without any tenant call, which is nearly every
248
+ * account. Only a record carrying no identity at all pays for the sweep.
249
+ */ export async function findUserByUidAcrossPools(uid) {
250
+ var _collapseCrossPoolUidRows_;
251
+ if (!uid) return null;
252
+ const matches = [];
253
+ try {
254
+ const record = await auth().getUser(uid);
255
+ if (isIdentifiedUserRecord(record)) return {
256
+ record,
257
+ tenantId: null
258
+ };
259
+ // Held, not returned. It is still the answer if nothing better exists —
260
+ // an anonymous or half-created account legitimately looks like this, and
261
+ // refusing it here would break every caller that handles those.
262
+ matches.push({
263
+ record,
264
+ tenantId: null
265
+ });
266
+ } catch (unused) {
267
+ // Not in the project pool.
268
+ }
269
+ for (const tenantId of (await listAuthTenantIds())){
270
+ try {
271
+ const record = await authForPool(tenantId).getUser(uid);
272
+ matches.push({
273
+ record,
274
+ tenantId
275
+ });
276
+ // Stop at the first pool that knows who this is. Scanning the rest
277
+ // would cost a call per enterprise tenant on a path that runs during
278
+ // sign-in and billing, and a uid identified in two tenant pools is not
279
+ // a shape Firebase can produce — tenant uids are minted per pool.
280
+ if (isIdentifiedUserRecord(record)) break;
281
+ } catch (unused) {
282
+ // Keep looking.
283
+ }
284
+ }
285
+ if (!matches.length) return null;
286
+ return (_collapseCrossPoolUidRows_ = collapseCrossPoolUidRows(matches)[0]) != null ? _collapseCrossPoolUidRows_ : null;
287
+ }
288
+ /** How many tenant users a single page will carry before reporting a cut. */ const TENANT_USER_CAP = 500;
289
+ /**
290
+ * How many pools may have a call in flight at once.
291
+ *
292
+ * Every sweep below asks each GCIP tenant pool the same question, and the
293
+ * pools are independent: one tenant's answer never informs another's. Asked in
294
+ * series that costs `N` round trips for `N` enterprise customers, so the staff
295
+ * list gets slower every time one is signed — the wrong direction for the
296
+ * feature that adds them. Asked together it costs one.
297
+ *
298
+ * The number of CALLS is identical either way, so this buys latency and the
299
+ * Cloud Run seconds spent waiting on it, not Auth quota. That is also why it
300
+ * is bounded rather than an unbounded `Promise.all`: Identity Platform rate
301
+ * limits `listUsers` per project, and firing one call per tenant simultaneously
302
+ * would convert a latency problem into a `RESOURCE_EXHAUSTED` one at exactly
303
+ * the customer count where the latency stopped mattering.
304
+ */ const POOL_FETCH_CONCURRENCY = 8;
305
+ /**
306
+ * Run `work` over every pool id, at most {@link POOL_FETCH_CONCURRENCY} at a
307
+ * time, and return the results **in the order the ids were given**.
308
+ *
309
+ * Order is not cosmetic here. Callers concatenate these results into a user
310
+ * list and then trim it to a cap, so a result order that varied with which
311
+ * pool answered first would make the rows a staff member sees depend on
312
+ * network timing — the same query returning different people on a refresh.
313
+ */ async function mapPoolsConcurrently(tenantIds, work) {
314
+ const results = new Array(tenantIds.length);
315
+ let next = 0;
316
+ const workers = Array.from({
317
+ length: Math.min(POOL_FETCH_CONCURRENCY, tenantIds.length)
318
+ }, async ()=>{
319
+ for(;;){
320
+ const index = next++;
321
+ if (index >= tenantIds.length) return;
322
+ results[index] = await work(tenantIds[index]);
323
+ }
324
+ });
325
+ await Promise.all(workers);
326
+ return results;
327
+ }
328
+ /**
329
+ * One page of users across every pool: the project pool paginates as before,
330
+ * and tenant users are appended on the LAST page so they can never be lost
331
+ * between cursors. Callers render `tenantId` so an SSO account is
332
+ * recognisable — it is not editable the same way a project user is.
333
+ */ export async function listUsersAcrossPools(pageSize, pageToken) {
334
+ const page = await auth().listUsers(pageSize, pageToken);
335
+ const users = page.users.map((record)=>({
336
+ record,
337
+ tenantId: null
338
+ }));
339
+ if (page.pageToken) {
340
+ return {
341
+ users,
342
+ nextPageToken: page.pageToken,
343
+ tenantsIncluded: false,
344
+ tenantTruncated: []
345
+ };
346
+ }
347
+ const tenantTruncated = [];
348
+ // Every tenant asks for the same fixed page, so no pool's request depends on
349
+ // another's answer and they can all be in flight at once. A failed pool
350
+ // contributes no rows and does not stop the others: one enterprise tenant
351
+ // being unreachable must not blank the whole staff list.
352
+ //
353
+ // The id list is read ONCE and reused. It is cached on a 60s TTL, so asking
354
+ // twice can hand back two different lists, and pairing a page from one list
355
+ // with an id from the other would label a user with the wrong tenant.
356
+ const tenantIds = await listAuthTenantIds();
357
+ const tenantPages = await mapPoolsConcurrently(tenantIds, async (tenantId)=>{
358
+ try {
359
+ return await authForPool(tenantId).listUsers(TENANT_USER_CAP);
360
+ } catch (error) {
361
+ console.error(`listing users for tenant ${tenantId} failed`, error);
362
+ return null;
363
+ }
364
+ });
365
+ tenantPages.forEach((tenantPage, index)=>{
366
+ if (!tenantPage) return;
367
+ const tenantId = tenantIds[index];
368
+ users.push(...tenantPage.users.map((record)=>({
369
+ record,
370
+ tenantId
371
+ })));
372
+ if (tenantPage.pageToken) tenantTruncated.push(tenantId);
373
+ });
374
+ return {
375
+ // Marked only here, where every pool's rows are in hand (AGL-1962). The
376
+ // earlier return above carries project rows alone, and a uid cannot
377
+ // collide with itself inside one pool, so there is nothing to find there.
378
+ users: markCrossPoolUidCollisions(users),
379
+ nextPageToken: null,
380
+ tenantsIncluded: true,
381
+ tenantTruncated
382
+ };
383
+ }
384
+ /**
385
+ * Every account across every pool, up to a cap — the only way to FILTER them.
386
+ *
387
+ * Firebase Auth's `listUsers` takes a page size and a cursor and nothing else:
388
+ * no predicate, no ordering, no search. So a staff filter on anything but an
389
+ * exact email, uid or phone number (which have their own O(1) lookups) can
390
+ * only be answered by reading the accounts and matching them.
391
+ *
392
+ * That is an expensive read, so it happens on an ASK and never on a mount —
393
+ * the route calls this only when a request carries a filter, and pages the
394
+ * cheap way otherwise.
395
+ *
396
+ * `truncated` is true when the cap was reached, and it must be shown rather
397
+ * than swallowed: a staff list that searched part of the directory and said
398
+ * "no matches" is worse than one that admits where it stopped.
399
+ */ export async function scanUsersAcrossPools(cap) {
400
+ const users = [];
401
+ let truncated = false;
402
+ let pageToken;
403
+ do {
404
+ const remaining = cap - users.length;
405
+ if (remaining <= 0) {
406
+ truncated = true;
407
+ break;
408
+ }
409
+ // `listUsers` caps a page at 1000 whatever it is asked for.
410
+ const page = await auth().listUsers(Math.min(remaining, 1000), pageToken);
411
+ users.push(...page.users.map((record)=>({
412
+ record,
413
+ tenantId: null
414
+ })));
415
+ pageToken = page.pageToken;
416
+ if (pageToken && users.length >= cap) truncated = true;
417
+ }while (pageToken && users.length < cap)
418
+ const tenantTruncated = [];
419
+ const tenantIds = await listAuthTenantIds();
420
+ /*
421
+ * The remaining budget is fixed BEFORE any pool is asked.
422
+ *
423
+ * Sizing each pool's page from the running total is what forced these calls
424
+ * into a series — pool `k`'s page size could not be known until pool `k-1`
425
+ * had answered. Nothing needs that: a page sized to the whole remaining
426
+ * budget is always at least as large as the shrinking one, so the first
427
+ * `cap` rows in tenant order are the same users either way, and the pools
428
+ * can be asked together.
429
+ *
430
+ * The trade is bandwidth, not quota: `listUsers` is billed per CALL, and the
431
+ * call count is unchanged. Only when the cap actually binds does this carry
432
+ * rows that are then trimmed.
433
+ */ const budget = cap - users.length;
434
+ if (budget <= 0) {
435
+ truncated = true;
436
+ } else {
437
+ const pageSize = Math.min(budget, TENANT_USER_CAP);
438
+ const pages = await mapPoolsConcurrently(tenantIds, async (tenantId)=>{
439
+ try {
440
+ return await authForPool(tenantId).listUsers(pageSize);
441
+ } catch (error) {
442
+ console.error(`listing users for tenant ${tenantId} failed`, error);
443
+ return null;
444
+ }
445
+ });
446
+ pages.forEach((page, index)=>{
447
+ if (!page) return;
448
+ const tenantId = tenantIds[index];
449
+ const room = cap - users.length;
450
+ const rows = page.users.map((record)=>({
451
+ record,
452
+ tenantId
453
+ }));
454
+ // A pool whose rows will not fit is still a pool that was cut short, and
455
+ // it has to say so. A scan that silently dropped one would answer "no
456
+ // such account" for everyone in it — the exact failure this module
457
+ // exists to prevent, wearing the shape of a complete result.
458
+ if (rows.length > room) {
459
+ truncated = true;
460
+ tenantTruncated.push(tenantId);
461
+ } else if (page.pageToken) {
462
+ tenantTruncated.push(tenantId);
463
+ }
464
+ if (room > 0) users.push(...rows.slice(0, room));
465
+ });
466
+ }
467
+ return {
468
+ users: markCrossPoolUidCollisions(users),
469
+ truncated,
470
+ tenantTruncated
471
+ };
472
+ }
473
+ /**
474
+ * Write custom claims to the pool the uid ACTUALLY lives in (AGL-1993).
475
+ *
476
+ * The hazard this exists to remove: `setCustomUserClaims` is per-pool, and a
477
+ * grant aimed at the wrong pool does not error — on the project pool it
478
+ * either throws `user-not-found` (loud, fine) or, when a phantom shadow record
479
+ * shares the uid, silently succeeds against the WRONG record. That is not
480
+ * hypothetical: on 2026-08-18 a staff account's uid existed in both the
481
+ * project pool and `aglyn-org-y5v14` at once. That phantom won every lookup
482
+ * until AGL-2005 made {@link findUserByUidAcrossPools} rank an IDENTIFIED
483
+ * record above an unidentified one rather than taking the first pool to
484
+ * answer.
485
+ *
486
+ * So resolution and mutation are bound together here rather than left to each
487
+ * caller to pair correctly, and the resolved pool is RETURNED so the audit row
488
+ * can record which pool was written — "granted staff to uid X" is not a
489
+ * complete record when a uid can name two accounts.
490
+ *
491
+ * Never assumes a pool. Staff may live in the project pool, in Aglyn's own
492
+ * tenant, or in a CUSTOMER org's tenant, and hard-coding any of them would
493
+ * silently fail the moment a third exists.
494
+ */ export async function setClaimsInOwningPool(uid, claims) {
495
+ var _found_record_email;
496
+ const found = await findUserByUidAcrossPools(uid);
497
+ if (!found) return null;
498
+ await authForPool(found.tenantId).setCustomUserClaims(uid, claims);
499
+ return {
500
+ uid,
501
+ tenantId: found.tenantId,
502
+ email: (_found_record_email = found.record.email) != null ? _found_record_email : null,
503
+ claims
504
+ };
505
+ }
506
+ /**
507
+ * Every uid carrying the `staff` claim, across all pools. Staff is a custom
508
+ * claim, which is not queryable, so this scans — the caller caches it.
509
+ */ export async function listStaffUidsAcrossPools() {
510
+ /*
511
+ * Paging WITHIN a pool stays serial — each page's cursor comes out of the
512
+ * one before it, so there is nothing to overlap. Across pools there is: the
513
+ * pools are independent, and a staff notification waiting on them one after
514
+ * another gets slower with every enterprise customer signed.
515
+ *
516
+ * Each scan collects into its own array rather than a shared one, so the
517
+ * result is ordered by POOL rather than by which pool happened to answer
518
+ * first. A caller diffing this list against a stored copy would otherwise
519
+ * see phantom changes on every run.
520
+ */ const scan = async (pool)=>{
521
+ const found = [];
522
+ let pageToken;
523
+ do {
524
+ const page = await pool.listUsers(1000, pageToken);
525
+ for (const record of page.users){
526
+ var _record_customClaims;
527
+ if ((_record_customClaims = record.customClaims) == null ? void 0 : _record_customClaims['staff']) found.push(record.uid);
528
+ }
529
+ pageToken = page.pageToken;
530
+ }while (pageToken)
531
+ return found;
532
+ };
533
+ const tenantIds = await listAuthTenantIds();
534
+ const [projectUids, tenantUidLists] = await Promise.all([
535
+ scan(auth()),
536
+ mapPoolsConcurrently(tenantIds, async (tenantId)=>{
537
+ try {
538
+ return await scan(authForPool(tenantId));
539
+ } catch (error) {
540
+ console.error(`staff scan for tenant ${tenantId} failed`, error);
541
+ return [];
542
+ }
543
+ })
544
+ ]);
545
+ return [
546
+ ...projectUids,
547
+ ...tenantUidLists.flat()
548
+ ];
549
+ }
550
+
551
+ //# sourceMappingURL=auth-pools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/auth-pools.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Cross-pool Firebase Auth lookups (AGL-1122).\n *\n * Enterprise SSO (AGL-1101) signs users into a per-org **GCIP tenant**, and a\n * tenant has its OWN user pool. A project-level `getUserByEmail` /\n * `listUsers` does not see those users at all — measured on aglyn-main:\n * the owner of aglyn-org returned `auth/user-not-found` at project level and\n * was present only under tenant `aglyn-org-y5v14`.\n *\n * That silently broke five separate things: staff could not find, view,\n * disable or grant staff to an SSO user; adding one to an org or a site by\n * email 404'd into \"send them an invite\" for an account that already exists;\n * and `notifyStaff` skipped SSO staff entirely. Fixing those one route at a\n * time would guarantee the next new route reintroduces the bug, so every\n * caller goes through here instead.\n *\n * Custom claims are per-pool too: `setCustomUserClaims` on the project pool\n * cannot grant staff to a tenant user. `authForPool` hands back the right\n * `BaseAuth` so a mutation lands in the pool the user actually lives in.\n */\n\nimport type { BaseAuth, UserRecord } from 'firebase-admin/auth'\nimport firebaseAdmin from './firebase-admin'\n\nconst auth = () => firebaseAdmin.app().auth()\n\n/** A user plus the pool they were found in (`null` = the project pool). */\nexport interface PooledUserRecord {\n record: UserRecord\n /** GCIP tenant id, or null when the user is in the project-level pool. */\n tenantId: string | null\n /**\n * Other pools that ALSO hold this uid (AGL-1962). Normally absent: a uid is\n * unique within a pool, and the pools are meant to hold different people.\n *\n * It is populated when the same uid turns up in more than one pool, which\n * means something minted a custom token for one pool's uid against another\n * pool — `signInWithCustomToken` CREATES the account when the uid is absent,\n * so the mistake manufactures an empty shadow account rather than failing.\n * Measured on production: `QQ7fixtureUid0000000000000001` existed in both the\n * project pool and `aglyn-org-y5v14`, the project copy created by\n * `/api/presence/token` minting an SSO user's tenant uid at project level.\n *\n * This is the marker, not a deletion: `listUsersAcrossPools` still returns\n * every record, and `collapseCrossPoolUidRows` — which is what a display\n * uses — keeps this field on the row it keeps, so a collision is still\n * legible after the rows are merged (AGL-2005).\n */\n uidAlsoInPools?: (string | null)[]\n}\n\n/**\n * Does this record show that a human ever authenticated as it? (AGL-2005)\n *\n * An address or a provider entry is the evidence. `signInWithCustomToken`\n * mints an account out of nothing when the uid is absent from the pool the\n * token was minted in, and the record it manufactures has neither: measured\n * on production, a project-pool twin of a tenant uid had `email: null` and\n * `providerData: []` while the real SSO record carried both an address and\n * `saml.aglyn-workspace`.\n *\n * `displayName` and custom claims deliberately do NOT count. Both can be\n * written onto any record after the fact — `updateProfile` and `grantStaff`\n * in the staff console do exactly that — so a shadow that a staff action had\n * already landed on would start qualifying as an identity and go on winning\n * the lookups it should be losing. The bar has to be a signal the forgery\n * cannot acquire by being acted upon.\n */\nexport function isIdentifiedUserRecord(record: UserRecord): boolean {\n if (!record) return false\n return Boolean(record.email) || (record.providerData?.length ?? 0) > 0\n}\n\n/**\n * How strongly a record identifies its human, for choosing between two that\n * share a uid (AGL-2005). Ordering only — the numbers have no other meaning.\n *\n * Email and provider data lead because they are what `isIdentifiedUserRecord`\n * trusts; the rest break ties between records that both qualify.\n */\nexport function identityStrength(record: UserRecord): number {\n if (!record) return 0\n let score = 0\n if (record.email) score += 8\n if ((record.providerData?.length ?? 0) > 0) score += 4\n if (record.phoneNumber) score += 2\n if (record.displayName) score += 1\n if (Object.keys(record.customClaims ?? {}).length > 0) score += 1\n return score\n}\n\n/**\n * Collapse rows sharing a uid to ONE row apiece (AGL-2005).\n *\n * **decided:** \"We still have two users list in this list with the\n * same uid but one without an email attached, this needs fixed we should only\n * see one user, even if they are sso.\"\n *\n * AGL-1962 deliberately refused to merge, on the grounds that collapsing two\n * accounts could hide an identity split. That reasoning still stands for two\n * DISTINCT accounts — which is why this keys on **uid** and never on email.\n * Two people who happen to share an address are two accounts with two uids\n * and stay two rows; merging them on email would delete a real user from the\n * staff console, a worse bug than the one being fixed and an invisible one.\n *\n * A repeated uid is different in kind: a uid is unique WITHIN a pool, so the\n * same uid in two pools is not two people, it is one person plus an artifact\n * of a cross-pool custom-token mint. Those merge.\n *\n * Two rules make the merge safe:\n *\n * - **The identified record wins.** Ranked by `identityStrength`, so an\n * emailless, providerless artifact can never be the row staff see — the\n * exact inversion that let the shadow win every uid lookup.\n * - **The collision is not discarded.** `markCrossPoolUidCollisions` runs\n * here rather than being left to the caller, so the surviving row always\n * carries `uidAlsoInPools` naming the pools that were folded into it. One\n * row, and it still says it was two.\n *\n * The survivor takes the FIRST occurrence's position so list ordering does\n * not shuffle, and ties keep the earlier row so the result is deterministic.\n */\nexport function collapseCrossPoolUidRows(\n users: PooledUserRecord[],\n): PooledUserRecord[] {\n // Marked here, not by the caller. A caller that forgot would silently turn\n // this into the cover-up the merge is explicitly not allowed to be.\n const marked = markCrossPoolUidCollisions(users)\n const winnerByUid = new Map<string, PooledUserRecord>()\n for (const user of marked) {\n const incumbent = winnerByUid.get(user.record.uid)\n if (\n !incumbent ||\n identityStrength(user.record) > identityStrength(incumbent.record)\n ) {\n winnerByUid.set(user.record.uid, user)\n }\n }\n const emitted = new Set<string>()\n const collapsed: PooledUserRecord[] = []\n for (const user of marked) {\n const uid = user.record.uid\n if (emitted.has(uid)) continue\n emitted.add(uid)\n collapsed.push(winnerByUid.get(uid) ?? user)\n }\n return collapsed\n}\n\n/**\n * Flag every uid that appears in more than one pool, in place of hiding it.\n *\n * Pure and total, so a spec can drive it without Firebase. Callers must pass\n * the rows they intend to show together — a collision it cannot see is one\n * it cannot report.\n */\nexport function markCrossPoolUidCollisions(\n users: PooledUserRecord[],\n): PooledUserRecord[] {\n const poolsByUid = new Map<string, (string | null)[]>()\n for (const user of users) {\n const pools = poolsByUid.get(user.record.uid) ?? []\n pools.push(user.tenantId)\n poolsByUid.set(user.record.uid, pools)\n }\n return users.map((user) => {\n const pools = poolsByUid.get(user.record.uid) ?? []\n if (pools.length < 2) return user\n // Everyone holding this uid EXCEPT this row's own pool. Compared by\n // index, not by value: two rows from the same pool would be a Firebase\n // impossibility, but filtering by value would silently drop a real one.\n const others = pools.filter((_, index) => index !== pools.indexOf(user.tenantId))\n return { ...user, uidAlsoInPools: others }\n })\n}\n\n/**\n * Every GCIP tenant on the project. Cached briefly: this is asked once per\n * lookup and the set changes only when an enterprise org is onboarded.\n * Fails soft to an empty list — a tenant-listing outage must degrade to\n * today's project-only behaviour rather than break sign-in-adjacent routes.\n */\nlet tenantCache: { ids: string[]; at: number } | null = null\nconst TENANT_CACHE_MS = 60_000\n\nexport async function listAuthTenantIds(): Promise<string[]> {\n const now = Date.now()\n if (tenantCache && now - tenantCache.at < TENANT_CACHE_MS) {\n return tenantCache.ids\n }\n try {\n const ids: string[] = []\n let pageToken: string | undefined\n do {\n const page = await auth().tenantManager().listTenants(100, pageToken)\n ids.push(...page.tenants.map((tenant) => tenant.tenantId))\n pageToken = page.pageToken\n } while (pageToken)\n tenantCache = { ids, at: now }\n return ids\n } catch (error) {\n console.error('tenant listing failed; falling back to project pool', error)\n return tenantCache?.ids ?? []\n }\n}\n\n/** Test seam — drops the tenant cache so a spec can vary the tenant set. */\nexport function resetAuthTenantCache(): void {\n tenantCache = null\n}\n\n/** The `BaseAuth` for a pool: the project pool for null, else the tenant's. */\nexport function authForPool(tenantId: string | null | undefined): BaseAuth {\n return tenantId\n ? auth().tenantManager().authForTenant(tenantId)\n : auth()\n}\n\n/**\n * Find a user by email across the project pool and every GCIP tenant.\n *\n * The project pool is tried first — it holds every non-SSO user, which is\n * almost all of them, so the common case stays a single call. Returns null\n * when no pool has the address, which is the only honest answer for \"this\n * person has no Aglyn account\".\n */\nexport async function findUserByEmailAcrossPools(\n email: string,\n): Promise<PooledUserRecord | null> {\n const address = email.trim().toLowerCase()\n if (!address) return null\n try {\n return { record: await auth().getUserByEmail(address), tenantId: null }\n } catch {\n // Not in the project pool — fall through to the tenants.\n }\n for (const tenantId of await listAuthTenantIds()) {\n try {\n const record = await authForPool(tenantId).getUserByEmail(address)\n return { record, tenantId }\n } catch {\n // Not in this tenant; keep looking.\n }\n }\n return null\n}\n\n/**\n * Find a user by uid across the project pool and every GCIP tenant. Same\n * shape as the email lookup — a uid is only unique WITHIN a pool, so the\n * caller needs to know which pool answered before it mutates anything.\n *\n * **First pool to answer is NOT good enough** (AGL-2005). This used to return\n * the project pool's record the moment it existed, which is how a forged\n * emailless twin won every uid lookup for a real SSO user: `grantStaff`,\n * `disable`, `setRole`, `updateProfile`, `revokeRefreshTokens`, impersonate,\n * usage-email, resolve-people, org-lockdown and erasure all landed on a\n * record nobody can sign in as. Two of those were caught having happened —\n * a `revokeRefreshTokens` dated 2026-08-14 sat on the twin while the real\n * account's `tokensValidAfterTime` never moved, so \"sign out everywhere\" did\n * nothing at all.\n *\n * So a pool's answer is accepted only when the record it returns actually\n * identifies someone, and the winner is chosen by the SAME rule the staff\n * list collapses rows with. That equivalence is the point: the row staff\n * click is the record the action mutates, by construction rather than by\n * coincidence.\n *\n * The fast path survives intact — a project account with an address or a\n * provider is returned without any tenant call, which is nearly every\n * account. Only a record carrying no identity at all pays for the sweep.\n */\nexport async function findUserByUidAcrossPools(\n uid: string,\n): Promise<PooledUserRecord | null> {\n if (!uid) return null\n const matches: PooledUserRecord[] = []\n try {\n const record = await auth().getUser(uid)\n if (isIdentifiedUserRecord(record)) return { record, tenantId: null }\n // Held, not returned. It is still the answer if nothing better exists —\n // an anonymous or half-created account legitimately looks like this, and\n // refusing it here would break every caller that handles those.\n matches.push({ record, tenantId: null })\n } catch {\n // Not in the project pool.\n }\n for (const tenantId of await listAuthTenantIds()) {\n try {\n const record = await authForPool(tenantId).getUser(uid)\n matches.push({ record, tenantId })\n // Stop at the first pool that knows who this is. Scanning the rest\n // would cost a call per enterprise tenant on a path that runs during\n // sign-in and billing, and a uid identified in two tenant pools is not\n // a shape Firebase can produce — tenant uids are minted per pool.\n if (isIdentifiedUserRecord(record)) break\n } catch {\n // Keep looking.\n }\n }\n if (!matches.length) return null\n return collapseCrossPoolUidRows(matches)[0] ?? null\n}\n\nexport interface PooledUserPage {\n users: PooledUserRecord[]\n /** Project-pool cursor; null when the project pool is exhausted. */\n nextPageToken: string | null\n /**\n * True once the project pool is exhausted and the tenant users have been\n * appended. Tenant pools are NOT paginated here — enterprise SSO pools are\n * small, and silently dropping their tail would recreate exactly the\n * invisibility this module exists to fix. `tenantTruncated` says when that\n * assumption stopped holding rather than letting it fail quietly.\n */\n tenantsIncluded: boolean\n /** Tenants whose users hit the cap and were cut short. */\n tenantTruncated: string[]\n}\n\n/** How many tenant users a single page will carry before reporting a cut. */\nconst TENANT_USER_CAP = 500\n\n/**\n * How many pools may have a call in flight at once.\n *\n * Every sweep below asks each GCIP tenant pool the same question, and the\n * pools are independent: one tenant's answer never informs another's. Asked in\n * series that costs `N` round trips for `N` enterprise customers, so the staff\n * list gets slower every time one is signed — the wrong direction for the\n * feature that adds them. Asked together it costs one.\n *\n * The number of CALLS is identical either way, so this buys latency and the\n * Cloud Run seconds spent waiting on it, not Auth quota. That is also why it\n * is bounded rather than an unbounded `Promise.all`: Identity Platform rate\n * limits `listUsers` per project, and firing one call per tenant simultaneously\n * would convert a latency problem into a `RESOURCE_EXHAUSTED` one at exactly\n * the customer count where the latency stopped mattering.\n */\nconst POOL_FETCH_CONCURRENCY = 8\n\n/**\n * Run `work` over every pool id, at most {@link POOL_FETCH_CONCURRENCY} at a\n * time, and return the results **in the order the ids were given**.\n *\n * Order is not cosmetic here. Callers concatenate these results into a user\n * list and then trim it to a cap, so a result order that varied with which\n * pool answered first would make the rows a staff member sees depend on\n * network timing — the same query returning different people on a refresh.\n */\nasync function mapPoolsConcurrently<T>(\n tenantIds: string[],\n work: (tenantId: string) => Promise<T>,\n): Promise<T[]> {\n const results = new Array<T>(tenantIds.length)\n let next = 0\n const workers = Array.from(\n { length: Math.min(POOL_FETCH_CONCURRENCY, tenantIds.length) },\n async () => {\n for (;;) {\n const index = next++\n if (index >= tenantIds.length) return\n results[index] = await work(tenantIds[index])\n }\n },\n )\n await Promise.all(workers)\n return results\n}\n\n/**\n * One page of users across every pool: the project pool paginates as before,\n * and tenant users are appended on the LAST page so they can never be lost\n * between cursors. Callers render `tenantId` so an SSO account is\n * recognisable — it is not editable the same way a project user is.\n */\nexport async function listUsersAcrossPools(\n pageSize: number,\n pageToken?: string,\n): Promise<PooledUserPage> {\n const page = await auth().listUsers(pageSize, pageToken)\n const users: PooledUserRecord[] = page.users.map((record) => ({\n record,\n tenantId: null,\n }))\n if (page.pageToken) {\n return {\n users,\n nextPageToken: page.pageToken,\n tenantsIncluded: false,\n tenantTruncated: [],\n }\n }\n const tenantTruncated: string[] = []\n // Every tenant asks for the same fixed page, so no pool's request depends on\n // another's answer and they can all be in flight at once. A failed pool\n // contributes no rows and does not stop the others: one enterprise tenant\n // being unreachable must not blank the whole staff list.\n //\n // The id list is read ONCE and reused. It is cached on a 60s TTL, so asking\n // twice can hand back two different lists, and pairing a page from one list\n // with an id from the other would label a user with the wrong tenant.\n const tenantIds = await listAuthTenantIds()\n const tenantPages = await mapPoolsConcurrently(tenantIds, async (tenantId) => {\n try {\n return await authForPool(tenantId).listUsers(TENANT_USER_CAP)\n } catch (error) {\n console.error(`listing users for tenant ${tenantId} failed`, error)\n return null\n }\n })\n tenantPages.forEach((tenantPage, index) => {\n if (!tenantPage) return\n const tenantId = tenantIds[index]\n users.push(...tenantPage.users.map((record) => ({ record, tenantId })))\n if (tenantPage.pageToken) tenantTruncated.push(tenantId)\n })\n return {\n // Marked only here, where every pool's rows are in hand (AGL-1962). The\n // earlier return above carries project rows alone, and a uid cannot\n // collide with itself inside one pool, so there is nothing to find there.\n users: markCrossPoolUidCollisions(users),\n nextPageToken: null,\n tenantsIncluded: true,\n tenantTruncated,\n }\n}\n\n/**\n * Every account across every pool, up to a cap — the only way to FILTER them.\n *\n * Firebase Auth's `listUsers` takes a page size and a cursor and nothing else:\n * no predicate, no ordering, no search. So a staff filter on anything but an\n * exact email, uid or phone number (which have their own O(1) lookups) can\n * only be answered by reading the accounts and matching them.\n *\n * That is an expensive read, so it happens on an ASK and never on a mount —\n * the route calls this only when a request carries a filter, and pages the\n * cheap way otherwise.\n *\n * `truncated` is true when the cap was reached, and it must be shown rather\n * than swallowed: a staff list that searched part of the directory and said\n * \"no matches\" is worse than one that admits where it stopped.\n */\nexport async function scanUsersAcrossPools(\n cap: number,\n): Promise<{\n users: PooledUserRecord[]\n truncated: boolean\n tenantTruncated: string[]\n}> {\n const users: PooledUserRecord[] = []\n let truncated = false\n let pageToken: string | undefined\n\n do {\n const remaining = cap - users.length\n if (remaining <= 0) {\n truncated = true\n break\n }\n // `listUsers` caps a page at 1000 whatever it is asked for.\n const page = await auth().listUsers(Math.min(remaining, 1000), pageToken)\n users.push(...page.users.map((record) => ({ record, tenantId: null })))\n pageToken = page.pageToken\n if (pageToken && users.length >= cap) truncated = true\n } while (pageToken && users.length < cap)\n\n const tenantTruncated: string[] = []\n const tenantIds = await listAuthTenantIds()\n /*\n * The remaining budget is fixed BEFORE any pool is asked.\n *\n * Sizing each pool's page from the running total is what forced these calls\n * into a series — pool `k`'s page size could not be known until pool `k-1`\n * had answered. Nothing needs that: a page sized to the whole remaining\n * budget is always at least as large as the shrinking one, so the first\n * `cap` rows in tenant order are the same users either way, and the pools\n * can be asked together.\n *\n * The trade is bandwidth, not quota: `listUsers` is billed per CALL, and the\n * call count is unchanged. Only when the cap actually binds does this carry\n * rows that are then trimmed.\n */\n const budget = cap - users.length\n if (budget <= 0) {\n truncated = true\n } else {\n const pageSize = Math.min(budget, TENANT_USER_CAP)\n const pages = await mapPoolsConcurrently(tenantIds, async (tenantId) => {\n try {\n return await authForPool(tenantId).listUsers(pageSize)\n } catch (error) {\n console.error(`listing users for tenant ${tenantId} failed`, error)\n return null\n }\n })\n pages.forEach((page, index) => {\n if (!page) return\n const tenantId = tenantIds[index]\n const room = cap - users.length\n const rows = page.users.map((record) => ({ record, tenantId }))\n // A pool whose rows will not fit is still a pool that was cut short, and\n // it has to say so. A scan that silently dropped one would answer \"no\n // such account\" for everyone in it — the exact failure this module\n // exists to prevent, wearing the shape of a complete result.\n if (rows.length > room) {\n truncated = true\n tenantTruncated.push(tenantId)\n } else if (page.pageToken) {\n tenantTruncated.push(tenantId)\n }\n if (room > 0) users.push(...rows.slice(0, room))\n })\n }\n\n return {\n users: markCrossPoolUidCollisions(users),\n truncated,\n tenantTruncated,\n }\n}\n\n/** Where a claim mutation landed, so a caller can audit the POOL as well. */\nexport interface StaffClaimWrite {\n uid: string\n /** The pool the claim was written to; null = the project pool. */\n tenantId: string | null\n email: string | null\n claims: Record<string, unknown>\n}\n\n/**\n * Write custom claims to the pool the uid ACTUALLY lives in (AGL-1993).\n *\n * The hazard this exists to remove: `setCustomUserClaims` is per-pool, and a\n * grant aimed at the wrong pool does not error — on the project pool it\n * either throws `user-not-found` (loud, fine) or, when a phantom shadow record\n * shares the uid, silently succeeds against the WRONG record. That is not\n * hypothetical: on 2026-08-18 a staff account's uid existed in both the\n * project pool and `aglyn-org-y5v14` at once. That phantom won every lookup\n * until AGL-2005 made {@link findUserByUidAcrossPools} rank an IDENTIFIED\n * record above an unidentified one rather than taking the first pool to\n * answer.\n *\n * So resolution and mutation are bound together here rather than left to each\n * caller to pair correctly, and the resolved pool is RETURNED so the audit row\n * can record which pool was written — \"granted staff to uid X\" is not a\n * complete record when a uid can name two accounts.\n *\n * Never assumes a pool. Staff may live in the project pool, in Aglyn's own\n * tenant, or in a CUSTOMER org's tenant, and hard-coding any of them would\n * silently fail the moment a third exists.\n */\nexport async function setClaimsInOwningPool(\n uid: string,\n claims: Record<string, unknown>,\n): Promise<StaffClaimWrite | null> {\n const found = await findUserByUidAcrossPools(uid)\n if (!found) return null\n await authForPool(found.tenantId).setCustomUserClaims(uid, claims)\n return {\n uid,\n tenantId: found.tenantId,\n email: found.record.email ?? null,\n claims,\n }\n}\n\n/**\n * Every uid carrying the `staff` claim, across all pools. Staff is a custom\n * claim, which is not queryable, so this scans — the caller caches it.\n */\nexport async function listStaffUidsAcrossPools(): Promise<string[]> {\n /*\n * Paging WITHIN a pool stays serial — each page's cursor comes out of the\n * one before it, so there is nothing to overlap. Across pools there is: the\n * pools are independent, and a staff notification waiting on them one after\n * another gets slower with every enterprise customer signed.\n *\n * Each scan collects into its own array rather than a shared one, so the\n * result is ordered by POOL rather than by which pool happened to answer\n * first. A caller diffing this list against a stored copy would otherwise\n * see phantom changes on every run.\n */\n const scan = async (pool: BaseAuth): Promise<string[]> => {\n const found: string[] = []\n let pageToken: string | undefined\n do {\n const page = await pool.listUsers(1000, pageToken)\n for (const record of page.users) {\n if (record.customClaims?.['staff']) found.push(record.uid)\n }\n pageToken = page.pageToken\n } while (pageToken)\n return found\n }\n const tenantIds = await listAuthTenantIds()\n const [projectUids, tenantUidLists] = await Promise.all([\n scan(auth()),\n mapPoolsConcurrently(tenantIds, async (tenantId) => {\n try {\n return await scan(authForPool(tenantId))\n } catch (error) {\n console.error(`staff scan for tenant ${tenantId} failed`, error)\n return [] as string[]\n }\n }),\n ])\n return [...projectUids, ...tenantUidLists.flat()]\n}\n"],"names":["firebaseAdmin","auth","app","isIdentifiedUserRecord","record","Boolean","email","providerData","length","identityStrength","score","phoneNumber","displayName","Object","keys","customClaims","collapseCrossPoolUidRows","users","marked","markCrossPoolUidCollisions","winnerByUid","Map","user","incumbent","get","uid","set","emitted","Set","collapsed","has","add","push","poolsByUid","pools","tenantId","map","others","filter","_","index","indexOf","uidAlsoInPools","tenantCache","TENANT_CACHE_MS","listAuthTenantIds","now","Date","at","ids","pageToken","page","tenantManager","listTenants","tenants","tenant","error","console","resetAuthTenantCache","authForPool","authForTenant","findUserByEmailAcrossPools","address","trim","toLowerCase","getUserByEmail","findUserByUidAcrossPools","matches","getUser","TENANT_USER_CAP","POOL_FETCH_CONCURRENCY","mapPoolsConcurrently","tenantIds","work","results","Array","next","workers","from","Math","min","Promise","all","listUsersAcrossPools","pageSize","listUsers","nextPageToken","tenantsIncluded","tenantTruncated","tenantPages","forEach","tenantPage","scanUsersAcrossPools","cap","truncated","remaining","budget","pages","room","rows","slice","setClaimsInOwningPool","claims","found","setCustomUserClaims","listStaffUidsAcrossPools","scan","pool","projectUids","tenantUidLists","flat"],"mappings":"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"}