@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/jest.integration.config.d.ts +39 -0
- package/jest.integration.setup.d.ts +17 -0
- package/package.json +51 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/render-cache.d.ts +184 -0
- package/src/lib/render-cache.js +198 -0
- package/src/lib/render-cache.js.map +1 -0
- package/src/lib/server/account-addresses.d.ts +160 -0
- package/src/lib/server/account-addresses.js +311 -0
- package/src/lib/server/account-addresses.js.map +1 -0
- package/src/lib/server/account-emails.d.ts +219 -0
- package/src/lib/server/account-emails.js +640 -0
- package/src/lib/server/account-emails.js.map +1 -0
- package/src/lib/server/admin-audit.d.ts +180 -0
- package/src/lib/server/admin-audit.js +233 -0
- package/src/lib/server/admin-audit.js.map +1 -0
- package/src/lib/server/analytics-retention.d.ts +63 -0
- package/src/lib/server/analytics-retention.js +66 -0
- package/src/lib/server/analytics-retention.js.map +1 -0
- package/src/lib/server/api-http.d.ts +159 -0
- package/src/lib/server/api-http.js +230 -0
- package/src/lib/server/api-http.js.map +1 -0
- package/src/lib/server/api-keys.d.ts +117 -0
- package/src/lib/server/api-keys.js +298 -0
- package/src/lib/server/api-keys.js.map +1 -0
- package/src/lib/server/auth-handoff.d.ts +165 -0
- package/src/lib/server/auth-handoff.js +482 -0
- package/src/lib/server/auth-handoff.js.map +1 -0
- package/src/lib/server/auth-pools.d.ts +246 -0
- package/src/lib/server/auth-pools.js +551 -0
- package/src/lib/server/auth-pools.js.map +1 -0
- package/src/lib/server/billing-addons.d.ts +171 -0
- package/src/lib/server/billing-addons.js +277 -0
- package/src/lib/server/billing-addons.js.map +1 -0
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +95 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +205 -0
- package/src/lib/server/campaign-conversion-attribution.js +279 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/client-error-report.d.ts +301 -0
- package/src/lib/server/client-error-report.js +755 -0
- package/src/lib/server/client-error-report.js.map +1 -0
- package/src/lib/server/connect-account-status.d.ts +74 -0
- package/src/lib/server/connect-account-status.js +94 -0
- package/src/lib/server/connect-account-status.js.map +1 -0
- package/src/lib/server/connect-payout-failure.d.ts +103 -0
- package/src/lib/server/connect-payout-failure.js +127 -0
- package/src/lib/server/connect-payout-failure.js.map +1 -0
- package/src/lib/server/console-api-rate-limit.d.ts +81 -0
- package/src/lib/server/console-api-rate-limit.js +111 -0
- package/src/lib/server/console-api-rate-limit.js.map +1 -0
- package/src/lib/server/console-domains.d.ts +326 -0
- package/src/lib/server/console-domains.js +779 -0
- package/src/lib/server/console-domains.js.map +1 -0
- package/src/lib/server/consume-once.d.ts +91 -0
- package/src/lib/server/consume-once.js +96 -0
- package/src/lib/server/consume-once.js.map +1 -0
- package/src/lib/server/contact-company-link.d.ts +85 -0
- package/src/lib/server/contact-company-link.js +105 -0
- package/src/lib/server/contact-company-link.js.map +1 -0
- package/src/lib/server/contact-email-engagement.d.ts +32 -0
- package/src/lib/server/contact-email-engagement.js +145 -0
- package/src/lib/server/contact-email-engagement.js.map +1 -0
- package/src/lib/server/contact-email-index.d.ts +56 -0
- package/src/lib/server/contact-email-index.js +158 -0
- package/src/lib/server/contact-email-index.js.map +1 -0
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +88 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-merge.d.ts +55 -0
- package/src/lib/server/contact-merge.js +279 -0
- package/src/lib/server/contact-merge.js.map +1 -0
- package/src/lib/server/contact-suppression.d.ts +153 -0
- package/src/lib/server/contact-suppression.js +259 -0
- package/src/lib/server/contact-suppression.js.map +1 -0
- package/src/lib/server/crm-booking-activity.d.ts +86 -0
- package/src/lib/server/crm-booking-activity.js +251 -0
- package/src/lib/server/crm-booking-activity.js.map +1 -0
- package/src/lib/server/crm-email-activity.d.ts +112 -0
- package/src/lib/server/crm-email-activity.js +143 -0
- package/src/lib/server/crm-email-activity.js.map +1 -0
- package/src/lib/server/crm-inbound-email.d.ts +145 -0
- package/src/lib/server/crm-inbound-email.js +287 -0
- package/src/lib/server/crm-inbound-email.js.map +1 -0
- package/src/lib/server/crm-next-activity.d.ts +74 -0
- package/src/lib/server/crm-next-activity.js +194 -0
- package/src/lib/server/crm-next-activity.js.map +1 -0
- package/src/lib/server/crm-records.d.ts +144 -0
- package/src/lib/server/crm-records.js +218 -0
- package/src/lib/server/crm-records.js.map +1 -0
- package/src/lib/server/csp-aggregate.d.ts +62 -0
- package/src/lib/server/csp-aggregate.js +276 -0
- package/src/lib/server/csp-aggregate.js.map +1 -0
- package/src/lib/server/data-storage-gate.d.ts +66 -0
- package/src/lib/server/data-storage-gate.js +48 -0
- package/src/lib/server/data-storage-gate.js.map +1 -0
- package/src/lib/server/dataset-live-pages.d.ts +179 -0
- package/src/lib/server/dataset-live-pages.js +421 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dns-probe.d.ts +53 -0
- package/src/lib/server/dns-probe.js +184 -0
- package/src/lib/server/dns-probe.js.map +1 -0
- package/src/lib/server/document-id.d.ts +29 -0
- package/src/lib/server/document-id.js +100 -0
- package/src/lib/server/document-id.js.map +1 -0
- package/src/lib/server/domain-provider-vercel.d.ts +33 -0
- package/src/lib/server/domain-provider-vercel.js +298 -0
- package/src/lib/server/domain-provider-vercel.js.map +1 -0
- package/src/lib/server/domain-provider-webhook.d.ts +73 -0
- package/src/lib/server/domain-provider-webhook.js +223 -0
- package/src/lib/server/domain-provider-webhook.js.map +1 -0
- package/src/lib/server/domain-provider-wildcard.d.ts +54 -0
- package/src/lib/server/domain-provider-wildcard.js +163 -0
- package/src/lib/server/domain-provider-wildcard.js.map +1 -0
- package/src/lib/server/domain-provider.d.ts +197 -0
- package/src/lib/server/domain-provider.js +221 -0
- package/src/lib/server/domain-provider.js.map +1 -0
- package/src/lib/server/duplicate-activity.d.ts +54 -0
- package/src/lib/server/duplicate-activity.js +67 -0
- package/src/lib/server/duplicate-activity.js.map +1 -0
- package/src/lib/server/duplicate-resource.d.ts +55 -0
- package/src/lib/server/duplicate-resource.js +474 -0
- package/src/lib/server/duplicate-resource.js.map +1 -0
- package/src/lib/server/dynamic-list-materialize.d.ts +153 -0
- package/src/lib/server/dynamic-list-materialize.js +581 -0
- package/src/lib/server/dynamic-list-materialize.js.map +1 -0
- package/src/lib/server/edit-access-authz.d.ts +56 -0
- package/src/lib/server/edit-access-authz.js +107 -0
- package/src/lib/server/edit-access-authz.js.map +1 -0
- package/src/lib/server/edit-access-token.d.ts +72 -0
- package/src/lib/server/edit-access-token.js +116 -0
- package/src/lib/server/edit-access-token.js.map +1 -0
- package/src/lib/server/edit-hint-token.d.ts +83 -0
- package/src/lib/server/edit-hint-token.js +119 -0
- package/src/lib/server/edit-hint-token.js.map +1 -0
- package/src/lib/server/email-delivery-log.d.ts +509 -0
- package/src/lib/server/email-delivery-log.js +1043 -0
- package/src/lib/server/email-delivery-log.js.map +1 -0
- package/src/lib/server/email-flow-gate.d.ts +80 -0
- package/src/lib/server/email-flow-gate.js +153 -0
- package/src/lib/server/email-flow-gate.js.map +1 -0
- package/src/lib/server/email-marketing-gate.d.ts +333 -0
- package/src/lib/server/email-marketing-gate.js +639 -0
- package/src/lib/server/email-marketing-gate.js.map +1 -0
- package/src/lib/server/email-metering.d.ts +293 -0
- package/src/lib/server/email-metering.js +359 -0
- package/src/lib/server/email-metering.js.map +1 -0
- package/src/lib/server/email-revenue-attribution.d.ts +185 -0
- package/src/lib/server/email-revenue-attribution.js +300 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-rate.d.ts +232 -0
- package/src/lib/server/email-send-rate.js +417 -0
- package/src/lib/server/email-send-rate.js.map +1 -0
- package/src/lib/server/email-sender-reputation.d.ts +317 -0
- package/src/lib/server/email-sender-reputation.js +464 -0
- package/src/lib/server/email-sender-reputation.js.map +1 -0
- package/src/lib/server/email-suppression.d.ts +490 -0
- package/src/lib/server/email-suppression.js +648 -0
- package/src/lib/server/email-suppression.js.map +1 -0
- package/src/lib/server/email-topic-confirmation.d.ts +150 -0
- package/src/lib/server/email-topic-confirmation.js +272 -0
- package/src/lib/server/email-topic-confirmation.js.map +1 -0
- package/src/lib/server/email-unsubscribe-link.d.ts +174 -0
- package/src/lib/server/email-unsubscribe-link.js +236 -0
- package/src/lib/server/email-unsubscribe-link.js.map +1 -0
- package/src/lib/server/erase-person.d.ts +116 -0
- package/src/lib/server/erase-person.js +272 -0
- package/src/lib/server/erase-person.js.map +1 -0
- package/src/lib/server/erase.d.ts +458 -0
- package/src/lib/server/erase.js +1506 -0
- package/src/lib/server/erase.js.map +1 -0
- package/src/lib/server/firebase-admin.d.ts +71 -0
- package/src/lib/server/firebase-admin.js +343 -0
- package/src/lib/server/firebase-admin.js.map +1 -0
- package/src/lib/server/form-dataset-binding-token.d.ts +35 -0
- package/src/lib/server/form-dataset-binding-token.js +107 -0
- package/src/lib/server/form-dataset-binding-token.js.map +1 -0
- package/src/lib/server/free-workspace-cap.d.ts +201 -0
- package/src/lib/server/free-workspace-cap.js +404 -0
- package/src/lib/server/free-workspace-cap.js.map +1 -0
- package/src/lib/server/ga4-measurement-protocol.d.ts +256 -0
- package/src/lib/server/ga4-measurement-protocol.js +558 -0
- package/src/lib/server/ga4-measurement-protocol.js.map +1 -0
- package/src/lib/server/host-email-tokens.d.ts +47 -0
- package/src/lib/server/host-email-tokens.js +72 -0
- package/src/lib/server/host-email-tokens.js.map +1 -0
- package/src/lib/server/host-memberships.d.ts +54 -0
- package/src/lib/server/host-memberships.js +194 -0
- package/src/lib/server/host-memberships.js.map +1 -0
- package/src/lib/server/host-sending-domain.d.ts +292 -0
- package/src/lib/server/host-sending-domain.js +451 -0
- package/src/lib/server/host-sending-domain.js.map +1 -0
- package/src/lib/server/host-visitor-records.d.ts +150 -0
- package/src/lib/server/host-visitor-records.js +262 -0
- package/src/lib/server/host-visitor-records.js.map +1 -0
- package/src/lib/server/id-token-refusal.d.ts +28 -0
- package/src/lib/server/id-token-refusal.js +111 -0
- package/src/lib/server/id-token-refusal.js.map +1 -0
- package/src/lib/server/legal-acceptance.d.ts +214 -0
- package/src/lib/server/legal-acceptance.js +305 -0
- package/src/lib/server/legal-acceptance.js.map +1 -0
- package/src/lib/server/list-members.d.ts +211 -0
- package/src/lib/server/list-members.js +176 -0
- package/src/lib/server/list-members.js.map +1 -0
- package/src/lib/server/live-page-usage.d.ts +202 -0
- package/src/lib/server/live-page-usage.js +300 -0
- package/src/lib/server/live-page-usage.js.map +1 -0
- package/src/lib/server/lockdown.d.ts +313 -0
- package/src/lib/server/lockdown.js +643 -0
- package/src/lib/server/lockdown.js.map +1 -0
- package/src/lib/server/media-cdn-rate-limit.d.ts +188 -0
- package/src/lib/server/media-cdn-rate-limit.js +204 -0
- package/src/lib/server/media-cdn-rate-limit.js.map +1 -0
- package/src/lib/server/media-delivery.d.ts +267 -0
- package/src/lib/server/media-delivery.js +485 -0
- package/src/lib/server/media-delivery.js.map +1 -0
- package/src/lib/server/media-download-tokens.d.ts +161 -0
- package/src/lib/server/media-download-tokens.js +386 -0
- package/src/lib/server/media-download-tokens.js.map +1 -0
- package/src/lib/server/media-quarantine.d.ts +127 -0
- package/src/lib/server/media-quarantine.js +170 -0
- package/src/lib/server/media-quarantine.js.map +1 -0
- package/src/lib/server/media-signing.d.ts +126 -0
- package/src/lib/server/media-signing.js +159 -0
- package/src/lib/server/media-signing.js.map +1 -0
- package/src/lib/server/media-storage-path.d.ts +96 -0
- package/src/lib/server/media-storage-path.js +111 -0
- package/src/lib/server/media-storage-path.js.map +1 -0
- package/src/lib/server/media-strong-digest.d.ts +85 -0
- package/src/lib/server/media-strong-digest.js +190 -0
- package/src/lib/server/media-strong-digest.js.map +1 -0
- package/src/lib/server/media-tombstone.d.ts +276 -0
- package/src/lib/server/media-tombstone.js +421 -0
- package/src/lib/server/media-tombstone.js.map +1 -0
- package/src/lib/server/media-variants.d.ts +309 -0
- package/src/lib/server/media-variants.js +428 -0
- package/src/lib/server/media-variants.js.map +1 -0
- package/src/lib/server/member-email-aliases.d.ts +180 -0
- package/src/lib/server/member-email-aliases.js +420 -0
- package/src/lib/server/member-email-aliases.js.map +1 -0
- package/src/lib/server/member-photo.d.ts +88 -0
- package/src/lib/server/member-photo.js +185 -0
- package/src/lib/server/member-photo.js.map +1 -0
- package/src/lib/server/membership-recover-throttle.d.ts +211 -0
- package/src/lib/server/membership-recover-throttle.js +231 -0
- package/src/lib/server/membership-recover-throttle.js.map +1 -0
- package/src/lib/server/notifications.d.ts +55 -0
- package/src/lib/server/notifications.js +134 -0
- package/src/lib/server/notifications.js.map +1 -0
- package/src/lib/server/org-billing.d.ts +97 -0
- package/src/lib/server/org-billing.js +236 -0
- package/src/lib/server/org-billing.js.map +1 -0
- package/src/lib/server/organizations.d.ts +837 -0
- package/src/lib/server/organizations.js +1814 -0
- package/src/lib/server/organizations.js.map +1 -0
- package/src/lib/server/password-reset-throttle.d.ts +84 -0
- package/src/lib/server/password-reset-throttle.js +102 -0
- package/src/lib/server/password-reset-throttle.js.map +1 -0
- package/src/lib/server/personal-data-export.d.ts +241 -0
- package/src/lib/server/personal-data-export.js +858 -0
- package/src/lib/server/personal-data-export.js.map +1 -0
- package/src/lib/server/platform-domain-names.d.ts +62 -0
- package/src/lib/server/platform-domain-names.js +197 -0
- package/src/lib/server/platform-domain-names.js.map +1 -0
- package/src/lib/server/platform-marketing-consent.d.ts +133 -0
- package/src/lib/server/platform-marketing-consent.js +209 -0
- package/src/lib/server/platform-marketing-consent.js.map +1 -0
- package/src/lib/server/rate-limit-store.d.ts +702 -0
- package/src/lib/server/rate-limit-store.js +1220 -0
- package/src/lib/server/rate-limit-store.js.map +1 -0
- package/src/lib/server/realm-plugins.d.ts +67 -0
- package/src/lib/server/realm-plugins.js +250 -0
- package/src/lib/server/realm-plugins.js.map +1 -0
- package/src/lib/server/recaptcha-allowlist.d.ts +90 -0
- package/src/lib/server/recaptcha-allowlist.js +409 -0
- package/src/lib/server/recaptcha-allowlist.js.map +1 -0
- package/src/lib/server/release-flags.d.ts +105 -0
- package/src/lib/server/release-flags.js +183 -0
- package/src/lib/server/release-flags.js.map +1 -0
- package/src/lib/server/replay-erasures.d.ts +126 -0
- package/src/lib/server/replay-erasures.js +217 -0
- package/src/lib/server/replay-erasures.js.map +1 -0
- package/src/lib/server/resolve-people.d.ts +66 -0
- package/src/lib/server/resolve-people.js +112 -0
- package/src/lib/server/resolve-people.js.map +1 -0
- package/src/lib/server/safe-equal.d.ts +45 -0
- package/src/lib/server/safe-equal.js +58 -0
- package/src/lib/server/safe-equal.js.map +1 -0
- package/src/lib/server/sending-domain-debt.d.ts +127 -0
- package/src/lib/server/sending-domain-debt.js +268 -0
- package/src/lib/server/sending-domain-debt.js.map +1 -0
- package/src/lib/server/sending-domain-recheck.d.ts +151 -0
- package/src/lib/server/sending-domain-recheck.js +253 -0
- package/src/lib/server/sending-domain-recheck.js.map +1 -0
- package/src/lib/server/sending-domains.d.ts +364 -0
- package/src/lib/server/sending-domains.js +683 -0
- package/src/lib/server/sending-domains.js.map +1 -0
- package/src/lib/server/serve-media-cdn.d.ts +433 -0
- package/src/lib/server/serve-media-cdn.js +1370 -0
- package/src/lib/server/serve-media-cdn.js.map +1 -0
- package/src/lib/server/serve-plugin-fetch.d.ts +61 -0
- package/src/lib/server/serve-plugin-fetch.js +294 -0
- package/src/lib/server/serve-plugin-fetch.js.map +1 -0
- package/src/lib/server/sms-keywords.d.ts +63 -0
- package/src/lib/server/sms-keywords.js +146 -0
- package/src/lib/server/sms-keywords.js.map +1 -0
- package/src/lib/server/sso-break-glass-owners.d.ts +91 -0
- package/src/lib/server/sso-break-glass-owners.js +227 -0
- package/src/lib/server/sso-break-glass-owners.js.map +1 -0
- package/src/lib/server/sso-domain-policy.d.ts +140 -0
- package/src/lib/server/sso-domain-policy.js +239 -0
- package/src/lib/server/sso-domain-policy.js.map +1 -0
- package/src/lib/server/sso-drift-logic.d.ts +106 -0
- package/src/lib/server/sso-drift-logic.js +95 -0
- package/src/lib/server/sso-drift-logic.js.map +1 -0
- package/src/lib/server/sso-enforcement.d.ts +204 -0
- package/src/lib/server/sso-enforcement.js +309 -0
- package/src/lib/server/sso-enforcement.js.map +1 -0
- package/src/lib/server/sso-provisioning.d.ts +320 -0
- package/src/lib/server/sso-provisioning.js +715 -0
- package/src/lib/server/sso-provisioning.js.map +1 -0
- package/src/lib/server/staff-alert-email.d.ts +41 -0
- package/src/lib/server/staff-alert-email.js +63 -0
- package/src/lib/server/staff-alert-email.js.map +1 -0
- package/src/lib/server/stripe-account-mode.d.ts +156 -0
- package/src/lib/server/stripe-account-mode.js +162 -0
- package/src/lib/server/stripe-account-mode.js.map +1 -0
- package/src/lib/server/tenant-write-lockdown.d.ts +131 -0
- package/src/lib/server/tenant-write-lockdown.js +176 -0
- package/src/lib/server/tenant-write-lockdown.js.map +1 -0
- package/src/lib/server/test-firestore.d.ts +45 -0
- package/src/lib/server/test-firestore.js +124 -0
- package/src/lib/server/test-firestore.js.map +1 -0
- package/src/lib/server/token-revocation.d.ts +140 -0
- package/src/lib/server/token-revocation.js +255 -0
- package/src/lib/server/token-revocation.js.map +1 -0
- package/src/lib/server/update-existing.d.ts +32 -0
- package/src/lib/server/update-existing.js +73 -0
- package/src/lib/server/update-existing.js.map +1 -0
- package/src/lib/server/upload-cors-reconcile.d.ts +118 -0
- package/src/lib/server/upload-cors-reconcile.js +234 -0
- package/src/lib/server/upload-cors-reconcile.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +262 -0
- package/src/lib/server/upsert-contact.js +546 -0
- package/src/lib/server/upsert-contact.js.map +1 -0
- package/src/lib/server/usage-invoice.d.ts +203 -0
- package/src/lib/server/usage-invoice.js +397 -0
- package/src/lib/server/usage-invoice.js.map +1 -0
- package/src/lib/server/user-profiles.d.ts +151 -0
- package/src/lib/server/user-profiles.js +364 -0
- package/src/lib/server/user-profiles.js.map +1 -0
- package/src/lib/server/vercel-drain-signature.d.ts +30 -0
- package/src/lib/server/vercel-drain-signature.js +61 -0
- package/src/lib/server/vercel-drain-signature.js.map +1 -0
- package/src/lib/server/vercel-log-drain.d.ts +259 -0
- package/src/lib/server/vercel-log-drain.js +479 -0
- package/src/lib/server/vercel-log-drain.js.map +1 -0
- package/src/lib/server/verify-email-cooldown.d.ts +102 -0
- package/src/lib/server/verify-email-cooldown.js +114 -0
- package/src/lib/server/verify-email-cooldown.js.map +1 -0
- package/src/lib/server/visitor-write-rate-limit.d.ts +87 -0
- package/src/lib/server/visitor-write-rate-limit.js +117 -0
- package/src/lib/server/visitor-write-rate-limit.js.map +1 -0
- package/src/lib/server/workspace-domains.d.ts +156 -0
- package/src/lib/server/workspace-domains.js +271 -0
- package/src/lib/server/workspace-domains.js.map +1 -0
- package/src/tenancy.d.ts +115 -0
- package/src/tenancy.js +142 -0
- package/src/tenancy.js.map +1 -0
|
@@ -0,0 +1,702 @@
|
|
|
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 RateLimitResult } from './api-http';
|
|
18
|
+
/**
|
|
19
|
+
* Durable, cross-instance rate limiting (AGL-794).
|
|
20
|
+
*
|
|
21
|
+
* Every limiter in the codebase was a per-instance `Map`, each carrying its
|
|
22
|
+
* own "best-effort, serverless instances are ephemeral" caveat. On Vercel that
|
|
23
|
+
* is close to no limit at all: the counter resets on every cold start and each
|
|
24
|
+
* concurrent instance keeps its own, so the effective cap is roughly
|
|
25
|
+
* `limit × instances` and an attacker can widen it just by going wider. That
|
|
26
|
+
* is tolerable for blunting accidental bursts; it is not a brute-force
|
|
27
|
+
* defense.
|
|
28
|
+
*
|
|
29
|
+
* This backs the same fixed-window shape with a Firestore counter, so the cap
|
|
30
|
+
* is global. `api-http.ts` deliberately stays pure — it has no imports and is
|
|
31
|
+
* unit-tested directly — so the storage-backed variant lives here instead of
|
|
32
|
+
* being bolted onto it.
|
|
33
|
+
*
|
|
34
|
+
* **Cost is the reason this isn't the default everywhere.** Each call is a
|
|
35
|
+
* transaction (one read + one write). That is the right trade for a password
|
|
36
|
+
* unlock attempt; it is the wrong trade for an analytics beacon, which can
|
|
37
|
+
* fire on every page view. Use `checkRateLimit` for volume, this for
|
|
38
|
+
* consequence.
|
|
39
|
+
*/
|
|
40
|
+
/** Collection holding one document per (key, window). Server-writes only. */
|
|
41
|
+
export declare const RATE_LIMIT_COLLECTION = "rateLimits";
|
|
42
|
+
/**
|
|
43
|
+
* Document-id prefix for degradation markers (AGL-1679).
|
|
44
|
+
*
|
|
45
|
+
* `degraded: true` used to exist only in a `console.error`, which means a
|
|
46
|
+
* Firestore blip silently dropped every durable limiter — auth, password
|
|
47
|
+
* reset, and now the public REST API's per-key quota — back to a per-instance
|
|
48
|
+
* cap for as long as it lasted, and nobody found out. Fail-soft is only a
|
|
49
|
+
* defensible choice if someone can tell that it fired.
|
|
50
|
+
*
|
|
51
|
+
* This is deliberately not an alerting stack. It is the cheapest thing that
|
|
52
|
+
* makes a degraded window answerable after the fact: when an episode ends,
|
|
53
|
+
* the instance writes one summary document into the SAME collection, so it
|
|
54
|
+
* inherits the deny-all rule and the `expiresAt` TTL policy that already
|
|
55
|
+
* exist rather than needing a new collection, a rules deploy and a second TTL
|
|
56
|
+
* policy. Ids are minute-bucketed so concurrent instances converge on a
|
|
57
|
+
* handful of documents:
|
|
58
|
+
*
|
|
59
|
+
* ```
|
|
60
|
+
* rateLimits/degraded_1755100800000
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Written on RECOVERY, never during the outage — the store is unreachable
|
|
64
|
+
* exactly when the episode is happening, so a marker written then would be
|
|
65
|
+
* the one write guaranteed to fail.
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEGRADATION_DOC_PREFIX = "degraded_";
|
|
68
|
+
interface DegradationEpisode {
|
|
69
|
+
/** Calls that fell back to the in-process limiter. */
|
|
70
|
+
count: number;
|
|
71
|
+
firstAtMs: number;
|
|
72
|
+
lastAtMs: number;
|
|
73
|
+
/** Last failure's code/message, truncated. */
|
|
74
|
+
code: string;
|
|
75
|
+
/** Last minute we logged, so a sustained outage cannot flood the log. */
|
|
76
|
+
lastLoggedBucket: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The degradation episode in progress on this instance, or `null`. Exposed
|
|
80
|
+
* for tests and for any future health surface; callers must not treat it as a
|
|
81
|
+
* global view — it only ever describes the instance that answers.
|
|
82
|
+
*/
|
|
83
|
+
export declare function currentRateLimitDegradation(): Readonly<DegradationEpisode> | null;
|
|
84
|
+
/** Test seam: forget any in-progress episode. */
|
|
85
|
+
export declare function resetRateLimitDegradationForTests(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Document-id prefix for signup-refusal markers (AGL-1907).
|
|
88
|
+
*
|
|
89
|
+
* `/api/orgs/create` has been rate-limited since AGL-1534 (3/h per uid, 10/h
|
|
90
|
+
* per IP) and AGL-1536 watches org-creation VOLUME — but volume only counts
|
|
91
|
+
* the signups that SUCCEEDED. A scripted farm that trips the limiter is
|
|
92
|
+
* therefore invisible in exactly the moment it is being contained: the 429s
|
|
93
|
+
* are the attack's signature and nothing recorded them. Before Sep 1 that is
|
|
94
|
+
* the difference between "the limiter held" and "we have no idea whether it
|
|
95
|
+
* was ever tested".
|
|
96
|
+
*
|
|
97
|
+
* Written into the SAME `rateLimits` collection as the counters and the
|
|
98
|
+
* AGL-1679 degradation markers, for the same reason that one gave: it inherits
|
|
99
|
+
* the deny-all security rule and the `expiresAt` TTL policy that already
|
|
100
|
+
* exist, instead of needing a new collection, a rules deploy and a second TTL
|
|
101
|
+
* policy. Minute-bucketed so concurrent instances converge:
|
|
102
|
+
*
|
|
103
|
+
* ```
|
|
104
|
+
* rateLimits/signupRefused_1755100800000
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* **The timestamp field is `refusedAtMs`, deliberately not `lastAtMs`.** The
|
|
108
|
+
* AGL-1693 rate-limiter health probe queries this collection with
|
|
109
|
+
* `where('lastAtMs', '>=', cutoff).orderBy('lastAtMs','desc').limit(N)`. A
|
|
110
|
+
* refusal marker carrying `lastAtMs` would be picked up by that range and,
|
|
111
|
+
* under a flood, could fill the limit and push the real degradation markers
|
|
112
|
+
* out of the result — silently blinding a sibling alarm. A distinct field name
|
|
113
|
+
* keeps the two queries disjoint at the index level rather than relying on the
|
|
114
|
+
* id-prefix filter that runs after the read.
|
|
115
|
+
*/
|
|
116
|
+
export declare const SIGNUP_REFUSAL_DOC_PREFIX = "signupRefused_";
|
|
117
|
+
/**
|
|
118
|
+
* What refused. Bounded on purpose — this is a public health body.
|
|
119
|
+
*
|
|
120
|
+
* `uid` and `ip` are this limiter's two caps. `locked` and `unreadable` are
|
|
121
|
+
* written by the `beforeUserCreated` blocking function in `cloud/functions`,
|
|
122
|
+
* which refuses on the Identity Platform path before any Aglyn route runs
|
|
123
|
+
* and therefore never reaches `consumeRateLimit` (AGL-2583). It cannot import
|
|
124
|
+
* this module — it is a plain npm package outside the workspace — so it
|
|
125
|
+
* writes the same document shape by hand and
|
|
126
|
+
* `apps/console/specs/signup-refusal-marker-wiring.spec.ts` fails if the two
|
|
127
|
+
* ever disagree about the collection, the prefix or the fields.
|
|
128
|
+
*/
|
|
129
|
+
export type SignupRefusalReason = 'uid' | 'ip' | 'locked' | 'unreadable';
|
|
130
|
+
/**
|
|
131
|
+
* Record one refused org-creation attempt.
|
|
132
|
+
*
|
|
133
|
+
* Fire-and-forget and best-effort, like `flushDegradation`: refusing the
|
|
134
|
+
* request is the control, and failing a 429 because its breadcrumb could not
|
|
135
|
+
* be written would be strictly worse than not having the breadcrumb. Callers
|
|
136
|
+
* must not await this on the response path.
|
|
137
|
+
*
|
|
138
|
+
* **On the write-per-refusal cost.** A refused request is free to the attacker
|
|
139
|
+
* and now costs one transaction — the usual amplification objection. It does
|
|
140
|
+
* not apply here in kind, only in degree: `/api/orgs/create` already spends
|
|
141
|
+
* TWO `consumeRateLimit` transactions on every hit including the refused ones
|
|
142
|
+
* (AGL-1534 counts over-limit attempts by design), so this is a third write on
|
|
143
|
+
* a path that was already three, not a new class of cost. If it ever shows up
|
|
144
|
+
* on the bill the fix is in-process coalescing like `episode` above, not
|
|
145
|
+
* dropping the signal.
|
|
146
|
+
*
|
|
147
|
+
* Nothing identifying is stored. The counts are per-reason only; the uid and
|
|
148
|
+
* IP that were refused stay in the (hashed) limiter keys, which this never
|
|
149
|
+
* reads.
|
|
150
|
+
*/
|
|
151
|
+
export declare function recordSignupRefusal(reason: SignupRefusalReason, options?: {
|
|
152
|
+
now?: number;
|
|
153
|
+
firestore?: any;
|
|
154
|
+
}): void;
|
|
155
|
+
/**
|
|
156
|
+
* Document-id prefix for server-error markers (AGL-1921).
|
|
157
|
+
*
|
|
158
|
+
* ## Why a Firestore marker when the errors already go to Cloud Logging
|
|
159
|
+
*
|
|
160
|
+
* `reportServerError` forwards every `onRequestError` to a `server-errors` log
|
|
161
|
+
* in `aglyn-main`, which is the right sink for triage — Error Reporting groups
|
|
162
|
+
* them and a log-match policy can page on them. It is not a sink anything in
|
|
163
|
+
* this repo can READ. Measured 2026-08-24 against the production credential:
|
|
164
|
+
* `POST logging.googleapis.com/v2/entries:list` for that log answers
|
|
165
|
+
* **403 `Permission denied for all log views`**, because the firebase-admin
|
|
166
|
+
* service account can create log entries and cannot list them. So the only
|
|
167
|
+
* reader the log has is a GCP alert policy that does not exist yet, and
|
|
168
|
+
* creating one is the click, not a thing that ships with a commit.
|
|
169
|
+
*
|
|
170
|
+
* That is the AGL-2486 shape exactly — a detector written into a place nothing
|
|
171
|
+
* watches. This marker is the second copy of the count, in a store we can
|
|
172
|
+
* already read, so `/api/health/server-errors` can grade it and the readers
|
|
173
|
+
* that already exist (the 15-minute GitHub uptime probe, the external keyword
|
|
174
|
+
* monitors, `docs.aglyn.com/status`) become the listeners on day one.
|
|
175
|
+
*
|
|
176
|
+
* Written into the SAME `rateLimits` collection as the counters, the AGL-1679
|
|
177
|
+
* degradation markers and the AGL-1907 refusal markers, for the reason those
|
|
178
|
+
* two gave: it inherits the deny-all security rule and the `expiresAt` TTL
|
|
179
|
+
* policy that already exist, instead of needing a new collection, a rules
|
|
180
|
+
* deploy and a second TTL policy — i.e. instead of needing a console action
|
|
181
|
+
* nobody can take from code. Minute bucketed so concurrent instances
|
|
182
|
+
* converge:
|
|
183
|
+
*
|
|
184
|
+
* ```
|
|
185
|
+
* rateLimits/serverError_1755100800000
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* **The timestamp field is `erroredAtMs`**, deliberately neither `lastAtMs`
|
|
189
|
+
* (AGL-1679's) nor `refusedAtMs` (AGL-1907's), for the reason spelled out on
|
|
190
|
+
* `SIGNUP_REFUSAL_DOC_PREFIX`: each health probe range-queries its own field,
|
|
191
|
+
* and a shared field would let one signal's flood fill another's read limit
|
|
192
|
+
* and silently blind it. Three signals, three disjoint indexes.
|
|
193
|
+
*/
|
|
194
|
+
export declare const SERVER_ERROR_DOC_PREFIX = "serverError_";
|
|
195
|
+
/**
|
|
196
|
+
* Record one uncaught server-side error, for `/api/health/server-errors`.
|
|
197
|
+
*
|
|
198
|
+
* Called from `reportServerError`, which is called from each app's
|
|
199
|
+
* `onRequestError` hook — so this counts exactly what that hook can see: an
|
|
200
|
+
* uncaught throw in a render or a route handler. It does NOT see an error that
|
|
201
|
+
* kills the process first, a platform-level 5xx (function timeout, OOM,
|
|
202
|
+
* cold-start 502), or anything thrown in the edge runtime. Those need the
|
|
203
|
+
* Vercel log drain; `docs/UPTIME_AND_SLA.md` carries the list.
|
|
204
|
+
*
|
|
205
|
+
* Fire-and-forget and never throws. Nothing identifying is stored — a count
|
|
206
|
+
* and which deployment produced it. The message, the stack and the route
|
|
207
|
+
* pattern stay in the Logging entry, which is not public; this marker is read
|
|
208
|
+
* by an endpoint that is.
|
|
209
|
+
*/
|
|
210
|
+
export declare function recordServerError(service: string, options?: {
|
|
211
|
+
now?: number;
|
|
212
|
+
firestore?: any;
|
|
213
|
+
}): void;
|
|
214
|
+
/**
|
|
215
|
+
* Errors this instance is holding but has not written yet. Exposed for tests
|
|
216
|
+
* and for anyone reasoning about the coalescing window; callers must not treat
|
|
217
|
+
* it as a global view — it only ever describes the instance that answers.
|
|
218
|
+
*/
|
|
219
|
+
export declare function pendingServerErrors(): number;
|
|
220
|
+
/** Test seam: forget anything held. */
|
|
221
|
+
export declare function resetServerErrorsForTests(): void;
|
|
222
|
+
/**
|
|
223
|
+
* Document-id prefix for the beacon's last-landed marker (AGL-2713).
|
|
224
|
+
*
|
|
225
|
+
* ## Why this one is not bucketed, and not queried
|
|
226
|
+
*
|
|
227
|
+
* Its four siblings above count OCCURRENCES over a window, so each one wants
|
|
228
|
+
* a stream of minute-bucketed documents and a range query over its own
|
|
229
|
+
* timestamp field. This answers a single question — *when did a heartbeat
|
|
230
|
+
* last reach Cloud Logging from this deployment* — so it is one document per
|
|
231
|
+
* service, read by id:
|
|
232
|
+
*
|
|
233
|
+
* ```
|
|
234
|
+
* rateLimits/beaconHeartbeat_console-web
|
|
235
|
+
* rateLimits/beaconHeartbeat_tenant-web
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* A point read needs no index at all, which matters more than the cost: an
|
|
239
|
+
* index this repository cannot deploy without a token is an index that is not
|
|
240
|
+
* there when the failure is. Same collection as the siblings, for the reason
|
|
241
|
+
* they each gave — it inherits the deny-all rule and the `expiresAt` TTL
|
|
242
|
+
* policy that already exist, instead of needing a rules deploy nobody can
|
|
243
|
+
* make from code.
|
|
244
|
+
*
|
|
245
|
+
* **The timestamp field is `heartbeatAtMs`**, deliberately none of `lastAtMs`
|
|
246
|
+
* (AGL-1679), `refusedAtMs` (AGL-1907), `erroredAtMs` (AGL-1921) or
|
|
247
|
+
* `servedAtMs` (AGL-2583). This one is not range-queried, so the isolation
|
|
248
|
+
* argument the others make is not what earns it a field of its own — but a
|
|
249
|
+
* document carrying a sibling's field would enter that sibling's window and be
|
|
250
|
+
* counted as an episode it is not.
|
|
251
|
+
*
|
|
252
|
+
* ⚠️ **Evidence of a LANDING, never of a failure.** Nothing writes here when
|
|
253
|
+
* a heartbeat misses. So the worst a corrupt, stale or unreadable marker can
|
|
254
|
+
* do is make `/api/health/error-beacon` red sooner, and a deployment whose
|
|
255
|
+
* credential has never once worked has no marker at all and reds on its first
|
|
256
|
+
* probe. That asymmetry is what lets the door tolerate a miss without becoming
|
|
257
|
+
* a door that cannot go red.
|
|
258
|
+
*/
|
|
259
|
+
export declare const BEACON_HEARTBEAT_DOC_PREFIX = "beaconHeartbeat_";
|
|
260
|
+
/**
|
|
261
|
+
* Record that a heartbeat reached Cloud Logging (AGL-2713).
|
|
262
|
+
*
|
|
263
|
+
* Fire-and-forget and never throws, like `flushServerErrors`: this runs
|
|
264
|
+
* inside a monitoring probe, and a breadcrumb must never be the reason the
|
|
265
|
+
* probe it is attached to fails. A write that does not land simply means the
|
|
266
|
+
* next miss is graded without it, which is the safe direction.
|
|
267
|
+
*
|
|
268
|
+
* The caller is memoized per instance on a five-minute TTL, so the write rate
|
|
269
|
+
* is one per instance per five minutes however hard the public endpoint is
|
|
270
|
+
* hit — orders of magnitude under the per-document ceiling that produced
|
|
271
|
+
* AGL-2404's contention storm.
|
|
272
|
+
*/
|
|
273
|
+
export declare function recordBeaconHeartbeat(service: string, options?: {
|
|
274
|
+
now?: number;
|
|
275
|
+
firestore?: any;
|
|
276
|
+
}): void;
|
|
277
|
+
/**
|
|
278
|
+
* When a heartbeat last landed for this deployment, or null (AGL-2713).
|
|
279
|
+
*
|
|
280
|
+
* Null covers three cases that must all grade the same way: no marker has
|
|
281
|
+
* ever been written, the document holds nothing usable, and the store could
|
|
282
|
+
* not be read inside its budget. Every one of them means *there is no proof a
|
|
283
|
+
* heartbeat landed recently*, and the caller forgives a miss only on proof.
|
|
284
|
+
* Folding them is therefore not a shortcut — distinguishing them could only
|
|
285
|
+
* ever be used to forgive something unproven.
|
|
286
|
+
*
|
|
287
|
+
* The budget is the reason this is not a bare `get()`. It runs on the failure
|
|
288
|
+
* path, where the credential is already misbehaving, and a Firestore client
|
|
289
|
+
* whose own token will not mint retries with backoff for far longer than a
|
|
290
|
+
* health endpoint may take to answer.
|
|
291
|
+
*/
|
|
292
|
+
export declare function readBeaconHeartbeat(service: string, options?: {
|
|
293
|
+
firestore?: any;
|
|
294
|
+
budgetMs?: number;
|
|
295
|
+
}): Promise<number | null>;
|
|
296
|
+
/**
|
|
297
|
+
* Document-id prefix for signup-page serve markers (AGL-2583).
|
|
298
|
+
*
|
|
299
|
+
* ## Why traffic is worth storing at all
|
|
300
|
+
*
|
|
301
|
+
* `/api/health/signups` counts orgs created in the trailing hour and goes red
|
|
302
|
+
* when there are TOO MANY. Zero — the reading that means nobody on earth can
|
|
303
|
+
* sign up — is its healthiest possible score, and that is how AGL-2581 refused
|
|
304
|
+
* every account creation for three days with the signup monitor green
|
|
305
|
+
* throughout. The missing half is a DENOMINATOR: zero accounts is an outage
|
|
306
|
+
* only if people were trying, and a quiet night otherwise.
|
|
307
|
+
*
|
|
308
|
+
* These markers are that denominator. Every render of the signup page fetches
|
|
309
|
+
* `/api/lockdown-status?feature=signups` to decide whether to show the paused
|
|
310
|
+
* notice, so that route is the one server touch a real arrival always makes,
|
|
311
|
+
* and counting it needs no new endpoint, no client beacon and no analytics
|
|
312
|
+
* vendor in the alerting path.
|
|
313
|
+
*
|
|
314
|
+
* Written into the SAME `rateLimits` collection as the counters and the three
|
|
315
|
+
* sibling marker kinds, for the reason they each gave: it inherits the
|
|
316
|
+
* deny-all security rule and the `expiresAt` TTL policy that already exist,
|
|
317
|
+
* instead of needing a new collection, a rules deploy and a second TTL policy.
|
|
318
|
+
* Minute bucketed so concurrent instances converge:
|
|
319
|
+
*
|
|
320
|
+
* ```
|
|
321
|
+
* rateLimits/signupServed_1755100800000
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* **The timestamp field is `servedAtMs`**, deliberately none of `lastAtMs`
|
|
325
|
+
* (AGL-1679), `refusedAtMs` (AGL-1907) or `erroredAtMs` (AGL-1921), for the
|
|
326
|
+
* reason spelled out on `SIGNUP_REFUSAL_DOC_PREFIX`: each health probe
|
|
327
|
+
* range-queries its own field, and a shared field would let one signal's flood
|
|
328
|
+
* fill another's read limit and silently blind it. Four signals, four disjoint
|
|
329
|
+
* indexes.
|
|
330
|
+
*/
|
|
331
|
+
export declare const SIGNUP_SERVED_DOC_PREFIX = "signupServed_";
|
|
332
|
+
/**
|
|
333
|
+
* Record one serving of the signup page, for `/api/health/signup-volume`.
|
|
334
|
+
*
|
|
335
|
+
* Nothing identifying is stored — a count and a timestamp. No IP, no user
|
|
336
|
+
* agent, no referrer, no campaign: this marker is read by an endpoint that is
|
|
337
|
+
* public, and the drought verdict needs a number, not an audience.
|
|
338
|
+
*
|
|
339
|
+
* Never throws, and callers must not await it.
|
|
340
|
+
*/
|
|
341
|
+
export declare function recordSignupServed(options?: {
|
|
342
|
+
now?: number;
|
|
343
|
+
firestore?: any;
|
|
344
|
+
}): void;
|
|
345
|
+
/**
|
|
346
|
+
* Serves this instance is holding but has not written yet. Exposed for tests
|
|
347
|
+
* and for anyone reasoning about the coalescing window; it only ever describes
|
|
348
|
+
* the instance that answers.
|
|
349
|
+
*/
|
|
350
|
+
export declare function pendingSignupServes(): number;
|
|
351
|
+
/** Test seam: forget anything held. */
|
|
352
|
+
export declare function resetSignupServesForTests(): void;
|
|
353
|
+
/**
|
|
354
|
+
* Document-id prefix for org-creation ATTEMPT markers (AGL-2714).
|
|
355
|
+
*
|
|
356
|
+
* ## Why serves were the wrong denominator
|
|
357
|
+
*
|
|
358
|
+
* The drought verdict beside this one asks "did traffic arrive and no account
|
|
359
|
+
* come out", and it took its denominator from `signupServed_` — a hit on
|
|
360
|
+
* `/api/lockdown-status?feature=signups`, which the signup page fetches on
|
|
361
|
+
* every render. That counts LOOKERS. Crawlers, link previews, a person
|
|
362
|
+
* reading the plan names and closing the tab, and the diagnostic page load
|
|
363
|
+
* somebody makes while investigating the alert itself all land in it.
|
|
364
|
+
*
|
|
365
|
+
* At this platform's conversion rate an hour with a handful of arrivals and
|
|
366
|
+
* no account is the ordinary case, so the check could not tell a door that
|
|
367
|
+
* does not open from a quiet afternoon — and it paged as though the service
|
|
368
|
+
* were down. No threshold fixes that; the quantity is wrong.
|
|
369
|
+
*
|
|
370
|
+
* An ATTEMPT is somebody who authenticated, filled the form and asked for an
|
|
371
|
+
* org. If those arrive and no org appears, something is broken, and the
|
|
372
|
+
* question has an answer that does not depend on how many people were merely
|
|
373
|
+
* browsing.
|
|
374
|
+
*
|
|
375
|
+
* ## Why this is stronger than the refusal marker beside it
|
|
376
|
+
*
|
|
377
|
+
* `signupRefusal_` counts requests this route TURNED AWAY, which is a
|
|
378
|
+
* deliberate outcome the code chose. An attempt marker is written before any
|
|
379
|
+
* outcome is known, so the pair spans what refusals cannot see on their own:
|
|
380
|
+
* a request that 500s, hangs, or dies in the platform between the limiter and
|
|
381
|
+
* the write leaves an attempt and no org, and no refusal at all.
|
|
382
|
+
*
|
|
383
|
+
* **The field is `attemptedAtMs`**, deliberately none of `lastAtMs`
|
|
384
|
+
* (AGL-1679), `refusedAtMs` (AGL-1907), `erroredAtMs` (AGL-1921) or
|
|
385
|
+
* `servedAtMs` (AGL-2583) — same rule the four before it follow: each health
|
|
386
|
+
* probe range-queries its own field, and a shared field would let one
|
|
387
|
+
* signal's flood fill another's read limit and silently blind it.
|
|
388
|
+
*/
|
|
389
|
+
export declare const SIGNUP_ATTEMPT_DOC_PREFIX = "signupAttempted_";
|
|
390
|
+
/**
|
|
391
|
+
* Record one attempt to create an org, for `/api/health/signup-volume`.
|
|
392
|
+
*
|
|
393
|
+
* Written per attempt with no in-process coalescing, unlike the serve marker.
|
|
394
|
+
* The serve marker guards a PUBLIC page whose traffic is unbounded; this one
|
|
395
|
+
* sits behind an authenticated route the limiter already caps at 3 per uid and
|
|
396
|
+
* 10 per IP an hour, so the write rate is bounded by the limiter rather than
|
|
397
|
+
* by the internet. It is also the third write on a path that already spends
|
|
398
|
+
* two `consumeRateLimit` transactions per hit, which is the same accounting
|
|
399
|
+
* `recordSignupRefusal` sets out.
|
|
400
|
+
*
|
|
401
|
+
* `FieldValue.increment` with a merge rather than a transaction: increments
|
|
402
|
+
* are commutative server-side, so instances converging on one minute's
|
|
403
|
+
* document neither contend nor retry.
|
|
404
|
+
*
|
|
405
|
+
* Nothing identifying is stored — a count and a timestamp. This marker is read
|
|
406
|
+
* by an endpoint that is public, and the drought verdict needs a number, not
|
|
407
|
+
* an audience.
|
|
408
|
+
*
|
|
409
|
+
* Never throws, and callers must not await it: creating the org is the
|
|
410
|
+
* control, and a monitoring breadcrumb must never be the reason a signup gets
|
|
411
|
+
* slower or fails.
|
|
412
|
+
*/
|
|
413
|
+
export declare function recordSignupAttempt(options?: {
|
|
414
|
+
now?: number;
|
|
415
|
+
firestore?: any;
|
|
416
|
+
}): void;
|
|
417
|
+
/**
|
|
418
|
+
* Wall-clock budget for one durable counter round trip (AGL-2404).
|
|
419
|
+
*
|
|
420
|
+
* Without a bound this function could not fail at all — it could only hang.
|
|
421
|
+
* The counter used to be a read-modify-write `runTransaction` on a single hot
|
|
422
|
+
* document: it contended, retried with backoff, and on a contended key
|
|
423
|
+
* routinely outran the platform's function ceiling, so the request died as a
|
|
424
|
+
* **504 with no body and no `Retry-After`** instead of the cheap 429 the
|
|
425
|
+
* limiter exists to produce. Measured against production on 2026-08-19 at
|
|
426
|
+
* `/api/protection/unlock`: sequential traffic refused cleanly at the 10th
|
|
427
|
+
* attempt, while **two** concurrent requests on one key were already enough
|
|
428
|
+
* to produce a 504 at ~10.3 s — the account-default function ceiling, since
|
|
429
|
+
* that route declares no `maxDuration`.
|
|
430
|
+
*
|
|
431
|
+
* AGL-2416 removed the read-modify-write (see {@link consumeRateLimit}), so
|
|
432
|
+
* the retry storm this bound was catching should no longer occur. The bound
|
|
433
|
+
* STAYS: it is what makes the function answerable at all, and a store that is
|
|
434
|
+
* slow for any other reason — a hot document at Firestore's own single-doc
|
|
435
|
+
* write ceiling, a network stall — must still produce a decision rather than
|
|
436
|
+
* a held function.
|
|
437
|
+
*
|
|
438
|
+
* 2.5 s is chosen to sit far below the smallest ceiling any caller runs at
|
|
439
|
+
* (10 s at the Vercel account default) while leaving room for the two round
|
|
440
|
+
* trips below, which measured ~0.6 s end to end on the same endpoint.
|
|
441
|
+
*/
|
|
442
|
+
export declare const RATE_LIMIT_TRANSACTION_BUDGET_MS = 2500;
|
|
443
|
+
/**
|
|
444
|
+
* Thrown when the durable counter could not reach a decision inside
|
|
445
|
+
* {@link RATE_LIMIT_TRANSACTION_BUDGET_MS}. Distinct from a store *error* on
|
|
446
|
+
* purpose — see the classification note on {@link consumeRateLimit}.
|
|
447
|
+
*/
|
|
448
|
+
export declare class RateLimitContentionError extends Error {
|
|
449
|
+
constructor(message?: string);
|
|
450
|
+
}
|
|
451
|
+
export interface DurableRateLimitOptions {
|
|
452
|
+
limit?: number;
|
|
453
|
+
windowMs?: number;
|
|
454
|
+
now?: number;
|
|
455
|
+
/** Injectable for tests; defaults to the Admin SDK's Firestore. */
|
|
456
|
+
firestore?: any;
|
|
457
|
+
/** Injectable for tests; defaults to {@link RATE_LIMIT_TRANSACTION_BUDGET_MS}. */
|
|
458
|
+
budgetMs?: number;
|
|
459
|
+
}
|
|
460
|
+
export interface DurableRateLimitResult extends RateLimitResult {
|
|
461
|
+
/**
|
|
462
|
+
* True when the durable store was unreachable and the in-memory limiter
|
|
463
|
+
* answered instead — the cap held for this instance only.
|
|
464
|
+
*/
|
|
465
|
+
degraded: boolean;
|
|
466
|
+
/**
|
|
467
|
+
* True when the counter could not be read and written inside its budget
|
|
468
|
+
* because the key is contended, and the request was refused on that basis
|
|
469
|
+
* rather than on a counted overage (AGL-2404). Always `false` on an
|
|
470
|
+
* allowed result.
|
|
471
|
+
*/
|
|
472
|
+
contended: boolean;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Counts one request against `key`'s fixed window, globally.
|
|
476
|
+
*
|
|
477
|
+
* Fails *soft, not open*: if Firestore is unreachable the in-memory limiter
|
|
478
|
+
* answers and the result is flagged `degraded`. Failing fully open would let
|
|
479
|
+
* an attacker disable brute-force protection by inducing a storage error;
|
|
480
|
+
* failing fully closed would lock legitimate visitors out of a site because of
|
|
481
|
+
* an unrelated Firestore blip. Degrading to the per-instance cap keeps some
|
|
482
|
+
* protection and keeps the site usable, and says which happened.
|
|
483
|
+
*
|
|
484
|
+
* ## Two failures, two postures (AGL-2404)
|
|
485
|
+
*
|
|
486
|
+
* "Firestore is unreachable" and "this one document is contended" are not the
|
|
487
|
+
* same event and must not get the same answer:
|
|
488
|
+
*
|
|
489
|
+
* - **The store is down** (fast error — `UNAVAILABLE`, a missing app, a
|
|
490
|
+
* credential failure). Fail SOFT, exactly as before: degrade to the
|
|
491
|
+
* per-instance cap and flag `degraded`. A real visitor must not lose access
|
|
492
|
+
* to a customer's site over an unrelated blip.
|
|
493
|
+
* - **The key is contended** (`ABORTED`, `DEADLINE_EXCEEDED`, or the budget
|
|
494
|
+
* above elapsing). Fail CLOSED: refuse with `contended: true` and let the
|
|
495
|
+
* caller answer 429 with its usual `Retry-After`. Degrading here would be a
|
|
496
|
+
* partial bypass — concurrent requests on ONE key are cheap to generate,
|
|
497
|
+
* and if they dropped the cap to a per-instance count then going wide would
|
|
498
|
+
* widen the cap, which is precisely the property AGL-794 removed.
|
|
499
|
+
*
|
|
500
|
+
* Refusing on contention is never worse than the 504 it replaces. A 504 is
|
|
501
|
+
* also a failed request, but it costs a full function timeout, carries no
|
|
502
|
+
* `Retry-After` for a well-behaved client to back off on, and invites an
|
|
503
|
+
* immediate retry that deepens the contention. Contention on a single key
|
|
504
|
+
* also means, at nearly every call site, either abuse or a client
|
|
505
|
+
* double-submitting: the keys are per (host, screen, IP), per uid, or per API
|
|
506
|
+
* key, so a legitimate visitor essentially never races themselves.
|
|
507
|
+
*
|
|
508
|
+
* ## The counter is an atomic increment, not a transaction (AGL-2416)
|
|
509
|
+
*
|
|
510
|
+
* It used to be `runTransaction(read count, write count + 1)`. That is
|
|
511
|
+
* optimistic concurrency on ONE document, so two writers in flight at once
|
|
512
|
+
* already race: measured in production on 2026-08-19, **two** concurrent
|
|
513
|
+
* requests on a single fresh key were enough to make a transaction lose,
|
|
514
|
+
* retry, and blow past its budget. Two is ordinary client behaviour — a
|
|
515
|
+
* double-submit, a mobile retry, two visitors behind one NAT — and the
|
|
516
|
+
* documented 120/min per API key is not even reachable without concurrency,
|
|
517
|
+
* so an integration built to the published budget met this by design.
|
|
518
|
+
*
|
|
519
|
+
* `set({count: increment(1)}, {merge: true})` is applied by the server with
|
|
520
|
+
* no read set to conflict over, so concurrent writers do not abort each
|
|
521
|
+
* other. The decision then needs the value, which costs a read back — the
|
|
522
|
+
* same two round trips a transaction spent, and the same billing: one
|
|
523
|
+
* document read plus one document write.
|
|
524
|
+
*
|
|
525
|
+
* **The trade is exactness for headroom, and it is deliberately biased.** The
|
|
526
|
+
* read-back can observe increments from writers that landed after ours, so
|
|
527
|
+
* under `C` concurrent requests a caller may see a count up to `C - 1` higher
|
|
528
|
+
* than its own position. The consequences, precisely:
|
|
529
|
+
*
|
|
530
|
+
* - It can only ever refuse EARLY, never late. The count is monotonic within
|
|
531
|
+
* a window and always includes this request's own increment, so an admitted
|
|
532
|
+
* request is one where at most `limit` increments had landed. The cap can
|
|
533
|
+
* never be exceeded, and going concurrent can never widen it.
|
|
534
|
+
* - "Exactly `limit` admitted" becomes "**at most** `limit` admitted". At the
|
|
535
|
+
* window's edge, `C` simultaneous requests can all read the same post-burst
|
|
536
|
+
* total and all be refused, so as few as `limit - C + 1` are admitted where
|
|
537
|
+
* `limit` would have been. Bounded by the concurrency, confined to one
|
|
538
|
+
* window, and in the customer-favourable direction for a *security* limiter
|
|
539
|
+
* — which is what all nine of this store's call sites are.
|
|
540
|
+
* - A fixed-window limiter already admits up to `2 × limit` across a window
|
|
541
|
+
* boundary, so exactness was never a property this shape had end to end.
|
|
542
|
+
* - **A call abandoned on its budget may already have counted.** The write
|
|
543
|
+
* lands before the read-back, so a caller that gives up during the read-back
|
|
544
|
+
* still spent its increment. The transaction it replaced committed nothing
|
|
545
|
+
* when it timed out — which is exactly how AGL-2416 could observe "10
|
|
546
|
+
* admitted" after a burst of 504s — so this is a genuine change, not a
|
|
547
|
+
* restatement. It errs in the same safe direction: the counter over-counts,
|
|
548
|
+
* so the cap still cannot be exceeded, and a caller can never buy budget by
|
|
549
|
+
* timing out.
|
|
550
|
+
*
|
|
551
|
+
* The failure classification above is unchanged and still load-bearing: the
|
|
552
|
+
* budget can still elapse on a genuinely slow store, and when it does the
|
|
553
|
+
* answer is still a refusal rather than a degradation.
|
|
554
|
+
*/
|
|
555
|
+
export declare function consumeRateLimit(key: string, options?: DurableRateLimitOptions): Promise<DurableRateLimitResult>;
|
|
556
|
+
export default consumeRateLimit;
|
|
557
|
+
/**
|
|
558
|
+
* Document id for the signup canary's verdict (AGL-2715).
|
|
559
|
+
*
|
|
560
|
+
* One document, not a bucketed series: this records the LAST walk, and a
|
|
561
|
+
* history of walks answers a question nobody asks during an incident. The
|
|
562
|
+
* walk that matters is the most recent one.
|
|
563
|
+
*
|
|
564
|
+
* Written by the scheduled canary, read by `/api/health/journeys`. The split
|
|
565
|
+
* is the same one the beacon heartbeat makes and for a stronger reason: the
|
|
566
|
+
* canary CREATES AN ACCOUNT AND AN ORG, and the door that reports it is
|
|
567
|
+
* public and unauthenticated. A probe that walked the signup itself would
|
|
568
|
+
* hand anyone with `curl` an org factory.
|
|
569
|
+
*
|
|
570
|
+
* ⚠️ NOT in the `rateLimits` TTL's usual seven-day shape. There is exactly one
|
|
571
|
+
* of these and it is overwritten on every run, so it costs one document
|
|
572
|
+
* forever; `expiresAt` is still stamped, far enough out that a canary which
|
|
573
|
+
* stopped running expires its own evidence rather than leaving a stale pass
|
|
574
|
+
* lying around to be misread. The health verdict's own staleness window is
|
|
575
|
+
* two hours, so expiry is a backstop, not the mechanism.
|
|
576
|
+
*/
|
|
577
|
+
export declare const SIGNUP_CANARY_DOC_ID = "signupCanary_production";
|
|
578
|
+
export interface SignupCanaryVerdict {
|
|
579
|
+
/** Whether every step of the walk succeeded. */
|
|
580
|
+
ok: boolean;
|
|
581
|
+
/** Which step broke. Never an error message, never a uid or an email. */
|
|
582
|
+
failedStep?: string | null;
|
|
583
|
+
/** Wall clock for the whole walk. */
|
|
584
|
+
elapsedMs: number;
|
|
585
|
+
/** Whether the walk deleted everything it created. */
|
|
586
|
+
reapedCleanly: boolean;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Record the outcome of one signup walk.
|
|
590
|
+
*
|
|
591
|
+
* AWAITED by its caller, unlike every other recorder in this file. Those are
|
|
592
|
+
* breadcrumbs on a request path where the user's work is the control and the
|
|
593
|
+
* marker must never delay it. This one IS the work: a canary whose verdict
|
|
594
|
+
* did not land has not reported, and the door it feeds would rather red on a
|
|
595
|
+
* missing marker than believe a walk nobody wrote down.
|
|
596
|
+
*
|
|
597
|
+
* `set` without merge, deliberately: this document is a snapshot of the last
|
|
598
|
+
* walk, and merging would let a field from a previous run survive into a
|
|
599
|
+
* verdict it was not part of — a `failedStep` outliving the failure that
|
|
600
|
+
* produced it, read beside a fresh `ok: true`.
|
|
601
|
+
*/
|
|
602
|
+
export declare function recordSignupCanaryWalk(verdict: SignupCanaryVerdict, options?: {
|
|
603
|
+
now?: number;
|
|
604
|
+
firestore?: any;
|
|
605
|
+
}): Promise<void>;
|
|
606
|
+
/**
|
|
607
|
+
* The last recorded walk, or null (AGL-2715).
|
|
608
|
+
*
|
|
609
|
+
* Null covers the same three cases `readBeaconHeartbeat` folds, for the same
|
|
610
|
+
* reason: no marker has ever been written, the document holds nothing usable,
|
|
611
|
+
* and the store would not answer inside its budget. Each one means *nothing
|
|
612
|
+
* here has demonstrated that a stranger can sign up*, and the verdict grades
|
|
613
|
+
* all three as `canary-unavailable`. Distinguishing them could only ever be
|
|
614
|
+
* used to forgive something unproven.
|
|
615
|
+
*/
|
|
616
|
+
export declare function readSignupCanaryWalk(options?: {
|
|
617
|
+
firestore?: any;
|
|
618
|
+
budgetMs?: number;
|
|
619
|
+
}): Promise<{
|
|
620
|
+
walkedAtMs?: number;
|
|
621
|
+
ok?: boolean;
|
|
622
|
+
failedStep?: string | null;
|
|
623
|
+
elapsedMs?: number;
|
|
624
|
+
reapedCleanly?: boolean;
|
|
625
|
+
} | null>;
|
|
626
|
+
/**
|
|
627
|
+
* Document id for the App Check attestation sample (AGL-2715).
|
|
628
|
+
*
|
|
629
|
+
* The rate is published by Google to Cloud Monitoring, which a public health
|
|
630
|
+
* endpoint has no business calling: it needs a second credential, it is slow,
|
|
631
|
+
* and its quota is not something a route anyone can curl should be able to
|
|
632
|
+
* spend. So a scheduled job samples it and this holds the answer, exactly as
|
|
633
|
+
* the signup canary's own verdict is held.
|
|
634
|
+
*
|
|
635
|
+
* One document, overwritten each run — the last reading is the only one that
|
|
636
|
+
* answers "is attestation working now".
|
|
637
|
+
*/
|
|
638
|
+
/**
|
|
639
|
+
* Where the metered-traffic sampler leaves its reading (AGL-2720).
|
|
640
|
+
*
|
|
641
|
+
* One document, like every sibling here. The sampler owns the judgement about
|
|
642
|
+
* whether the total moved; this file only carries it.
|
|
643
|
+
*/
|
|
644
|
+
export declare const EDGE_ADMISSION_DOC_ID = "edgeAdmission_production";
|
|
645
|
+
/**
|
|
646
|
+
* Read the last metered-traffic sample.
|
|
647
|
+
*
|
|
648
|
+
* Deliberately has NO max-age rejection, which is the one place this differs
|
|
649
|
+
* from {@link readAppCheckAttestation}. The quantity graded is already an age
|
|
650
|
+
* — how long since traffic moved — so discarding an old marker would turn the
|
|
651
|
+
* loudest possible reading into `edge-admission-unavailable` and lose which
|
|
652
|
+
* of the two things broke.
|
|
653
|
+
*/
|
|
654
|
+
export declare function readEdgeAdmission(options?: {
|
|
655
|
+
firestore?: any;
|
|
656
|
+
budgetMs?: number;
|
|
657
|
+
}): Promise<{
|
|
658
|
+
sampledAtMs?: number;
|
|
659
|
+
advancedAtMs?: number;
|
|
660
|
+
day?: string;
|
|
661
|
+
total?: number;
|
|
662
|
+
} | null>;
|
|
663
|
+
export declare const APP_CHECK_ATTESTATION_DOC_ID = "appCheckAttestation_production";
|
|
664
|
+
/**
|
|
665
|
+
* Record one sampling of the App Check verification counts.
|
|
666
|
+
*
|
|
667
|
+
* Awaited by its caller, like the canary's verdict and unlike the breadcrumbs
|
|
668
|
+
* in this file: the sample IS the work, and a door would rather report a
|
|
669
|
+
* missing reading than believe one nobody wrote down.
|
|
670
|
+
*/
|
|
671
|
+
export declare function recordAppCheckAttestation(samples: Array<{
|
|
672
|
+
result?: string;
|
|
673
|
+
security?: string;
|
|
674
|
+
service?: string;
|
|
675
|
+
count?: number;
|
|
676
|
+
}>, options?: {
|
|
677
|
+
now?: number;
|
|
678
|
+
firestore?: any;
|
|
679
|
+
}): Promise<void>;
|
|
680
|
+
/**
|
|
681
|
+
* The last attestation sample, or null (AGL-2715).
|
|
682
|
+
*
|
|
683
|
+
* Null folds the three cases its siblings fold — never written, unusable, and
|
|
684
|
+
* the store would not answer — because each means *nothing here has shown that
|
|
685
|
+
* attestation is working*, and the verdict grades all three as
|
|
686
|
+
* `attestation-unavailable`.
|
|
687
|
+
*
|
|
688
|
+
* A sample older than the window it describes is also null. A stale reading is
|
|
689
|
+
* not a reading: reporting a six-hour-old rate as current is how a check comes
|
|
690
|
+
* to say attestation is fine long after it stopped being.
|
|
691
|
+
*/
|
|
692
|
+
export declare function readAppCheckAttestation(options?: {
|
|
693
|
+
firestore?: any;
|
|
694
|
+
budgetMs?: number;
|
|
695
|
+
now?: number;
|
|
696
|
+
maxAgeMs?: number;
|
|
697
|
+
}): Promise<Array<{
|
|
698
|
+
result?: string;
|
|
699
|
+
security?: string;
|
|
700
|
+
service?: string;
|
|
701
|
+
count?: number;
|
|
702
|
+
}> | null>;
|