@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,364 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * The personal profile document, `users/{uid}` (AGL-1127).
19
+ *
20
+ * It holds what Manage Account → Basic info edits (`firstName`, `lastName`,
21
+ * `phoneNumber`, `organization`) plus the avatar the console shows for you.
22
+ * It is NOT an identity source: email and display name for OTHER people are
23
+ * read from the org roster (`orgs/{id}/members/{uid}`), which works for SSO
24
+ * accounts that project-level auth cannot see at all (AGL-1122). Nothing here
25
+ * duplicates them, so there is nothing to drift.
26
+ *
27
+ * Until this existed, no account-creation path wrote the doc — it was born
28
+ * the first time someone saved Basic info, so a fresh account rendered the
29
+ * form against a document that did not exist. Measured on production
30
+ * 2026-07-30: 1 of 3 accounts had one, and the account that did had exactly
31
+ * the four Basic-info fields.
32
+ */ import { splitDisplayName } from "@aglyn/shared-util-tools";
33
+ import { isBlankAddress, normalizeAddress, normalizePhone } from "@aglyn/aglyn/server";
34
+ import { FieldValue } from "firebase-admin/firestore";
35
+ import { getContactSuppression, suppressPhoneContact } from "./contact-suppression.js";
36
+ import firebaseAdmin from "./firebase-admin.js";
37
+ const firestore = ()=>firebaseAdmin.app().firestore();
38
+ /**
39
+ * Create `users/{uid}` if it is missing and prefill the name fields from the
40
+ * identity provider, without ever overwriting what the user has typed.
41
+ *
42
+ * Only ABSENT fields are written, so re-running on every sign-in — which is
43
+ * what the SSO route does — cannot undo an edit: rename yourself in Basic
44
+ * info and the IdP's copy stays out of it.
45
+ *
46
+ * Best-effort by contract. Provisioning access must not fail because a
47
+ * cosmetic prefill did, so the caller is expected to let a rejection through
48
+ * rather than surface it; the profile self-heals on the next sign-in.
49
+ *
50
+ * @returns what the seed actually wrote, for logging and tests.
51
+ */ export async function seedUserProfile(uid, input = {}) {
52
+ var _input_firestore;
53
+ var _input_photoUrl, _input_phoneNumber;
54
+ const ref = ((_input_firestore = input.firestore) != null ? _input_firestore : firestore()).collection('users').doc(uid);
55
+ const snapshot = await ref.get();
56
+ const { firstName, lastName } = splitDisplayName(input.displayName);
57
+ const blank = (value)=>typeof value !== 'string' || !value.trim();
58
+ const seed = {};
59
+ if (firstName && blank(snapshot.get('firstName'))) seed['firstName'] = firstName;
60
+ if (lastName && blank(snapshot.get('lastName'))) seed['lastName'] = lastName;
61
+ // Same absent-only rule: a directory photo fills an empty avatar, and never
62
+ // replaces one somebody uploaded. `photoUrl` here, not `photoURL` — this
63
+ // doc's spelling differs from the roster's, and writing the roster's would
64
+ // add a field Manage Account does not read.
65
+ //
66
+ // THE REMOVAL MARKER (AGL-2486). Absent-only is what protects an avatar
67
+ // somebody SET; it does nothing for one they REMOVED, because removing it
68
+ // is precisely what makes the field absent again. So this seed runs on the
69
+ // next sign-in, finds a blank field and a directory thumbnail, and puts
70
+ // back the picture the person just deleted — from the customer's own IdP,
71
+ // on every sign-in, forever.
72
+ //
73
+ // `propagateMemberPhoto` already refuses this on the roster row, and says
74
+ // why: it clears with `FieldValue.delete()` rather than `''` so the
75
+ // backfill's identical `blank()` cannot read the clear as "never set". The
76
+ // roster half got that treatment and this half did not, and no deletion
77
+ // sentinel can fix it here — an absent field is exactly the state a
78
+ // removal produces, so the two are indistinguishable without a marker.
79
+ //
80
+ // Hence `photoUrlErasedAt`, the same shape as `phoneNumberErasedAt` and
81
+ // `addressErasedAt` above and below, and owner-writable for the same
82
+ // reason: the only party who can clear it is the person it protects, and a
83
+ // person clearing their own marker is a person asking for IdP prefill
84
+ // again. Manage Account drops it on any save that stores a photo, so
85
+ // opting back in is just setting an avatar.
86
+ //
87
+ // Not a privacy control, unlike those two — an avatar is a picture the
88
+ // directory published, not a contact channel. It is a "stop overruling me"
89
+ // control, which is the whole of what "set AND CHANGE your own avatar"
90
+ // requires when a directory is asserting one on every sign-in.
91
+ if (((_input_photoUrl = input.photoUrl) == null ? void 0 : _input_photoUrl.trim()) && blank(snapshot.get('photoUrl'))) {
92
+ if (!snapshot.get('photoUrlErasedAt')) seed['photoUrl'] = input.photoUrl.trim();
93
+ }
94
+ // The phone is the one seeded field with a legal dimension, so it carries
95
+ // two guards the others do not (AGL-1592).
96
+ //
97
+ // THE DEFECT THIS EXISTS TO PREVENT. Privacy Policy v4 §11 says a user may
98
+ // ask us to delete the phone number we hold. Clearing the field is not
99
+ // enough: `/api/auth/sso-jit` and `/api/auth/session` both run this seed on
100
+ // EVERY sign-in, and "absent field" is exactly the condition that makes it
101
+ // write. A naive deletion therefore undoes itself the next time the person
102
+ // signs in, from the customer's IdP, with no trace — the honoured request
103
+ // and the resurrected number look identical in the data.
104
+ //
105
+ // Two keys, because neither one covers the whole defect:
106
+ //
107
+ // - `phoneNumberErasedAt` on this document catches the same account. It is
108
+ // free (the snapshot is already in hand) and it is what runs on the hot
109
+ // path, so the common case adds no read at all.
110
+ // - the number-keyed suppression record catches the SAME PERSON ARRIVING AS
111
+ // A DIFFERENT ACCOUNT — reprovisioned in the IdP, or signing up
112
+ // self-serve alongside their SSO seat. Their new `users/{uid}` has no
113
+ // marker on it, so the per-account key alone would re-store a number
114
+ // somebody asked us to stop holding.
115
+ //
116
+ // The suppression lookup is reached only when we are actually about to write
117
+ // a phone, which is the first sign-in of an account and no other time; an
118
+ // erased account short-circuits on the marker before spending the read.
119
+ //
120
+ // Note which suppressions block this: ONLY `erasePhoneOnFile`. "Stop calling
121
+ // me" is not "stop holding my number" — see contact-suppression.ts — and
122
+ // treating a do-not-call as a deletion request would answer a question the
123
+ // person did not ask.
124
+ if (((_input_phoneNumber = input.phoneNumber) == null ? void 0 : _input_phoneNumber.trim()) && blank(snapshot.get('phoneNumber'))) {
125
+ let refused = Boolean(snapshot.get('phoneNumberErasedAt'));
126
+ if (!refused) {
127
+ try {
128
+ var _input_firestore1;
129
+ const suppression = await getContactSuppression(input.phoneNumber, (_input_firestore1 = input.firestore) != null ? _input_firestore1 : undefined);
130
+ refused = Boolean(suppression == null ? void 0 : suppression.erasePhoneOnFile) && !(suppression == null ? void 0 : suppression.revokedAt);
131
+ } catch (error) {
132
+ // Fail CLOSED, in step with `isPhoneContactSuppressed`. A lookup that
133
+ // throws leaves us unable to say whether this number was erased on
134
+ // request, and re-storing it in that state is the failure this guard
135
+ // exists for. A cosmetic prefill is worth nothing next to it.
136
+ console.error('[user-profiles] suppression lookup failed; not seeding phone', error);
137
+ refused = true;
138
+ }
139
+ }
140
+ if (!refused) seed['phoneNumber'] = input.phoneNumber.trim();
141
+ }
142
+ // The address (AGL-1963), the last of AGL-1133's six and the only one that
143
+ // did not ship — it was written as blocked on AGL-1131 mapping attributes,
144
+ // which completed 2026-08-01, and nothing picked it back up.
145
+ //
146
+ // NORMALIZE BEFORE DECIDING. `normalizeAddress` returns null for an address
147
+ // with nothing usable in it, which is what stops a sparse assertion — a
148
+ // country the IdP spells "United States", and no other field — from storing
149
+ // an object that every `if (address)` in the codebase reads as an address.
150
+ // Checking the raw parts first and normalizing after would write exactly
151
+ // that object.
152
+ //
153
+ // Absent-only, like the rest, but `isBlankAddress` rather than `blank()`:
154
+ // the stored value is a MAP, so the string check the other fields use would
155
+ // call every populated address blank and overwrite it on every sign-in.
156
+ //
157
+ // THE ERASURE GUARD, following the phone's precedent (Privacy Policy v4 §11,
158
+ // AGL-1592). An address is squarely personal data, and `/api/auth/sso-jit`
159
+ // and `/api/auth/session` both run this seed on EVERY sign-in — so "absent
160
+ // field" is the condition that makes it write, and a deletion would undo
161
+ // itself at the next SSO sign-in from the customer's own IdP, leaving the
162
+ // honoured request and the resurrected address identical in the data.
163
+ //
164
+ // ONE key, where the phone needs two, and the asymmetry is the point rather
165
+ // than an omission. `phoneNumberErasedAt` is paired with a NUMBER-KEYED
166
+ // suppression record because a phone number is a routing address: the same
167
+ // person arriving as a different account must not have it re-stored, and
168
+ // the number is the only thing that can recognise them. A postal address is
169
+ // not a channel we contact anyone on and there is no do-not-mail list for it
170
+ // to join, so the per-account marker is the whole of the mechanism here.
171
+ // Adding a second, address-keyed store would retain MORE personal data in
172
+ // the name of erasing it, which is the opposite of what §11 asks for.
173
+ //
174
+ // A STREET LINE IS REQUIRED, and this is the one field where the IdP's copy
175
+ // is held to a stricter rule than a typed one (AGL-1566).
176
+ //
177
+ // AGL-1963 asked that a partial assertion "not produce a half-address that
178
+ // looks populated" and named `normalizeAddress` as the mechanism. It is not
179
+ // sufficient: `normalizeAddress` returns null only when NOTHING survives, so
180
+ // a locality on its own survives, and this seed stored it. That was pinned
181
+ // as a judgement call, on the reasoning that a typed city-only address is
182
+ // stored happily too and a second rule here would give an SSO user and a
183
+ // typing user different profiles from the same input.
184
+ //
185
+ // AGL-1566 is the fact that reverses it. The `Aglyn Console SSO` app maps
186
+ // `city`, `region` and `postalCode` out of the Directory's **Home** address
187
+ // and maps NO street row, so a street-less fragment is not an edge case for
188
+ // this IdP — it is the only address it can ever assert, and the value is
189
+ // residential. The symmetry argument does not survive it either: the typing
190
+ // user CHOSE to enter a city, and the SSO user chose nothing at all. So the
191
+ // two inputs were never the same input.
192
+ //
193
+ // What that leaves is personal data we would receive, store, and have no use
194
+ // for — nothing outside the owner's own Manage Account form reads
195
+ // `users/{uid}.address`, and no tax, billing or shipping path touches it.
196
+ // Under data minimisation the honest answer to "why do we hold this
197
+ // person's home postcode" has to be better than "the directory offered it".
198
+ // For a complete address it is: the person gets a filled-in form instead of
199
+ // an empty one. For a city and a postcode it is nothing, because that is not
200
+ // an address anybody could use.
201
+ //
202
+ // Deliberately NOT pushed into `normalizeAddress` or into `resolveIdpAddress`.
203
+ // The resolver's job is to report what the assertion said, and lying about
204
+ // that would hide the exposure rather than remove it; `normalizeAddress` is
205
+ // shared with Manage Account, where a person storing only a city is their own
206
+ // business. The rule belongs exactly here, at the point where WE decide to
207
+ // persist something nobody asked us to hold.
208
+ const address = normalizeAddress(input.address);
209
+ if ((address == null ? void 0 : address.line1) && isBlankAddress(snapshot.get('address'))) {
210
+ if (!snapshot.get('addressErasedAt')) seed['address'] = address;
211
+ }
212
+ // An existing doc with nothing missing needs no write at all — the common
213
+ // case on every sign-in after the first.
214
+ if (snapshot.exists && !Object.keys(seed).length) {
215
+ return {
216
+ created: false,
217
+ fields: []
218
+ };
219
+ }
220
+ // A doc with no name to seed is still worth creating: it is where the
221
+ // avatar and the notification mutes land, and its absence is what made
222
+ // those read as "this account has none" rather than "not set yet".
223
+ if (!snapshot.exists) seed['createdAt'] = FieldValue.serverTimestamp();
224
+ await ref.set(seed, {
225
+ merge: true
226
+ });
227
+ return {
228
+ created: !snapshot.exists,
229
+ fields: Object.keys(seed).filter((key)=>key !== 'createdAt')
230
+ };
231
+ }
232
+ /**
233
+ * "Delete the phone number you hold for me" — the second half of Privacy
234
+ * Policy v4 §11 (AGL-1592). Deliberately adjacent to `seedUserProfile`,
235
+ * because the two are halves of one invariant: this sets the marker that one
236
+ * reads, and changing either without the other silently restores the defect.
237
+ *
238
+ * WHAT IT DOES, AND WHY IT IS NOT JUST A DELETE
239
+ *
240
+ * 1. Removes `users/{uid}.phoneNumber` outright (`FieldValue.delete()`, not
241
+ * `null` — the request was to stop holding it, and a nulled field still
242
+ * reads as "we have a phone slot for this person").
243
+ * 2. Stamps `phoneNumberErasedAt`, which is what stops the IdP re-asserting
244
+ * it on the next sign-in.
245
+ * 3. Writes a suppression record for the number, with `erasePhoneOnFile`.
246
+ *
247
+ * Step 3 is the one that looks contradictory and is not. Deleting every copy
248
+ * of a number does NOT protect the person from being called — it destroys the
249
+ * only artefact that could recognise them, so the same number arriving later
250
+ * from a customer's CRM, a support ticket, or their own re-typed profile is a
251
+ * number we have never heard of and will happily dial. The minimal retained
252
+ * record is what makes the deletion request mean what the person meant by it.
253
+ * The full reasoning, and the statutory carve-outs it rests on, are in
254
+ * contact-suppression.ts.
255
+ *
256
+ * §11 does not currently say this. It should — see the wording proposed on
257
+ * AGL-1592. Do not resolve the mismatch by making the code forget the number.
258
+ *
259
+ * WHY THE MARKER LIVES ON A CLIENT-WRITABLE DOCUMENT. `users/{uid}` is
260
+ * writable by its owner (and by staff) under the Firestore rules, so this
261
+ * marker is not tamper-proof. It does not need to be: the only party who can
262
+ * clear it is the person it protects, and a person clearing their own erasure
263
+ * marker is a person changing their mind, not an attacker. The adversary here
264
+ * is our own seeding code path, which is server-side and honours it. Moving
265
+ * the marker to a server-only document would buy nothing and cost a second
266
+ * read on every sign-in.
267
+ *
268
+ * A stale marker is harmless: it only ever suppresses an IdP prefill, and a
269
+ * user who types a number back in has a non-blank field, which the seed skips
270
+ * anyway.
271
+ *
272
+ * @param phoneNumber the number to suppress. Defaults to whatever the profile
273
+ * currently holds — pass it explicitly when the request named a number
274
+ * we do not have on file.
275
+ * @returns `suppressed` is the E.164 that went onto the do-not-contact list,
276
+ * or null when there was no recognizable number to record.
277
+ */ export async function forgetUserPhoneNumber(input) {
278
+ var _input_firestore, _input_phoneNumber;
279
+ const db = (_input_firestore = input.firestore) != null ? _input_firestore : firestore();
280
+ const ref = db.collection('users').doc(input.uid);
281
+ const snapshot = await ref.get();
282
+ const onFile = snapshot.exists ? snapshot.get('phoneNumber') : null;
283
+ const target = normalizePhone((_input_phoneNumber = input.phoneNumber) != null ? _input_phoneNumber : onFile);
284
+ // The suppression is written FIRST. If it fails, the number is still on the
285
+ // profile and the request is visibly unfulfilled — which is recoverable by
286
+ // retrying. The other order deletes our only copy of the number and then
287
+ // fails to record it, leaving nothing to retry with.
288
+ let suppressed = null;
289
+ if (target) {
290
+ var _input_recordedByUid, _input_note;
291
+ const result = await suppressPhoneContact(_extends({
292
+ phoneNumber: target,
293
+ source: 'erasure-request',
294
+ uid: input.uid,
295
+ recordedByUid: (_input_recordedByUid = input.recordedByUid) != null ? _input_recordedByUid : null,
296
+ note: (_input_note = input.note) != null ? _input_note : null,
297
+ erasePhoneOnFile: true
298
+ }, input.firestore ? {
299
+ firestore: input.firestore
300
+ } : {}));
301
+ suppressed = result.phoneNumber;
302
+ }
303
+ // The marker is stamped even when no number was recognizable, so a later
304
+ // IdP assertion for this account is refused too. Someone who asks us to
305
+ // stop holding their number has asked about the account, not only about the
306
+ // digits we happened to have at the time.
307
+ await ref.set({
308
+ phoneNumber: FieldValue.delete(),
309
+ phoneNumberErasedAt: FieldValue.serverTimestamp()
310
+ }, {
311
+ merge: true
312
+ });
313
+ return {
314
+ cleared: Boolean(onFile),
315
+ suppressed
316
+ };
317
+ }
318
+ /**
319
+ * "Delete the address you hold for me" (AGL-1963) — the address half of the
320
+ * same §11 invariant `forgetUserPhoneNumber` implements for the phone, and
321
+ * deliberately adjacent to both for the same reason: this sets the marker
322
+ * that `seedUserProfile` reads, and changing either without the other
323
+ * silently restores the defect.
324
+ *
325
+ * A guard whose marker nothing ever writes is decorative, so this exists
326
+ * before anything needs it. Two callers do:
327
+ *
328
+ * - Manage Account, when someone clears their own address. That is the
329
+ * common case by a wide margin — nobody files a support ticket to remove
330
+ * a street address, they empty the field — and without a marker their
331
+ * next SSO sign-in puts it straight back.
332
+ * - Staff, handling an erasure request against an account the person can no
333
+ * longer sign in to.
334
+ *
335
+ * `FieldValue.delete()`, not `null`, on the same reasoning as the phone: the
336
+ * request was to stop holding it, and a nulled field still reads as "we have
337
+ * an address slot for this person".
338
+ *
339
+ * REVERSIBLE BY THE PERSON IT PROTECTS, and only by them. `users/{uid}` is
340
+ * writable by its owner (and by staff) under the Firestore rules, so this
341
+ * marker is not tamper-proof and does not need to be — the adversary it
342
+ * guards against is our own seeding path, which is server-side and honours
343
+ * it. Someone who types an address back in has a non-blank field, which the
344
+ * seed skips anyway — and Manage Account drops the marker on that same save,
345
+ * so opting back into IdP prefill is just filling the field in again.
346
+ */ export async function forgetUserAddress(input) {
347
+ var _input_firestore;
348
+ const db = (_input_firestore = input.firestore) != null ? _input_firestore : firestore();
349
+ const ref = db.collection('users').doc(input.uid);
350
+ const snapshot = await ref.get();
351
+ const onFile = snapshot.exists && !isBlankAddress(snapshot.get('address'));
352
+ await ref.set({
353
+ address: FieldValue.delete(),
354
+ addressErasedAt: FieldValue.serverTimestamp()
355
+ }, {
356
+ merge: true
357
+ });
358
+ return {
359
+ cleared: Boolean(onFile)
360
+ };
361
+ }
362
+ export default seedUserProfile;
363
+
364
+ //# sourceMappingURL=user-profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/user-profiles.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 * The personal profile document, `users/{uid}` (AGL-1127).\n *\n * It holds what Manage Account → Basic info edits (`firstName`, `lastName`,\n * `phoneNumber`, `organization`) plus the avatar the console shows for you.\n * It is NOT an identity source: email and display name for OTHER people are\n * read from the org roster (`orgs/{id}/members/{uid}`), which works for SSO\n * accounts that project-level auth cannot see at all (AGL-1122). Nothing here\n * duplicates them, so there is nothing to drift.\n *\n * Until this existed, no account-creation path wrote the doc — it was born\n * the first time someone saved Basic info, so a fresh account rendered the\n * form against a document that did not exist. Measured on production\n * 2026-07-30: 1 of 3 accounts had one, and the account that did had exactly\n * the four Basic-info fields.\n */\n\nimport { splitDisplayName } from '@aglyn/shared-util-tools'\nimport {\n isBlankAddress,\n normalizeAddress,\n normalizePhone,\n type AglynPostalAddress,\n} from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n getContactSuppression,\n suppressPhoneContact,\n} from './contact-suppression'\nimport firebaseAdmin from './firebase-admin'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nexport interface SeedUserProfileInput {\n /** The provider's single name string, when the assertion carries one. */\n displayName?: string | null\n /**\n * Avatar and phone from the same assertion (AGL-1131). Callers pass these\n * through `resolveIdpPhotoUrl` / `resolveIdpPhone`, which is where the\n * https-only check on the photo lives — this function does not re-validate.\n */\n photoUrl?: string | null\n phoneNumber?: string | null\n /**\n * Postal address from the same assertion (AGL-1963). Callers pass this\n * through `resolveIdpAddress`, whose loose `IdpAddressParts` is structurally\n * an `AglynPostalAddress` — it is the seed below, not the caller, that\n * decides whether the parts amount to an address at all. Stored only when\n * the assertion carries a STREET LINE (AGL-1566): a directory that releases\n * a home city and postcode and no street has given us personal data with no\n * use, not an address.\n */\n address?: AglynPostalAddress | null\n /** Injectable for tests; defaults to the admin app's Firestore. */\n firestore?: any\n}\n\n/**\n * Create `users/{uid}` if it is missing and prefill the name fields from the\n * identity provider, without ever overwriting what the user has typed.\n *\n * Only ABSENT fields are written, so re-running on every sign-in — which is\n * what the SSO route does — cannot undo an edit: rename yourself in Basic\n * info and the IdP's copy stays out of it.\n *\n * Best-effort by contract. Provisioning access must not fail because a\n * cosmetic prefill did, so the caller is expected to let a rejection through\n * rather than surface it; the profile self-heals on the next sign-in.\n *\n * @returns what the seed actually wrote, for logging and tests.\n */\nexport async function seedUserProfile(\n uid: string,\n input: SeedUserProfileInput = {},\n): Promise<{ created: boolean; fields: string[] }> {\n const ref = (input.firestore ?? firestore()).collection('users').doc(uid)\n const snapshot = await ref.get()\n const { firstName, lastName } = splitDisplayName(input.displayName)\n\n const blank = (value: unknown) =>\n typeof value !== 'string' || !value.trim()\n\n const seed: Record<string, unknown> = {}\n if (firstName && blank(snapshot.get('firstName'))) seed['firstName'] = firstName\n if (lastName && blank(snapshot.get('lastName'))) seed['lastName'] = lastName\n // Same absent-only rule: a directory photo fills an empty avatar, and never\n // replaces one somebody uploaded. `photoUrl` here, not `photoURL` — this\n // doc's spelling differs from the roster's, and writing the roster's would\n // add a field Manage Account does not read.\n //\n // THE REMOVAL MARKER (AGL-2486). Absent-only is what protects an avatar\n // somebody SET; it does nothing for one they REMOVED, because removing it\n // is precisely what makes the field absent again. So this seed runs on the\n // next sign-in, finds a blank field and a directory thumbnail, and puts\n // back the picture the person just deleted — from the customer's own IdP,\n // on every sign-in, forever.\n //\n // `propagateMemberPhoto` already refuses this on the roster row, and says\n // why: it clears with `FieldValue.delete()` rather than `''` so the\n // backfill's identical `blank()` cannot read the clear as \"never set\". The\n // roster half got that treatment and this half did not, and no deletion\n // sentinel can fix it here — an absent field is exactly the state a\n // removal produces, so the two are indistinguishable without a marker.\n //\n // Hence `photoUrlErasedAt`, the same shape as `phoneNumberErasedAt` and\n // `addressErasedAt` above and below, and owner-writable for the same\n // reason: the only party who can clear it is the person it protects, and a\n // person clearing their own marker is a person asking for IdP prefill\n // again. Manage Account drops it on any save that stores a photo, so\n // opting back in is just setting an avatar.\n //\n // Not a privacy control, unlike those two — an avatar is a picture the\n // directory published, not a contact channel. It is a \"stop overruling me\"\n // control, which is the whole of what \"set AND CHANGE your own avatar\"\n // requires when a directory is asserting one on every sign-in.\n if (input.photoUrl?.trim() && blank(snapshot.get('photoUrl'))) {\n if (!snapshot.get('photoUrlErasedAt')) seed['photoUrl'] = input.photoUrl.trim()\n }\n // The phone is the one seeded field with a legal dimension, so it carries\n // two guards the others do not (AGL-1592).\n //\n // THE DEFECT THIS EXISTS TO PREVENT. Privacy Policy v4 §11 says a user may\n // ask us to delete the phone number we hold. Clearing the field is not\n // enough: `/api/auth/sso-jit` and `/api/auth/session` both run this seed on\n // EVERY sign-in, and \"absent field\" is exactly the condition that makes it\n // write. A naive deletion therefore undoes itself the next time the person\n // signs in, from the customer's IdP, with no trace — the honoured request\n // and the resurrected number look identical in the data.\n //\n // Two keys, because neither one covers the whole defect:\n //\n // - `phoneNumberErasedAt` on this document catches the same account. It is\n // free (the snapshot is already in hand) and it is what runs on the hot\n // path, so the common case adds no read at all.\n // - the number-keyed suppression record catches the SAME PERSON ARRIVING AS\n // A DIFFERENT ACCOUNT — reprovisioned in the IdP, or signing up\n // self-serve alongside their SSO seat. Their new `users/{uid}` has no\n // marker on it, so the per-account key alone would re-store a number\n // somebody asked us to stop holding.\n //\n // The suppression lookup is reached only when we are actually about to write\n // a phone, which is the first sign-in of an account and no other time; an\n // erased account short-circuits on the marker before spending the read.\n //\n // Note which suppressions block this: ONLY `erasePhoneOnFile`. \"Stop calling\n // me\" is not \"stop holding my number\" — see contact-suppression.ts — and\n // treating a do-not-call as a deletion request would answer a question the\n // person did not ask.\n if (input.phoneNumber?.trim() && blank(snapshot.get('phoneNumber'))) {\n let refused = Boolean(snapshot.get('phoneNumberErasedAt'))\n if (!refused) {\n try {\n const suppression = await getContactSuppression(\n input.phoneNumber,\n input.firestore ?? undefined,\n )\n refused = Boolean(suppression?.erasePhoneOnFile) && !suppression?.revokedAt\n } catch (error) {\n // Fail CLOSED, in step with `isPhoneContactSuppressed`. A lookup that\n // throws leaves us unable to say whether this number was erased on\n // request, and re-storing it in that state is the failure this guard\n // exists for. A cosmetic prefill is worth nothing next to it.\n console.error(\n '[user-profiles] suppression lookup failed; not seeding phone',\n error,\n )\n refused = true\n }\n }\n if (!refused) seed['phoneNumber'] = input.phoneNumber.trim()\n }\n\n // The address (AGL-1963), the last of AGL-1133's six and the only one that\n // did not ship — it was written as blocked on AGL-1131 mapping attributes,\n // which completed 2026-08-01, and nothing picked it back up.\n //\n // NORMALIZE BEFORE DECIDING. `normalizeAddress` returns null for an address\n // with nothing usable in it, which is what stops a sparse assertion — a\n // country the IdP spells \"United States\", and no other field — from storing\n // an object that every `if (address)` in the codebase reads as an address.\n // Checking the raw parts first and normalizing after would write exactly\n // that object.\n //\n // Absent-only, like the rest, but `isBlankAddress` rather than `blank()`:\n // the stored value is a MAP, so the string check the other fields use would\n // call every populated address blank and overwrite it on every sign-in.\n //\n // THE ERASURE GUARD, following the phone's precedent (Privacy Policy v4 §11,\n // AGL-1592). An address is squarely personal data, and `/api/auth/sso-jit`\n // and `/api/auth/session` both run this seed on EVERY sign-in — so \"absent\n // field\" is the condition that makes it write, and a deletion would undo\n // itself at the next SSO sign-in from the customer's own IdP, leaving the\n // honoured request and the resurrected address identical in the data.\n //\n // ONE key, where the phone needs two, and the asymmetry is the point rather\n // than an omission. `phoneNumberErasedAt` is paired with a NUMBER-KEYED\n // suppression record because a phone number is a routing address: the same\n // person arriving as a different account must not have it re-stored, and\n // the number is the only thing that can recognise them. A postal address is\n // not a channel we contact anyone on and there is no do-not-mail list for it\n // to join, so the per-account marker is the whole of the mechanism here.\n // Adding a second, address-keyed store would retain MORE personal data in\n // the name of erasing it, which is the opposite of what §11 asks for.\n //\n // A STREET LINE IS REQUIRED, and this is the one field where the IdP's copy\n // is held to a stricter rule than a typed one (AGL-1566).\n //\n // AGL-1963 asked that a partial assertion \"not produce a half-address that\n // looks populated\" and named `normalizeAddress` as the mechanism. It is not\n // sufficient: `normalizeAddress` returns null only when NOTHING survives, so\n // a locality on its own survives, and this seed stored it. That was pinned\n // as a judgement call, on the reasoning that a typed city-only address is\n // stored happily too and a second rule here would give an SSO user and a\n // typing user different profiles from the same input.\n //\n // AGL-1566 is the fact that reverses it. The `Aglyn Console SSO` app maps\n // `city`, `region` and `postalCode` out of the Directory's **Home** address\n // and maps NO street row, so a street-less fragment is not an edge case for\n // this IdP — it is the only address it can ever assert, and the value is\n // residential. The symmetry argument does not survive it either: the typing\n // user CHOSE to enter a city, and the SSO user chose nothing at all. So the\n // two inputs were never the same input.\n //\n // What that leaves is personal data we would receive, store, and have no use\n // for — nothing outside the owner's own Manage Account form reads\n // `users/{uid}.address`, and no tax, billing or shipping path touches it.\n // Under data minimisation the honest answer to \"why do we hold this\n // person's home postcode\" has to be better than \"the directory offered it\".\n // For a complete address it is: the person gets a filled-in form instead of\n // an empty one. For a city and a postcode it is nothing, because that is not\n // an address anybody could use.\n //\n // Deliberately NOT pushed into `normalizeAddress` or into `resolveIdpAddress`.\n // The resolver's job is to report what the assertion said, and lying about\n // that would hide the exposure rather than remove it; `normalizeAddress` is\n // shared with Manage Account, where a person storing only a city is their own\n // business. The rule belongs exactly here, at the point where WE decide to\n // persist something nobody asked us to hold.\n const address = normalizeAddress(input.address)\n if (address?.line1 && isBlankAddress(snapshot.get('address'))) {\n if (!snapshot.get('addressErasedAt')) seed['address'] = address\n }\n\n // An existing doc with nothing missing needs no write at all — the common\n // case on every sign-in after the first.\n if (snapshot.exists && !Object.keys(seed).length) {\n return { created: false, fields: [] }\n }\n // A doc with no name to seed is still worth creating: it is where the\n // avatar and the notification mutes land, and its absence is what made\n // those read as \"this account has none\" rather than \"not set yet\".\n if (!snapshot.exists) seed['createdAt'] = FieldValue.serverTimestamp()\n\n await ref.set(seed, { merge: true })\n return {\n created: !snapshot.exists,\n fields: Object.keys(seed).filter((key) => key !== 'createdAt'),\n }\n}\n\n/**\n * \"Delete the phone number you hold for me\" — the second half of Privacy\n * Policy v4 §11 (AGL-1592). Deliberately adjacent to `seedUserProfile`,\n * because the two are halves of one invariant: this sets the marker that one\n * reads, and changing either without the other silently restores the defect.\n *\n * WHAT IT DOES, AND WHY IT IS NOT JUST A DELETE\n *\n * 1. Removes `users/{uid}.phoneNumber` outright (`FieldValue.delete()`, not\n * `null` — the request was to stop holding it, and a nulled field still\n * reads as \"we have a phone slot for this person\").\n * 2. Stamps `phoneNumberErasedAt`, which is what stops the IdP re-asserting\n * it on the next sign-in.\n * 3. Writes a suppression record for the number, with `erasePhoneOnFile`.\n *\n * Step 3 is the one that looks contradictory and is not. Deleting every copy\n * of a number does NOT protect the person from being called — it destroys the\n * only artefact that could recognise them, so the same number arriving later\n * from a customer's CRM, a support ticket, or their own re-typed profile is a\n * number we have never heard of and will happily dial. The minimal retained\n * record is what makes the deletion request mean what the person meant by it.\n * The full reasoning, and the statutory carve-outs it rests on, are in\n * contact-suppression.ts.\n *\n * §11 does not currently say this. It should — see the wording proposed on\n * AGL-1592. Do not resolve the mismatch by making the code forget the number.\n *\n * WHY THE MARKER LIVES ON A CLIENT-WRITABLE DOCUMENT. `users/{uid}` is\n * writable by its owner (and by staff) under the Firestore rules, so this\n * marker is not tamper-proof. It does not need to be: the only party who can\n * clear it is the person it protects, and a person clearing their own erasure\n * marker is a person changing their mind, not an attacker. The adversary here\n * is our own seeding code path, which is server-side and honours it. Moving\n * the marker to a server-only document would buy nothing and cost a second\n * read on every sign-in.\n *\n * A stale marker is harmless: it only ever suppresses an IdP prefill, and a\n * user who types a number back in has a non-blank field, which the seed skips\n * anyway.\n *\n * @param phoneNumber the number to suppress. Defaults to whatever the profile\n * currently holds — pass it explicitly when the request named a number\n * we do not have on file.\n * @returns `suppressed` is the E.164 that went onto the do-not-contact list,\n * or null when there was no recognizable number to record.\n */\nexport async function forgetUserPhoneNumber(input: {\n uid: string\n phoneNumber?: string | null\n /** Staff member who took the request, for the suppression record. */\n recordedByUid?: string | null\n note?: string | null\n firestore?: any\n}): Promise<{ cleared: boolean; suppressed: string | null }> {\n const db = input.firestore ?? firestore()\n const ref = db.collection('users').doc(input.uid)\n const snapshot = await ref.get()\n const onFile = snapshot.exists ? snapshot.get('phoneNumber') : null\n const target = normalizePhone(input.phoneNumber ?? onFile)\n\n // The suppression is written FIRST. If it fails, the number is still on the\n // profile and the request is visibly unfulfilled — which is recoverable by\n // retrying. The other order deletes our only copy of the number and then\n // fails to record it, leaving nothing to retry with.\n let suppressed: string | null = null\n if (target) {\n const result = await suppressPhoneContact({\n phoneNumber: target,\n source: 'erasure-request',\n uid: input.uid,\n recordedByUid: input.recordedByUid ?? null,\n note: input.note ?? null,\n erasePhoneOnFile: true,\n ...(input.firestore ? { firestore: input.firestore } : {}),\n })\n suppressed = result.phoneNumber\n }\n\n // The marker is stamped even when no number was recognizable, so a later\n // IdP assertion for this account is refused too. Someone who asks us to\n // stop holding their number has asked about the account, not only about the\n // digits we happened to have at the time.\n await ref.set(\n {\n phoneNumber: FieldValue.delete(),\n phoneNumberErasedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n return { cleared: Boolean(onFile), suppressed }\n}\n\n/**\n * \"Delete the address you hold for me\" (AGL-1963) — the address half of the\n * same §11 invariant `forgetUserPhoneNumber` implements for the phone, and\n * deliberately adjacent to both for the same reason: this sets the marker\n * that `seedUserProfile` reads, and changing either without the other\n * silently restores the defect.\n *\n * A guard whose marker nothing ever writes is decorative, so this exists\n * before anything needs it. Two callers do:\n *\n * - Manage Account, when someone clears their own address. That is the\n * common case by a wide margin — nobody files a support ticket to remove\n * a street address, they empty the field — and without a marker their\n * next SSO sign-in puts it straight back.\n * - Staff, handling an erasure request against an account the person can no\n * longer sign in to.\n *\n * `FieldValue.delete()`, not `null`, on the same reasoning as the phone: the\n * request was to stop holding it, and a nulled field still reads as \"we have\n * an address slot for this person\".\n *\n * REVERSIBLE BY THE PERSON IT PROTECTS, and only by them. `users/{uid}` is\n * writable by its owner (and by staff) under the Firestore rules, so this\n * marker is not tamper-proof and does not need to be — the adversary it\n * guards against is our own seeding path, which is server-side and honours\n * it. Someone who types an address back in has a non-blank field, which the\n * seed skips anyway — and Manage Account drops the marker on that same save,\n * so opting back into IdP prefill is just filling the field in again.\n */\nexport async function forgetUserAddress(input: {\n uid: string\n firestore?: any\n}): Promise<{ cleared: boolean }> {\n const db = input.firestore ?? firestore()\n const ref = db.collection('users').doc(input.uid)\n const snapshot = await ref.get()\n const onFile = snapshot.exists && !isBlankAddress(snapshot.get('address'))\n await ref.set(\n {\n address: FieldValue.delete(),\n addressErasedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n return { cleared: Boolean(onFile) }\n}\n\nexport default seedUserProfile\n"],"names":["splitDisplayName","isBlankAddress","normalizeAddress","normalizePhone","FieldValue","getContactSuppression","suppressPhoneContact","firebaseAdmin","firestore","app","seedUserProfile","uid","input","ref","collection","doc","snapshot","get","firstName","lastName","displayName","blank","value","trim","seed","photoUrl","phoneNumber","refused","Boolean","suppression","undefined","erasePhoneOnFile","revokedAt","error","console","address","line1","exists","Object","keys","length","created","fields","serverTimestamp","set","merge","filter","key","forgetUserPhoneNumber","db","onFile","target","suppressed","result","source","recordedByUid","note","delete","phoneNumberErasedAt","cleared","forgetUserAddress","addressErasedAt"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Is `signature` the drain signature for `rawBody` under `secret`?
19
+ *
20
+ * `secret` is passed in rather than read from the environment here so the
21
+ * caller owns the "unset means reject" decision visibly, and so the test can
22
+ * prove rejection without mutating `process.env`.
23
+ */
24
+ export declare function isValidDrainSignature(rawBody: string, signature: string | null | undefined, secret: string | undefined): boolean;
25
+ /**
26
+ * The signature Vercel would send for this body — test and tooling helper.
27
+ * Exported so the spec signs its fixtures the way the real sender does
28
+ * instead of restating the algorithm, which would let both drift together.
29
+ */
30
+ export declare function drainSignatureFor(rawBody: string, secret: string): string;
@@ -0,0 +1,61 @@
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
+ * `x-vercel-signature` verification for the log-drain receiver (AGL-1921).
18
+ *
19
+ * Its own module, with no dependency beyond `node:crypto`, so the security
20
+ * boundary can be tested without a Firebase app, a credential or a fetch —
21
+ * and so nothing in the ingest path can be imported without it.
22
+ *
23
+ * Vercel documents the header as `hmac-sha1(rawBody, drainSecret)` in hex
24
+ * (docs/drains/security). Two consequences that are easy to get wrong:
25
+ *
26
+ * 1. **RAW body.** `await request.text()` BEFORE any parse. Parsing and
27
+ * re-serializing changes key order and whitespace and will not reproduce
28
+ * the digest, and the failure looks like a wrong secret rather than a bug.
29
+ * 2. **Timing-safe compare.** A plain `===` on a hex digest leaks the digest
30
+ * a byte at a time to an attacker who can post repeatedly, which for an
31
+ * endpoint whose URL is discoverable is the entire attack.
32
+ *
33
+ * FAILS CLOSED at every branch: no secret configured, no header, wrong
34
+ * length, wrong value — all false. There is no configuration of this function
35
+ * under which an unsigned body is accepted.
36
+ */ import { createHmac, timingSafeEqual } from "node:crypto";
37
+ /**
38
+ * Is `signature` the drain signature for `rawBody` under `secret`?
39
+ *
40
+ * `secret` is passed in rather than read from the environment here so the
41
+ * caller owns the "unset means reject" decision visibly, and so the test can
42
+ * prove rejection without mutating `process.env`.
43
+ */ export function isValidDrainSignature(rawBody, signature, secret) {
44
+ if (!secret) return false;
45
+ if (!signature) return false;
46
+ const expected = createHmac('sha1', secret).update(rawBody, 'utf8').digest('hex');
47
+ const provided = signature.trim().toLowerCase();
48
+ // `timingSafeEqual` THROWS on a length mismatch, which would be a 500 and a
49
+ // length oracle at once; compare lengths first and reject.
50
+ if (provided.length !== expected.length) return false;
51
+ return timingSafeEqual(Buffer.from(provided, 'utf8'), Buffer.from(expected, 'utf8'));
52
+ }
53
+ /**
54
+ * The signature Vercel would send for this body — test and tooling helper.
55
+ * Exported so the spec signs its fixtures the way the real sender does
56
+ * instead of restating the algorithm, which would let both drift together.
57
+ */ export function drainSignatureFor(rawBody, secret) {
58
+ return createHmac('sha1', secret).update(rawBody, 'utf8').digest('hex');
59
+ }
60
+
61
+ //# sourceMappingURL=vercel-drain-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/vercel-drain-signature.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 * `x-vercel-signature` verification for the log-drain receiver (AGL-1921).\n *\n * Its own module, with no dependency beyond `node:crypto`, so the security\n * boundary can be tested without a Firebase app, a credential or a fetch —\n * and so nothing in the ingest path can be imported without it.\n *\n * Vercel documents the header as `hmac-sha1(rawBody, drainSecret)` in hex\n * (docs/drains/security). Two consequences that are easy to get wrong:\n *\n * 1. **RAW body.** `await request.text()` BEFORE any parse. Parsing and\n * re-serializing changes key order and whitespace and will not reproduce\n * the digest, and the failure looks like a wrong secret rather than a bug.\n * 2. **Timing-safe compare.** A plain `===` on a hex digest leaks the digest\n * a byte at a time to an attacker who can post repeatedly, which for an\n * endpoint whose URL is discoverable is the entire attack.\n *\n * FAILS CLOSED at every branch: no secret configured, no header, wrong\n * length, wrong value — all false. There is no configuration of this function\n * under which an unsigned body is accepted.\n */\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\n/**\n * Is `signature` the drain signature for `rawBody` under `secret`?\n *\n * `secret` is passed in rather than read from the environment here so the\n * caller owns the \"unset means reject\" decision visibly, and so the test can\n * prove rejection without mutating `process.env`.\n */\nexport function isValidDrainSignature(\n rawBody: string,\n signature: string | null | undefined,\n secret: string | undefined,\n): boolean {\n if (!secret) return false\n if (!signature) return false\n const expected = createHmac('sha1', secret)\n .update(rawBody, 'utf8')\n .digest('hex')\n const provided = signature.trim().toLowerCase()\n // `timingSafeEqual` THROWS on a length mismatch, which would be a 500 and a\n // length oracle at once; compare lengths first and reject.\n if (provided.length !== expected.length) return false\n return timingSafeEqual(\n Buffer.from(provided, 'utf8'),\n Buffer.from(expected, 'utf8'),\n )\n}\n\n/**\n * The signature Vercel would send for this body — test and tooling helper.\n * Exported so the spec signs its fixtures the way the real sender does\n * instead of restating the algorithm, which would let both drift together.\n */\nexport function drainSignatureFor(rawBody: string, secret: string): string {\n return createHmac('sha1', secret).update(rawBody, 'utf8').digest('hex')\n}\n"],"names":["createHmac","timingSafeEqual","isValidDrainSignature","rawBody","signature","secret","expected","update","digest","provided","trim","toLowerCase","length","Buffer","from","drainSignatureFor"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,SAASA,UAAU,EAAEC,eAAe,QAAQ,cAAa;AAEzD;;;;;;CAMC,GACD,OAAO,SAASC,sBACdC,OAAe,EACfC,SAAoC,EACpCC,MAA0B;IAE1B,IAAI,CAACA,QAAQ,OAAO;IACpB,IAAI,CAACD,WAAW,OAAO;IACvB,MAAME,WAAWN,WAAW,QAAQK,QACjCE,MAAM,CAACJ,SAAS,QAChBK,MAAM,CAAC;IACV,MAAMC,WAAWL,UAAUM,IAAI,GAAGC,WAAW;IAC7C,4EAA4E;IAC5E,2DAA2D;IAC3D,IAAIF,SAASG,MAAM,KAAKN,SAASM,MAAM,EAAE,OAAO;IAChD,OAAOX,gBACLY,OAAOC,IAAI,CAACL,UAAU,SACtBI,OAAOC,IAAI,CAACR,UAAU;AAE1B;AAEA;;;;CAIC,GACD,OAAO,SAASS,kBAAkBZ,OAAe,EAAEE,MAAc;IAC/D,OAAOL,WAAW,QAAQK,QAAQE,MAAM,CAACJ,SAAS,QAAQK,MAAM,CAAC;AACnE"}