@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 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/rate-limit-store.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 { deploymentRegion } from '@aglyn/aglyn/app-utils/deployment-shape'\nimport { createHash } from 'crypto'\n// The atomic counter primitive for the AGL-1921 server-error markers below.\n// Commutative server-side, so instances converging on one minute's document\n// neither contend nor retry — see `flushServerErrors`.\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from './firebase-admin'\nimport {\n checkRateLimit,\n DEFAULT_RATE_LIMIT,\n DEFAULT_RATE_WINDOW_MS,\n type RateLimitResult,\n} from './api-http'\n\n/**\n * Durable, cross-instance rate limiting (AGL-794).\n *\n * Every limiter in the codebase was a per-instance `Map`, each carrying its\n * own \"best-effort, serverless instances are ephemeral\" caveat. On Vercel that\n * is close to no limit at all: the counter resets on every cold start and each\n * concurrent instance keeps its own, so the effective cap is roughly\n * `limit × instances` and an attacker can widen it just by going wider. That\n * is tolerable for blunting accidental bursts; it is not a brute-force\n * defense.\n *\n * This backs the same fixed-window shape with a Firestore counter, so the cap\n * is global. `api-http.ts` deliberately stays pure — it has no imports and is\n * unit-tested directly — so the storage-backed variant lives here instead of\n * being bolted onto it.\n *\n * **Cost is the reason this isn't the default everywhere.** Each call is a\n * transaction (one read + one write). That is the right trade for a password\n * unlock attempt; it is the wrong trade for an analytics beacon, which can\n * fire on every page view. Use `checkRateLimit` for volume, this for\n * consequence.\n */\n\n/** Collection holding one document per (key, window). Server-writes only. */\nexport const RATE_LIMIT_COLLECTION = 'rateLimits'\n\n/**\n * Document-id prefix for degradation markers (AGL-1679).\n *\n * `degraded: true` used to exist only in a `console.error`, which means a\n * Firestore blip silently dropped every durable limiter — auth, password\n * reset, and now the public REST API's per-key quota — back to a per-instance\n * cap for as long as it lasted, and nobody found out. Fail-soft is only a\n * defensible choice if someone can tell that it fired.\n *\n * This is deliberately not an alerting stack. It is the cheapest thing that\n * makes a degraded window answerable after the fact: when an episode ends,\n * the instance writes one summary document into the SAME collection, so it\n * inherits the deny-all rule and the `expiresAt` TTL policy that already\n * exist rather than needing a new collection, a rules deploy and a second TTL\n * policy. Ids are minute-bucketed so concurrent instances converge on a\n * handful of documents:\n *\n * ```\n * rateLimits/degraded_1755100800000\n * ```\n *\n * Written on RECOVERY, never during the outage — the store is unreachable\n * exactly when the episode is happening, so a marker written then would be\n * the one write guaranteed to fail.\n */\nexport const DEGRADATION_DOC_PREFIX = 'degraded_'\n\n/** Marker id granularity, and the log re-notice interval for a long episode. */\nconst DEGRADATION_BUCKET_MS = 60_000\n\n/** How long a marker survives the TTL sweep — long enough to look back. */\nconst DEGRADATION_RETENTION_MS = 30 * 24 * 60 * 60 * 1000\n\ninterface DegradationEpisode {\n /** Calls that fell back to the in-process limiter. */\n count: number\n firstAtMs: number\n lastAtMs: number\n /** Last failure's code/message, truncated. */\n code: string\n /** Last minute we logged, so a sustained outage cannot flood the log. */\n lastLoggedBucket: number\n}\n\n/**\n * The episode currently in progress on THIS instance, or null when healthy.\n * Module-scoped on purpose: it is a per-instance observation being reported,\n * and there is nowhere durable to keep it while the durable store is down.\n */\nlet episode: DegradationEpisode | null = null\n\n/** Short, stable error code for the marker and the log line. */\nfunction failureCode(error: unknown): string {\n const code = (error as { code?: unknown })?.code\n if (typeof code === 'string' || typeof code === 'number') return String(code)\n return String((error as { message?: unknown })?.message ?? 'unknown').slice(\n 0,\n 120,\n )\n}\n\n/**\n * Record one fallback. Logs at the start of an episode and at most once a\n * minute after that — the public REST API now calls this path on every\n * request, so a per-call `console.error` during a Firestore outage would be a\n * log flood that buries the signal it is meant to be.\n */\nfunction noteDegradation(nowMs: number, error: unknown): void {\n const bucket = Math.floor(nowMs / DEGRADATION_BUCKET_MS)\n if (!episode) {\n episode = {\n count: 1,\n firstAtMs: nowMs,\n lastAtMs: nowMs,\n code: failureCode(error),\n lastLoggedBucket: bucket,\n }\n console.error(\n '[rate-limit] degraded: durable store unavailable, falling back to the per-instance cap',\n { code: episode.code, atMs: nowMs },\n )\n return\n }\n episode.count += 1\n episode.lastAtMs = nowMs\n episode.code = failureCode(error)\n if (bucket > episode.lastLoggedBucket) {\n episode.lastLoggedBucket = bucket\n console.error('[rate-limit] degraded: still falling back', {\n code: episode.code,\n count: episode.count,\n sinceMs: episode.firstAtMs,\n })\n }\n}\n\n/**\n * An episode ended: close it out and leave a durable record of the window.\n *\n * Fire-and-forget and best-effort — this is a diagnostic, and failing a\n * customer's request because the postmortem breadcrumb could not be written\n * would be a worse bug than the one it documents. The transaction shape\n * matches the counter above so a marker merges cleanly when several instances\n * recover into the same minute.\n */\nfunction flushDegradation(firestore: any, nowMs: number): void {\n const ended = episode\n if (!ended) return\n // Cleared BEFORE the await so a concurrent recovery cannot flush it twice.\n episode = null\n console.error('[rate-limit] recovered from degraded window', {\n count: ended.count,\n firstAtMs: ended.firstAtMs,\n lastAtMs: ended.lastAtMs,\n code: ended.code,\n })\n\n const bucketStart =\n Math.floor(ended.firstAtMs / DEGRADATION_BUCKET_MS) * DEGRADATION_BUCKET_MS\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${DEGRADATION_DOC_PREFIX}${bucketStart}`)\n\n void firestore\n .runTransaction(async (tx: any) => {\n const snapshot = await tx.get(ref)\n const priorCalls = (snapshot.exists ? snapshot.get('calls') : 0) ?? 0\n const priorEpisodes = (snapshot.exists ? snapshot.get('episodes') : 0) ?? 0\n const priorFirst = snapshot.exists ? snapshot.get('firstAtMs') : undefined\n const priorLast = snapshot.exists ? snapshot.get('lastAtMs') : undefined\n tx.set(\n ref,\n {\n calls: priorCalls + ended.count,\n episodes: priorEpisodes + 1,\n firstAtMs: Math.min(priorFirst ?? ended.firstAtMs, ended.firstAtMs),\n lastAtMs: Math.max(priorLast ?? ended.lastAtMs, ended.lastAtMs),\n code: ended.code,\n region: deploymentRegion(),\n // Same TTL field the counters use, so the policy already configured\n // on this collection sweeps these too — just far later.\n expiresAt: new Date(nowMs + DEGRADATION_RETENTION_MS),\n },\n { merge: true },\n )\n })\n .catch(() => undefined)\n}\n\n/**\n * The degradation episode in progress on this instance, or `null`. Exposed\n * for tests and for any future health surface; callers must not treat it as a\n * global view — it only ever describes the instance that answers.\n */\nexport function currentRateLimitDegradation(): Readonly<DegradationEpisode> | null {\n return episode\n}\n\n/** Test seam: forget any in-progress episode. */\nexport function resetRateLimitDegradationForTests(): void {\n episode = null\n}\n\n/**\n * Document-id prefix for signup-refusal markers (AGL-1907).\n *\n * `/api/orgs/create` has been rate-limited since AGL-1534 (3/h per uid, 10/h\n * per IP) and AGL-1536 watches org-creation VOLUME — but volume only counts\n * the signups that SUCCEEDED. A scripted farm that trips the limiter is\n * therefore invisible in exactly the moment it is being contained: the 429s\n * are the attack's signature and nothing recorded them. Before Sep 1 that is\n * the difference between \"the limiter held\" and \"we have no idea whether it\n * was ever tested\".\n *\n * Written into the SAME `rateLimits` collection as the counters and the\n * AGL-1679 degradation markers, for the same reason that one gave: it inherits\n * the deny-all security rule and the `expiresAt` TTL policy that already\n * exist, instead of needing a new collection, a rules deploy and a second TTL\n * policy. Minute-bucketed so concurrent instances converge:\n *\n * ```\n * rateLimits/signupRefused_1755100800000\n * ```\n *\n * **The timestamp field is `refusedAtMs`, deliberately not `lastAtMs`.** The\n * AGL-1693 rate-limiter health probe queries this collection with\n * `where('lastAtMs', '>=', cutoff).orderBy('lastAtMs','desc').limit(N)`. A\n * refusal marker carrying `lastAtMs` would be picked up by that range and,\n * under a flood, could fill the limit and push the real degradation markers\n * out of the result — silently blinding a sibling alarm. A distinct field name\n * keeps the two queries disjoint at the index level rather than relying on the\n * id-prefix filter that runs after the read.\n */\nexport const SIGNUP_REFUSAL_DOC_PREFIX = 'signupRefused_'\n\n/** Marker id granularity. Matches the degradation markers. */\nconst SIGNUP_REFUSAL_BUCKET_MS = 60_000\n\n/**\n * How long a refusal marker survives the TTL sweep. Shorter than the 30 days\n * degradation markers get: these can be written once a minute under sustained\n * pressure, and the question they answer (\"was there a wave, and when\") is\n * asked in the days after launch, not the month after.\n */\nconst SIGNUP_REFUSAL_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * What refused. Bounded on purpose — this is a public health body.\n *\n * `uid` and `ip` are this limiter's two caps. `locked` and `unreadable` are\n * written by the `beforeUserCreated` blocking function in `cloud/functions`,\n * which refuses on the Identity Platform path before any Aglyn route runs\n * and therefore never reaches `consumeRateLimit` (AGL-2583). It cannot import\n * this module — it is a plain npm package outside the workspace — so it\n * writes the same document shape by hand and\n * `apps/console/specs/signup-refusal-marker-wiring.spec.ts` fails if the two\n * ever disagree about the collection, the prefix or the fields.\n */\nexport type SignupRefusalReason = 'uid' | 'ip' | 'locked' | 'unreadable'\n\n/**\n * Record one refused org-creation attempt.\n *\n * Fire-and-forget and best-effort, like `flushDegradation`: refusing the\n * request is the control, and failing a 429 because its breadcrumb could not\n * be written would be strictly worse than not having the breadcrumb. Callers\n * must not await this on the response path.\n *\n * **On the write-per-refusal cost.** A refused request is free to the attacker\n * and now costs one transaction — the usual amplification objection. It does\n * not apply here in kind, only in degree: `/api/orgs/create` already spends\n * TWO `consumeRateLimit` transactions on every hit including the refused ones\n * (AGL-1534 counts over-limit attempts by design), so this is a third write on\n * a path that was already three, not a new class of cost. If it ever shows up\n * on the bill the fix is in-process coalescing like `episode` above, not\n * dropping the signal.\n *\n * Nothing identifying is stored. The counts are per-reason only; the uid and\n * IP that were refused stay in the (hashed) limiter keys, which this never\n * reads.\n */\nexport function recordSignupRefusal(\n reason: SignupRefusalReason,\n options?: { now?: number; firestore?: any },\n): void {\n const nowMs = options?.now ?? Date.now()\n const bucketStart =\n Math.floor(nowMs / SIGNUP_REFUSAL_BUCKET_MS) * SIGNUP_REFUSAL_BUCKET_MS\n let firestore: any\n try {\n firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n } catch {\n // No Admin app (a unit test, a misconfigured instance). The refusal still\n // happened and the caller still refuses; only the breadcrumb is lost.\n return\n }\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${SIGNUP_REFUSAL_DOC_PREFIX}${bucketStart}`)\n\n void firestore\n .runTransaction(async (tx: any) => {\n const snapshot = await tx.get(ref)\n const priorTotal = (snapshot.exists ? snapshot.get('refusals') : 0) ?? 0\n const priorByReason =\n (snapshot.exists ? snapshot.get('byReason') : undefined) ?? {}\n const priorFirst = snapshot.exists\n ? snapshot.get('firstRefusedAtMs')\n : undefined\n tx.set(\n ref,\n {\n refusals: priorTotal + 1,\n byReason: {\n ...priorByReason,\n [reason]: ((priorByReason as any)?.[reason] ?? 0) + 1,\n },\n firstRefusedAtMs: Math.min(priorFirst ?? nowMs, nowMs),\n // NOT `lastAtMs` — see the prefix doc above.\n refusedAtMs: Math.max(\n (snapshot.exists ? snapshot.get('refusedAtMs') : 0) ?? 0,\n nowMs,\n ),\n expiresAt: new Date(nowMs + SIGNUP_REFUSAL_RETENTION_MS),\n },\n { merge: true },\n )\n })\n .catch(() => undefined)\n}\n\n/**\n * Document-id prefix for server-error markers (AGL-1921).\n *\n * ## Why a Firestore marker when the errors already go to Cloud Logging\n *\n * `reportServerError` forwards every `onRequestError` to a `server-errors` log\n * in `aglyn-main`, which is the right sink for triage — Error Reporting groups\n * them and a log-match policy can page on them. It is not a sink anything in\n * this repo can READ. Measured 2026-08-24 against the production credential:\n * `POST logging.googleapis.com/v2/entries:list` for that log answers\n * **403 `Permission denied for all log views`**, because the firebase-admin\n * service account can create log entries and cannot list them. So the only\n * reader the log has is a GCP alert policy that does not exist yet, and\n * creating one is the click, not a thing that ships with a commit.\n *\n * That is the AGL-2486 shape exactly — a detector written into a place nothing\n * watches. This marker is the second copy of the count, in a store we can\n * already read, so `/api/health/server-errors` can grade it and the readers\n * that already exist (the 15-minute GitHub uptime probe, the external keyword\n * monitors, `docs.aglyn.com/status`) become the listeners on day one.\n *\n * Written into the SAME `rateLimits` collection as the counters, the AGL-1679\n * degradation markers and the AGL-1907 refusal markers, for the reason those\n * two gave: it inherits the deny-all security rule and the `expiresAt` TTL\n * policy that already exist, instead of needing a new collection, a rules\n * deploy and a second TTL policy — i.e. instead of needing a console action\n * nobody can take from code. Minute bucketed so concurrent instances\n * converge:\n *\n * ```\n * rateLimits/serverError_1755100800000\n * ```\n *\n * **The timestamp field is `erroredAtMs`**, deliberately neither `lastAtMs`\n * (AGL-1679's) nor `refusedAtMs` (AGL-1907's), for the reason spelled out on\n * `SIGNUP_REFUSAL_DOC_PREFIX`: each health probe range-queries its own field,\n * and a shared field would let one signal's flood fill another's read limit\n * and silently blind it. Three signals, three disjoint indexes.\n */\nexport const SERVER_ERROR_DOC_PREFIX = 'serverError_'\n\n/** Marker id granularity. Matches both sibling marker kinds. */\nconst SERVER_ERROR_BUCKET_MS = 60_000\n\n/**\n * How long a server-error marker survives the TTL sweep. Seven days, matching\n * the refusal markers rather than the degradation markers' thirty: these can\n * be written every few seconds under a spike, and the question they answer\n * (\"was there a spike, when, and on which deployment\") is asked in the days\n * after an incident, not the month after.\n */\nconst SERVER_ERROR_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * Minimum spacing between marker writes from ONE instance (AGL-1921).\n *\n * The failure being watched for is a SPIKE, and a spike is exactly when an\n * unbounded recorder turns one incident into a billing incident — the same\n * argument that gave `reportServerError` its per-minute Logging budget. A\n * write per error would also be a write per error onto a SINGLE document,\n * which is where Firestore's per-document write ceiling lives and where\n * AGL-2404's contention storm came from.\n *\n * So errors coalesce in process and land at most once every five seconds per\n * instance — twelve writes a minute at the very worst, against a threshold\n * measured in single digits over half an hour. The count is never rounded\n * down: what is pending is added to the next flush, and a bucket rollover\n * flushes what it was holding before the new bucket starts.\n *\n * **The first error of a window is written IMMEDIATELY** (`lastFlushAtMs`\n * starts at 0), so a lone error is durable within a second and the alarm\n * never waits on a second one to become visible.\n */\nconst SERVER_ERROR_FLUSH_INTERVAL_MS = 5_000\n\ninterface ServerErrorAccumulator {\n /** The minute bucket these pending errors belong to. */\n bucketStart: number\n /** Errors observed and not yet written. */\n pending: number\n /** Pending split by deployment — `console-web`, `tenant-web`. */\n pendingByService: Record<string, number>\n /** When this instance last wrote, so the spacing above can be enforced. */\n lastFlushAtMs: number\n /** The newest error in the bucket, which becomes `erroredAtMs`. */\n lastErrorAtMs: number\n}\n\n/**\n * The errors this instance is holding, or null when it is holding none.\n * Module-scoped for the same reason `episode` above is: it is a per-instance\n * observation on its way to a durable place.\n */\nlet serverErrors: ServerErrorAccumulator | null = null\n\n/**\n * Write what the accumulator is holding, and forget it.\n *\n * Fire-and-forget and best-effort, like `flushDegradation`: this runs while a\n * request is already failing, and a monitoring breadcrumb must never be the\n * reason a second thing breaks.\n *\n * `FieldValue.increment` with a merge, rather than the read-modify-write\n * transaction the two sibling markers use: increments are commutative\n * server-side, so several instances converging on one minute's document\n * neither contend nor retry — which is the AGL-2416 lesson applied to a path\n * that only ever runs during an incident.\n */\nfunction flushServerErrors(firestore: any, nowMs: number): void {\n const held = serverErrors\n if (!held || held.pending <= 0) return\n // Zeroed BEFORE the await so a concurrent flush cannot write them twice.\n const errors = held.pending\n const byService = held.pendingByService\n const erroredAtMs = held.lastErrorAtMs\n const bucketStart = held.bucketStart\n held.pending = 0\n held.pendingByService = {}\n held.lastFlushAtMs = nowMs\n\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${SERVER_ERROR_DOC_PREFIX}${bucketStart}`)\n const byServiceIncrements: Record<string, unknown> = {}\n for (const [service, count] of Object.entries(byService)) {\n byServiceIncrements[service] = FieldValue.increment(count)\n }\n void Promise.resolve(\n ref.set(\n {\n errors: FieldValue.increment(errors),\n byService: byServiceIncrements,\n // NOT `lastAtMs`, NOT `refusedAtMs` — see the prefix doc above.\n erroredAtMs,\n expiresAt: new Date(nowMs + SERVER_ERROR_RETENTION_MS),\n },\n { merge: true },\n ),\n ).catch(() => undefined)\n}\n\n/**\n * Record one uncaught server-side error, for `/api/health/server-errors`.\n *\n * Called from `reportServerError`, which is called from each app's\n * `onRequestError` hook — so this counts exactly what that hook can see: an\n * uncaught throw in a render or a route handler. It does NOT see an error that\n * kills the process first, a platform-level 5xx (function timeout, OOM,\n * cold-start 502), or anything thrown in the edge runtime. Those need the\n * Vercel log drain; `docs/UPTIME_AND_SLA.md` carries the list.\n *\n * Fire-and-forget and never throws. Nothing identifying is stored — a count\n * and which deployment produced it. The message, the stack and the route\n * pattern stay in the Logging entry, which is not public; this marker is read\n * by an endpoint that is.\n */\nexport function recordServerError(\n service: string,\n options?: { now?: number; firestore?: any },\n): void {\n const nowMs = options?.now ?? Date.now()\n const bucketStart =\n Math.floor(nowMs / SERVER_ERROR_BUCKET_MS) * SERVER_ERROR_BUCKET_MS\n let firestore: any\n try {\n firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n } catch {\n // No Admin app (a unit test, a misconfigured instance). The error still\n // happened and the caller still reports it to Logging; only the count is\n // lost — and a deployment that cannot reach Firestore at all is already\n // red on `/api/health`'s own `firestore` check.\n return\n }\n\n // A rollover flushes what the OLD bucket was holding before anything is\n // added to the new one, so a minute's count is never smeared into the next.\n if (serverErrors && serverErrors.bucketStart !== bucketStart) {\n flushServerErrors(firestore, nowMs)\n serverErrors = null\n }\n if (!serverErrors) {\n serverErrors = {\n bucketStart,\n pending: 0,\n pendingByService: {},\n // 0, so the first error of a bucket is written immediately.\n lastFlushAtMs: 0,\n lastErrorAtMs: nowMs,\n }\n }\n serverErrors.pending += 1\n serverErrors.pendingByService[service] =\n (serverErrors.pendingByService[service] ?? 0) + 1\n serverErrors.lastErrorAtMs = Math.max(serverErrors.lastErrorAtMs, nowMs)\n\n if (nowMs - serverErrors.lastFlushAtMs >= SERVER_ERROR_FLUSH_INTERVAL_MS) {\n flushServerErrors(firestore, nowMs)\n }\n}\n\n/**\n * Errors this instance is holding but has not written yet. Exposed for tests\n * and for anyone reasoning about the coalescing window; callers must not treat\n * it as a global view — it only ever describes the instance that answers.\n */\nexport function pendingServerErrors(): number {\n return serverErrors?.pending ?? 0\n}\n\n/** Test seam: forget anything held. */\nexport function resetServerErrorsForTests(): void {\n serverErrors = null\n}\n\n/**\n * Document-id prefix for the beacon's last-landed marker (AGL-2713).\n *\n * ## Why this one is not bucketed, and not queried\n *\n * Its four siblings above count OCCURRENCES over a window, so each one wants\n * a stream of minute-bucketed documents and a range query over its own\n * timestamp field. This answers a single question — *when did a heartbeat\n * last reach Cloud Logging from this deployment* — so it is one document per\n * service, read by id:\n *\n * ```\n * rateLimits/beaconHeartbeat_console-web\n * rateLimits/beaconHeartbeat_tenant-web\n * ```\n *\n * A point read needs no index at all, which matters more than the cost: an\n * index this repository cannot deploy without a token is an index that is not\n * there when the failure is. Same collection as the siblings, for the reason\n * they each gave — it inherits the deny-all rule and the `expiresAt` TTL\n * policy that already exist, instead of needing a rules deploy nobody can\n * make from code.\n *\n * **The timestamp field is `heartbeatAtMs`**, deliberately none of `lastAtMs`\n * (AGL-1679), `refusedAtMs` (AGL-1907), `erroredAtMs` (AGL-1921) or\n * `servedAtMs` (AGL-2583). This one is not range-queried, so the isolation\n * argument the others make is not what earns it a field of its own — but a\n * document carrying a sibling's field would enter that sibling's window and be\n * counted as an episode it is not.\n *\n * ⚠️ **Evidence of a LANDING, never of a failure.** Nothing writes here when\n * a heartbeat misses. So the worst a corrupt, stale or unreadable marker can\n * do is make `/api/health/error-beacon` red sooner, and a deployment whose\n * credential has never once worked has no marker at all and reds on its first\n * probe. That asymmetry is what lets the door tolerate a miss without becoming\n * a door that cannot go red.\n */\nexport const BEACON_HEARTBEAT_DOC_PREFIX = 'beaconHeartbeat_'\n\n/**\n * How long a last-landed marker survives the TTL sweep.\n *\n * Seven days, matching the refusal and server-error markers. It is refreshed\n * on every successful probe, so the only way it ages out is a deployment that\n * stopped answering entirely — by which time the door has been red for days\n * and the marker's disappearance changes nothing.\n */\nconst BEACON_HEARTBEAT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\n\n/** How long the failure path waits for the marker before giving up on it. */\nconst BEACON_HEARTBEAT_READ_BUDGET_MS = 2_000\n\n/**\n * Record that a heartbeat reached Cloud Logging (AGL-2713).\n *\n * Fire-and-forget and never throws, like `flushServerErrors`: this runs\n * inside a monitoring probe, and a breadcrumb must never be the reason the\n * probe it is attached to fails. A write that does not land simply means the\n * next miss is graded without it, which is the safe direction.\n *\n * The caller is memoized per instance on a five-minute TTL, so the write rate\n * is one per instance per five minutes however hard the public endpoint is\n * hit — orders of magnitude under the per-document ceiling that produced\n * AGL-2404's contention storm.\n */\nexport function recordBeaconHeartbeat(\n service: string,\n options?: { now?: number; firestore?: any },\n): void {\n const nowMs = options?.now ?? Date.now()\n let firestore: any\n try {\n firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n } catch {\n // No Admin app. The heartbeat still landed and the probe still reports\n // green off the write itself; only the evidence for forgiving a FUTURE\n // miss is lost, and a miss with no evidence reds.\n return\n }\n void Promise.resolve(\n firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${BEACON_HEARTBEAT_DOC_PREFIX}${service}`)\n .set(\n {\n // NOT `lastAtMs`, NOT `refusedAtMs`, NOT `erroredAtMs`, NOT\n // `servedAtMs` — see the prefix doc above.\n heartbeatAtMs: nowMs,\n service,\n expiresAt: new Date(nowMs + BEACON_HEARTBEAT_RETENTION_MS),\n },\n { merge: true },\n ),\n ).catch(() => undefined)\n}\n\n/**\n * When a heartbeat last landed for this deployment, or null (AGL-2713).\n *\n * Null covers three cases that must all grade the same way: no marker has\n * ever been written, the document holds nothing usable, and the store could\n * not be read inside its budget. Every one of them means *there is no proof a\n * heartbeat landed recently*, and the caller forgives a miss only on proof.\n * Folding them is therefore not a shortcut — distinguishing them could only\n * ever be used to forgive something unproven.\n *\n * The budget is the reason this is not a bare `get()`. It runs on the failure\n * path, where the credential is already misbehaving, and a Firestore client\n * whose own token will not mint retries with backoff for far longer than a\n * health endpoint may take to answer.\n */\nexport async function readBeaconHeartbeat(\n service: string,\n options?: { firestore?: any; budgetMs?: number },\n): Promise<number | null> {\n try {\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await withBudget<any>(\n firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${BEACON_HEARTBEAT_DOC_PREFIX}${service}`)\n .get(),\n options?.budgetMs ?? BEACON_HEARTBEAT_READ_BUDGET_MS,\n )\n const landedAtMs = snapshot.get('heartbeatAtMs')\n return typeof landedAtMs === 'number' && Number.isFinite(landedAtMs)\n ? landedAtMs\n : null\n } catch {\n return null\n }\n}\n\n/**\n * Document-id prefix for signup-page serve markers (AGL-2583).\n *\n * ## Why traffic is worth storing at all\n *\n * `/api/health/signups` counts orgs created in the trailing hour and goes red\n * when there are TOO MANY. Zero — the reading that means nobody on earth can\n * sign up — is its healthiest possible score, and that is how AGL-2581 refused\n * every account creation for three days with the signup monitor green\n * throughout. The missing half is a DENOMINATOR: zero accounts is an outage\n * only if people were trying, and a quiet night otherwise.\n *\n * These markers are that denominator. Every render of the signup page fetches\n * `/api/lockdown-status?feature=signups` to decide whether to show the paused\n * notice, so that route is the one server touch a real arrival always makes,\n * and counting it needs no new endpoint, no client beacon and no analytics\n * vendor in the alerting path.\n *\n * Written into the SAME `rateLimits` collection as the counters and the three\n * sibling marker kinds, for the reason they each gave: it inherits the\n * deny-all security rule and the `expiresAt` TTL policy that already exist,\n * instead of needing a new collection, a rules deploy and a second TTL policy.\n * Minute bucketed so concurrent instances converge:\n *\n * ```\n * rateLimits/signupServed_1755100800000\n * ```\n *\n * **The timestamp field is `servedAtMs`**, deliberately none of `lastAtMs`\n * (AGL-1679), `refusedAtMs` (AGL-1907) or `erroredAtMs` (AGL-1921), for the\n * reason spelled out on `SIGNUP_REFUSAL_DOC_PREFIX`: each health probe\n * range-queries its own field, and a shared field would let one signal's flood\n * fill another's read limit and silently blind it. Four signals, four disjoint\n * indexes.\n */\nexport const SIGNUP_SERVED_DOC_PREFIX = 'signupServed_'\n\n/** Marker id granularity. Matches every sibling marker kind. */\nconst SIGNUP_SERVED_BUCKET_MS = 60_000\n\n/**\n * How long a serve marker survives the TTL sweep. Seven days, matching the\n * refusal and server-error markers: the question these answer (\"were people\n * arriving while nothing was being created\") is asked during an incident and\n * in the days after it, never a month later.\n */\nconst SIGNUP_SERVED_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * Minimum spacing between marker writes from ONE instance.\n *\n * The same argument `SERVER_ERROR_FLUSH_INTERVAL_MS` makes, for the same\n * reason: this counts a PUBLIC page's traffic, so an unbounded recorder turns\n * a marketing campaign into a write-per-visitor on a single hot document —\n * which is where Firestore's per-document ceiling lives and where AGL-2404's\n * contention storm came from. Serves coalesce in process and land at most once\n * every five seconds per instance.\n *\n * **The first serve of a bucket is written IMMEDIATELY** (`lastFlushAtMs`\n * starts at 0), so the denominator is never behind the numerator: an hour that\n * saw traffic says so within a second of the first arrival.\n */\nconst SIGNUP_SERVED_FLUSH_INTERVAL_MS = 5_000\n\ninterface SignupServedAccumulator {\n /** The minute bucket these pending serves belong to. */\n bucketStart: number\n /** Serves observed and not yet written. */\n pending: number\n /** When this instance last wrote, so the spacing above can be enforced. */\n lastFlushAtMs: number\n /** The newest serve in the bucket, which becomes `servedAtMs`. */\n lastServedAtMs: number\n}\n\n/**\n * The serves this instance is holding, or null when it is holding none.\n * Module-scoped for the same reason its siblings are: a per-instance\n * observation on its way to a durable place.\n */\nlet signupServes: SignupServedAccumulator | null = null\n\n/**\n * Write what the accumulator is holding, and forget it.\n *\n * Fire-and-forget and best-effort: this runs on the signup page's own\n * critical path, and a monitoring breadcrumb must never be the reason the\n * page that sells the product gets slower or fails.\n *\n * `FieldValue.increment` with a merge, like the server-error marker: server\n * side increments are commutative, so instances converging on one minute's\n * document neither contend nor retry.\n */\nfunction flushSignupServes(firestore: any, nowMs: number): void {\n const held = signupServes\n if (!held || held.pending <= 0) return\n // Zeroed BEFORE the write so a concurrent flush cannot count them twice.\n const serves = held.pending\n const servedAtMs = held.lastServedAtMs\n const bucketStart = held.bucketStart\n held.pending = 0\n held.lastFlushAtMs = nowMs\n\n try {\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${SIGNUP_SERVED_DOC_PREFIX}${bucketStart}`)\n void Promise.resolve(\n ref.set(\n {\n serves: FieldValue.increment(serves),\n // NOT `lastAtMs`, NOT `refusedAtMs`, NOT `erroredAtMs` — see the\n // prefix doc above.\n servedAtMs,\n expiresAt: new Date(nowMs + SIGNUP_SERVED_RETENTION_MS),\n },\n { merge: true },\n ),\n ).catch(() => undefined)\n } catch {\n // A Firestore handle that throws SYNCHRONOUSLY — a half-initialized admin\n // app, a stub in a test — must not escape onto the signup page's request.\n // The serve is lost from the denominator, which can only make the drought\n // alarm quieter, never louder.\n }\n}\n\n/**\n * Record one serving of the signup page, for `/api/health/signup-volume`.\n *\n * Nothing identifying is stored — a count and a timestamp. No IP, no user\n * agent, no referrer, no campaign: this marker is read by an endpoint that is\n * public, and the drought verdict needs a number, not an audience.\n *\n * Never throws, and callers must not await it.\n */\nexport function recordSignupServed(options?: {\n now?: number\n firestore?: any\n}): void {\n const nowMs = options?.now ?? Date.now()\n const bucketStart =\n Math.floor(nowMs / SIGNUP_SERVED_BUCKET_MS) * SIGNUP_SERVED_BUCKET_MS\n let firestore: any\n try {\n firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n } catch {\n // No Admin app (a unit test, a misconfigured instance). The page still\n // serves; only the denominator loses a tick, and a deployment that cannot\n // reach Firestore at all is already red on `/api/health`.\n return\n }\n\n // A rollover flushes what the OLD bucket was holding before anything is\n // added to the new one, so a minute's count is never smeared into the next.\n if (signupServes && signupServes.bucketStart !== bucketStart) {\n flushSignupServes(firestore, nowMs)\n signupServes = null\n }\n if (!signupServes) {\n signupServes = {\n bucketStart,\n pending: 0,\n // 0, so the first serve of a bucket is written immediately.\n lastFlushAtMs: 0,\n lastServedAtMs: nowMs,\n }\n }\n signupServes.pending += 1\n signupServes.lastServedAtMs = Math.max(signupServes.lastServedAtMs, nowMs)\n\n if (nowMs - signupServes.lastFlushAtMs >= SIGNUP_SERVED_FLUSH_INTERVAL_MS) {\n flushSignupServes(firestore, nowMs)\n }\n}\n\n/**\n * Serves this instance is holding but has not written yet. Exposed for tests\n * and for anyone reasoning about the coalescing window; it only ever describes\n * the instance that answers.\n */\nexport function pendingSignupServes(): number {\n return signupServes?.pending ?? 0\n}\n\n/** Test seam: forget anything held. */\nexport function resetSignupServesForTests(): void {\n signupServes = null\n}\n\n/**\n * Document-id prefix for org-creation ATTEMPT markers (AGL-2714).\n *\n * ## Why serves were the wrong denominator\n *\n * The drought verdict beside this one asks \"did traffic arrive and no account\n * come out\", and it took its denominator from `signupServed_` — a hit on\n * `/api/lockdown-status?feature=signups`, which the signup page fetches on\n * every render. That counts LOOKERS. Crawlers, link previews, a person\n * reading the plan names and closing the tab, and the diagnostic page load\n * somebody makes while investigating the alert itself all land in it.\n *\n * At this platform's conversion rate an hour with a handful of arrivals and\n * no account is the ordinary case, so the check could not tell a door that\n * does not open from a quiet afternoon — and it paged as though the service\n * were down. No threshold fixes that; the quantity is wrong.\n *\n * An ATTEMPT is somebody who authenticated, filled the form and asked for an\n * org. If those arrive and no org appears, something is broken, and the\n * question has an answer that does not depend on how many people were merely\n * browsing.\n *\n * ## Why this is stronger than the refusal marker beside it\n *\n * `signupRefusal_` counts requests this route TURNED AWAY, which is a\n * deliberate outcome the code chose. An attempt marker is written before any\n * outcome is known, so the pair spans what refusals cannot see on their own:\n * a request that 500s, hangs, or dies in the platform between the limiter and\n * the write leaves an attempt and no org, and no refusal at all.\n *\n * **The field is `attemptedAtMs`**, deliberately none of `lastAtMs`\n * (AGL-1679), `refusedAtMs` (AGL-1907), `erroredAtMs` (AGL-1921) or\n * `servedAtMs` (AGL-2583) — same rule the four before it follow: each health\n * probe range-queries its own field, and a shared field would let one\n * signal's flood fill another's read limit and silently blind it.\n */\nexport const SIGNUP_ATTEMPT_DOC_PREFIX = 'signupAttempted_'\n\n/** Marker id granularity. Matches every sibling marker kind. */\nconst SIGNUP_ATTEMPT_BUCKET_MS = 60_000\n\n/**\n * How long an attempt marker survives the TTL sweep. Seven days, matching the\n * serve and refusal markers it is read beside — the same incident asks all\n * three, and a denominator that outlived its numerator would answer nothing.\n */\nconst SIGNUP_ATTEMPT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * Record one attempt to create an org, for `/api/health/signup-volume`.\n *\n * Written per attempt with no in-process coalescing, unlike the serve marker.\n * The serve marker guards a PUBLIC page whose traffic is unbounded; this one\n * sits behind an authenticated route the limiter already caps at 3 per uid and\n * 10 per IP an hour, so the write rate is bounded by the limiter rather than\n * by the internet. It is also the third write on a path that already spends\n * two `consumeRateLimit` transactions per hit, which is the same accounting\n * `recordSignupRefusal` sets out.\n *\n * `FieldValue.increment` with a merge rather than a transaction: increments\n * are commutative server-side, so instances converging on one minute's\n * document neither contend nor retry.\n *\n * Nothing identifying is stored — a count and a timestamp. This marker is read\n * by an endpoint that is public, and the drought verdict needs a number, not\n * an audience.\n *\n * Never throws, and callers must not await it: creating the org is the\n * control, and a monitoring breadcrumb must never be the reason a signup gets\n * slower or fails.\n */\nexport function recordSignupAttempt(options?: {\n now?: number\n firestore?: any\n}): void {\n const nowMs = options?.now ?? Date.now()\n const bucketStart =\n Math.floor(nowMs / SIGNUP_ATTEMPT_BUCKET_MS) * SIGNUP_ATTEMPT_BUCKET_MS\n let firestore: any\n try {\n firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n } catch {\n // No Admin app (a unit test, a misconfigured instance). The signup still\n // proceeds; only the denominator loses a tick. Losing an attempt can only\n // make the drought alarm quieter, never louder — the opposite bias to the\n // one that would matter here.\n return\n }\n try {\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(`${SIGNUP_ATTEMPT_DOC_PREFIX}${bucketStart}`)\n void Promise.resolve(\n ref.set(\n {\n attempts: FieldValue.increment(1),\n // NOT `lastAtMs`, NOT `refusedAtMs`, NOT `erroredAtMs`, NOT\n // `servedAtMs` — see the prefix doc above.\n attemptedAtMs: nowMs,\n expiresAt: new Date(nowMs + SIGNUP_ATTEMPT_RETENTION_MS),\n },\n { merge: true },\n ),\n ).catch(() => undefined)\n } catch {\n // A Firestore handle that throws SYNCHRONOUSLY — a half-initialized admin\n // app, a stub in a test — must not escape onto the signup request.\n }\n}\n\n/**\n * Wall-clock budget for one durable counter round trip (AGL-2404).\n *\n * Without a bound this function could not fail at all — it could only hang.\n * The counter used to be a read-modify-write `runTransaction` on a single hot\n * document: it contended, retried with backoff, and on a contended key\n * routinely outran the platform's function ceiling, so the request died as a\n * **504 with no body and no `Retry-After`** instead of the cheap 429 the\n * limiter exists to produce. Measured against production on 2026-08-19 at\n * `/api/protection/unlock`: sequential traffic refused cleanly at the 10th\n * attempt, while **two** concurrent requests on one key were already enough\n * to produce a 504 at ~10.3 s — the account-default function ceiling, since\n * that route declares no `maxDuration`.\n *\n * AGL-2416 removed the read-modify-write (see {@link consumeRateLimit}), so\n * the retry storm this bound was catching should no longer occur. The bound\n * STAYS: it is what makes the function answerable at all, and a store that is\n * slow for any other reason — a hot document at Firestore's own single-doc\n * write ceiling, a network stall — must still produce a decision rather than\n * a held function.\n *\n * 2.5 s is chosen to sit far below the smallest ceiling any caller runs at\n * (10 s at the Vercel account default) while leaving room for the two round\n * trips below, which measured ~0.6 s end to end on the same endpoint.\n */\nexport const RATE_LIMIT_TRANSACTION_BUDGET_MS = 2_500\n\n/**\n * Thrown when the durable counter could not reach a decision inside\n * {@link RATE_LIMIT_TRANSACTION_BUDGET_MS}. Distinct from a store *error* on\n * purpose — see the classification note on {@link consumeRateLimit}.\n */\nexport class RateLimitContentionError extends Error {\n constructor(message = 'rate-limit counter contended') {\n super(message)\n this.name = 'RateLimitContentionError'\n }\n}\n\n/**\n * gRPC status codes that mean \"the store is up, this document is hot\":\n * `ABORTED` (10) is what Firestore returns when a transaction loses its\n * optimistic race too many times, and `DEADLINE_EXCEEDED` (4) is the same\n * condition observed as a timeout. Neither is an outage, so neither may take\n * the fail-soft path — degrading on contention would let a caller widen its\n * own cap just by going concurrent, which is the exact defect AGL-794 closed.\n */\nconst CONTENTION_CODES = new Set([4, 10])\n\nfunction isContention(error: unknown): boolean {\n if (error instanceof RateLimitContentionError) return true\n const code = (error as { code?: unknown })?.code\n return typeof code === 'number' && CONTENTION_CODES.has(code)\n}\n\n/**\n * Resolve `work`, or reject with {@link RateLimitContentionError} once\n * `budgetMs` elapses.\n *\n * The losing promise is explicitly silenced: an abandoned transaction that\n * rejects later would otherwise surface as an unhandled rejection and, on a\n * strict runtime, take the instance down — turning a contention fix into an\n * availability bug.\n */\nasync function withBudget<T>(work: Promise<T>, budgetMs: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined\n work.catch(() => undefined)\n try {\n return await Promise.race([\n work,\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new RateLimitContentionError()), budgetMs)\n }),\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/**\n * Add one to `ref`'s counter and report the total (AGL-2416).\n *\n * Two round trips, in this order and no other. The write MUST land before the\n * read: reading first and writing second is a read-modify-write again, only\n * without the transaction that made it safe, which would under-count under\n * concurrency and let a caller widen its own cap by racing itself.\n *\n * `merge: true` creates the document on the first request of a window —\n * `increment` on a missing field starts from zero — so no separate create is\n * needed, and `windowStartMs`/`expiresAt` are re-stamped idempotently on every\n * write rather than only on the first.\n *\n * A read-back that carries no usable count means the store accepted a write\n * and then failed to show it. That is a malfunction, not contention, so it\n * throws a plain error and takes the fail-SOFT path — the same posture as any\n * other store failure. Returning `0` instead would read as \"no requests yet\"\n * and admit every caller: fail-open, from the one branch that exists because\n * the store is misbehaving.\n */\nasync function countOne(\n ref: any,\n windowStartMs: number,\n resetMs: number,\n windowMs: number,\n): Promise<number> {\n await ref.set(\n {\n count: firebaseAdmin.firestore.FieldValue.increment(1),\n windowStartMs,\n // For a Firestore TTL policy on `expiresAt` — without one these\n // documents accumulate forever. See docs/RATE_LIMITING.md.\n expiresAt: new Date(resetMs + windowMs),\n },\n { merge: true },\n )\n const snapshot = await ref.get()\n const observed = Number(snapshot?.get?.('count'))\n if (!Number.isFinite(observed) || observed < 1) {\n throw new Error('rate-limit counter read-back returned no count')\n }\n return observed\n}\n\nexport interface DurableRateLimitOptions {\n limit?: number\n windowMs?: number\n now?: number\n /** Injectable for tests; defaults to the Admin SDK's Firestore. */\n firestore?: any\n /** Injectable for tests; defaults to {@link RATE_LIMIT_TRANSACTION_BUDGET_MS}. */\n budgetMs?: number\n}\n\nexport interface DurableRateLimitResult extends RateLimitResult {\n /**\n * True when the durable store was unreachable and the in-memory limiter\n * answered instead — the cap held for this instance only.\n */\n degraded: boolean\n /**\n * True when the counter could not be read and written inside its budget\n * because the key is contended, and the request was refused on that basis\n * rather than on a counted overage (AGL-2404). Always `false` on an\n * allowed result.\n */\n contended: boolean\n}\n\n/**\n * Document id for a (key, window) pair.\n *\n * The key is hashed rather than embedded: callers key on client IPs, and an\n * IP is personal data that would otherwise sit in plaintext document ids\n * (which also show up in any index export). Hashing additionally makes the id\n * safe — Firestore ids may not contain `/`, which IPv6-mapped and\n * path-derived keys can. Truncated to 32 hex chars: collisions would merely\n * merge two callers into one bucket, and 128 bits is far past needing that.\n */\nfunction bucketId(key: string, windowStartMs: number): string {\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 32)\n return `${hash}_${windowStartMs}`\n}\n\n/**\n * Counts one request against `key`'s fixed window, globally.\n *\n * Fails *soft, not open*: if Firestore is unreachable the in-memory limiter\n * answers and the result is flagged `degraded`. Failing fully open would let\n * an attacker disable brute-force protection by inducing a storage error;\n * failing fully closed would lock legitimate visitors out of a site because of\n * an unrelated Firestore blip. Degrading to the per-instance cap keeps some\n * protection and keeps the site usable, and says which happened.\n *\n * ## Two failures, two postures (AGL-2404)\n *\n * \"Firestore is unreachable\" and \"this one document is contended\" are not the\n * same event and must not get the same answer:\n *\n * - **The store is down** (fast error — `UNAVAILABLE`, a missing app, a\n * credential failure). Fail SOFT, exactly as before: degrade to the\n * per-instance cap and flag `degraded`. A real visitor must not lose access\n * to a customer's site over an unrelated blip.\n * - **The key is contended** (`ABORTED`, `DEADLINE_EXCEEDED`, or the budget\n * above elapsing). Fail CLOSED: refuse with `contended: true` and let the\n * caller answer 429 with its usual `Retry-After`. Degrading here would be a\n * partial bypass — concurrent requests on ONE key are cheap to generate,\n * and if they dropped the cap to a per-instance count then going wide would\n * widen the cap, which is precisely the property AGL-794 removed.\n *\n * Refusing on contention is never worse than the 504 it replaces. A 504 is\n * also a failed request, but it costs a full function timeout, carries no\n * `Retry-After` for a well-behaved client to back off on, and invites an\n * immediate retry that deepens the contention. Contention on a single key\n * also means, at nearly every call site, either abuse or a client\n * double-submitting: the keys are per (host, screen, IP), per uid, or per API\n * key, so a legitimate visitor essentially never races themselves.\n *\n * ## The counter is an atomic increment, not a transaction (AGL-2416)\n *\n * It used to be `runTransaction(read count, write count + 1)`. That is\n * optimistic concurrency on ONE document, so two writers in flight at once\n * already race: measured in production on 2026-08-19, **two** concurrent\n * requests on a single fresh key were enough to make a transaction lose,\n * retry, and blow past its budget. Two is ordinary client behaviour — a\n * double-submit, a mobile retry, two visitors behind one NAT — and the\n * documented 120/min per API key is not even reachable without concurrency,\n * so an integration built to the published budget met this by design.\n *\n * `set({count: increment(1)}, {merge: true})` is applied by the server with\n * no read set to conflict over, so concurrent writers do not abort each\n * other. The decision then needs the value, which costs a read back — the\n * same two round trips a transaction spent, and the same billing: one\n * document read plus one document write.\n *\n * **The trade is exactness for headroom, and it is deliberately biased.** The\n * read-back can observe increments from writers that landed after ours, so\n * under `C` concurrent requests a caller may see a count up to `C - 1` higher\n * than its own position. The consequences, precisely:\n *\n * - It can only ever refuse EARLY, never late. The count is monotonic within\n * a window and always includes this request's own increment, so an admitted\n * request is one where at most `limit` increments had landed. The cap can\n * never be exceeded, and going concurrent can never widen it.\n * - \"Exactly `limit` admitted\" becomes \"**at most** `limit` admitted\". At the\n * window's edge, `C` simultaneous requests can all read the same post-burst\n * total and all be refused, so as few as `limit - C + 1` are admitted where\n * `limit` would have been. Bounded by the concurrency, confined to one\n * window, and in the customer-favourable direction for a *security* limiter\n * — which is what all nine of this store's call sites are.\n * - A fixed-window limiter already admits up to `2 × limit` across a window\n * boundary, so exactness was never a property this shape had end to end.\n * - **A call abandoned on its budget may already have counted.** The write\n * lands before the read-back, so a caller that gives up during the read-back\n * still spent its increment. The transaction it replaced committed nothing\n * when it timed out — which is exactly how AGL-2416 could observe \"10\n * admitted\" after a burst of 504s — so this is a genuine change, not a\n * restatement. It errs in the same safe direction: the counter over-counts,\n * so the cap still cannot be exceeded, and a caller can never buy budget by\n * timing out.\n *\n * The failure classification above is unchanged and still load-bearing: the\n * budget can still elapse on a genuinely slow store, and when it does the\n * answer is still a refusal rather than a degradation.\n */\nexport async function consumeRateLimit(\n key: string,\n options?: DurableRateLimitOptions,\n): Promise<DurableRateLimitResult> {\n const limit = options?.limit ?? DEFAULT_RATE_LIMIT\n const windowMs = options?.windowMs ?? DEFAULT_RATE_WINDOW_MS\n const now = options?.now ?? Date.now()\n const windowStartMs = Math.floor(now / windowMs) * windowMs\n const resetMs = windowStartMs + windowMs\n\n try {\n const firestore =\n options?.firestore ?? firebaseAdmin.app().firestore()\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(bucketId(key, windowStartMs))\n\n // Explicit type argument: `firestore` is `any`, so the helper's promise\n // infers as `any` and would widen the counter to `unknown`.\n const count = await withBudget<number>(\n countOne(ref, windowStartMs, resetMs, windowMs),\n options?.budgetMs ?? RATE_LIMIT_TRANSACTION_BUDGET_MS,\n )\n\n // The store answered, so any episode this instance was in is over. The\n // marker is written here rather than in the `catch` because the store is\n // unreachable exactly while the episode is happening (AGL-1679).\n if (episode) flushDegradation(firestore, now)\n\n return {\n allowed: count <= limit,\n limit,\n remaining: Math.max(0, limit - count),\n resetMs,\n degraded: false,\n contended: false,\n }\n } catch (error) {\n if (isContention(error)) {\n // NOT `noteDegradation`: nothing degraded. The durable counter is still\n // the only authority and it still holds — this caller simply did not get\n // a turn. Recording it as a degradation would put contention markers in\n // front of `/api/health/rate-limits`, whose whole question is \"did the\n // durable store stop answering\", and answer it wrongly.\n //\n // `resetMs` stays the true end of the window so `X-RateLimit-Reset`\n // remains honest, which also makes the caller's `Retry-After` a bounded\n // back-off to the window boundary — the one client behaviour that\n // actually relieves the contention.\n return {\n allowed: false,\n limit,\n remaining: 0,\n resetMs,\n degraded: false,\n contended: true,\n }\n }\n noteDegradation(now, error)\n return {\n ...checkRateLimit(key, { limit, windowMs, now }),\n degraded: true,\n contended: false,\n }\n }\n}\n\nexport default consumeRateLimit\n\n/**\n * Document id for the signup canary's verdict (AGL-2715).\n *\n * One document, not a bucketed series: this records the LAST walk, and a\n * history of walks answers a question nobody asks during an incident. The\n * walk that matters is the most recent one.\n *\n * Written by the scheduled canary, read by `/api/health/journeys`. The split\n * is the same one the beacon heartbeat makes and for a stronger reason: the\n * canary CREATES AN ACCOUNT AND AN ORG, and the door that reports it is\n * public and unauthenticated. A probe that walked the signup itself would\n * hand anyone with `curl` an org factory.\n *\n * ⚠️ NOT in the `rateLimits` TTL's usual seven-day shape. There is exactly one\n * of these and it is overwritten on every run, so it costs one document\n * forever; `expiresAt` is still stamped, far enough out that a canary which\n * stopped running expires its own evidence rather than leaving a stale pass\n * lying around to be misread. The health verdict's own staleness window is\n * two hours, so expiry is a backstop, not the mechanism.\n */\nexport const SIGNUP_CANARY_DOC_ID = 'signupCanary_production'\n\n/** How long a recorded walk survives the sweep. */\nconst SIGNUP_CANARY_RETENTION_MS = 30 * 24 * 60 * 60 * 1000\n\n/** Budget for the single-document read on the health path. */\nconst SIGNUP_CANARY_READ_BUDGET_MS = 2_000\n\nexport interface SignupCanaryVerdict {\n /** Whether every step of the walk succeeded. */\n ok: boolean\n /** Which step broke. Never an error message, never a uid or an email. */\n failedStep?: string | null\n /** Wall clock for the whole walk. */\n elapsedMs: number\n /** Whether the walk deleted everything it created. */\n reapedCleanly: boolean\n}\n\n/**\n * Record the outcome of one signup walk.\n *\n * AWAITED by its caller, unlike every other recorder in this file. Those are\n * breadcrumbs on a request path where the user's work is the control and the\n * marker must never delay it. This one IS the work: a canary whose verdict\n * did not land has not reported, and the door it feeds would rather red on a\n * missing marker than believe a walk nobody wrote down.\n *\n * `set` without merge, deliberately: this document is a snapshot of the last\n * walk, and merging would let a field from a previous run survive into a\n * verdict it was not part of — a `failedStep` outliving the failure that\n * produced it, read beside a fresh `ok: true`.\n */\nexport async function recordSignupCanaryWalk(\n verdict: SignupCanaryVerdict,\n options?: { now?: number; firestore?: any },\n): Promise<void> {\n const nowMs = options?.now ?? Date.now()\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n await firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(SIGNUP_CANARY_DOC_ID)\n .set({\n walkedAtMs: nowMs,\n ok: verdict.ok === true,\n failedStep: verdict.failedStep ?? null,\n elapsedMs: Number(verdict.elapsedMs) || 0,\n reapedCleanly: verdict.reapedCleanly === true,\n expiresAt: new Date(nowMs + SIGNUP_CANARY_RETENTION_MS),\n })\n}\n\n/**\n * The last recorded walk, or null (AGL-2715).\n *\n * Null covers the same three cases `readBeaconHeartbeat` folds, for the same\n * reason: no marker has ever been written, the document holds nothing usable,\n * and the store would not answer inside its budget. Each one means *nothing\n * here has demonstrated that a stranger can sign up*, and the verdict grades\n * all three as `canary-unavailable`. Distinguishing them could only ever be\n * used to forgive something unproven.\n */\nexport async function readSignupCanaryWalk(options?: {\n firestore?: any\n budgetMs?: number\n}): Promise<{\n walkedAtMs?: number\n ok?: boolean\n failedStep?: string | null\n elapsedMs?: number\n reapedCleanly?: boolean\n} | null> {\n try {\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await withBudget<any>(\n firestore.collection(RATE_LIMIT_COLLECTION).doc(SIGNUP_CANARY_DOC_ID).get(),\n options?.budgetMs ?? SIGNUP_CANARY_READ_BUDGET_MS,\n )\n if (!snapshot?.exists) return null\n const walkedAtMs = snapshot.get('walkedAtMs')\n if (typeof walkedAtMs !== 'number' || !Number.isFinite(walkedAtMs)) return null\n return {\n walkedAtMs,\n ok: snapshot.get('ok') === true,\n failedStep: snapshot.get('failedStep') ?? null,\n elapsedMs: snapshot.get('elapsedMs'),\n reapedCleanly: snapshot.get('reapedCleanly'),\n }\n } catch {\n return null\n }\n}\n\n/**\n * Document id for the App Check attestation sample (AGL-2715).\n *\n * The rate is published by Google to Cloud Monitoring, which a public health\n * endpoint has no business calling: it needs a second credential, it is slow,\n * and its quota is not something a route anyone can curl should be able to\n * spend. So a scheduled job samples it and this holds the answer, exactly as\n * the signup canary's own verdict is held.\n *\n * One document, overwritten each run — the last reading is the only one that\n * answers \"is attestation working now\".\n */\n/**\n * Where the metered-traffic sampler leaves its reading (AGL-2720).\n *\n * One document, like every sibling here. The sampler owns the judgement about\n * whether the total moved; this file only carries it.\n */\nexport const EDGE_ADMISSION_DOC_ID = 'edgeAdmission_production'\n\n/** Budget for the single-document read on the health path. */\nconst EDGE_ADMISSION_READ_BUDGET_MS = 2_000\n\n/**\n * Read the last metered-traffic sample.\n *\n * Deliberately has NO max-age rejection, which is the one place this differs\n * from {@link readAppCheckAttestation}. The quantity graded is already an age\n * — how long since traffic moved — so discarding an old marker would turn the\n * loudest possible reading into `edge-admission-unavailable` and lose which\n * of the two things broke.\n */\nexport async function readEdgeAdmission(options?: {\n firestore?: any\n budgetMs?: number\n}): Promise<{\n sampledAtMs?: number\n advancedAtMs?: number\n day?: string\n total?: number\n} | null> {\n try {\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await withBudget<any>(\n firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(EDGE_ADMISSION_DOC_ID)\n .get(),\n options?.budgetMs ?? EDGE_ADMISSION_READ_BUDGET_MS,\n )\n if (!snapshot?.exists) return null\n const advancedAtMs = snapshot.get('advancedAtMs')\n if (typeof advancedAtMs !== 'number') return null\n return {\n sampledAtMs: snapshot.get('sampledAtMs'),\n advancedAtMs,\n day: snapshot.get('day'),\n total: snapshot.get('total'),\n }\n } catch {\n return null\n }\n}\n\nexport const APP_CHECK_ATTESTATION_DOC_ID = 'appCheckAttestation_production'\n\n/** How long a sample survives the sweep. */\nconst APP_CHECK_ATTESTATION_RETENTION_MS = 30 * 24 * 60 * 60 * 1000\n\n/** Budget for the single-document read on the health path. */\nconst APP_CHECK_ATTESTATION_READ_BUDGET_MS = 2_000\n\n/**\n * Record one sampling of the App Check verification counts.\n *\n * Awaited by its caller, like the canary's verdict and unlike the breadcrumbs\n * in this file: the sample IS the work, and a door would rather report a\n * missing reading than believe one nobody wrote down.\n */\nexport async function recordAppCheckAttestation(\n samples: Array<{\n result?: string\n security?: string\n service?: string\n count?: number\n }>,\n options?: { now?: number; firestore?: any },\n): Promise<void> {\n const nowMs = options?.now ?? Date.now()\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n await firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(APP_CHECK_ATTESTATION_DOC_ID)\n .set({\n sampledAtMs: nowMs,\n samples,\n expiresAt: new Date(nowMs + APP_CHECK_ATTESTATION_RETENTION_MS),\n })\n}\n\n/**\n * The last attestation sample, or null (AGL-2715).\n *\n * Null folds the three cases its siblings fold — never written, unusable, and\n * the store would not answer — because each means *nothing here has shown that\n * attestation is working*, and the verdict grades all three as\n * `attestation-unavailable`.\n *\n * A sample older than the window it describes is also null. A stale reading is\n * not a reading: reporting a six-hour-old rate as current is how a check comes\n * to say attestation is fine long after it stopped being.\n */\nexport async function readAppCheckAttestation(options?: {\n firestore?: any\n budgetMs?: number\n now?: number\n maxAgeMs?: number\n}): Promise<Array<{\n result?: string\n security?: string\n service?: string\n count?: number\n}> | null> {\n try {\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await withBudget<any>(\n firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(APP_CHECK_ATTESTATION_DOC_ID)\n .get(),\n options?.budgetMs ?? APP_CHECK_ATTESTATION_READ_BUDGET_MS,\n )\n if (!snapshot?.exists) return null\n const sampledAtMs = snapshot.get('sampledAtMs')\n if (typeof sampledAtMs !== 'number') return null\n const nowMs = options?.now ?? Date.now()\n // Twice the window it covers: one missed sampling is tolerated, two are\n // not, and the reading still describes a period that overlaps now.\n const maxAgeMs = options?.maxAgeMs ?? 12 * 60 * 60 * 1000\n if (nowMs - sampledAtMs > maxAgeMs) return null\n const samples = snapshot.get('samples')\n return Array.isArray(samples) ? samples : null\n } catch {\n return null\n }\n}\n"],"names":["deploymentRegion","createHash","FieldValue","firebaseAdmin","checkRateLimit","DEFAULT_RATE_LIMIT","DEFAULT_RATE_WINDOW_MS","RATE_LIMIT_COLLECTION","DEGRADATION_DOC_PREFIX","DEGRADATION_BUCKET_MS","DEGRADATION_RETENTION_MS","episode","failureCode","error","code","String","message","slice","noteDegradation","nowMs","bucket","Math","floor","count","firstAtMs","lastAtMs","lastLoggedBucket","console","atMs","sinceMs","flushDegradation","firestore","ended","bucketStart","ref","collection","doc","runTransaction","tx","snapshot","get","priorCalls","exists","priorEpisodes","priorFirst","undefined","priorLast","set","calls","episodes","min","max","region","expiresAt","Date","merge","catch","currentRateLimitDegradation","resetRateLimitDegradationForTests","SIGNUP_REFUSAL_DOC_PREFIX","SIGNUP_REFUSAL_BUCKET_MS","SIGNUP_REFUSAL_RETENTION_MS","recordSignupRefusal","reason","options","now","app","priorTotal","priorByReason","refusals","byReason","firstRefusedAtMs","refusedAtMs","SERVER_ERROR_DOC_PREFIX","SERVER_ERROR_BUCKET_MS","SERVER_ERROR_RETENTION_MS","SERVER_ERROR_FLUSH_INTERVAL_MS","serverErrors","flushServerErrors","held","pending","errors","byService","pendingByService","erroredAtMs","lastErrorAtMs","lastFlushAtMs","byServiceIncrements","service","Object","entries","increment","Promise","resolve","recordServerError","pendingServerErrors","resetServerErrorsForTests","BEACON_HEARTBEAT_DOC_PREFIX","BEACON_HEARTBEAT_RETENTION_MS","BEACON_HEARTBEAT_READ_BUDGET_MS","recordBeaconHeartbeat","heartbeatAtMs","readBeaconHeartbeat","withBudget","budgetMs","landedAtMs","Number","isFinite","SIGNUP_SERVED_DOC_PREFIX","SIGNUP_SERVED_BUCKET_MS","SIGNUP_SERVED_RETENTION_MS","SIGNUP_SERVED_FLUSH_INTERVAL_MS","signupServes","flushSignupServes","serves","servedAtMs","lastServedAtMs","recordSignupServed","pendingSignupServes","resetSignupServesForTests","SIGNUP_ATTEMPT_DOC_PREFIX","SIGNUP_ATTEMPT_BUCKET_MS","SIGNUP_ATTEMPT_RETENTION_MS","recordSignupAttempt","attempts","attemptedAtMs","RATE_LIMIT_TRANSACTION_BUDGET_MS","RateLimitContentionError","Error","name","CONTENTION_CODES","Set","isContention","has","work","timer","race","_","reject","setTimeout","clearTimeout","countOne","windowStartMs","resetMs","windowMs","observed","bucketId","key","hash","update","digest","consumeRateLimit","limit","allowed","remaining","degraded","contended","SIGNUP_CANARY_DOC_ID","SIGNUP_CANARY_RETENTION_MS","SIGNUP_CANARY_READ_BUDGET_MS","recordSignupCanaryWalk","verdict","walkedAtMs","ok","failedStep","elapsedMs","reapedCleanly","readSignupCanaryWalk","EDGE_ADMISSION_DOC_ID","EDGE_ADMISSION_READ_BUDGET_MS","readEdgeAdmission","advancedAtMs","sampledAtMs","day","total","APP_CHECK_ATTESTATION_DOC_ID","APP_CHECK_ATTESTATION_RETENTION_MS","APP_CHECK_ATTESTATION_READ_BUDGET_MS","recordAppCheckAttestation","samples","readAppCheckAttestation","maxAgeMs","Array","isArray"],"mappings":"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"}
@@ -0,0 +1,67 @@
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 PluginContributions, type RealmPluginInstall } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Server-side plugin config read (AGL-428): the stored overrides merged over
20
+ * the plugin's declared defaults (type-coerced — the docs are
21
+ * manager-writable). Without a registered schema the raw doc (or {}) comes
22
+ * back, so handlers degrade to their own fallbacks.
23
+ *
24
+ * `hostId` resolves the SITE's answer on top of the workspace's (AGL-428,
25
+ * AGL-1014), and a handler that has one must pass it. A per-site setting the console can
26
+ * store and the server never reads is not a setting: the operator lowers the
27
+ * flagship branch's booking horizon, the console shows the number they typed,
28
+ * and `/api/bookings/slots` keeps offering the workspace's — a disagreement
29
+ * with no error and no surface that shows both sides.
30
+ *
31
+ * The site read is CONDITIONAL on being handed a host, so a request with no
32
+ * site in scope pays nothing for a document that could not apply to it. It
33
+ * runs alongside the org read rather than after it: the two documents are
34
+ * independent, and sequencing them would put a second serial round trip on
35
+ * every request that resolves config.
36
+ */
37
+ export declare function getPluginConfig(orgId: string | null | undefined, pluginId: string, options?: {
38
+ hostId?: string | null;
39
+ }): Promise<Record<string, unknown>>;
40
+ /**
41
+ * One version-doc read, shaped for the remote loaders' `resolveVersion`.
42
+ *
43
+ * Also the staff-takedown gate (AGL-948): a listing carrying `hiddenAt`
44
+ * resolves to null, so an already-installed plugin stops loading the
45
+ * moment staff hide it. Takedown used to only de-list — a plugin pulled
46
+ * for abuse kept executing in every workspace that had installed it until
47
+ * someone separately wrote a revocation. It lives HERE rather than in the
48
+ * join because every remote path funnels through this function: the
49
+ * console and site realm joins, and both apps' remote-server-bundle
50
+ * loaders.
51
+ *
52
+ * `deletedAt` is deliberately NOT a blocker. A publisher retiring a
53
+ * listing must not break the sites already paying for it; unpublish
54
+ * blocks new installs (`install-plugin`) and that is all it means.
55
+ */
56
+ export declare function resolveMarketplacePluginVersion(listingId: string, version: string): Promise<{
57
+ sha256: string;
58
+ signature?: string;
59
+ trust?: string;
60
+ hostAbi?: number;
61
+ pluginId?: string;
62
+ contributes?: PluginContributions;
63
+ } | null>;
64
+ export declare function getRealmPluginInstalls(options: {
65
+ orgId?: string;
66
+ hostId?: string;
67
+ }): Promise<RealmPluginInstall[]>;
@@ -0,0 +1,250 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { getPluginConfigSchema, isPluginRevoked, mergePluginConfig, readPluginContributions, resolvePluginConfig } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin } from "./firebase-admin.js";
19
+ import { resolveOrgIdForHost } from "./organizations.js";
20
+ import { tenantDataTag, withRenderCache } from "../render-cache.js";
21
+ /**
22
+ * One `pluginSettings` document, or `undefined` if it cannot be read.
23
+ *
24
+ * A failed read degrades to "no answer at this level" rather than throwing,
25
+ * so a transient Firestore error costs a handler its overrides and not its
26
+ * request. The caller layers what comes back, so a missing site document and
27
+ * a missing site override are the same thing to it.
28
+ */ async function readPluginSettings(scope, scopeId, pluginId) {
29
+ try {
30
+ return (await firebaseAdmin.app().firestore().collection(scope).doc(scopeId).collection('pluginSettings').doc(pluginId).get()).data();
31
+ } catch (unused) {
32
+ return undefined;
33
+ }
34
+ }
35
+ /**
36
+ * Server-side plugin config read (AGL-428): the stored overrides merged over
37
+ * the plugin's declared defaults (type-coerced — the docs are
38
+ * manager-writable). Without a registered schema the raw doc (or {}) comes
39
+ * back, so handlers degrade to their own fallbacks.
40
+ *
41
+ * `hostId` resolves the SITE's answer on top of the workspace's (AGL-428,
42
+ * AGL-1014), and a handler that has one must pass it. A per-site setting the console can
43
+ * store and the server never reads is not a setting: the operator lowers the
44
+ * flagship branch's booking horizon, the console shows the number they typed,
45
+ * and `/api/bookings/slots` keeps offering the workspace's — a disagreement
46
+ * with no error and no surface that shows both sides.
47
+ *
48
+ * The site read is CONDITIONAL on being handed a host, so a request with no
49
+ * site in scope pays nothing for a document that could not apply to it. It
50
+ * runs alongside the org read rather than after it: the two documents are
51
+ * independent, and sequencing them would put a second serial round trip on
52
+ * every request that resolves config.
53
+ */ export async function getPluginConfig(orgId, pluginId, options = {}) {
54
+ const schema = getPluginConfigSchema(pluginId);
55
+ const hostId = options.hostId;
56
+ if (!orgId && !hostId) return schema ? mergePluginConfig(schema, null) : {};
57
+ const [org, host] = await Promise.all([
58
+ orgId ? readPluginSettings('orgs', orgId, pluginId) : undefined,
59
+ hostId ? readPluginSettings('hosts', hostId, pluginId) : undefined
60
+ ]);
61
+ // Coercion happens once, at the end, inside `resolvePluginConfig` — a
62
+ // malformed site value falls back to the WORKSPACE value it was trying to
63
+ // replace rather than skipping past it to the schema default.
64
+ if (schema) return resolvePluginConfig(schema, {
65
+ org,
66
+ host
67
+ });
68
+ // No schema: there is no field list to layer by, so the site document
69
+ // simply narrows the workspace's, key for key.
70
+ return _extends({}, org != null ? org : {}, host != null ? host : {});
71
+ }
72
+ /**
73
+ * One version-doc read, shaped for the remote loaders' `resolveVersion`.
74
+ *
75
+ * Also the staff-takedown gate (AGL-948): a listing carrying `hiddenAt`
76
+ * resolves to null, so an already-installed plugin stops loading the
77
+ * moment staff hide it. Takedown used to only de-list — a plugin pulled
78
+ * for abuse kept executing in every workspace that had installed it until
79
+ * someone separately wrote a revocation. It lives HERE rather than in the
80
+ * join because every remote path funnels through this function: the
81
+ * console and site realm joins, and both apps' remote-server-bundle
82
+ * loaders.
83
+ *
84
+ * `deletedAt` is deliberately NOT a blocker. A publisher retiring a
85
+ * listing must not break the sites already paying for it; unpublish
86
+ * blocks new installs (`install-plugin`) and that is all it means.
87
+ */ export async function resolveMarketplacePluginVersion(listingId, version) {
88
+ var _data_manifest, _data_manifest1, _data_manifest2;
89
+ const firestore = firebaseAdmin.app().firestore();
90
+ const listingRef = firestore.collection('marketplaceListings').doc(listingId);
91
+ // THE REVOCATION READ IS IN THIS `Promise.all`, NOT AFTER IT (2026-08-26).
92
+ //
93
+ // It is keyed on `listingId` alone, so it never depended on the two reads
94
+ // above — awaiting it below them cost a SECOND sequential Firestore round
95
+ // trip on every resolution, and `resolveRealmPluginInstalls` calls this
96
+ // once per pinned install. That is the shape that grows with the number of
97
+ // plugins an org has installed: `/api/orgs/realm-plugins` is deliberately
98
+ // uncached (see the TTL note below), and at a P75 of 907ms it was already
99
+ // the slowest org-scoped console route before anyone uploaded in volume.
100
+ //
101
+ // The trade is one extra read on the paths that exit early (a hidden
102
+ // listing, a missing version doc) in exchange for halving the round trips
103
+ // on the path that actually happens. The kill switch is UNCHANGED: the
104
+ // check below still runs before any value is returned, and it still reads
105
+ // live — this moves when the read is issued, never whether it is honored.
106
+ const [listing, snapshot, revocationSnapshot] = await Promise.all([
107
+ listingRef.get(),
108
+ listingRef.collection('pluginVersions').doc(version).get(),
109
+ firestore.collection('revocations').doc(listingId).get()
110
+ ]);
111
+ // A missing listing doc is NOT a blocker: Firestore does not cascade to
112
+ // subcollections, so a hard-deleted listing leaves working installs
113
+ // resolving off an orphaned version doc. Only an explicit takedown stops
114
+ // them.
115
+ if (listing.get('hiddenAt')) return null;
116
+ const data = snapshot.data();
117
+ if (!(data == null ? void 0 : data.sha256)) return null;
118
+ // THE KILL SWITCH BELONGS AT THE CHOKEPOINT (AGL-2307).
119
+ //
120
+ // It used to live one level up, in `resolveRealmPluginInstalls` — which is
121
+ // the path the tenant render and the console gate take, and NOT the path
122
+ // remote SERVER bundles take. `loadRemoteServerBundles` resolves through
123
+ // this function and nothing else, so a per-version revocation left a realm
124
+ // server handler running: `hiddenAt` caught a full takedown, but a targeted
125
+ // revocation deliberately does not hide the listing, which is the whole
126
+ // difference between the two controls.
127
+ //
128
+ // Placed here rather than added as a second copy at the server loader, for
129
+ // the reason this file's own comment gives about `hiddenAt`: this is the
130
+ // one function every realm consumer funnels through, and a control that has
131
+ // to be remembered per consumer is one that will be missed by the next one.
132
+ const revocation = revocationSnapshot.data();
133
+ if (isPluginRevoked(revocation, version)) return null;
134
+ const hostAbi = Number((_data_manifest = data.manifest) == null ? void 0 : _data_manifest.hostAbi);
135
+ // Where the loaders may run this version (AGL-3116), from the manifest it
136
+ // was published and reviewed with. A block that does not read is treated
137
+ // as absent, so the documented default applies rather than "nowhere".
138
+ const contributes = readPluginContributions((_data_manifest1 = data.manifest) == null ? void 0 : _data_manifest1.contributes);
139
+ const pluginId = typeof ((_data_manifest2 = data.manifest) == null ? void 0 : _data_manifest2.id) === 'string' && data.manifest.id ? String(data.manifest.id) : undefined;
140
+ return _extends({
141
+ sha256: String(data.sha256)
142
+ }, data.signature ? {
143
+ signature: String(data.signature)
144
+ } : {}, data.trust ? {
145
+ trust: String(data.trust)
146
+ } : {}, Number.isInteger(hostAbi) && hostAbi > 0 ? {
147
+ hostAbi
148
+ } : {}, pluginId ? {
149
+ pluginId
150
+ } : {}, contributes ? {
151
+ contributes
152
+ } : {});
153
+ }
154
+ /**
155
+ * SECURITY-RELEVANT TTL (AGL-1302): the join drops revoked and taken-down
156
+ * versions, so this cache bounds how long a killed plugin keeps loading on
157
+ * published sites — 60s, on par with the page ISR window that already
158
+ * bounded it. Only the HOST-scoped call (the tenant render path) is cached;
159
+ * the console's org-scoped call stays fresh so an install shows up in the
160
+ * workspace the moment it lands.
161
+ */ const REALM_PLUGIN_INSTALLS_TTL_SECONDS = 60;
162
+ export async function getRealmPluginInstalls(options) {
163
+ const hostId = options.hostId;
164
+ if (!hostId || options.orgId) return resolveRealmPluginInstalls(options);
165
+ try {
166
+ return await withRenderCache({
167
+ key: [
168
+ 'tenant-realm-plugins',
169
+ hostId
170
+ ],
171
+ revalidate: REALM_PLUGIN_INSTALLS_TTL_SECONDS,
172
+ tags: [
173
+ tenantDataTag(hostId)
174
+ ],
175
+ read: ()=>resolveRealmPluginInstalls(options)
176
+ });
177
+ } catch (error) {
178
+ console.error(error);
179
+ return resolveRealmPluginInstalls(options);
180
+ }
181
+ }
182
+ async function resolveRealmPluginInstalls(options) {
183
+ var _options_orgId;
184
+ const firestore = firebaseAdmin.app().firestore();
185
+ const orgId = (_options_orgId = options.orgId) != null ? _options_orgId : options.hostId ? await resolveOrgIdForHost(options.hostId) : null;
186
+ // Switchboard gate (AGL-424): a workspace with an EXPLICIT
187
+ // `enabledPlugins` list only realm-loads listings on it (install sync
188
+ // keeps the list in step; toggling one off disables it without
189
+ // uninstalling). Absent field = default-open, so pre-switchboard
190
+ // installs keep loading.
191
+ let enabledFilter = null;
192
+ if (orgId) {
193
+ const configured = (await firestore.collection('orgs').doc(orgId).get()).get('enabledPlugins');
194
+ if (Array.isArray(configured)) enabledFilter = configured.map(String);
195
+ }
196
+ const pins = new Map();
197
+ const collect = async (ref)=>{
198
+ const snapshot = await ref.get();
199
+ for (const doc of snapshot.docs){
200
+ const version = doc.get('version');
201
+ if (typeof version === 'string' && version) {
202
+ pins.set(doc.id, {
203
+ listingId: doc.id,
204
+ version
205
+ });
206
+ }
207
+ }
208
+ };
209
+ if (orgId) {
210
+ await collect(firestore.collection('orgs').doc(orgId).collection('installs'));
211
+ }
212
+ if (options.hostId) {
213
+ // Host pins win over org pins for the same listing (more specific).
214
+ await collect(firestore.collection('hosts').doc(options.hostId).collection('installs'));
215
+ }
216
+ if (enabledFilter) {
217
+ for (const listingId of [
218
+ ...pins.keys()
219
+ ]){
220
+ if (!enabledFilter.includes(listingId)) pins.delete(listingId);
221
+ }
222
+ }
223
+ if (!pins.size) return [];
224
+ const installs = await Promise.all([
225
+ ...pins.values()
226
+ ].map(async (pin)=>{
227
+ const pinned = await resolveMarketplacePluginVersion(pin.listingId, pin.version);
228
+ // The revocation check moved INTO `resolveMarketplacePluginVersion`
229
+ // (AGL-2307) so the remote-server loader — which resolves through that
230
+ // function and never reaches this join — gets it too. A `null` here
231
+ // therefore already means "taken down, unknown, or killed".
232
+ if (!pinned || pinned.trust !== 'realm' || !pinned.signature) return null;
233
+ return _extends({
234
+ listingId: pin.listingId,
235
+ version: pin.version,
236
+ sha256: pinned.sha256,
237
+ trust: 'realm',
238
+ signature: pinned.signature
239
+ }, pinned.hostAbi !== undefined ? {
240
+ hostAbi: pinned.hostAbi
241
+ } : {}, pinned.pluginId ? {
242
+ pluginId: pinned.pluginId
243
+ } : {}, pinned.contributes ? {
244
+ contributes: pinned.contributes
245
+ } : {});
246
+ }));
247
+ return installs.filter((install)=>Boolean(install));
248
+ }
249
+
250
+ //# sourceMappingURL=realm-plugins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/realm-plugins.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n getPluginConfigSchema,\n isPluginRevoked,\n mergePluginConfig,\n readPluginContributions,\n resolvePluginConfig,\n type PluginContributions,\n type PluginRevocation,\n type RealmPluginInstall,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from './firebase-admin'\nimport { resolveOrgIdForHost } from './organizations'\nimport { tenantDataTag, withRenderCache } from '../render-cache'\n\n/**\n * One `pluginSettings` document, or `undefined` if it cannot be read.\n *\n * A failed read degrades to \"no answer at this level\" rather than throwing,\n * so a transient Firestore error costs a handler its overrides and not its\n * request. The caller layers what comes back, so a missing site document and\n * a missing site override are the same thing to it.\n */\nasync function readPluginSettings(\n scope: 'orgs' | 'hosts',\n scopeId: string,\n pluginId: string,\n): Promise<Record<string, unknown> | undefined> {\n try {\n return (\n await firebaseAdmin\n .app()\n .firestore()\n .collection(scope)\n .doc(scopeId)\n .collection('pluginSettings')\n .doc(pluginId)\n .get()\n ).data()\n } catch {\n return undefined\n }\n}\n\n/**\n * Server-side plugin config read (AGL-428): the stored overrides merged over\n * the plugin's declared defaults (type-coerced — the docs are\n * manager-writable). Without a registered schema the raw doc (or {}) comes\n * back, so handlers degrade to their own fallbacks.\n *\n * `hostId` resolves the SITE's answer on top of the workspace's (AGL-428,\n * AGL-1014), and a handler that has one must pass it. A per-site setting the console can\n * store and the server never reads is not a setting: the operator lowers the\n * flagship branch's booking horizon, the console shows the number they typed,\n * and `/api/bookings/slots` keeps offering the workspace's — a disagreement\n * with no error and no surface that shows both sides.\n *\n * The site read is CONDITIONAL on being handed a host, so a request with no\n * site in scope pays nothing for a document that could not apply to it. It\n * runs alongside the org read rather than after it: the two documents are\n * independent, and sequencing them would put a second serial round trip on\n * every request that resolves config.\n */\nexport async function getPluginConfig(\n orgId: string | null | undefined,\n pluginId: string,\n options: { hostId?: string | null } = {},\n): Promise<Record<string, unknown>> {\n const schema = getPluginConfigSchema(pluginId)\n const hostId = options.hostId\n if (!orgId && !hostId) return schema ? mergePluginConfig(schema, null) : {}\n const [org, host] = await Promise.all([\n orgId ? readPluginSettings('orgs', orgId, pluginId) : undefined,\n hostId ? readPluginSettings('hosts', hostId, pluginId) : undefined,\n ])\n // Coercion happens once, at the end, inside `resolvePluginConfig` — a\n // malformed site value falls back to the WORKSPACE value it was trying to\n // replace rather than skipping past it to the schema default.\n if (schema) return resolvePluginConfig(schema, { org, host })\n // No schema: there is no field list to layer by, so the site document\n // simply narrows the workspace's, key for key.\n return { ...(org ?? {}), ...(host ?? {}) }\n}\n\n/**\n * Server-side resolution of a workspace's TRUSTED-REALM plugin installs\n * (AGL-420). Install docs pin `{version, sha256}`, but the trust grant\n * (`trust: 'realm'` + the platform Ed25519 `signature`) lives on the\n * server-only version doc — staff sign AFTER review, possibly long after\n * the install — so this join is the single source the loaders consume:\n *\n * 1. Read the org's installs (and the host's, when a host is in scope).\n * 2. Join each pin with its `marketplaceListings/{id}/pluginVersions/{v}`\n * doc; only versions carrying `trust: 'realm'` survive.\n * 3. Drop revoked versions (`revocations/{listingId}` kill switch) — a\n * revocation beats a still-present trust grant.\n * 4. Drop hidden listings (AGL-948) — `resolveMarketplacePluginVersion`\n * returns null for a listing under staff takedown. `deletedAt` is not\n * a blocker; see that function for why the two differ.\n *\n * The returned sha256/signature come from the VERSION doc, not the install\n * copy, so a tampered install doc cannot smuggle different bytes past the\n * loader's content check.\n */\n\ninterface InstallPin {\n listingId: string\n version: string\n}\n\n/**\n * One version-doc read, shaped for the remote loaders' `resolveVersion`.\n *\n * Also the staff-takedown gate (AGL-948): a listing carrying `hiddenAt`\n * resolves to null, so an already-installed plugin stops loading the\n * moment staff hide it. Takedown used to only de-list — a plugin pulled\n * for abuse kept executing in every workspace that had installed it until\n * someone separately wrote a revocation. It lives HERE rather than in the\n * join because every remote path funnels through this function: the\n * console and site realm joins, and both apps' remote-server-bundle\n * loaders.\n *\n * `deletedAt` is deliberately NOT a blocker. A publisher retiring a\n * listing must not break the sites already paying for it; unpublish\n * blocks new installs (`install-plugin`) and that is all it means.\n */\nexport async function resolveMarketplacePluginVersion(\n listingId: string,\n version: string,\n): Promise<{\n sha256: string\n signature?: string\n trust?: string\n hostAbi?: number\n pluginId?: string\n contributes?: PluginContributions\n} | null> {\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n // THE REVOCATION READ IS IN THIS `Promise.all`, NOT AFTER IT (2026-08-26).\n //\n // It is keyed on `listingId` alone, so it never depended on the two reads\n // above — awaiting it below them cost a SECOND sequential Firestore round\n // trip on every resolution, and `resolveRealmPluginInstalls` calls this\n // once per pinned install. That is the shape that grows with the number of\n // plugins an org has installed: `/api/orgs/realm-plugins` is deliberately\n // uncached (see the TTL note below), and at a P75 of 907ms it was already\n // the slowest org-scoped console route before anyone uploaded in volume.\n //\n // The trade is one extra read on the paths that exit early (a hidden\n // listing, a missing version doc) in exchange for halving the round trips\n // on the path that actually happens. The kill switch is UNCHANGED: the\n // check below still runs before any value is returned, and it still reads\n // live — this moves when the read is issued, never whether it is honored.\n const [listing, snapshot, revocationSnapshot] = await Promise.all([\n listingRef.get(),\n listingRef.collection('pluginVersions').doc(version).get(),\n firestore.collection('revocations').doc(listingId).get(),\n ])\n // A missing listing doc is NOT a blocker: Firestore does not cascade to\n // subcollections, so a hard-deleted listing leaves working installs\n // resolving off an orphaned version doc. Only an explicit takedown stops\n // them.\n if (listing.get('hiddenAt')) return null\n const data = snapshot.data()\n if (!data?.sha256) return null\n // THE KILL SWITCH BELONGS AT THE CHOKEPOINT (AGL-2307).\n //\n // It used to live one level up, in `resolveRealmPluginInstalls` — which is\n // the path the tenant render and the console gate take, and NOT the path\n // remote SERVER bundles take. `loadRemoteServerBundles` resolves through\n // this function and nothing else, so a per-version revocation left a realm\n // server handler running: `hiddenAt` caught a full takedown, but a targeted\n // revocation deliberately does not hide the listing, which is the whole\n // difference between the two controls.\n //\n // Placed here rather than added as a second copy at the server loader, for\n // the reason this file's own comment gives about `hiddenAt`: this is the\n // one function every realm consumer funnels through, and a control that has\n // to be remembered per consumer is one that will be missed by the next one.\n const revocation = revocationSnapshot.data() as PluginRevocation | undefined\n if (isPluginRevoked(revocation, version)) return null\n const hostAbi = Number(data.manifest?.hostAbi)\n // Where the loaders may run this version (AGL-3116), from the manifest it\n // was published and reviewed with. A block that does not read is treated\n // as absent, so the documented default applies rather than \"nowhere\".\n const contributes = readPluginContributions(data.manifest?.contributes)\n const pluginId =\n typeof data.manifest?.id === 'string' && data.manifest.id\n ? String(data.manifest.id)\n : undefined\n return {\n sha256: String(data.sha256),\n ...(data.signature ? { signature: String(data.signature) } : {}),\n ...(data.trust ? { trust: String(data.trust) } : {}),\n ...(Number.isInteger(hostAbi) && hostAbi > 0 ? { hostAbi } : {}),\n ...(pluginId ? { pluginId } : {}),\n ...(contributes ? { contributes } : {}),\n }\n}\n\n/**\n * SECURITY-RELEVANT TTL (AGL-1302): the join drops revoked and taken-down\n * versions, so this cache bounds how long a killed plugin keeps loading on\n * published sites — 60s, on par with the page ISR window that already\n * bounded it. Only the HOST-scoped call (the tenant render path) is cached;\n * the console's org-scoped call stays fresh so an install shows up in the\n * workspace the moment it lands.\n */\nconst REALM_PLUGIN_INSTALLS_TTL_SECONDS = 60\n\nexport async function getRealmPluginInstalls(options: {\n orgId?: string\n hostId?: string\n}): Promise<RealmPluginInstall[]> {\n const hostId = options.hostId\n if (!hostId || options.orgId) return resolveRealmPluginInstalls(options)\n try {\n return await withRenderCache({\n key: ['tenant-realm-plugins', hostId],\n revalidate: REALM_PLUGIN_INSTALLS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read: () => resolveRealmPluginInstalls(options),\n })\n } catch (error) {\n console.error(error)\n return resolveRealmPluginInstalls(options)\n }\n}\n\nasync function resolveRealmPluginInstalls(options: {\n orgId?: string\n hostId?: string\n}): Promise<RealmPluginInstall[]> {\n const firestore = firebaseAdmin.app().firestore()\n const orgId =\n options.orgId ??\n (options.hostId ? await resolveOrgIdForHost(options.hostId) : null)\n\n // Switchboard gate (AGL-424): a workspace with an EXPLICIT\n // `enabledPlugins` list only realm-loads listings on it (install sync\n // keeps the list in step; toggling one off disables it without\n // uninstalling). Absent field = default-open, so pre-switchboard\n // installs keep loading.\n let enabledFilter: readonly string[] | null = null\n if (orgId) {\n const configured = (\n await firestore.collection('orgs').doc(orgId).get()\n ).get('enabledPlugins')\n if (Array.isArray(configured)) enabledFilter = configured.map(String)\n }\n\n const pins = new Map<string, InstallPin>()\n const collect = async (\n ref: FirebaseFirestore.CollectionReference,\n ): Promise<void> => {\n const snapshot = await ref.get()\n for (const doc of snapshot.docs) {\n const version = doc.get('version')\n if (typeof version === 'string' && version) {\n pins.set(doc.id, { listingId: doc.id, version })\n }\n }\n }\n if (orgId) {\n await collect(firestore.collection('orgs').doc(orgId).collection('installs'))\n }\n if (options.hostId) {\n // Host pins win over org pins for the same listing (more specific).\n await collect(\n firestore.collection('hosts').doc(options.hostId).collection('installs'),\n )\n }\n if (enabledFilter) {\n for (const listingId of [...pins.keys()]) {\n if (!enabledFilter.includes(listingId)) pins.delete(listingId)\n }\n }\n if (!pins.size) return []\n\n const installs = await Promise.all(\n [...pins.values()].map(async (pin): Promise<RealmPluginInstall | null> => {\n const pinned = await resolveMarketplacePluginVersion(\n pin.listingId,\n pin.version,\n )\n // The revocation check moved INTO `resolveMarketplacePluginVersion`\n // (AGL-2307) so the remote-server loader — which resolves through that\n // function and never reaches this join — gets it too. A `null` here\n // therefore already means \"taken down, unknown, or killed\".\n if (!pinned || pinned.trust !== 'realm' || !pinned.signature) return null\n return {\n listingId: pin.listingId,\n version: pin.version,\n sha256: pinned.sha256,\n trust: 'realm',\n signature: pinned.signature,\n ...(pinned.hostAbi !== undefined ? { hostAbi: pinned.hostAbi } : {}),\n ...(pinned.pluginId ? { pluginId: pinned.pluginId } : {}),\n ...(pinned.contributes ? { contributes: pinned.contributes } : {}),\n }\n }),\n )\n return installs.filter((install): install is RealmPluginInstall =>\n Boolean(install),\n )\n}\n"],"names":["getPluginConfigSchema","isPluginRevoked","mergePluginConfig","readPluginContributions","resolvePluginConfig","firebaseAdmin","resolveOrgIdForHost","tenantDataTag","withRenderCache","readPluginSettings","scope","scopeId","pluginId","app","firestore","collection","doc","get","data","undefined","getPluginConfig","orgId","options","schema","hostId","org","host","Promise","all","resolveMarketplacePluginVersion","listingId","version","listingRef","listing","snapshot","revocationSnapshot","sha256","revocation","hostAbi","Number","manifest","contributes","id","String","signature","trust","isInteger","REALM_PLUGIN_INSTALLS_TTL_SECONDS","getRealmPluginInstalls","resolveRealmPluginInstalls","key","revalidate","tags","read","error","console","enabledFilter","configured","Array","isArray","map","pins","Map","collect","ref","docs","set","keys","includes","delete","size","installs","values","pin","pinned","filter","install","Boolean"],"mappings":"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"}