@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,479 @@
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
+ * The Vercel log-drain receiver (AGL-1921, third and last arm).
18
+ *
19
+ * ## Why this arm exists at all
20
+ *
21
+ * The two arms already shipped both start from OUR code running. The
22
+ * `onRequestError` hook (`reportServerError`) sees a render or route handler
23
+ * throw; `/api/health/server-errors` grades the markers that hook writes.
24
+ * Neither can see:
25
+ *
26
+ * - an error that kills the process before the hook runs,
27
+ * - a platform-level 5xx that never reaches our code at all — a function
28
+ * timeout, an OOM kill, a cold-start 502,
29
+ * - anything thrown in the edge runtime.
30
+ *
31
+ * A log drain sees all three, because Vercel emits the request log whether or
32
+ * not our code got far enough to have an opinion. That is why this is the arm
33
+ * that closes the issue rather than a fourth partial view.
34
+ *
35
+ * ## The shape of the risk, and what each rule below buys
36
+ *
37
+ * A drain streams EVERY request log for both projects. The account's whole
38
+ * monitoring budget is about $20/month, so the dangerous failure mode here is
39
+ * not missing an error — it is faithfully forwarding a million healthy 200s
40
+ * into Cloud Logging and getting a bill for it. Hence, in order:
41
+ *
42
+ * 1. **Signature first, fail closed.** HMAC-SHA1 of the RAW body keyed by the
43
+ * drain secret, timing-safe compared. Unsigned means unwritten, always.
44
+ * 2. **Filter before writing.** `isServerErrorEntry` is the cost gate: an
45
+ * entry that is not a server error costs one predicate and zero writes.
46
+ * 3. **Never our own route.** See `RECEIVER_ROUTE_PATH`.
47
+ * 4. **Never a deliberate 5xx.** A 503 is how the lockdown notice and every
48
+ * health contract SAY something, and forwarding a verdict as a fault makes
49
+ * the stream unreadable: measured over the 48 hours to 2026-09-09, 229 of
50
+ * the 251 entries in this log were `/api/health/*` reporting degraded and
51
+ * 22 were the only real outage in the window. See `isLockdownNoticeEntry`
52
+ * and `isHealthContractEntry` (AGL-2709).
53
+ * 5. **A per-instance budget per window**, with suppression REPORTED. A
54
+ * monitoring path that hides its own lossiness is the bug shape this repo
55
+ * keeps rediscovering.
56
+ *
57
+ * ## Where it writes, and why not next door
58
+ *
59
+ * `vercel-runtime`, DELIBERATELY NOT `server-errors`. That log is the
60
+ * `onRequestError` hook's, and the alert policy
61
+ * `projects/aglyn-main/alertPolicies/11610705614308437855` keys on it. A 500
62
+ * our code threw is seen by BOTH arms, so merging the streams would count one
63
+ * incident twice and make triage start by asking which arm saw it. The same
64
+ * separation `client-errors` and `server-errors` already keep, for the same
65
+ * reason.
66
+ *
67
+ * The entries carry a real `httpRequest.status`, which is what makes the
68
+ * policy AGL-1921 originally specified — a log filter on logName matching
69
+ * "vercel" and httpRequest.status at or above 500 — expressible without a
70
+ * log-based metric on a jsonPayload field first.
71
+ */ /**
72
+ * LAZY, and that is load-bearing rather than stylistic (AGL-1921, 2026-08-26).
73
+ *
74
+ * `client-error-report` reaches `firebase-admin`. A static import here would
75
+ * pull the whole SDK into anything that bundles this module — including the
76
+ * Cloud Run receiver, whose entire reason for existing is to run this gate
77
+ * OFF Vercel with nothing but `node:crypto` and a metadata-server token. That
78
+ * receiver always supplies its own {@link DrainIngestOptions.target}, so this
79
+ * path is never reached there and the SDK is never resolved.
80
+ *
81
+ * Called through `defaultLoggingTarget` below, never at module scope.
82
+ */ async function defaultLoggingTarget() {
83
+ const { beaconLoggingTarget } = await import("./client-error-report.js");
84
+ return beaconLoggingTarget();
85
+ }
86
+ /**
87
+ * The log id the drain writes to. Contains "vercel" on purpose: the alert
88
+ * policy `projects/aglyn-main/alertPolicies/14031689508473384486` matches
89
+ * `logName=~"vercel"`, so the name is load-bearing. Renaming this log without
90
+ * repointing that policy silences the platform-5xx arm.
91
+ */ export const VERCEL_RUNTIME_LOG_ID = 'vercel-runtime';
92
+ /** The env var holding the drain's signature secret. Fails closed if unset. */ export const DRAIN_SECRET_ENV = 'VERCEL_LOG_DRAIN_SECRET';
93
+ /**
94
+ * The receiver's own path — the FEEDBACK-LOOP GUARD (requirement 5).
95
+ *
96
+ * ⚠️ HISTORICAL, AND KEPT AS DEFENCE IN DEPTH (2026-08-26). The receiver USED
97
+ * to run on `aglyn-console`, whose logs the console drain collects, and the
98
+ * two cuts below were the answer to one receiver 500 draining back in.
99
+ *
100
+ * They were not enough, and the reason is worth stating plainly because the
101
+ * docstring that used to live here got it wrong: **both cuts are about the
102
+ * WRITE, and the cost was the DELIVERY.** Nothing inside a receiver can
103
+ * decline to be requested, and every delivery POST is itself a request that
104
+ * produces a log the same drain then delivers. Measured at 695K invocations
105
+ * in nine hours. Vercel's drain `sampling` rules are not a fix either — a
106
+ * single `{rate: 0}` rule with no path prefix moved delivery volume from 31
107
+ * to 32 per five minutes, i.e. the whole `schemas.log` filter block is stored
108
+ * and never applied.
109
+ *
110
+ * The receiver now lives OFF Vercel, at `cloud/log-drain` on Cloud Run, which
111
+ * no drain watches — so the loop is structurally impossible rather than
112
+ * filtered against. These cuts stay because they cost nothing and they are
113
+ * the backstop if anyone ever mounts this gate on a Vercel route again.
114
+ *
115
+ * Two structural cuts, not one:
116
+ *
117
+ * a. Every entry whose path is this route is dropped before the 5xx filter —
118
+ * so even the receiver's OWN 500s can never be forwarded, by anything, at
119
+ * any budget. It is not "we try not to error"; it is that its errors are
120
+ * unforwardable.
121
+ * b. Every line this module logs about itself is `console.warn`, i.e. Vercel
122
+ * `level: "warning"`, and the filter forwards only 5xx and `fatal`. A
123
+ * suppression summary drained back in is therefore dropped a second time,
124
+ * on a different property, by a different rule.
125
+ *
126
+ * Matched as a PREFIX so `/api/log-drain`, a future `/api/log-drain/test` and
127
+ * the query-bearing `proxy.path` form all fall on the same side of it.
128
+ */ export const RECEIVER_ROUTE_PATH = '/api/log-drain';
129
+ /**
130
+ * The lockdown notice route — a route whose 503 is the FEATURE, not a fault.
131
+ *
132
+ * `apps/tenant/app/api/locked/route.ts` answers every request with a real 503
133
+ * carrying `Retry-After`, and the middleware rewrites every path of a locked
134
+ * or bandwidth-capped host to it. That status is deliberate: a takedown
135
+ * notice answering 200 would tell crawlers and uptime checks the site is
136
+ * fine, which is the whole reason the notice is a route handler rather than a
137
+ * page.
138
+ *
139
+ * Forwarded, it is indistinguishable from an outage. One suspended host being
140
+ * crawled emits a 5xx per request, forever, into the arm whose only consumer
141
+ * is an alert policy — so the policy fires on a working feature and the next
142
+ * real incident arrives in a stream that is already red.
143
+ *
144
+ * Matched EXACTLY, not as a prefix: the notice is one path, and a future
145
+ * `/api/locked/*` would be a different route with no such guarantee about its
146
+ * status.
147
+ */ export const LOCKDOWN_NOTICE_ROUTE_PATH = '/api/locked';
148
+ /**
149
+ * Per-instance write budget, mirroring `reportServerError`'s (same file
150
+ * neighbourhood, same rationale): the failure being watched is a SPIKE, and a
151
+ * spike is when an unbounded forwarder turns one incident into a second one
152
+ * denominated in dollars. 60 entries a minute per instance is far more than
153
+ * any threshold policy needs to cross — the shape of the spike survives; the
154
+ * bill does not follow it.
155
+ */ const DRAIN_BUDGET_PER_WINDOW = 60;
156
+ const DRAIN_WINDOW_MS = 60000;
157
+ let drainWindowStartedAt = 0;
158
+ let drainWritten = 0;
159
+ let drainSuppressed = 0;
160
+ const MAX_MESSAGE = 1024;
161
+ const WRITE_TIMEOUT_MS = 4000;
162
+ /** Reset the budget counters. Test seam only — never called in production. */ export function resetDrainBudgetForTests() {
163
+ drainWindowStartedAt = 0;
164
+ drainWritten = 0;
165
+ drainSuppressed = 0;
166
+ }
167
+ function clampString(value, max) {
168
+ return typeof value === 'string' ? value.slice(0, max) : '';
169
+ }
170
+ /**
171
+ * The entries in one delivery, in either format Vercel will send.
172
+ *
173
+ * `json` delivers an array (and, per the reference's own example, sometimes a
174
+ * newline-separated sequence of objects); `ndjson` delivers one object per
175
+ * line. Parsing both means the drain's `deliveryFormat` can be changed in the
176
+ * dashboard without a deploy, and a malformed line is skipped rather than
177
+ * failing the whole delivery — Vercel disables a drain that errors too often,
178
+ * so a receiver that 500s on one bad line silences itself.
179
+ */ export function parseDrainPayload(rawBody) {
180
+ const body = rawBody.trim();
181
+ if (!body) return [];
182
+ try {
183
+ const parsed = JSON.parse(body);
184
+ if (Array.isArray(parsed)) {
185
+ return parsed.filter((entry)=>Boolean(entry) && typeof entry === 'object');
186
+ }
187
+ if (parsed && typeof parsed === 'object') return [
188
+ parsed
189
+ ];
190
+ } catch (unused) {
191
+ // Not a single JSON document — fall through to line-by-line.
192
+ }
193
+ const entries = [];
194
+ for (const line of body.split('\n')){
195
+ const trimmed = line.trim();
196
+ if (!trimmed) continue;
197
+ try {
198
+ const parsed = JSON.parse(trimmed);
199
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
200
+ entries.push(parsed);
201
+ }
202
+ } catch (unused) {
203
+ // One unreadable line never fails a delivery.
204
+ }
205
+ }
206
+ return entries;
207
+ }
208
+ /** Does this entry describe the receiver's own route? See RECEIVER_ROUTE_PATH. */ export function isReceiverEntry(entry) {
209
+ var _entry_proxy;
210
+ const paths = [
211
+ entry.path,
212
+ (_entry_proxy = entry.proxy) == null ? void 0 : _entry_proxy.path
213
+ ];
214
+ return paths.some((path)=>{
215
+ if (typeof path !== 'string') return false;
216
+ // `proxy.path` carries the query string; compare the path part only.
217
+ const [pathname] = path.split('?');
218
+ return pathname === RECEIVER_ROUTE_PATH || pathname.startsWith(`${RECEIVER_ROUTE_PATH}/`);
219
+ });
220
+ }
221
+ /**
222
+ * Is this the lockdown notice serving its deliberate 503? See
223
+ * {@link LOCKDOWN_NOTICE_ROUTE_PATH}.
224
+ *
225
+ * Narrow on purpose — the route being exempt must not make the route
226
+ * unwatchable. Only a clean 503 is excused: a 500 the handler threw, the
227
+ * `statusCode -1` of a crashed lambda, and a `fatal` line all still forward,
228
+ * because those say the notice itself is broken and a locked host is then
229
+ * serving nothing at all.
230
+ */ export function isLockdownNoticeEntry(entry) {
231
+ var _entry_proxy, _entry_proxy1;
232
+ if (entry.level === 'fatal' || entry.type === 'fatal') return false;
233
+ const statuses = [
234
+ entry.statusCode,
235
+ (_entry_proxy = entry.proxy) == null ? void 0 : _entry_proxy.statusCode
236
+ ].filter((status)=>typeof status === 'number');
237
+ if (!statuses.length || statuses.some((status)=>status !== 503)) return false;
238
+ return [
239
+ entry.path,
240
+ (_entry_proxy1 = entry.proxy) == null ? void 0 : _entry_proxy1.path
241
+ ].some((path)=>{
242
+ if (typeof path !== 'string') return false;
243
+ // `proxy.path` carries the query string; compare the path part only.
244
+ const [pathname] = path.split('?');
245
+ return pathname === LOCKDOWN_NOTICE_ROUTE_PATH;
246
+ });
247
+ }
248
+ /**
249
+ * The health contract's routes, whose 503 is a VERDICT and not a fault.
250
+ *
251
+ * Prefix, not an exact path: every endpoint under it speaks the same contract
252
+ * — `healthHttpStatus` answers 200 while a check reports ok and 503 the moment
253
+ * it reports degraded — so `/api/health`, `/api/health/crons` and
254
+ * `/api/health/render/site` are all the same case.
255
+ */ export const HEALTH_ROUTE_PREFIX = '/api/health';
256
+ /**
257
+ * Is this a health endpoint answering 503 because it is doing its job?
258
+ * (AGL-2709)
259
+ *
260
+ * ⚠️ THE STREAM WAS 91% THIS. Measured over the 48 hours to 2026-09-09, the
261
+ * drain forwarded 251 entries into `vercel-runtime`. **229 were `/api/health/*`
262
+ * answering 503** — `crons` 113, `funnel` 73, `server-errors` 18 — and 22 were
263
+ * the AGL-2708 outage, the only real server errors in the window. The policy
264
+ * on that log (`logName=~"vercel" AND severity>=ERROR`, one notification an
265
+ * hour) therefore mails about a working health check for as long as any
266
+ * subsystem is degraded: `/api/health/crons` was CORRECTLY reporting a broken
267
+ * job for six hours on 2026-09-07, which is six hours of "server errors" mail
268
+ * arriving in the same inbox that then had to notice 04:02 on 2026-09-09.
269
+ *
270
+ * Excusing it removes a DUPLICATE, not a signal, and the reason is structural
271
+ * rather than a judgement call: every `/api/health/*` route both apps ship has
272
+ * a monitor reading it, and `evaluateSubsystemReaders` fails the build if one
273
+ * ever does not. The 503 is already on the uptime probe's board, in an
274
+ * UptimeRobot monitor and on `docs.aglyn.com/status`, said in a body that
275
+ * names WHICH check failed — which the drain entry never could.
276
+ *
277
+ * Narrow in exactly the same three ways as {@link isLockdownNoticeEntry}, and
278
+ * each one is load-bearing:
279
+ *
280
+ * - a clean 503 only. A 500 the handler threw means the health route itself
281
+ * is broken, and a check that cannot answer must never look calm;
282
+ * - never a `fatal` line, which is process death rather than a verdict;
283
+ * - never `statusCode -1`, the crashed-lambda sentinel, which is not 503 and
284
+ * so fails the status test above.
285
+ */ export function isHealthContractEntry(entry) {
286
+ var _entry_proxy, _entry_proxy1;
287
+ if (entry.level === 'fatal' || entry.type === 'fatal') return false;
288
+ const statuses = [
289
+ entry.statusCode,
290
+ (_entry_proxy = entry.proxy) == null ? void 0 : _entry_proxy.statusCode
291
+ ].filter((status)=>typeof status === 'number');
292
+ if (!statuses.length || statuses.some((status)=>status !== 503)) return false;
293
+ return [
294
+ entry.path,
295
+ (_entry_proxy1 = entry.proxy) == null ? void 0 : _entry_proxy1.path
296
+ ].some((path)=>{
297
+ if (typeof path !== 'string') return false;
298
+ // `proxy.path` carries the query string; compare the path part only.
299
+ const [pathname] = path.split('?');
300
+ return pathname === HEALTH_ROUTE_PREFIX || pathname.startsWith(`${HEALTH_ROUTE_PREFIX}/`);
301
+ });
302
+ }
303
+ /**
304
+ * THE COST GATE (requirement 3). Everything false here is dropped unwritten.
305
+ *
306
+ * What counts as a server error:
307
+ *
308
+ * - `statusCode >= 500` or `proxy.statusCode >= 500` — the platform 5xx this
309
+ * whole arm exists for, including the ones our code never saw.
310
+ * - `statusCode === -1` — documented as "no response returned and the lambda
311
+ * crashed". The OOM/hard-crash case, and the single most important entry
312
+ * shape the `onRequestError` hook is structurally unable to produce.
313
+ * - `level === 'fatal'` or `type === 'fatal'` — the edge-runtime and
314
+ * process-death shape, which may carry no status at all.
315
+ *
316
+ * What deliberately does NOT count:
317
+ *
318
+ * - `level === 'error'`. That is any `console.error`, including the fail-soft
319
+ * warn/error lines the beacons themselves write, and forwarding it would
320
+ * mean paying per line for a stream the `onRequestError` hook already
321
+ * covers properly. If an errored request matters, it has a status.
322
+ * - `proxy.statusCode === -1`. Same sentinel, opposite meaning: on the proxy
323
+ * object it documents a BACKGROUND REVALIDATION, which is a healthy ISR
324
+ * refresh. Reading it as a crash would forward a steady trickle of normal
325
+ * traffic forever — the exact bill this gate exists to prevent.
326
+ */ export function isServerErrorEntry(entry) {
327
+ var _entry_proxy;
328
+ if (typeof entry.statusCode === 'number') {
329
+ if (entry.statusCode >= 500) return true;
330
+ if (entry.statusCode === -1) return true;
331
+ }
332
+ const proxyStatus = (_entry_proxy = entry.proxy) == null ? void 0 : _entry_proxy.statusCode;
333
+ if (typeof proxyStatus === 'number' && proxyStatus >= 500) return true;
334
+ if (entry.level === 'fatal' || entry.type === 'fatal') return true;
335
+ return false;
336
+ }
337
+ /**
338
+ * Everything above, in the order the cost argument requires.
339
+ * Exported so the tests can assert the gate without a transport.
340
+ */ export function selectForwardableEntries(entries) {
341
+ return entries.filter((entry)=>!isReceiverEntry(entry) && !isLockdownNoticeEntry(entry) && !isHealthContractEntry(entry) && isServerErrorEntry(entry));
342
+ }
343
+ /**
344
+ * One Cloud Logging entry, carrying ONLY fields that are already inside the
345
+ * envelope the shipped beacons send (requirement 8).
346
+ *
347
+ * Explicitly absent, and each for a reason:
348
+ * `proxy.path` (query string — the AGL-1538 boundary is origin+pathname and
349
+ * this is the same boundary), `proxy.clientIp` and `proxy.userAgent` and
350
+ * `proxy.referer` (visitor identity; also why Vercel's own IP-visibility
351
+ * toggle exists), request or response bodies (never delivered, never wanted).
352
+ * `path` IS forwarded because Vercel documents it as the function or dynamic
353
+ * path — the route PATTERN, which is what you group and alert on anyway, and
354
+ * exactly what `reportServerError` already sends as `route`.
355
+ */ function toLogEntry(entry) {
356
+ var _entry_proxy, _entry_proxy1, _entry_proxy2;
357
+ const status = typeof entry.statusCode === 'number' && entry.statusCode >= 0 ? entry.statusCode : typeof ((_entry_proxy = entry.proxy) == null ? void 0 : _entry_proxy.statusCode) === 'number' && entry.proxy.statusCode >= 0 ? entry.proxy.statusCode : undefined;
358
+ return {
359
+ severity: 'ERROR',
360
+ // The structured field the AGL-1921 policy matches on. A jsonPayload key
361
+ // would need a log-based metric first; this one is queryable as shipped.
362
+ httpRequest: {
363
+ requestMethod: clampString((_entry_proxy1 = entry.proxy) == null ? void 0 : _entry_proxy1.method, 16) || undefined,
364
+ status
365
+ },
366
+ jsonPayload: {
367
+ // No ReportedErrorEvent `@type`: these are NOT ingested into Error
368
+ // Reporting. A function timeout has no stack, would group as one giant
369
+ // bucket, and would drown the real grouped errors the hook reports.
370
+ source: clampString(entry.source, 32) || undefined,
371
+ level: clampString(entry.level, 16) || undefined,
372
+ type: clampString(entry.type, 32) || undefined,
373
+ route: clampString(entry.path, 512) || undefined,
374
+ entrypoint: clampString(entry.entrypoint, 256) || undefined,
375
+ host: clampString(entry.host, 256) || undefined,
376
+ environment: clampString(entry.environment, 32) || undefined,
377
+ region: clampString(entry.executionRegion, 32) || undefined,
378
+ project: clampString(entry.projectName, 64) || undefined,
379
+ projectId: clampString(entry.projectId, 64) || undefined,
380
+ deploymentId: clampString(entry.deploymentId, 64) || undefined,
381
+ requestId: clampString(entry.requestId, 64) || undefined,
382
+ logId: clampString(entry.id, 64) || undefined,
383
+ statusCode: typeof entry.statusCode === 'number' ? entry.statusCode : undefined,
384
+ proxyStatusCode: typeof ((_entry_proxy2 = entry.proxy) == null ? void 0 : _entry_proxy2.statusCode) === 'number' ? entry.proxy.statusCode : undefined,
385
+ // Clamped like the beacon's own message. This is where "Task timed out
386
+ // after 10.01 seconds" lives, which is the whole triage value of the
387
+ // platform-5xx case.
388
+ message: clampString(entry.message, MAX_MESSAGE) || undefined
389
+ }
390
+ };
391
+ }
392
+ /**
393
+ * Ingest ONE verified delivery: filter, budget, then a single batched write.
394
+ *
395
+ * Never throws. Vercel disables a drain whose endpoint fails often enough, so
396
+ * a receiver that propagated a Logging outage would end by turning itself off
397
+ * during the incident it exists to report.
398
+ */ export async function ingestDrainDelivery(entries, options) {
399
+ var _ref;
400
+ const received = entries.length;
401
+ const matched = selectForwardableEntries(entries);
402
+ const result = {
403
+ received,
404
+ matched: matched.length,
405
+ forwarded: 0,
406
+ suppressed: 0
407
+ };
408
+ if (!matched.length) return result;
409
+ const now = Date.now();
410
+ if (now - drainWindowStartedAt >= DRAIN_WINDOW_MS) {
411
+ if (drainSuppressed > 0) {
412
+ // ⚠️ The lossiness is REPORTED. `level: "warning"`, so guard (b) in
413
+ // RECEIVER_ROUTE_PATH keeps this line from ever draining back in.
414
+ console.warn(JSON.stringify({
415
+ tag: 'AGL-1921:vercel-log-drain',
416
+ suppressed: drainSuppressed,
417
+ written: drainWritten,
418
+ windowMs: DRAIN_WINDOW_MS
419
+ }));
420
+ }
421
+ drainWindowStartedAt = now;
422
+ drainWritten = 0;
423
+ drainSuppressed = 0;
424
+ }
425
+ const room = Math.max(0, DRAIN_BUDGET_PER_WINDOW - drainWritten);
426
+ const writable = matched.slice(0, room);
427
+ const suppressed = matched.length - writable.length;
428
+ if (suppressed > 0) drainSuppressed += suppressed;
429
+ result.suppressed = suppressed;
430
+ if (!writable.length) return result;
431
+ const target = await ((_ref = options == null ? void 0 : options.target) != null ? _ref : defaultLoggingTarget)();
432
+ if (!target) {
433
+ console.warn(JSON.stringify({
434
+ tag: 'AGL-1921:vercel-log-drain',
435
+ drop: writable.length,
436
+ reason: 'no-credential'
437
+ }));
438
+ result.code = 'no-credential';
439
+ return result;
440
+ }
441
+ try {
442
+ const response = await fetch('https://logging.googleapis.com/v2/entries:write', {
443
+ method: 'POST',
444
+ headers: {
445
+ Authorization: `Bearer ${target.token}`,
446
+ 'Content-Type': 'application/json'
447
+ },
448
+ body: JSON.stringify({
449
+ logName: `projects/${target.projectId}/logs/${VERCEL_RUNTIME_LOG_ID}`,
450
+ resource: {
451
+ type: 'global'
452
+ },
453
+ entries: writable.map(toLogEntry)
454
+ }),
455
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS)
456
+ });
457
+ if (!response.ok) {
458
+ console.warn(JSON.stringify({
459
+ tag: 'AGL-1921:vercel-log-drain',
460
+ status: response.status,
461
+ drop: writable.length
462
+ }));
463
+ result.code = `http-${response.status}`;
464
+ return result;
465
+ }
466
+ drainWritten += writable.length;
467
+ result.forwarded = writable.length;
468
+ return result;
469
+ } catch (error) {
470
+ console.warn(JSON.stringify({
471
+ tag: 'AGL-1921:vercel-log-drain',
472
+ transport: String(error).slice(0, 200)
473
+ }));
474
+ result.code = 'transport';
475
+ return result;
476
+ }
477
+ }
478
+
479
+ //# sourceMappingURL=vercel-log-drain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/vercel-log-drain.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The Vercel log-drain receiver (AGL-1921, third and last arm).\n *\n * ## Why this arm exists at all\n *\n * The two arms already shipped both start from OUR code running. The\n * `onRequestError` hook (`reportServerError`) sees a render or route handler\n * throw; `/api/health/server-errors` grades the markers that hook writes.\n * Neither can see:\n *\n * - an error that kills the process before the hook runs,\n * - a platform-level 5xx that never reaches our code at all — a function\n * timeout, an OOM kill, a cold-start 502,\n * - anything thrown in the edge runtime.\n *\n * A log drain sees all three, because Vercel emits the request log whether or\n * not our code got far enough to have an opinion. That is why this is the arm\n * that closes the issue rather than a fourth partial view.\n *\n * ## The shape of the risk, and what each rule below buys\n *\n * A drain streams EVERY request log for both projects. The account's whole\n * monitoring budget is about $20/month, so the dangerous failure mode here is\n * not missing an error — it is faithfully forwarding a million healthy 200s\n * into Cloud Logging and getting a bill for it. Hence, in order:\n *\n * 1. **Signature first, fail closed.** HMAC-SHA1 of the RAW body keyed by the\n * drain secret, timing-safe compared. Unsigned means unwritten, always.\n * 2. **Filter before writing.** `isServerErrorEntry` is the cost gate: an\n * entry that is not a server error costs one predicate and zero writes.\n * 3. **Never our own route.** See `RECEIVER_ROUTE_PATH`.\n * 4. **Never a deliberate 5xx.** A 503 is how the lockdown notice and every\n * health contract SAY something, and forwarding a verdict as a fault makes\n * the stream unreadable: measured over the 48 hours to 2026-09-09, 229 of\n * the 251 entries in this log were `/api/health/*` reporting degraded and\n * 22 were the only real outage in the window. See `isLockdownNoticeEntry`\n * and `isHealthContractEntry` (AGL-2709).\n * 5. **A per-instance budget per window**, with suppression REPORTED. A\n * monitoring path that hides its own lossiness is the bug shape this repo\n * keeps rediscovering.\n *\n * ## Where it writes, and why not next door\n *\n * `vercel-runtime`, DELIBERATELY NOT `server-errors`. That log is the\n * `onRequestError` hook's, and the alert policy\n * `projects/aglyn-main/alertPolicies/11610705614308437855` keys on it. A 500\n * our code threw is seen by BOTH arms, so merging the streams would count one\n * incident twice and make triage start by asking which arm saw it. The same\n * separation `client-errors` and `server-errors` already keep, for the same\n * reason.\n *\n * The entries carry a real `httpRequest.status`, which is what makes the\n * policy AGL-1921 originally specified — a log filter on logName matching\n * \"vercel\" and httpRequest.status at or above 500 — expressible without a\n * log-based metric on a jsonPayload field first.\n */\n\n/**\n * LAZY, and that is load-bearing rather than stylistic (AGL-1921, 2026-08-26).\n *\n * `client-error-report` reaches `firebase-admin`. A static import here would\n * pull the whole SDK into anything that bundles this module — including the\n * Cloud Run receiver, whose entire reason for existing is to run this gate\n * OFF Vercel with nothing but `node:crypto` and a metadata-server token. That\n * receiver always supplies its own {@link DrainIngestOptions.target}, so this\n * path is never reached there and the SDK is never resolved.\n *\n * Called through `defaultLoggingTarget` below, never at module scope.\n */\nasync function defaultLoggingTarget(): Promise<{\n token: string\n projectId: string\n} | null> {\n const { beaconLoggingTarget } = await import('./client-error-report')\n return beaconLoggingTarget()\n}\n\n/**\n * The log id the drain writes to. Contains \"vercel\" on purpose: the alert\n * policy `projects/aglyn-main/alertPolicies/14031689508473384486` matches\n * `logName=~\"vercel\"`, so the name is load-bearing. Renaming this log without\n * repointing that policy silences the platform-5xx arm.\n */\nexport const VERCEL_RUNTIME_LOG_ID = 'vercel-runtime'\n\n/** The env var holding the drain's signature secret. Fails closed if unset. */\nexport const DRAIN_SECRET_ENV = 'VERCEL_LOG_DRAIN_SECRET'\n\n/**\n * The receiver's own path — the FEEDBACK-LOOP GUARD (requirement 5).\n *\n * ⚠️ HISTORICAL, AND KEPT AS DEFENCE IN DEPTH (2026-08-26). The receiver USED\n * to run on `aglyn-console`, whose logs the console drain collects, and the\n * two cuts below were the answer to one receiver 500 draining back in.\n *\n * They were not enough, and the reason is worth stating plainly because the\n * docstring that used to live here got it wrong: **both cuts are about the\n * WRITE, and the cost was the DELIVERY.** Nothing inside a receiver can\n * decline to be requested, and every delivery POST is itself a request that\n * produces a log the same drain then delivers. Measured at 695K invocations\n * in nine hours. Vercel's drain `sampling` rules are not a fix either — a\n * single `{rate: 0}` rule with no path prefix moved delivery volume from 31\n * to 32 per five minutes, i.e. the whole `schemas.log` filter block is stored\n * and never applied.\n *\n * The receiver now lives OFF Vercel, at `cloud/log-drain` on Cloud Run, which\n * no drain watches — so the loop is structurally impossible rather than\n * filtered against. These cuts stay because they cost nothing and they are\n * the backstop if anyone ever mounts this gate on a Vercel route again.\n *\n * Two structural cuts, not one:\n *\n * a. Every entry whose path is this route is dropped before the 5xx filter —\n * so even the receiver's OWN 500s can never be forwarded, by anything, at\n * any budget. It is not \"we try not to error\"; it is that its errors are\n * unforwardable.\n * b. Every line this module logs about itself is `console.warn`, i.e. Vercel\n * `level: \"warning\"`, and the filter forwards only 5xx and `fatal`. A\n * suppression summary drained back in is therefore dropped a second time,\n * on a different property, by a different rule.\n *\n * Matched as a PREFIX so `/api/log-drain`, a future `/api/log-drain/test` and\n * the query-bearing `proxy.path` form all fall on the same side of it.\n */\nexport const RECEIVER_ROUTE_PATH = '/api/log-drain'\n\n/**\n * The lockdown notice route — a route whose 503 is the FEATURE, not a fault.\n *\n * `apps/tenant/app/api/locked/route.ts` answers every request with a real 503\n * carrying `Retry-After`, and the middleware rewrites every path of a locked\n * or bandwidth-capped host to it. That status is deliberate: a takedown\n * notice answering 200 would tell crawlers and uptime checks the site is\n * fine, which is the whole reason the notice is a route handler rather than a\n * page.\n *\n * Forwarded, it is indistinguishable from an outage. One suspended host being\n * crawled emits a 5xx per request, forever, into the arm whose only consumer\n * is an alert policy — so the policy fires on a working feature and the next\n * real incident arrives in a stream that is already red.\n *\n * Matched EXACTLY, not as a prefix: the notice is one path, and a future\n * `/api/locked/*` would be a different route with no such guarantee about its\n * status.\n */\nexport const LOCKDOWN_NOTICE_ROUTE_PATH = '/api/locked'\n\n/**\n * Per-instance write budget, mirroring `reportServerError`'s (same file\n * neighbourhood, same rationale): the failure being watched is a SPIKE, and a\n * spike is when an unbounded forwarder turns one incident into a second one\n * denominated in dollars. 60 entries a minute per instance is far more than\n * any threshold policy needs to cross — the shape of the spike survives; the\n * bill does not follow it.\n */\nconst DRAIN_BUDGET_PER_WINDOW = 60\nconst DRAIN_WINDOW_MS = 60_000\nlet drainWindowStartedAt = 0\nlet drainWritten = 0\nlet drainSuppressed = 0\n\nconst MAX_MESSAGE = 1_024\nconst WRITE_TIMEOUT_MS = 4_000\n\n/** Reset the budget counters. Test seam only — never called in production. */\nexport function resetDrainBudgetForTests(): void {\n drainWindowStartedAt = 0\n drainWritten = 0\n drainSuppressed = 0\n}\n\n/**\n * One log entry as a Vercel drain delivers it. Only the fields this receiver\n * reads are named; the wire object carries many more (client IP, user agent,\n * referer, the query-bearing `proxy.path`) which are deliberately NOT in this\n * type, because a field that has no name here cannot be forwarded by accident.\n */\nexport interface VercelDrainEntry {\n id?: string\n deploymentId?: string\n projectId?: string\n projectName?: string\n source?: string\n host?: string\n timestamp?: number\n level?: string\n type?: string\n message?: string\n path?: string\n entrypoint?: string\n requestId?: string\n statusCode?: number\n environment?: string\n executionRegion?: string\n proxy?: {\n method?: string\n statusCode?: number\n path?: string\n pathType?: string\n }\n}\n\nfunction clampString(value: unknown, max: number): string {\n return typeof value === 'string' ? value.slice(0, max) : ''\n}\n\n/**\n * The entries in one delivery, in either format Vercel will send.\n *\n * `json` delivers an array (and, per the reference's own example, sometimes a\n * newline-separated sequence of objects); `ndjson` delivers one object per\n * line. Parsing both means the drain's `deliveryFormat` can be changed in the\n * dashboard without a deploy, and a malformed line is skipped rather than\n * failing the whole delivery — Vercel disables a drain that errors too often,\n * so a receiver that 500s on one bad line silences itself.\n */\nexport function parseDrainPayload(rawBody: string): VercelDrainEntry[] {\n const body = rawBody.trim()\n if (!body) return []\n try {\n const parsed: unknown = JSON.parse(body)\n if (Array.isArray(parsed)) {\n return parsed.filter(\n (entry): entry is VercelDrainEntry =>\n Boolean(entry) && typeof entry === 'object',\n )\n }\n if (parsed && typeof parsed === 'object')\n return [parsed as VercelDrainEntry]\n } catch {\n // Not a single JSON document — fall through to line-by-line.\n }\n const entries: VercelDrainEntry[] = []\n for (const line of body.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed) continue\n try {\n const parsed: unknown = JSON.parse(trimmed)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n entries.push(parsed as VercelDrainEntry)\n }\n } catch {\n // One unreadable line never fails a delivery.\n }\n }\n return entries\n}\n\n/** Does this entry describe the receiver's own route? See RECEIVER_ROUTE_PATH. */\nexport function isReceiverEntry(entry: VercelDrainEntry): boolean {\n const paths = [entry.path, entry.proxy?.path]\n return paths.some((path) => {\n if (typeof path !== 'string') return false\n // `proxy.path` carries the query string; compare the path part only.\n const [pathname] = path.split('?')\n return (\n pathname === RECEIVER_ROUTE_PATH ||\n pathname.startsWith(`${RECEIVER_ROUTE_PATH}/`)\n )\n })\n}\n\n/**\n * Is this the lockdown notice serving its deliberate 503? See\n * {@link LOCKDOWN_NOTICE_ROUTE_PATH}.\n *\n * Narrow on purpose — the route being exempt must not make the route\n * unwatchable. Only a clean 503 is excused: a 500 the handler threw, the\n * `statusCode -1` of a crashed lambda, and a `fatal` line all still forward,\n * because those say the notice itself is broken and a locked host is then\n * serving nothing at all.\n */\nexport function isLockdownNoticeEntry(entry: VercelDrainEntry): boolean {\n if (entry.level === 'fatal' || entry.type === 'fatal') return false\n const statuses = [entry.statusCode, entry.proxy?.statusCode].filter(\n (status): status is number => typeof status === 'number',\n )\n if (!statuses.length || statuses.some((status) => status !== 503))\n return false\n return [entry.path, entry.proxy?.path].some((path) => {\n if (typeof path !== 'string') return false\n // `proxy.path` carries the query string; compare the path part only.\n const [pathname] = path.split('?')\n return pathname === LOCKDOWN_NOTICE_ROUTE_PATH\n })\n}\n\n/**\n * The health contract's routes, whose 503 is a VERDICT and not a fault.\n *\n * Prefix, not an exact path: every endpoint under it speaks the same contract\n * — `healthHttpStatus` answers 200 while a check reports ok and 503 the moment\n * it reports degraded — so `/api/health`, `/api/health/crons` and\n * `/api/health/render/site` are all the same case.\n */\nexport const HEALTH_ROUTE_PREFIX = '/api/health'\n\n/**\n * Is this a health endpoint answering 503 because it is doing its job?\n * (AGL-2709)\n *\n * ⚠️ THE STREAM WAS 91% THIS. Measured over the 48 hours to 2026-09-09, the\n * drain forwarded 251 entries into `vercel-runtime`. **229 were `/api/health/*`\n * answering 503** — `crons` 113, `funnel` 73, `server-errors` 18 — and 22 were\n * the AGL-2708 outage, the only real server errors in the window. The policy\n * on that log (`logName=~\"vercel\" AND severity>=ERROR`, one notification an\n * hour) therefore mails about a working health check for as long as any\n * subsystem is degraded: `/api/health/crons` was CORRECTLY reporting a broken\n * job for six hours on 2026-09-07, which is six hours of \"server errors\" mail\n * arriving in the same inbox that then had to notice 04:02 on 2026-09-09.\n *\n * Excusing it removes a DUPLICATE, not a signal, and the reason is structural\n * rather than a judgement call: every `/api/health/*` route both apps ship has\n * a monitor reading it, and `evaluateSubsystemReaders` fails the build if one\n * ever does not. The 503 is already on the uptime probe's board, in an\n * UptimeRobot monitor and on `docs.aglyn.com/status`, said in a body that\n * names WHICH check failed — which the drain entry never could.\n *\n * Narrow in exactly the same three ways as {@link isLockdownNoticeEntry}, and\n * each one is load-bearing:\n *\n * - a clean 503 only. A 500 the handler threw means the health route itself\n * is broken, and a check that cannot answer must never look calm;\n * - never a `fatal` line, which is process death rather than a verdict;\n * - never `statusCode -1`, the crashed-lambda sentinel, which is not 503 and\n * so fails the status test above.\n */\nexport function isHealthContractEntry(entry: VercelDrainEntry): boolean {\n if (entry.level === 'fatal' || entry.type === 'fatal') return false\n const statuses = [entry.statusCode, entry.proxy?.statusCode].filter(\n (status): status is number => typeof status === 'number',\n )\n if (!statuses.length || statuses.some((status) => status !== 503))\n return false\n return [entry.path, entry.proxy?.path].some((path) => {\n if (typeof path !== 'string') return false\n // `proxy.path` carries the query string; compare the path part only.\n const [pathname] = path.split('?')\n return (\n pathname === HEALTH_ROUTE_PREFIX ||\n pathname.startsWith(`${HEALTH_ROUTE_PREFIX}/`)\n )\n })\n}\n\n/**\n * THE COST GATE (requirement 3). Everything false here is dropped unwritten.\n *\n * What counts as a server error:\n *\n * - `statusCode >= 500` or `proxy.statusCode >= 500` — the platform 5xx this\n * whole arm exists for, including the ones our code never saw.\n * - `statusCode === -1` — documented as \"no response returned and the lambda\n * crashed\". The OOM/hard-crash case, and the single most important entry\n * shape the `onRequestError` hook is structurally unable to produce.\n * - `level === 'fatal'` or `type === 'fatal'` — the edge-runtime and\n * process-death shape, which may carry no status at all.\n *\n * What deliberately does NOT count:\n *\n * - `level === 'error'`. That is any `console.error`, including the fail-soft\n * warn/error lines the beacons themselves write, and forwarding it would\n * mean paying per line for a stream the `onRequestError` hook already\n * covers properly. If an errored request matters, it has a status.\n * - `proxy.statusCode === -1`. Same sentinel, opposite meaning: on the proxy\n * object it documents a BACKGROUND REVALIDATION, which is a healthy ISR\n * refresh. Reading it as a crash would forward a steady trickle of normal\n * traffic forever — the exact bill this gate exists to prevent.\n */\nexport function isServerErrorEntry(entry: VercelDrainEntry): boolean {\n if (typeof entry.statusCode === 'number') {\n if (entry.statusCode >= 500) return true\n if (entry.statusCode === -1) return true\n }\n const proxyStatus = entry.proxy?.statusCode\n if (typeof proxyStatus === 'number' && proxyStatus >= 500) return true\n if (entry.level === 'fatal' || entry.type === 'fatal') return true\n return false\n}\n\n/**\n * Everything above, in the order the cost argument requires.\n * Exported so the tests can assert the gate without a transport.\n */\nexport function selectForwardableEntries(\n entries: readonly VercelDrainEntry[],\n): VercelDrainEntry[] {\n return entries.filter(\n (entry) =>\n !isReceiverEntry(entry) &&\n !isLockdownNoticeEntry(entry) &&\n !isHealthContractEntry(entry) &&\n isServerErrorEntry(entry),\n )\n}\n\n/**\n * One Cloud Logging entry, carrying ONLY fields that are already inside the\n * envelope the shipped beacons send (requirement 8).\n *\n * Explicitly absent, and each for a reason:\n * `proxy.path` (query string — the AGL-1538 boundary is origin+pathname and\n * this is the same boundary), `proxy.clientIp` and `proxy.userAgent` and\n * `proxy.referer` (visitor identity; also why Vercel's own IP-visibility\n * toggle exists), request or response bodies (never delivered, never wanted).\n * `path` IS forwarded because Vercel documents it as the function or dynamic\n * path — the route PATTERN, which is what you group and alert on anyway, and\n * exactly what `reportServerError` already sends as `route`.\n */\nfunction toLogEntry(entry: VercelDrainEntry): Record<string, unknown> {\n const status =\n typeof entry.statusCode === 'number' && entry.statusCode >= 0\n ? entry.statusCode\n : typeof entry.proxy?.statusCode === 'number' &&\n entry.proxy.statusCode >= 0\n ? entry.proxy.statusCode\n : undefined\n return {\n severity: 'ERROR',\n // The structured field the AGL-1921 policy matches on. A jsonPayload key\n // would need a log-based metric first; this one is queryable as shipped.\n httpRequest: {\n requestMethod: clampString(entry.proxy?.method, 16) || undefined,\n status,\n },\n jsonPayload: {\n // No ReportedErrorEvent `@type`: these are NOT ingested into Error\n // Reporting. A function timeout has no stack, would group as one giant\n // bucket, and would drown the real grouped errors the hook reports.\n source: clampString(entry.source, 32) || undefined,\n level: clampString(entry.level, 16) || undefined,\n type: clampString(entry.type, 32) || undefined,\n route: clampString(entry.path, 512) || undefined,\n entrypoint: clampString(entry.entrypoint, 256) || undefined,\n host: clampString(entry.host, 256) || undefined,\n environment: clampString(entry.environment, 32) || undefined,\n region: clampString(entry.executionRegion, 32) || undefined,\n project: clampString(entry.projectName, 64) || undefined,\n projectId: clampString(entry.projectId, 64) || undefined,\n deploymentId: clampString(entry.deploymentId, 64) || undefined,\n requestId: clampString(entry.requestId, 64) || undefined,\n logId: clampString(entry.id, 64) || undefined,\n statusCode:\n typeof entry.statusCode === 'number' ? entry.statusCode : undefined,\n proxyStatusCode:\n typeof entry.proxy?.statusCode === 'number'\n ? entry.proxy.statusCode\n : undefined,\n // Clamped like the beacon's own message. This is where \"Task timed out\n // after 10.01 seconds\" lives, which is the whole triage value of the\n // platform-5xx case.\n message: clampString(entry.message, MAX_MESSAGE) || undefined,\n },\n }\n}\n\n/** What one delivery did, for the response body, the tests and the caller. */\nexport interface DrainIngestResult {\n /** Entries in the delivery. */\n received: number\n /** Entries that passed the receiver-path and 5xx gates. */\n matched: number\n /** Entries actually written to Cloud Logging. */\n forwarded: number\n /** Entries dropped by the per-window budget — REPORTED, never silent. */\n suppressed: number\n /** Set when nothing could be written at all; a stable code, never a message. */\n code?: 'no-credential' | 'transport' | `http-${number}`\n}\n\n/**\n * Where the write goes, and under whose credential.\n *\n * Injectable because the receiver has to be able to run somewhere that is NOT\n * a Vercel project this drain watches — a delivery POST is a request to\n * whatever hosts it, and a request produces a log the same drain then\n * delivers, which is the AGL-1921 feedback loop. Vercel's drain `sampling`\n * rules look like the answer and are NOT: measured 2026-08-26, a single\n * `{rate: 0}` rule with no path prefix — literally \"drop everything\" — changed\n * delivery volume from 31 to 32 per five minutes. The whole `schemas.log`\n * filter block is accepted by the API, echoed back on `GET`, and never\n * applied. Separation is the only control that works.\n *\n * The default keeps every existing caller on the Firebase admin credential.\n * A receiver hosted off Vercel (Cloud Run in `aglyn-main`) passes its own\n * resolver instead and never imports `firebase-admin` at all, which is what\n * lets this module be bundled for it without dragging the SDK along.\n */\nexport interface DrainIngestOptions {\n /**\n * Resolve the Cloud Logging bearer token and project. Returning `null`\n * means \"no credential\" and the delivery is dropped, reported, and answered\n * 200 — never an error, because Vercel disables a drain whose endpoint\n * fails often enough.\n */\n target?: () => Promise<{ token: string; projectId: string } | null>\n}\n\n/**\n * Ingest ONE verified delivery: filter, budget, then a single batched write.\n *\n * Never throws. Vercel disables a drain whose endpoint fails often enough, so\n * a receiver that propagated a Logging outage would end by turning itself off\n * during the incident it exists to report.\n */\nexport async function ingestDrainDelivery(\n entries: readonly VercelDrainEntry[],\n options?: DrainIngestOptions,\n): Promise<DrainIngestResult> {\n const received = entries.length\n const matched = selectForwardableEntries(entries)\n const result: DrainIngestResult = {\n received,\n matched: matched.length,\n forwarded: 0,\n suppressed: 0,\n }\n if (!matched.length) return result\n\n const now = Date.now()\n if (now - drainWindowStartedAt >= DRAIN_WINDOW_MS) {\n if (drainSuppressed > 0) {\n // ⚠️ The lossiness is REPORTED. `level: \"warning\"`, so guard (b) in\n // RECEIVER_ROUTE_PATH keeps this line from ever draining back in.\n console.warn(\n JSON.stringify({\n tag: 'AGL-1921:vercel-log-drain',\n suppressed: drainSuppressed,\n written: drainWritten,\n windowMs: DRAIN_WINDOW_MS,\n }),\n )\n }\n drainWindowStartedAt = now\n drainWritten = 0\n drainSuppressed = 0\n }\n\n const room = Math.max(0, DRAIN_BUDGET_PER_WINDOW - drainWritten)\n const writable = matched.slice(0, room)\n const suppressed = matched.length - writable.length\n if (suppressed > 0) drainSuppressed += suppressed\n result.suppressed = suppressed\n if (!writable.length) return result\n\n const target = await (options?.target ?? defaultLoggingTarget)()\n if (!target) {\n console.warn(\n JSON.stringify({\n tag: 'AGL-1921:vercel-log-drain',\n drop: writable.length,\n reason: 'no-credential',\n }),\n )\n result.code = 'no-credential'\n return result\n }\n\n try {\n const response = await fetch(\n 'https://logging.googleapis.com/v2/entries:write',\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${target.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n logName: `projects/${target.projectId}/logs/${VERCEL_RUNTIME_LOG_ID}`,\n resource: { type: 'global' },\n entries: writable.map(toLogEntry),\n }),\n signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),\n },\n )\n if (!response.ok) {\n console.warn(\n JSON.stringify({\n tag: 'AGL-1921:vercel-log-drain',\n status: response.status,\n drop: writable.length,\n }),\n )\n result.code = `http-${response.status}`\n return result\n }\n drainWritten += writable.length\n result.forwarded = writable.length\n return result\n } catch (error) {\n console.warn(\n JSON.stringify({\n tag: 'AGL-1921:vercel-log-drain',\n transport: String(error).slice(0, 200),\n }),\n )\n result.code = 'transport'\n return result\n }\n}\n"],"names":["defaultLoggingTarget","beaconLoggingTarget","VERCEL_RUNTIME_LOG_ID","DRAIN_SECRET_ENV","RECEIVER_ROUTE_PATH","LOCKDOWN_NOTICE_ROUTE_PATH","DRAIN_BUDGET_PER_WINDOW","DRAIN_WINDOW_MS","drainWindowStartedAt","drainWritten","drainSuppressed","MAX_MESSAGE","WRITE_TIMEOUT_MS","resetDrainBudgetForTests","clampString","value","max","slice","parseDrainPayload","rawBody","body","trim","parsed","JSON","parse","Array","isArray","filter","entry","Boolean","entries","line","split","trimmed","push","isReceiverEntry","paths","path","proxy","some","pathname","startsWith","isLockdownNoticeEntry","level","type","statuses","statusCode","status","length","HEALTH_ROUTE_PREFIX","isHealthContractEntry","isServerErrorEntry","proxyStatus","selectForwardableEntries","toLogEntry","undefined","severity","httpRequest","requestMethod","method","jsonPayload","source","route","entrypoint","host","environment","region","executionRegion","project","projectName","projectId","deploymentId","requestId","logId","id","proxyStatusCode","message","ingestDrainDelivery","options","received","matched","result","forwarded","suppressed","now","Date","console","warn","stringify","tag","written","windowMs","room","Math","writable","target","drop","reason","code","response","fetch","headers","Authorization","token","logName","resource","map","signal","AbortSignal","timeout","ok","error","transport","String"],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The cooldown on the AUTOMATIC verification send (AGL-2584).
19
+ *
20
+ * `/verify-email` asks for a link every time it mounts, so somebody who leaves
21
+ * the tab and comes back to see whether the mail arrived asks for a second
22
+ * one. Identity Platform throttles link minting per account, ahead of and
23
+ * independently of the route's own 5/hour budget, and that collision is what a
24
+ * returning visitor met: a mail that had genuinely been sent, reported to them
25
+ * as a send failure.
26
+ *
27
+ * Reopening the page is a request to know whether the first mail worked, not a
28
+ * request for another one — so an automatic send is skipped while a link
29
+ * minted for this uid is still young. The explicit "Resend verification email"
30
+ * button does not come through here: someone who says the mail never arrived
31
+ * is asking deliberately, and that affordance keeps the full per-uid budget.
32
+ *
33
+ * ## Why the state is server-side and not a `sentAt` in the browser
34
+ *
35
+ * The throttle this avoids is per ACCOUNT, so the memory of "a link was minted
36
+ * recently" has to be too. A marker held in the browser cannot see a link
37
+ * minted from a phone, a second browser, or a private window, and clearing
38
+ * site data erases it — each of which puts the collision straight back. It
39
+ * would also put the decision on the side of the wire that does not do the
40
+ * minting, which is where it went wrong the first time.
41
+ *
42
+ * ## Why the durable limiter, with a budget of one
43
+ *
44
+ * A cooldown *is* a limit of one per window, and the limiter already solves
45
+ * the parts that are easy to get wrong: it is global rather than per instance
46
+ * (a per-instance `Map` on Vercel is close to no limit at all, AGL-794), it
47
+ * hashes the key out of the document id, and it fails soft when Firestore is
48
+ * unreachable.
49
+ *
50
+ * Its window is fixed rather than rolling, so a link minted near a boundary
51
+ * can be followed by another as soon as the next window opens — the cooldown
52
+ * holds for somewhere between zero and its full length. Where a window does
53
+ * end early the outcome is one extra mint, which the route reports as the
54
+ * throttle it is rather than as a failure.
55
+ *
56
+ * ## It may only suppress on evidence, never on silence
57
+ *
58
+ * The limiter refuses for two unrelated reasons, and only one of them is a
59
+ * counted overage. `contended` means the counter could not be read and
60
+ * written inside its budget, and the budget is spent by any slowness at all —
61
+ * a cold instance paying for Admin SDK startup and two Firestore round trips
62
+ * reaches it without another caller in sight, which on a low-traffic
63
+ * deployment is the ordinary case rather than the rare one.
64
+ *
65
+ * That refusal carries no information about whether a link was recently
66
+ * minted, so it may not be read as one. The asymmetry is the whole argument:
67
+ * a duplicate mail is a nuisance the route already reports correctly, while a
68
+ * suppressed FIRST mail is an account that never receives the link it is
69
+ * waiting for — and because a suppressed send is answered 200, the account
70
+ * holder is told a mail is on its way that nobody ever sent. A cooldown whose
71
+ * purpose is politeness does not get to fail that way, so an unanswered gate
72
+ * sends.
73
+ */
74
+ /**
75
+ * How long an automatic send suppresses the next one. Long enough to cover the
76
+ * round trip this exists for — open the mail app, look, come back — and short
77
+ * enough that someone whose mail was eaten by a spam filter gets a fresh one
78
+ * from a later visit rather than only from the resend button.
79
+ */
80
+ export declare const VERIFY_EMAIL_AUTO_SEND_COOLDOWN_MS: number;
81
+ export interface VerifyEmailAutoSendResult {
82
+ /** True when this mount may mint a link. */
83
+ allowed: boolean;
84
+ /** Seconds until the cooldown lifts; 0 when allowed. */
85
+ retryAfterSeconds: number;
86
+ /** True when the durable store was unreachable and only a local cap applied. */
87
+ degraded: boolean;
88
+ }
89
+ export interface VerifyEmailAutoSendOptions {
90
+ now?: number;
91
+ /** Injectable for tests; defaults to the Admin SDK's Firestore. */
92
+ firestore?: unknown;
93
+ }
94
+ /**
95
+ * Asks whether the page's automatic send may mint a link for `uid`.
96
+ *
97
+ * Keyed on the uid alone, under its own prefix: the cooldown is one account's
98
+ * and never another's, and it counts separately from the per-uid hourly budget
99
+ * so a suppressed automatic send spends nothing a deliberate resend would want.
100
+ */
101
+ export declare function consumeVerifyEmailAutoSend(uid: string, options?: VerifyEmailAutoSendOptions): Promise<VerifyEmailAutoSendResult>;
102
+ export default consumeVerifyEmailAutoSend;