@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,343 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // Initializes the firebase-admin default app (cert credential, RTDB URL,
17
+ // service account, AppCheck) on module load. `firestoreDatabaseId` is the
18
+ // FIRESTORE_DATABASE_ID override (AGL-1490): unset → `(default)` as before;
19
+ // set → every accessor targets the named database (disaster-recovery cutover,
20
+ // see docs/DISASTER_RECOVERY.md).
21
+ import { firestoreDatabaseId } from "@aglyn/shared-util-fbserver";
22
+ import { decode, encode } from "@msgpack/msgpack";
23
+ import { getApp } from "firebase-admin/app";
24
+ import { getAuth } from "firebase-admin/auth";
25
+ import { assertIdTokenNotRevoked } from "./token-revocation.js";
26
+ import { getDatabase } from "firebase-admin/database";
27
+ import { getRemoteConfig } from "firebase-admin/remote-config";
28
+ import { FieldPath, FieldValue, Timestamp, getFirestore } from "firebase-admin/firestore";
29
+ import { getStorage } from "firebase-admin/storage";
30
+ function compress(value) {
31
+ return Buffer.from(encode(value));
32
+ }
33
+ function decompress(value) {
34
+ return decode(value);
35
+ }
36
+ export const hostConverter = {
37
+ toFirestore (data) {
38
+ if (data.$id) delete data.$id;
39
+ // data.updatedAt = Timestamp.now()
40
+ return data;
41
+ },
42
+ fromFirestore (snapshot) {
43
+ if (!snapshot.exists) return undefined;
44
+ const data = snapshot.data();
45
+ data.$id = snapshot.id;
46
+ return data;
47
+ }
48
+ };
49
+ export const screenConverter = {
50
+ toFirestore (data) {
51
+ if (data.$id) delete data.$id;
52
+ data.updatedAt = Timestamp.now();
53
+ return data;
54
+ },
55
+ fromFirestore (snapshot) {
56
+ if (!snapshot.exists) return undefined;
57
+ const data = snapshot.data();
58
+ data.$id = snapshot.id;
59
+ return data;
60
+ }
61
+ };
62
+ export const screenVersionConverter = {
63
+ toFirestore (data) {
64
+ if (data.elements) {
65
+ data.nodes = data.elements;
66
+ delete data.elements;
67
+ }
68
+ if (data['bundleId']) {
69
+ data.pluginId = data['bundleId'];
70
+ delete data['bundleId'];
71
+ }
72
+ if (data.nodes) data.nodes = compress(data.nodes);
73
+ if (data.$id) delete data.$id;
74
+ data.updatedAt = Timestamp.now();
75
+ return data;
76
+ },
77
+ fromFirestore (snapshot) {
78
+ if (!snapshot.exists) return undefined;
79
+ const data = snapshot.data();
80
+ if (data == null ? void 0 : data.elements) {
81
+ data.nodes = data.elements;
82
+ delete data.elements;
83
+ }
84
+ if (data == null ? void 0 : data['bundleId']) {
85
+ data.pluginId = data['bundleId'];
86
+ delete data['bundleId'];
87
+ }
88
+ if (data == null ? void 0 : data.nodes) {
89
+ // Nodes saved while updateDoc bypassed the client converter are plain
90
+ // maps rather than compressed bytes; decode only binary payloads.
91
+ data.nodes = ArrayBuffer.isView(data.nodes) ? decompress(data.nodes) : data.nodes;
92
+ }
93
+ data.$id = snapshot.id;
94
+ return data;
95
+ }
96
+ };
97
+ export const layoutConverter = {
98
+ toFirestore (data) {
99
+ if (data.$id) delete data.$id;
100
+ data.updatedAt = Timestamp.now();
101
+ return data;
102
+ },
103
+ fromFirestore (snapshot) {
104
+ if (!snapshot.exists) return undefined;
105
+ const data = snapshot.data();
106
+ data.$id = snapshot.id;
107
+ return data;
108
+ }
109
+ };
110
+ // Layout versions persist nodes exactly like screen versions (compressed).
111
+ export const layoutVersionConverter = screenVersionConverter;
112
+ /**
113
+ * Compatibility facade replacing firebase-admin v14's removed namespace API
114
+ * (`import * as admin from 'firebase-admin'`) so existing call sites
115
+ * (`firebaseAdmin.app().firestore()`, `firebaseAdmin.firestore.FieldValue`,
116
+ * `firebaseAdmin.database()`) keep working unchanged, backed internally by
117
+ * the modular SDK.
118
+ */ /**
119
+ * Auth wrapped so `verifyIdToken` also asks whether the account was revoked
120
+ * (AGL-1881).
121
+ *
122
+ * WHY HERE, and not at the call sites: the audit found `checkRevoked` set on
123
+ * 3 of 175 verifications, which is what a per-call-site opt-in converges to.
124
+ * Every server door in this repo reaches auth through `firebaseAdmin.app()` —
125
+ * all 117 console API routes, the marketplace/commerce/bookings plugin
126
+ * servers, `authForPool`, `release-flags` — so this ONE function is where the
127
+ * question can be asked once and be true everywhere, including on routes
128
+ * written next year by someone who has never read AGL-1881.
129
+ * `token-revocation.spec.ts` pins that the wrapping is real; the guard in
130
+ * `token-revocation-coverage.spec.ts` pins that no future door bypasses it.
131
+ *
132
+ * A Proxy rather than a hand-written facade because `BaseAuth` has ~40
133
+ * methods and a facade that listed 39 of them would silently drop the
134
+ * fortieth. Only `verifyIdToken` and `tenantManager` are intercepted;
135
+ * everything else is the SDK's own method, bound to the SDK's own instance.
136
+ *
137
+ * `tenantManager` is wrapped recursively because a GCIP tenant's
138
+ * `TenantAwareAuth` is a DIFFERENT auth object with its own `verifyIdToken`,
139
+ * and five console routes verify SSO tokens through it. Unwrapped, those five
140
+ * would be exactly the holes this exists to close.
141
+ *
142
+ * An explicit `checkRevoked: true` is left alone: firebase-admin then runs the
143
+ * same three checks itself, and doing both would buy one answer with two
144
+ * round trips.
145
+ */ /**
146
+ * The pool to ask about a verified token's account (AGL-2486).
147
+ *
148
+ * ## The bug this exists to fix
149
+ *
150
+ * `BaseAuth.verifyIdToken` does NOT reject a token minted in a GCIP tenant.
151
+ * Only `TenantAwareAuth` overrides it to compare `firebase.tenant` against
152
+ * its own id; the project-level handle checks a signature, an issuer and an
153
+ * audience, all of which are project-wide. So an SSO user's ID token verifies
154
+ * perfectly through `firebaseAdmin.app().auth()` — and every console API
155
+ * route verifies exactly that way.
156
+ *
157
+ * The revocation check then ran `getUser(uid)` on that same project-level
158
+ * handle. A tenant uid is not in the project pool: measured on production
159
+ * 2026-08-22, a staff account in `aglyn-org-y5v14` verified through the
160
+ * project handle, and a project-level `getUser` of the uid that token carries
161
+ * threw `auth/user-not-found`. `assertIdTokenNotRevoked` reads "not found" as
162
+ * "account deleted" — correctly, and fail-CLOSED by design — and threw
163
+ * `auth/id-token-revoked`. Every SSO user was therefore refused at every door
164
+ * that verifies a Bearer token, with whatever the route's catch-all produced;
165
+ * on `/api/presence/token` that was a 500 and live co-editing simply never
166
+ * started for them.
167
+ *
168
+ * ## Why the existing guard did not catch it
169
+ *
170
+ * `no-project-level-auth-lookup.spec.ts` reads the RECEIVER NAME of a
171
+ * `.getUser(` call, and `assertIdTokenNotRevoked` names its parameter `pool`
172
+ * precisely so that it passes. The name was honest about the contract and
173
+ * said nothing about the ARGUMENT, which was the project-level handle at the
174
+ * only call site there is. A guard that reads a name proves a name.
175
+ *
176
+ * ## What this does
177
+ *
178
+ * Sends the lookup to the pool named by the token's own `firebase.tenant`
179
+ * claim, so the contract `assertIdTokenNotRevoked` documents ("the lookup is
180
+ * asked of the pool the token belongs to") becomes structurally true instead
181
+ * of being an assumption about the caller. One place, so a route written next
182
+ * year inherits it — the same reasoning that put the revocation check here.
183
+ *
184
+ * Returns the handle unchanged for a project-pool token and for an auth
185
+ * already scoped to that same tenant.
186
+ *
187
+ * Returns NULL when the token names a tenant this handle cannot reach — and
188
+ * the caller then SKIPS the check rather than asking the wrong pool. That is
189
+ * the whole lesson of this bug: "not found in the pool I happened to ask" is
190
+ * not evidence that an account was deleted, it is evidence that the question
191
+ * went to the wrong place, and `assertIdTokenNotRevoked` already models "we
192
+ * could not ask" as fail-open. Falling back to the verifying handle would
193
+ * re-create the exact outage in the one configuration where routing fails.
194
+ * Unreachable in practice — the project-level `Auth` always has a
195
+ * `tenantManager`, and a `TenantAwareAuth` holding a token for a DIFFERENT
196
+ * tenant has already thrown `auth/mismatching-tenant-id` before this runs.
197
+ *
198
+ * The tenant handle is deliberately NOT wrapped in `revocationCheckedAuth`:
199
+ * it is used only for `getUser`, and re-wrapping would put the revocation
200
+ * check inside the revocation check.
201
+ */ function revocationPool(auth, decoded) {
202
+ var _decoded_firebase;
203
+ const self = auth;
204
+ const tenantId = decoded == null ? void 0 : (_decoded_firebase = decoded.firebase) == null ? void 0 : _decoded_firebase.tenant;
205
+ // `typeof`, not a truthy test: `strictNullChecks` is off repo-wide, so a
206
+ // bare `!tenantId` narrows nothing and would also swallow a real id.
207
+ if (typeof tenantId !== 'string' || !tenantId) return self;
208
+ // Already the right pool — a route that verified through
209
+ // `authForPool(tenantId)` has nothing to switch to, and `TenantAwareAuth`
210
+ // has no `tenantManager` to ask anyway.
211
+ if (auth.tenantId === tenantId) return self;
212
+ const tenantManager = auth.tenantManager;
213
+ if (typeof tenantManager !== 'function') return null;
214
+ try {
215
+ const manager = tenantManager.call(auth);
216
+ return manager.authForTenant(tenantId);
217
+ } catch (unused) {
218
+ return null;
219
+ }
220
+ }
221
+ function revocationCheckedAuth(target) {
222
+ return new Proxy(target, {
223
+ get (auth, prop, receiver) {
224
+ if (prop === 'verifyIdToken') {
225
+ return async (...args)=>{
226
+ const verify = Reflect.get(auth, prop, auth);
227
+ // Spread, never `(token, checkRevoked)`: passing an explicit
228
+ // `undefined` where the caller passed nothing changes what the SDK
229
+ // sees, and a wrapper that alters the call it forwards is not a
230
+ // wrapper.
231
+ const decoded = await verify.apply(auth, args);
232
+ if (args[1] === true) return decoded;
233
+ // The pool the TOKEN belongs to, which is not always the pool that
234
+ // verified it — see `revocationPool`. Never `receiver`: the lookup
235
+ // must not re-enter this proxy.
236
+ const pool = revocationPool(auth, decoded);
237
+ // `=== null`, not `!pool`: `strictNullChecks` is off repo-wide, so
238
+ // a falsy test narrows nothing here.
239
+ if (pool === null) {
240
+ var _decoded_firebase;
241
+ console.warn('[auth] revocation check skipped: no handle for tenant', decoded == null ? void 0 : (_decoded_firebase = decoded.firebase) == null ? void 0 : _decoded_firebase.tenant);
242
+ return decoded;
243
+ }
244
+ await assertIdTokenNotRevoked(pool, decoded);
245
+ return decoded;
246
+ };
247
+ }
248
+ if (prop === 'tenantManager') {
249
+ return (...args)=>{
250
+ const manager = Reflect.get(auth, prop, auth).apply(auth, args);
251
+ return new Proxy(manager, {
252
+ get (mgr, key, mgrReceiver) {
253
+ if (key === 'authForTenant') {
254
+ return (...tenantArgs)=>revocationCheckedAuth(Reflect.get(mgr, key, mgr).apply(mgr, tenantArgs));
255
+ }
256
+ const value = Reflect.get(mgr, key, mgrReceiver);
257
+ return typeof value === 'function' ? value.bind(mgr) : value;
258
+ }
259
+ });
260
+ };
261
+ }
262
+ const value = Reflect.get(auth, prop, receiver);
263
+ // Bound to the SDK instance: firebase-admin's Auth keeps private state
264
+ // on `this`, and an unbound method handed out through a Proxy loses it.
265
+ return typeof value === 'function' ? value.bind(auth) : value;
266
+ }
267
+ });
268
+ }
269
+ function wrapApp(app) {
270
+ return {
271
+ firestore: ()=>getFirestore(app, firestoreDatabaseId()),
272
+ auth: ()=>revocationCheckedAuth(getAuth(app)),
273
+ storage: ()=>getStorage(app),
274
+ // Release-flag management (AGL-230): the staff admin flags API reads
275
+ // and publishes the Remote Config template server-side.
276
+ remoteConfig: ()=>getRemoteConfig(app)
277
+ };
278
+ }
279
+ function firestoreNamespace() {
280
+ return getFirestore(getApp(), firestoreDatabaseId());
281
+ }
282
+ firestoreNamespace.FieldValue = FieldValue;
283
+ firestoreNamespace.Timestamp = Timestamp;
284
+ firestoreNamespace.FieldPath = FieldPath;
285
+ const firebaseAdmin = {
286
+ app: (name)=>wrapApp(name ? getApp(name) : getApp()),
287
+ firestore: firestoreNamespace,
288
+ database: ()=>getDatabase(getApp())
289
+ };
290
+ /**
291
+ * Email-verification gate (AGL-479). Email/password accounts must verify
292
+ * their address before any console access; OAuth accounts arrive with
293
+ * `email_verified: true`, so they pass untouched. `verifyConsoleIdToken`
294
+ * verifies the ID token exactly like `auth().verifyIdToken` and additionally
295
+ * throws `EmailNotVerifiedError` when the address is unverified — verified
296
+ * callers see identical behavior. Route handlers pair the raw verify with
297
+ * `emailUnverifiedResponse()` (a 403) so the denial stays distinct from an
298
+ * invalid-token 401. Fails closed: a token with no `email_verified` claim
299
+ * (e.g. some custom-token sign-ins) is treated as unverified.
300
+ *
301
+ * Exception (AGL-480): staff impersonation sessions carry an `impersonatedBy`
302
+ * claim (minted by /api/admin/impersonate). Staff have already authenticated
303
+ * and the act is audited, so the impersonated account's own verification
304
+ * state must not gate the support session — otherwise staff can't reach a
305
+ * brand-new, still-unverified owner, the exact account most likely to need
306
+ * help. `isImpersonationSession` gates that exemption.
307
+ */ export class EmailNotVerifiedError extends Error {
308
+ constructor(){
309
+ super('Email address not verified'), this.code = 'auth/email-not-verified';
310
+ this.name = 'EmailNotVerifiedError';
311
+ }
312
+ }
313
+ export function isEmailVerified(decoded) {
314
+ return decoded.email_verified === true;
315
+ }
316
+ /** Staff impersonation session (AGL-357/AGL-480): token minted with an
317
+ * `impersonatedBy` claim. Exempt from the email-verification gate. */ export function isImpersonationSession(decoded) {
318
+ return typeof decoded['impersonatedBy'] === 'string';
319
+ }
320
+ export async function verifyConsoleIdToken(idToken, checkRevoked) {
321
+ // `firebaseAdmin.app().auth()`, not a bare `getAuth` (AGL-1881): the raw
322
+ // SDK handle skips the revocation check, and this helper's whole promise is
323
+ // that it verifies "exactly like `auth().verifyIdToken`" plus the email
324
+ // gate. A door that reads as the STRICTER one while being the looser one is
325
+ // the worst shape available.
326
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken, checkRevoked);
327
+ if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {
328
+ throw new EmailNotVerifiedError();
329
+ }
330
+ return decoded;
331
+ }
332
+ /** 403 sent to callers whose email address is not yet verified (AGL-479). */ export function emailUnverifiedResponse() {
333
+ return Response.json({
334
+ error: 'Verify your email to continue',
335
+ reason: 'email-unverified'
336
+ }, {
337
+ status: 403
338
+ });
339
+ }
340
+ export { firebaseAdmin };
341
+ export default firebaseAdmin;
342
+
343
+ //# sourceMappingURL=firebase-admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/firebase-admin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * as Aglyn from '@aglyn/aglyn/server'\n// Initializes the firebase-admin default app (cert credential, RTDB URL,\n// service account, AppCheck) on module load. `firestoreDatabaseId` is the\n// FIRESTORE_DATABASE_ID override (AGL-1490): unset → `(default)` as before;\n// set → every accessor targets the named database (disaster-recovery cutover,\n// see docs/DISASTER_RECOVERY.md).\nimport { firestoreDatabaseId } from '@aglyn/shared-util-fbserver'\nimport { decode, encode } from '@msgpack/msgpack'\nimport { type App, getApp } from 'firebase-admin/app'\nimport {\n type BaseAuth,\n type DecodedIdToken,\n getAuth,\n} from 'firebase-admin/auth'\nimport { assertIdTokenNotRevoked } from './token-revocation'\nimport { getDatabase } from 'firebase-admin/database'\nimport { getRemoteConfig } from 'firebase-admin/remote-config'\nimport {\n FieldPath,\n FieldValue,\n type FirestoreDataConverter,\n Timestamp,\n getFirestore,\n} from 'firebase-admin/firestore'\nimport { getStorage } from 'firebase-admin/storage'\n\nfunction compress(value: any) {\n return Buffer.from(encode(value))\n}\nfunction decompress(value: any) {\n return decode(value)\n}\n\nexport const hostConverter: FirestoreDataConverter<Aglyn.AglynHost> = {\n toFirestore(data) {\n if (data.$id) delete data.$id\n // data.updatedAt = Timestamp.now()\n return data\n },\n fromFirestore(snapshot) {\n if (!snapshot.exists) return undefined\n const data = snapshot.data()\n data.$id = snapshot.id\n return data as Aglyn.AglynScreen\n },\n}\n\nexport const screenConverter: FirestoreDataConverter<Aglyn.AglynScreen> = {\n toFirestore(data) {\n if (data.$id) delete data.$id\n data.updatedAt = Timestamp.now()\n return data\n },\n fromFirestore(snapshot) {\n if (!snapshot.exists) return undefined\n const data = snapshot.data()\n data.$id = snapshot.id\n return data as Aglyn.AglynScreen\n },\n}\n\nexport const screenVersionConverter: FirestoreDataConverter<Aglyn.AglynScreenVersion> =\n {\n toFirestore(data) {\n if (data.elements) {\n data.nodes = data.elements\n delete data.elements\n }\n if (data['bundleId']) {\n data.pluginId = data['bundleId']\n delete data['bundleId']\n }\n if (data.nodes) data.nodes = compress(data.nodes) as any\n if (data.$id) delete data.$id\n data.updatedAt = Timestamp.now()\n return data\n },\n fromFirestore(snapshot) {\n if (!snapshot.exists) return undefined\n const data = snapshot.data()\n if (data?.elements) {\n data.nodes = data.elements\n delete data.elements\n }\n if (data?.['bundleId']) {\n data.pluginId = data['bundleId']\n delete data['bundleId']\n }\n if (data?.nodes) {\n // Nodes saved while updateDoc bypassed the client converter are plain\n // maps rather than compressed bytes; decode only binary payloads.\n data.nodes = ArrayBuffer.isView(data.nodes)\n ? decompress(data.nodes)\n : data.nodes\n }\n data.$id = snapshot.id\n return data as Aglyn.AglynScreenVersion\n },\n }\n\nexport const layoutConverter: FirestoreDataConverter<Aglyn.AglynLayout> = {\n toFirestore(data) {\n if (data.$id) delete data.$id\n data.updatedAt = Timestamp.now()\n return data\n },\n fromFirestore(snapshot) {\n if (!snapshot.exists) return undefined\n const data = snapshot.data()\n data.$id = snapshot.id\n return data as Aglyn.AglynLayout\n },\n}\n\n// Layout versions persist nodes exactly like screen versions (compressed).\nexport const layoutVersionConverter =\n screenVersionConverter as unknown as FirestoreDataConverter<Aglyn.AglynLayoutVersion>\n\n/**\n * Compatibility facade replacing firebase-admin v14's removed namespace API\n * (`import * as admin from 'firebase-admin'`) so existing call sites\n * (`firebaseAdmin.app().firestore()`, `firebaseAdmin.firestore.FieldValue`,\n * `firebaseAdmin.database()`) keep working unchanged, backed internally by\n * the modular SDK.\n */\n/**\n * Auth wrapped so `verifyIdToken` also asks whether the account was revoked\n * (AGL-1881).\n *\n * WHY HERE, and not at the call sites: the audit found `checkRevoked` set on\n * 3 of 175 verifications, which is what a per-call-site opt-in converges to.\n * Every server door in this repo reaches auth through `firebaseAdmin.app()` —\n * all 117 console API routes, the marketplace/commerce/bookings plugin\n * servers, `authForPool`, `release-flags` — so this ONE function is where the\n * question can be asked once and be true everywhere, including on routes\n * written next year by someone who has never read AGL-1881.\n * `token-revocation.spec.ts` pins that the wrapping is real; the guard in\n * `token-revocation-coverage.spec.ts` pins that no future door bypasses it.\n *\n * A Proxy rather than a hand-written facade because `BaseAuth` has ~40\n * methods and a facade that listed 39 of them would silently drop the\n * fortieth. Only `verifyIdToken` and `tenantManager` are intercepted;\n * everything else is the SDK's own method, bound to the SDK's own instance.\n *\n * `tenantManager` is wrapped recursively because a GCIP tenant's\n * `TenantAwareAuth` is a DIFFERENT auth object with its own `verifyIdToken`,\n * and five console routes verify SSO tokens through it. Unwrapped, those five\n * would be exactly the holes this exists to close.\n *\n * An explicit `checkRevoked: true` is left alone: firebase-admin then runs the\n * same three checks itself, and doing both would buy one answer with two\n * round trips.\n */\n/**\n * The pool to ask about a verified token's account (AGL-2486).\n *\n * ## The bug this exists to fix\n *\n * `BaseAuth.verifyIdToken` does NOT reject a token minted in a GCIP tenant.\n * Only `TenantAwareAuth` overrides it to compare `firebase.tenant` against\n * its own id; the project-level handle checks a signature, an issuer and an\n * audience, all of which are project-wide. So an SSO user's ID token verifies\n * perfectly through `firebaseAdmin.app().auth()` — and every console API\n * route verifies exactly that way.\n *\n * The revocation check then ran `getUser(uid)` on that same project-level\n * handle. A tenant uid is not in the project pool: measured on production\n * 2026-08-22, a staff account in `aglyn-org-y5v14` verified through the\n * project handle, and a project-level `getUser` of the uid that token carries\n * threw `auth/user-not-found`. `assertIdTokenNotRevoked` reads \"not found\" as\n * \"account deleted\" — correctly, and fail-CLOSED by design — and threw\n * `auth/id-token-revoked`. Every SSO user was therefore refused at every door\n * that verifies a Bearer token, with whatever the route's catch-all produced;\n * on `/api/presence/token` that was a 500 and live co-editing simply never\n * started for them.\n *\n * ## Why the existing guard did not catch it\n *\n * `no-project-level-auth-lookup.spec.ts` reads the RECEIVER NAME of a\n * `.getUser(` call, and `assertIdTokenNotRevoked` names its parameter `pool`\n * precisely so that it passes. The name was honest about the contract and\n * said nothing about the ARGUMENT, which was the project-level handle at the\n * only call site there is. A guard that reads a name proves a name.\n *\n * ## What this does\n *\n * Sends the lookup to the pool named by the token's own `firebase.tenant`\n * claim, so the contract `assertIdTokenNotRevoked` documents (\"the lookup is\n * asked of the pool the token belongs to\") becomes structurally true instead\n * of being an assumption about the caller. One place, so a route written next\n * year inherits it — the same reasoning that put the revocation check here.\n *\n * Returns the handle unchanged for a project-pool token and for an auth\n * already scoped to that same tenant.\n *\n * Returns NULL when the token names a tenant this handle cannot reach — and\n * the caller then SKIPS the check rather than asking the wrong pool. That is\n * the whole lesson of this bug: \"not found in the pool I happened to ask\" is\n * not evidence that an account was deleted, it is evidence that the question\n * went to the wrong place, and `assertIdTokenNotRevoked` already models \"we\n * could not ask\" as fail-open. Falling back to the verifying handle would\n * re-create the exact outage in the one configuration where routing fails.\n * Unreachable in practice — the project-level `Auth` always has a\n * `tenantManager`, and a `TenantAwareAuth` holding a token for a DIFFERENT\n * tenant has already thrown `auth/mismatching-tenant-id` before this runs.\n *\n * The tenant handle is deliberately NOT wrapped in `revocationCheckedAuth`:\n * it is used only for `getUser`, and re-wrapping would put the revocation\n * check inside the revocation check.\n */\nfunction revocationPool(\n auth: object,\n decoded: DecodedIdToken,\n): Pick<BaseAuth, 'getUser'> | null {\n const self = auth as unknown as Pick<BaseAuth, 'getUser'>\n const tenantId = decoded?.firebase?.tenant\n // `typeof`, not a truthy test: `strictNullChecks` is off repo-wide, so a\n // bare `!tenantId` narrows nothing and would also swallow a real id.\n if (typeof tenantId !== 'string' || !tenantId) return self\n // Already the right pool — a route that verified through\n // `authForPool(tenantId)` has nothing to switch to, and `TenantAwareAuth`\n // has no `tenantManager` to ask anyway.\n if ((auth as { tenantId?: string }).tenantId === tenantId) return self\n const tenantManager = (auth as { tenantManager?: () => unknown })\n .tenantManager\n if (typeof tenantManager !== 'function') return null\n try {\n const manager = tenantManager.call(auth) as {\n authForTenant: (id: string) => Pick<BaseAuth, 'getUser'>\n }\n return manager.authForTenant(tenantId)\n } catch {\n return null\n }\n}\n\nfunction revocationCheckedAuth<T extends object>(target: T): T {\n return new Proxy(target, {\n get(auth, prop, receiver) {\n if (prop === 'verifyIdToken') {\n return async (...args: unknown[]) => {\n const verify = Reflect.get(auth, prop, auth) as (\n ...a: unknown[]\n ) => Promise<DecodedIdToken>\n // Spread, never `(token, checkRevoked)`: passing an explicit\n // `undefined` where the caller passed nothing changes what the SDK\n // sees, and a wrapper that alters the call it forwards is not a\n // wrapper.\n const decoded = await verify.apply(auth, args)\n if (args[1] === true) return decoded\n // The pool the TOKEN belongs to, which is not always the pool that\n // verified it — see `revocationPool`. Never `receiver`: the lookup\n // must not re-enter this proxy.\n const pool = revocationPool(auth, decoded)\n // `=== null`, not `!pool`: `strictNullChecks` is off repo-wide, so\n // a falsy test narrows nothing here.\n if (pool === null) {\n console.warn(\n '[auth] revocation check skipped: no handle for tenant',\n decoded?.firebase?.tenant,\n )\n return decoded\n }\n await assertIdTokenNotRevoked(pool, decoded)\n return decoded\n }\n }\n if (prop === 'tenantManager') {\n return (...args: unknown[]) => {\n const manager = (\n Reflect.get(auth, prop, auth) as (...a: unknown[]) => object\n ).apply(auth, args)\n return new Proxy(manager, {\n get(mgr, key, mgrReceiver) {\n if (key === 'authForTenant') {\n return (...tenantArgs: unknown[]) =>\n revocationCheckedAuth(\n (\n Reflect.get(mgr, key, mgr) as (...a: unknown[]) => object\n ).apply(mgr, tenantArgs),\n )\n }\n const value = Reflect.get(mgr, key, mgrReceiver)\n return typeof value === 'function' ? value.bind(mgr) : value\n },\n })\n }\n }\n const value = Reflect.get(auth, prop, receiver)\n // Bound to the SDK instance: firebase-admin's Auth keeps private state\n // on `this`, and an unbound method handed out through a Proxy loses it.\n return typeof value === 'function' ? value.bind(auth) : value\n },\n })\n}\n\nfunction wrapApp(app: App) {\n return {\n firestore: () => getFirestore(app, firestoreDatabaseId()),\n auth: () => revocationCheckedAuth(getAuth(app)),\n storage: () => getStorage(app),\n // Release-flag management (AGL-230): the staff admin flags API reads\n // and publishes the Remote Config template server-side.\n remoteConfig: () => getRemoteConfig(app),\n }\n}\n\nfunction firestoreNamespace() {\n return getFirestore(getApp(), firestoreDatabaseId())\n}\nfirestoreNamespace.FieldValue = FieldValue\nfirestoreNamespace.Timestamp = Timestamp\nfirestoreNamespace.FieldPath = FieldPath\n\nconst firebaseAdmin = {\n app: (name?: string) => wrapApp(name ? getApp(name) : getApp()),\n firestore: firestoreNamespace,\n database: () => getDatabase(getApp()),\n}\n\n/**\n * Email-verification gate (AGL-479). Email/password accounts must verify\n * their address before any console access; OAuth accounts arrive with\n * `email_verified: true`, so they pass untouched. `verifyConsoleIdToken`\n * verifies the ID token exactly like `auth().verifyIdToken` and additionally\n * throws `EmailNotVerifiedError` when the address is unverified — verified\n * callers see identical behavior. Route handlers pair the raw verify with\n * `emailUnverifiedResponse()` (a 403) so the denial stays distinct from an\n * invalid-token 401. Fails closed: a token with no `email_verified` claim\n * (e.g. some custom-token sign-ins) is treated as unverified.\n *\n * Exception (AGL-480): staff impersonation sessions carry an `impersonatedBy`\n * claim (minted by /api/admin/impersonate). Staff have already authenticated\n * and the act is audited, so the impersonated account's own verification\n * state must not gate the support session — otherwise staff can't reach a\n * brand-new, still-unverified owner, the exact account most likely to need\n * help. `isImpersonationSession` gates that exemption.\n */\nexport class EmailNotVerifiedError extends Error {\n readonly code = 'auth/email-not-verified'\n constructor() {\n super('Email address not verified')\n this.name = 'EmailNotVerifiedError'\n }\n}\n\nexport function isEmailVerified(decoded: DecodedIdToken): boolean {\n return decoded.email_verified === true\n}\n\n/** Staff impersonation session (AGL-357/AGL-480): token minted with an\n * `impersonatedBy` claim. Exempt from the email-verification gate. */\nexport function isImpersonationSession(decoded: DecodedIdToken): boolean {\n return typeof decoded['impersonatedBy'] === 'string'\n}\n\nexport async function verifyConsoleIdToken(\n idToken: string,\n checkRevoked?: boolean,\n): Promise<DecodedIdToken> {\n // `firebaseAdmin.app().auth()`, not a bare `getAuth` (AGL-1881): the raw\n // SDK handle skips the revocation check, and this helper's whole promise is\n // that it verifies \"exactly like `auth().verifyIdToken`\" plus the email\n // gate. A door that reads as the STRICTER one while being the looser one is\n // the worst shape available.\n const decoded = await firebaseAdmin\n .app()\n .auth()\n .verifyIdToken(idToken, checkRevoked)\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n throw new EmailNotVerifiedError()\n }\n return decoded\n}\n\n/** 403 sent to callers whose email address is not yet verified (AGL-479). */\nexport function emailUnverifiedResponse(): Response {\n return Response.json(\n { error: 'Verify your email to continue', reason: 'email-unverified' },\n { status: 403 },\n )\n}\n\nexport { firebaseAdmin }\nexport default firebaseAdmin\n"],"names":["firestoreDatabaseId","decode","encode","getApp","getAuth","assertIdTokenNotRevoked","getDatabase","getRemoteConfig","FieldPath","FieldValue","Timestamp","getFirestore","getStorage","compress","value","Buffer","from","decompress","hostConverter","toFirestore","data","$id","fromFirestore","snapshot","exists","undefined","id","screenConverter","updatedAt","now","screenVersionConverter","elements","nodes","pluginId","ArrayBuffer","isView","layoutConverter","layoutVersionConverter","revocationPool","auth","decoded","self","tenantId","firebase","tenant","tenantManager","manager","call","authForTenant","revocationCheckedAuth","target","Proxy","get","prop","receiver","args","verify","Reflect","apply","pool","console","warn","mgr","key","mgrReceiver","tenantArgs","bind","wrapApp","app","firestore","storage","remoteConfig","firestoreNamespace","firebaseAdmin","name","database","EmailNotVerifiedError","Error","code","isEmailVerified","email_verified","isImpersonationSession","verifyConsoleIdToken","idToken","checkRevoked","verifyIdToken","emailUnverifiedResponse","Response","json","error","reason","status"],"mappings":"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"}
@@ -0,0 +1,35 @@
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 { FormDatasetBinding } from '@aglyn/aglyn/app-utils/form-dataset-binding';
18
+ /**
19
+ * The token a page carries for one form's binding on one site.
20
+ *
21
+ * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this
22
+ * secret signs; a caller that renders a page decides what an unsigned form
23
+ * does.
24
+ */
25
+ export declare function signFormDatasetBinding(hostId: string, binding: FormDatasetBinding): string;
26
+ /**
27
+ * The binding a token carries, or null when it is not a valid token for
28
+ * `hostId`.
29
+ *
30
+ * Null for anything else at all — not a string, oversized, another version, a
31
+ * signature that does not match, a site other than this one, or no signing
32
+ * secret — because the only safe reading of a binding that cannot be verified
33
+ * is that there is none.
34
+ */
35
+ export declare function verifyFormDatasetBinding(hostId: string, token: unknown): FormDatasetBinding | null;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { createHmac } from "crypto";
18
+ import { tokenSigningSecret } from "./media-signing.js";
19
+ import { safeEqual } from "./safe-equal.js";
20
+ /**
21
+ * A form's dataset binding, signed into the page that renders the form.
22
+ *
23
+ * `/api/forms/submit` is public and unauthenticated, so nothing in its body
24
+ * may choose which dataset a record lands in: a dataset id taken from the body
25
+ * let anyone add rows to any dataset the site can see. The binding is instead
26
+ * read off the form by the compose that renders the page, signed here, and
27
+ * carried by the form; the route writes only where a valid signature says.
28
+ *
29
+ * What a token grants is what the published form already does. Anyone can read
30
+ * one out of the page, and replaying it writes to that form's dataset with
31
+ * that form's field map, for the site it was minted on — nothing a visitor
32
+ * submitting the form could not already do.
33
+ *
34
+ * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`, the secret the
35
+ * other tenant tokens use, under its own `form-dataset-binding:` prefix so it
36
+ * can never be replayed as any other kind of token. No expiry: a published
37
+ * page can be served long after it rendered, and an expired token would drop
38
+ * a live form's records. Rotating the secret, or the version, retires every
39
+ * token at once.
40
+ */ const VERSION = 'v1';
41
+ const PREFIX = 'form-dataset-binding';
42
+ /** Far above any real binding, so an oversized body is refused unparsed. */ const MAX_TOKEN_CHARS = 8192;
43
+ const signature = (payload)=>createHmac('sha256', tokenSigningSecret()).update(`${PREFIX}:${VERSION}:${payload}`).digest('hex');
44
+ /**
45
+ * The token a page carries for one form's binding on one site.
46
+ *
47
+ * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this
48
+ * secret signs; a caller that renders a page decides what an unsigned form
49
+ * does.
50
+ */ export function signFormDatasetBinding(hostId, binding) {
51
+ const body = _extends({
52
+ h: hostId
53
+ }, binding.datasetId ? {
54
+ d: binding.datasetId
55
+ } : {}, binding.datasetName ? {
56
+ n: binding.datasetName
57
+ } : {}, {
58
+ m: binding.fieldMap
59
+ });
60
+ const payload = Buffer.from(JSON.stringify(body)).toString('base64url');
61
+ return `${VERSION}.${payload}.${signature(payload)}`;
62
+ }
63
+ const text = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max) : '';
64
+ /**
65
+ * The binding a token carries, or null when it is not a valid token for
66
+ * `hostId`.
67
+ *
68
+ * Null for anything else at all — not a string, oversized, another version, a
69
+ * signature that does not match, a site other than this one, or no signing
70
+ * secret — because the only safe reading of a binding that cannot be verified
71
+ * is that there is none.
72
+ */ export function verifyFormDatasetBinding(hostId, token) {
73
+ try {
74
+ if (typeof token !== 'string' || token.length > MAX_TOKEN_CHARS) return null;
75
+ const [version, payload, signed, extra] = token.split('.');
76
+ if (version !== VERSION || !payload || !signed || extra !== undefined) {
77
+ return null;
78
+ }
79
+ if (!safeEqual(signed, signature(payload))) return null;
80
+ const body = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
81
+ if (!hostId || (body == null ? void 0 : body.h) !== hostId) return null;
82
+ const datasetId = text(body.d, 128);
83
+ const datasetName = text(body.n, 60);
84
+ if (!datasetId && !datasetName) return null;
85
+ const fieldMap = {};
86
+ if (body.m && typeof body.m === 'object' && !Array.isArray(body.m)) {
87
+ for (const [key, value] of Object.entries(body.m)){
88
+ const submittedKey = text(key, 64);
89
+ const datasetFieldId = text(value, 64);
90
+ if (submittedKey && datasetFieldId) {
91
+ fieldMap[submittedKey] = datasetFieldId;
92
+ }
93
+ }
94
+ }
95
+ return _extends({}, datasetId ? {
96
+ datasetId
97
+ } : {}, datasetName ? {
98
+ datasetName
99
+ } : {}, {
100
+ fieldMap
101
+ });
102
+ } catch (unused) {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ //# sourceMappingURL=form-dataset-binding-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/form-dataset-binding-token.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 type { FormDatasetBinding } from '@aglyn/aglyn/app-utils/form-dataset-binding'\nimport { createHmac } from 'crypto'\nimport { tokenSigningSecret } from './media-signing'\nimport { safeEqual } from './safe-equal'\n\n/**\n * A form's dataset binding, signed into the page that renders the form.\n *\n * `/api/forms/submit` is public and unauthenticated, so nothing in its body\n * may choose which dataset a record lands in: a dataset id taken from the body\n * let anyone add rows to any dataset the site can see. The binding is instead\n * read off the form by the compose that renders the page, signed here, and\n * carried by the form; the route writes only where a valid signature says.\n *\n * What a token grants is what the published form already does. Anyone can read\n * one out of the page, and replaying it writes to that form's dataset with\n * that form's field map, for the site it was minted on — nothing a visitor\n * submitting the form could not already do.\n *\n * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`, the secret the\n * other tenant tokens use, under its own `form-dataset-binding:` prefix so it\n * can never be replayed as any other kind of token. No expiry: a published\n * page can be served long after it rendered, and an expired token would drop\n * a live form's records. Rotating the secret, or the version, retires every\n * token at once.\n */\n\nconst VERSION = 'v1'\nconst PREFIX = 'form-dataset-binding'\n/** Far above any real binding, so an oversized body is refused unparsed. */\nconst MAX_TOKEN_CHARS = 8192\n\ninterface SignedBinding {\n h: string\n d?: string\n n?: string\n m: Record<string, string>\n}\n\nconst signature = (payload: string): string =>\n createHmac('sha256', tokenSigningSecret())\n .update(`${PREFIX}:${VERSION}:${payload}`)\n .digest('hex')\n\n/**\n * The token a page carries for one form's binding on one site.\n *\n * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this\n * secret signs; a caller that renders a page decides what an unsigned form\n * does.\n */\nexport function signFormDatasetBinding(\n hostId: string,\n binding: FormDatasetBinding,\n): string {\n const body: SignedBinding = {\n h: hostId,\n ...(binding.datasetId ? { d: binding.datasetId } : {}),\n ...(binding.datasetName ? { n: binding.datasetName } : {}),\n m: binding.fieldMap,\n }\n const payload = Buffer.from(JSON.stringify(body)).toString('base64url')\n return `${VERSION}.${payload}.${signature(payload)}`\n}\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The binding a token carries, or null when it is not a valid token for\n * `hostId`.\n *\n * Null for anything else at all — not a string, oversized, another version, a\n * signature that does not match, a site other than this one, or no signing\n * secret — because the only safe reading of a binding that cannot be verified\n * is that there is none.\n */\nexport function verifyFormDatasetBinding(\n hostId: string,\n token: unknown,\n): FormDatasetBinding | null {\n try {\n if (typeof token !== 'string' || token.length > MAX_TOKEN_CHARS) return null\n const [version, payload, signed, extra] = token.split('.')\n if (version !== VERSION || !payload || !signed || extra !== undefined) {\n return null\n }\n if (!safeEqual(signed, signature(payload))) return null\n const body = JSON.parse(\n Buffer.from(payload, 'base64url').toString('utf8'),\n ) as Partial<SignedBinding>\n if (!hostId || body?.h !== hostId) return null\n const datasetId = text(body.d, 128)\n const datasetName = text(body.n, 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n if (body.m && typeof body.m === 'object' && !Array.isArray(body.m)) {\n for (const [key, value] of Object.entries(body.m)) {\n const submittedKey = text(key, 64)\n const datasetFieldId = text(value, 64)\n if (submittedKey && datasetFieldId) {\n fieldMap[submittedKey] = datasetFieldId\n }\n }\n }\n return {\n ...(datasetId ? { datasetId } : {}),\n ...(datasetName ? { datasetName } : {}),\n fieldMap,\n }\n } catch {\n return null\n }\n}\n"],"names":["createHmac","tokenSigningSecret","safeEqual","VERSION","PREFIX","MAX_TOKEN_CHARS","signature","payload","update","digest","signFormDatasetBinding","hostId","binding","body","h","datasetId","d","datasetName","n","m","fieldMap","Buffer","from","JSON","stringify","toString","text","value","max","trim","slice","verifyFormDatasetBinding","token","length","version","signed","extra","split","undefined","parse","Array","isArray","key","Object","entries","submittedKey","datasetFieldId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,kBAAkB,QAAQ,qBAAiB;AACpD,SAASC,SAAS,QAAQ,kBAAc;AAExC;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,MAAMC,UAAU;AAChB,MAAMC,SAAS;AACf,0EAA0E,GAC1E,MAAMC,kBAAkB;AASxB,MAAMC,YAAY,CAACC,UACjBP,WAAW,UAAUC,sBAClBO,MAAM,CAAC,GAAGJ,OAAO,CAAC,EAAED,QAAQ,CAAC,EAAEI,SAAS,EACxCE,MAAM,CAAC;AAEZ;;;;;;CAMC,GACD,OAAO,SAASC,uBACdC,MAAc,EACdC,OAA2B;IAE3B,MAAMC,OAAsB;QAC1BC,GAAGH;OACCC,QAAQG,SAAS,GAAG;QAAEC,GAAGJ,QAAQG,SAAS;IAAC,IAAI,CAAC,GAChDH,QAAQK,WAAW,GAAG;QAAEC,GAAGN,QAAQK,WAAW;IAAC,IAAI,CAAC;QACxDE,GAAGP,QAAQQ,QAAQ;;IAErB,MAAMb,UAAUc,OAAOC,IAAI,CAACC,KAAKC,SAAS,CAACX,OAAOY,QAAQ,CAAC;IAC3D,OAAO,GAAGtB,QAAQ,CAAC,EAAEI,QAAQ,CAAC,EAAED,UAAUC,UAAU;AACtD;AAEA,MAAMmB,OAAO,CAACC,OAAgBC,MAC5B,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,KAAK,CAAC,GAAGF,OAAO;AAE3D;;;;;;;;CAQC,GACD,OAAO,SAASG,yBACdpB,MAAc,EACdqB,KAAc;IAEd,IAAI;QACF,IAAI,OAAOA,UAAU,YAAYA,MAAMC,MAAM,GAAG5B,iBAAiB,OAAO;QACxE,MAAM,CAAC6B,SAAS3B,SAAS4B,QAAQC,MAAM,GAAGJ,MAAMK,KAAK,CAAC;QACtD,IAAIH,YAAY/B,WAAW,CAACI,WAAW,CAAC4B,UAAUC,UAAUE,WAAW;YACrE,OAAO;QACT;QACA,IAAI,CAACpC,UAAUiC,QAAQ7B,UAAUC,WAAW,OAAO;QACnD,MAAMM,OAAOU,KAAKgB,KAAK,CACrBlB,OAAOC,IAAI,CAACf,SAAS,aAAakB,QAAQ,CAAC;QAE7C,IAAI,CAACd,UAAUE,CAAAA,wBAAAA,KAAMC,CAAC,MAAKH,QAAQ,OAAO;QAC1C,MAAMI,YAAYW,KAAKb,KAAKG,CAAC,EAAE;QAC/B,MAAMC,cAAcS,KAAKb,KAAKK,CAAC,EAAE;QACjC,IAAI,CAACH,aAAa,CAACE,aAAa,OAAO;QACvC,MAAMG,WAAmC,CAAC;QAC1C,IAAIP,KAAKM,CAAC,IAAI,OAAON,KAAKM,CAAC,KAAK,YAAY,CAACqB,MAAMC,OAAO,CAAC5B,KAAKM,CAAC,GAAG;YAClE,KAAK,MAAM,CAACuB,KAAKf,MAAM,IAAIgB,OAAOC,OAAO,CAAC/B,KAAKM,CAAC,EAAG;gBACjD,MAAM0B,eAAenB,KAAKgB,KAAK;gBAC/B,MAAMI,iBAAiBpB,KAAKC,OAAO;gBACnC,IAAIkB,gBAAgBC,gBAAgB;oBAClC1B,QAAQ,CAACyB,aAAa,GAAGC;gBAC3B;YACF;QACF;QACA,OAAO,aACD/B,YAAY;YAAEA;QAAU,IAAI,CAAC,GAC7BE,cAAc;YAAEA;QAAY,IAAI,CAAC;YACrCG;;IAEJ,EAAE,eAAM;QACN,OAAO;IACT;AACF"}