@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,417 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* THE PLATFORM SEND-RATE GOVERNOR — durable half (AGL-2409).
|
|
19
|
+
*
|
|
20
|
+
* The policy is pure and lives in `@aglyn/shared-util-email`
|
|
21
|
+
* (`send-rate.ts`); this adds the Firestore counter that makes the ceiling
|
|
22
|
+
* global rather than per-instance, the configuration read that makes a ramp a
|
|
23
|
+
* value change instead of a deploy, and the installation that puts the whole
|
|
24
|
+
* thing on `sendEmail`'s path.
|
|
25
|
+
*
|
|
26
|
+
* ## Storage: the `rateLimits` collection, on purpose
|
|
27
|
+
*
|
|
28
|
+
* Both documents live in `rateLimits`, the same collection AGL-794's counters,
|
|
29
|
+
* AGL-1679's degradation markers and AGL-1907's signup-refusal markers already
|
|
30
|
+
* use, for the reason those two state: it inherits the deny-all security rule
|
|
31
|
+
* and the `expiresAt` TTL policy that already exist rather than needing a new
|
|
32
|
+
* collection, a rules deploy and a second TTL policy.
|
|
33
|
+
*
|
|
34
|
+
* - `rateLimits/sendRate_{windowStartMs}` — one document per hour, holding
|
|
35
|
+
* the count. Carries `expiresAt`, so the TTL policy sweeps it.
|
|
36
|
+
* - `rateLimits/sendRateConfig` — the ceiling. **Carries NO `expiresAt`, and
|
|
37
|
+
* must never be given one**, or the TTL policy that serves the counters
|
|
38
|
+
* would quietly delete the configuration and the platform would silently
|
|
39
|
+
* revert to the compiled-in default. `emailSendRateConfigWrite` is the one
|
|
40
|
+
* writer and `email-send-rate.spec.ts` asserts the field is absent.
|
|
41
|
+
*
|
|
42
|
+
* The window document does NOT carry `lastAtMs`. AGL-1693's rate-limiter
|
|
43
|
+
* health probe queries this collection with `where('lastAtMs', '>=', cutoff)`;
|
|
44
|
+
* a per-hour document in that range would compete with the degradation
|
|
45
|
+
* markers the probe exists to find. It uses `sentAtMs`, which keeps the two
|
|
46
|
+
* queries disjoint at the index level — the same reasoning, and the same
|
|
47
|
+
* mistake avoided, as `refusedAtMs` on the signup markers.
|
|
48
|
+
*
|
|
49
|
+
* ## Failure posture: OPEN
|
|
50
|
+
*
|
|
51
|
+
* If the counter or the config cannot be read, every send is granted. That is
|
|
52
|
+
* the opposite of `consumeRateLimit`'s fail-closed-on-contention rule, and the
|
|
53
|
+
* difference is what the two controls protect. A brute-force limiter that
|
|
54
|
+
* fails open lets an attacker in; this one failing open lets a campaign out.
|
|
55
|
+
* The cost of failing CLOSED here is refusing a paying customer's campaign
|
|
56
|
+
* because of an unrelated Firestore blip — and, worse, refusing a bulk sweep
|
|
57
|
+
* that a customer's month depends on. Neither is worth the hour of ramp it
|
|
58
|
+
* would buy back, and the operator can see it: a degraded grant is reported
|
|
59
|
+
* and surfaced on /admin/emails.
|
|
60
|
+
*/ import { EMAIL_SEND_RATE_WINDOW_MS, emailSendRateVerdict, emailSendRateWindowStartMs, normalizeEmailSendRateConfig, orgHourlyCampaignCeiling, setEmailSendGovernor } from "@aglyn/shared-util-email";
|
|
61
|
+
import { firebaseAdmin } from "./firebase-admin.js";
|
|
62
|
+
import { RATE_LIMIT_COLLECTION } from "./rate-limit-store.js";
|
|
63
|
+
/** Document id of the live ceiling. NEVER written with `expiresAt`. */ export const EMAIL_SEND_RATE_CONFIG_DOC = 'sendRateConfig';
|
|
64
|
+
/** Id prefix for the per-hour counters. */ export const EMAIL_SEND_RATE_WINDOW_PREFIX = 'sendRate_';
|
|
65
|
+
/** The counter document id for a window. */ export function emailSendRateWindowDocId(windowStartMs) {
|
|
66
|
+
return `${EMAIL_SEND_RATE_WINDOW_PREFIX}${windowStartMs}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Config cache TTL.
|
|
70
|
+
*
|
|
71
|
+
* 15s, the same number and the same reasoning as the platform lockdown doc:
|
|
72
|
+
* this sits on a path that every outbound message crosses, so a per-send read
|
|
73
|
+
* would be one Firestore read per email platform-wide, and 15s is the
|
|
74
|
+
* worst-case lag between staff moving the ramp and a warm process honouring
|
|
75
|
+
* it. A ramp is measured in hours; fifteen seconds is free.
|
|
76
|
+
*/ const CONFIG_TTL_MS = 15000;
|
|
77
|
+
let configCache;
|
|
78
|
+
let configPending;
|
|
79
|
+
/** Drop the in-process config cache — called by the console after a write. */ export function invalidateEmailSendRateConfigCache() {
|
|
80
|
+
configCache = undefined;
|
|
81
|
+
configPending = undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The live ceiling. Falls back to the compiled-in default on any read error,
|
|
85
|
+
* which is the fail-open posture stated above: an unreachable config must not
|
|
86
|
+
* be read as a ceiling of zero.
|
|
87
|
+
*/ export async function readEmailSendRateConfig(options) {
|
|
88
|
+
var _ref;
|
|
89
|
+
const now = (_ref = options == null ? void 0 : options.now) != null ? _ref : Date.now();
|
|
90
|
+
if (!(options == null ? void 0 : options.firestore) && configCache && now - configCache.at < CONFIG_TTL_MS) {
|
|
91
|
+
return configCache.config;
|
|
92
|
+
}
|
|
93
|
+
const load = async ()=>{
|
|
94
|
+
let config = normalizeEmailSendRateConfig(null);
|
|
95
|
+
try {
|
|
96
|
+
var _ref;
|
|
97
|
+
const firestore = (_ref = options == null ? void 0 : options.firestore) != null ? _ref : firebaseAdmin.app().firestore();
|
|
98
|
+
const snapshot = await firestore.collection(RATE_LIMIT_COLLECTION).doc(EMAIL_SEND_RATE_CONFIG_DOC).get();
|
|
99
|
+
config = normalizeEmailSendRateConfig(snapshot.exists ? snapshot.data() : null);
|
|
100
|
+
} catch (unused) {
|
|
101
|
+
// Fail open: an unreachable config is an outage, not a ceiling of zero.
|
|
102
|
+
}
|
|
103
|
+
return config;
|
|
104
|
+
};
|
|
105
|
+
// An injected firestore is a test or a one-off read; never cached, so a
|
|
106
|
+
// spec cannot poison the process cache for the next one.
|
|
107
|
+
if (options == null ? void 0 : options.firestore) return load();
|
|
108
|
+
if (!configPending) {
|
|
109
|
+
configPending = load().then((config)=>{
|
|
110
|
+
configCache = {
|
|
111
|
+
at: now,
|
|
112
|
+
config
|
|
113
|
+
};
|
|
114
|
+
return config;
|
|
115
|
+
}).finally(()=>{
|
|
116
|
+
configPending = undefined;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return configPending;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Counts `count` messages against the current hour, globally, and answers
|
|
123
|
+
* whether they may go.
|
|
124
|
+
*
|
|
125
|
+
* The transaction reads the counter and writes an ABSOLUTE value derived from
|
|
126
|
+
* that read — not `FieldValue.increment` — because the read is the authority
|
|
127
|
+
* for the decision. Firestore aborts and re-runs the callback when a
|
|
128
|
+
* document it read has moved, so two concurrent sweeps cannot both see the
|
|
129
|
+
* same headroom and both take it. An increment would be atomic on the number
|
|
130
|
+
* and useless for the decision, which is exactly the read-then-write shape
|
|
131
|
+
* AGL-2267 is about one surface over.
|
|
132
|
+
*
|
|
133
|
+
* A refused send writes NOTHING. A caller that will retry next hour must not
|
|
134
|
+
* have spent budget on being told no.
|
|
135
|
+
*/ export async function consumeEmailSendBudget(options) {
|
|
136
|
+
var _options_now, _options_config;
|
|
137
|
+
const now = (_options_now = options.now) != null ? _options_now : Date.now();
|
|
138
|
+
const windowStartMs = emailSendRateWindowStartMs(now);
|
|
139
|
+
const resetMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS;
|
|
140
|
+
const config = (_options_config = options.config) != null ? _options_config : await readEmailSendRateConfig({
|
|
141
|
+
firestore: options.firestore,
|
|
142
|
+
now
|
|
143
|
+
});
|
|
144
|
+
const fallback = ()=>_extends({}, emailSendRateVerdict({
|
|
145
|
+
priority: options.priority,
|
|
146
|
+
used: 0,
|
|
147
|
+
count: options.count,
|
|
148
|
+
ceiling: config.perHour,
|
|
149
|
+
enabled: false,
|
|
150
|
+
windowStartMs
|
|
151
|
+
}), {
|
|
152
|
+
windowStartMs,
|
|
153
|
+
degraded: true
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
var _options_firestore;
|
|
157
|
+
const firestore = (_options_firestore = options.firestore) != null ? _options_firestore : firebaseAdmin.app().firestore();
|
|
158
|
+
const ref = firestore.collection(RATE_LIMIT_COLLECTION).doc(emailSendRateWindowDocId(windowStartMs));
|
|
159
|
+
const verdict = await firestore.runTransaction(async (tx)=>{
|
|
160
|
+
var _ref;
|
|
161
|
+
const snapshot = await tx.get(ref);
|
|
162
|
+
const used = Number((_ref = snapshot.exists ? snapshot.get('count') : 0) != null ? _ref : 0);
|
|
163
|
+
const decision = emailSendRateVerdict({
|
|
164
|
+
priority: options.priority,
|
|
165
|
+
used,
|
|
166
|
+
count: options.count,
|
|
167
|
+
ceiling: config.perHour,
|
|
168
|
+
enabled: config.enabled,
|
|
169
|
+
windowStartMs
|
|
170
|
+
});
|
|
171
|
+
if (!decision.allowed) return decision;
|
|
172
|
+
tx.set(ref, {
|
|
173
|
+
count: decision.used + Math.max(0, Math.floor(Number(options.count) || 0)),
|
|
174
|
+
windowStartMs,
|
|
175
|
+
// NOT `lastAtMs` — see the storage note at the top of this file.
|
|
176
|
+
sentAtMs: now,
|
|
177
|
+
// The TTL field the counters in this collection already use. Two
|
|
178
|
+
// windows of retention so the console can show the hour just gone.
|
|
179
|
+
expiresAt: new Date(resetMs + EMAIL_SEND_RATE_WINDOW_MS)
|
|
180
|
+
}, {
|
|
181
|
+
merge: true
|
|
182
|
+
});
|
|
183
|
+
return decision;
|
|
184
|
+
});
|
|
185
|
+
return _extends({}, verdict, {
|
|
186
|
+
windowStartMs,
|
|
187
|
+
degraded: false
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('[send-rate] counter unavailable — allowing', error);
|
|
191
|
+
return fallback();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* What the current hour looks like. Read-only, for the staff console.
|
|
196
|
+
*
|
|
197
|
+
* Returns `used: 0` rather than throwing when the window document does not
|
|
198
|
+
* exist, which is the ordinary state of a quiet hour.
|
|
199
|
+
*/ export async function readEmailSendRateWindow(options) {
|
|
200
|
+
var _ref;
|
|
201
|
+
const now = (_ref = options == null ? void 0 : options.now) != null ? _ref : Date.now();
|
|
202
|
+
const windowStartMs = emailSendRateWindowStartMs(now);
|
|
203
|
+
const resetMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS;
|
|
204
|
+
try {
|
|
205
|
+
var _ref1, _ref2;
|
|
206
|
+
const firestore = (_ref1 = options == null ? void 0 : options.firestore) != null ? _ref1 : firebaseAdmin.app().firestore();
|
|
207
|
+
const snapshot = await firestore.collection(RATE_LIMIT_COLLECTION).doc(emailSendRateWindowDocId(windowStartMs)).get();
|
|
208
|
+
const used = Number((_ref2 = snapshot.exists ? snapshot.get('count') : 0) != null ? _ref2 : 0);
|
|
209
|
+
return {
|
|
210
|
+
windowStartMs,
|
|
211
|
+
resetMs,
|
|
212
|
+
used: Number.isFinite(used) && used > 0 ? Math.floor(used) : 0
|
|
213
|
+
};
|
|
214
|
+
} catch (unused) {
|
|
215
|
+
return {
|
|
216
|
+
windowStartMs,
|
|
217
|
+
resetMs,
|
|
218
|
+
used: 0
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Id prefix for the per-org, per-hour campaign counters. */ export const EMAIL_ORG_SEND_RATE_WINDOW_PREFIX = 'sendRateOrg_';
|
|
223
|
+
/**
|
|
224
|
+
* The per-org counter document id for a window.
|
|
225
|
+
*
|
|
226
|
+
* A separate document per org rather than a map field on the platform window,
|
|
227
|
+
* so two orgs sending in the same hour contend on their own documents instead
|
|
228
|
+
* of serialising on one. The platform window is already a single hot document
|
|
229
|
+
* and adding N org fields to it would make every campaign in the hour a write
|
|
230
|
+
* conflict with every other.
|
|
231
|
+
*/ export function emailOrgSendRateWindowDocId(windowStartMs, orgId) {
|
|
232
|
+
return `${EMAIL_ORG_SEND_RATE_WINDOW_PREFIX}${windowStartMs}_${orgId}`;
|
|
233
|
+
}
|
|
234
|
+
/** The current hour for one org. Read-only, for a usage surface. */ export async function readOrgEmailSendWindow(options) {
|
|
235
|
+
var _options_now;
|
|
236
|
+
const now = (_options_now = options.now) != null ? _options_now : Date.now();
|
|
237
|
+
const windowStartMs = emailSendRateWindowStartMs(now);
|
|
238
|
+
const resetMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS;
|
|
239
|
+
if (!options.orgId) return {
|
|
240
|
+
windowStartMs,
|
|
241
|
+
resetMs,
|
|
242
|
+
used: 0
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
var _options_firestore, _ref;
|
|
246
|
+
const firestore = (_options_firestore = options.firestore) != null ? _options_firestore : firebaseAdmin.app().firestore();
|
|
247
|
+
const snapshot = await firestore.collection(RATE_LIMIT_COLLECTION).doc(emailOrgSendRateWindowDocId(windowStartMs, options.orgId)).get();
|
|
248
|
+
const used = Number((_ref = snapshot.exists ? snapshot.get('count') : 0) != null ? _ref : 0);
|
|
249
|
+
return {
|
|
250
|
+
windowStartMs,
|
|
251
|
+
resetMs,
|
|
252
|
+
used: Number.isFinite(used) && used > 0 ? Math.floor(used) : 0
|
|
253
|
+
};
|
|
254
|
+
} catch (unused) {
|
|
255
|
+
return {
|
|
256
|
+
windowStartMs,
|
|
257
|
+
resetMs,
|
|
258
|
+
used: 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* THE PER-ORG SHARE OF THE PLATFORM HOUR.
|
|
264
|
+
*
|
|
265
|
+
* The platform governor bounds total volume; it does not bound how much of
|
|
266
|
+
* that total ONE tenant may take. Without this, a single org with a large
|
|
267
|
+
* audience occupies the whole hour and every other customer's campaigns are
|
|
268
|
+
* refused by a ceiling they did nothing to reach — one tenant denying service
|
|
269
|
+
* to the rest, on a limit they cannot see.
|
|
270
|
+
*
|
|
271
|
+
* The ceiling is derived, not configured: `orgHourlyCampaignCeiling` is a
|
|
272
|
+
* share of whatever the live platform ceiling currently is, so a staff ramp
|
|
273
|
+
* moves both together and the two can never drift into contradiction. See
|
|
274
|
+
* `send-ceilings.ts` for the arithmetic and the relations it maintains.
|
|
275
|
+
*
|
|
276
|
+
* **Campaigns only.** This function is not on `sendEmail`'s path and is called
|
|
277
|
+
* from the campaign sender alone. A transactional message can never reach it,
|
|
278
|
+
* which is the same boundary `emailSendRateVerdict` enforces one layer down
|
|
279
|
+
* and for the same reason: a password reset refused by a throttle converts a
|
|
280
|
+
* reputation risk into an outage on somebody else's business.
|
|
281
|
+
*
|
|
282
|
+
* ## Fails OPEN
|
|
283
|
+
*
|
|
284
|
+
* An unreachable counter grants the send, matching `consumeEmailSendBudget`.
|
|
285
|
+
* A refusal produced by a Firestore blip is a refused campaign for a paying
|
|
286
|
+
* customer, and the hour of pacing it buys back is not worth it.
|
|
287
|
+
*
|
|
288
|
+
* ## Claimed, not reconciled
|
|
289
|
+
*
|
|
290
|
+
* The claim is taken for the whole batch and never refunded, unlike the
|
|
291
|
+
* monthly reservation. The window is one hour and TTL-swept, so an
|
|
292
|
+
* undelivered remainder costs the org the rest of that hour and nothing
|
|
293
|
+
* after it — where an unreconciled MONTHLY claim would cost the rest of the
|
|
294
|
+
* month, which is why that one is reconciled and this one is not. The error
|
|
295
|
+
* is in the direction that can only ever pace mail more, never let more out.
|
|
296
|
+
*/ export async function claimOrgEmailSendBudget(options) {
|
|
297
|
+
var _options_now;
|
|
298
|
+
const now = (_options_now = options.now) != null ? _options_now : Date.now();
|
|
299
|
+
const windowStartMs = emailSendRateWindowStartMs(now);
|
|
300
|
+
const retryAtMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS;
|
|
301
|
+
const ceiling = orgHourlyCampaignCeiling(options.platformPerHour);
|
|
302
|
+
const count = Math.max(0, Math.floor(Number(options.count) || 0));
|
|
303
|
+
const granted = (used, degraded)=>({
|
|
304
|
+
allowed: true,
|
|
305
|
+
used,
|
|
306
|
+
ceiling,
|
|
307
|
+
remaining: Math.max(0, ceiling - (used + count)),
|
|
308
|
+
retryAtMs,
|
|
309
|
+
degraded
|
|
310
|
+
});
|
|
311
|
+
// A parked control still reports the ceiling, so a surface reading this
|
|
312
|
+
// shows a real number rather than blanking while the control is off.
|
|
313
|
+
if (options.enabled === false) return granted(0, false);
|
|
314
|
+
// No org means the counter has nowhere to live. The monthly reservation
|
|
315
|
+
// refuses in this case because an unattributable campaign must not send
|
|
316
|
+
// unbounded; here the monthly claim has already made that decision, so
|
|
317
|
+
// failing open costs nothing it has not already been charged for.
|
|
318
|
+
if (!options.orgId) return granted(0, true);
|
|
319
|
+
try {
|
|
320
|
+
var _options_firestore;
|
|
321
|
+
const firestore = (_options_firestore = options.firestore) != null ? _options_firestore : firebaseAdmin.app().firestore();
|
|
322
|
+
const ref = firestore.collection(RATE_LIMIT_COLLECTION).doc(emailOrgSendRateWindowDocId(windowStartMs, options.orgId));
|
|
323
|
+
return await firestore.runTransaction(async (tx)=>{
|
|
324
|
+
var _ref;
|
|
325
|
+
const snapshot = await tx.get(ref);
|
|
326
|
+
const rawUsed = Number((_ref = snapshot.exists ? snapshot.get('count') : 0) != null ? _ref : 0);
|
|
327
|
+
// A corrupt or negative counter must not read as headroom.
|
|
328
|
+
const used = Number.isFinite(rawUsed) && rawUsed > 0 ? Math.floor(rawUsed) : 0;
|
|
329
|
+
if (used + count > ceiling) {
|
|
330
|
+
// A refused claim writes NOTHING. A campaign that will be retried
|
|
331
|
+
// next hour must not have spent budget on being told no.
|
|
332
|
+
return {
|
|
333
|
+
allowed: false,
|
|
334
|
+
used,
|
|
335
|
+
ceiling,
|
|
336
|
+
remaining: Math.max(0, ceiling - used),
|
|
337
|
+
retryAtMs,
|
|
338
|
+
degraded: false
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// An absolute write from this transaction's own read, not
|
|
342
|
+
// `FieldValue.increment` — the read is the authority for the decision,
|
|
343
|
+
// and an increment would be atomic on the number while proving nothing
|
|
344
|
+
// about the value the decision was made from.
|
|
345
|
+
tx.set(ref, {
|
|
346
|
+
count: used + count,
|
|
347
|
+
windowStartMs,
|
|
348
|
+
orgId: options.orgId,
|
|
349
|
+
// NOT `lastAtMs` — the rate-limiter health probe queries this
|
|
350
|
+
// collection on that field and a per-hour document in its range
|
|
351
|
+
// would compete with the markers it exists to find.
|
|
352
|
+
sentAtMs: now,
|
|
353
|
+
expiresAt: new Date(retryAtMs + EMAIL_SEND_RATE_WINDOW_MS)
|
|
354
|
+
}, {
|
|
355
|
+
merge: true
|
|
356
|
+
});
|
|
357
|
+
return granted(used, false);
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error('[send-rate] org window unavailable — allowing', error);
|
|
361
|
+
return granted(0, true);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The document the console writes when staff move the ramp.
|
|
366
|
+
*
|
|
367
|
+
* Returned rather than written so the route owns the write (and the audit row
|
|
368
|
+
* beside it), and so a spec can assert the SHAPE without a Firestore. The one
|
|
369
|
+
* thing it must never contain is `expiresAt`.
|
|
370
|
+
*/ export function emailSendRateConfigWrite(input) {
|
|
371
|
+
var _input_now, _input_actorEmail;
|
|
372
|
+
const normalized = normalizeEmailSendRateConfig({
|
|
373
|
+
perHour: input.perHour,
|
|
374
|
+
enabled: input.enabled,
|
|
375
|
+
note: input.note,
|
|
376
|
+
updatedAtMs: (_input_now = input.now) != null ? _input_now : Date.now(),
|
|
377
|
+
updatedByEmail: (_input_actorEmail = input.actorEmail) != null ? _input_actorEmail : null
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
perHour: normalized.perHour,
|
|
381
|
+
enabled: normalized.enabled,
|
|
382
|
+
note: normalized.note,
|
|
383
|
+
updatedAtMs: normalized.updatedAtMs,
|
|
384
|
+
updatedByEmail: normalized.updatedByEmail
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Puts the durable governor on `sendEmail`'s path.
|
|
389
|
+
*
|
|
390
|
+
* **Called at module load**, from the bottom of this file, so that importing
|
|
391
|
+
* `@aglyn/tenant-data-admin` anywhere is enough — every server surface in the
|
|
392
|
+
* product already imports that barrel, and `export *` forces this module to
|
|
393
|
+
* evaluate. The alternative, an `installEmailSendGovernor()` call at each
|
|
394
|
+
* server entrypoint, is the 37-places-to-remember shape this codebase already
|
|
395
|
+
* rejected once for the `context` tag.
|
|
396
|
+
*
|
|
397
|
+
* Idempotent: installing twice replaces the same closure with an equivalent
|
|
398
|
+
* one, and the closure holds no state — the state is the Firestore document.
|
|
399
|
+
*/ export function installEmailSendGovernor() {
|
|
400
|
+
setEmailSendGovernor(async (request)=>{
|
|
401
|
+
const result = await consumeEmailSendBudget({
|
|
402
|
+
priority: request.priority,
|
|
403
|
+
count: request.count
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
allowed: result.allowed,
|
|
407
|
+
ceiling: result.ceiling,
|
|
408
|
+
used: result.used,
|
|
409
|
+
remaining: result.remaining,
|
|
410
|
+
retryAtMs: result.retryAtMs,
|
|
411
|
+
degraded: result.degraded
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
installEmailSendGovernor();
|
|
416
|
+
|
|
417
|
+
//# sourceMappingURL=email-send-rate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/email-send-rate.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 PLATFORM SEND-RATE GOVERNOR — durable half (AGL-2409).\n *\n * The policy is pure and lives in `@aglyn/shared-util-email`\n * (`send-rate.ts`); this adds the Firestore counter that makes the ceiling\n * global rather than per-instance, the configuration read that makes a ramp a\n * value change instead of a deploy, and the installation that puts the whole\n * thing on `sendEmail`'s path.\n *\n * ## Storage: the `rateLimits` collection, on purpose\n *\n * Both documents live in `rateLimits`, the same collection AGL-794's counters,\n * AGL-1679's degradation markers and AGL-1907's signup-refusal markers already\n * use, for the reason those two state: it inherits the deny-all security rule\n * and the `expiresAt` TTL policy that already exist rather than needing a new\n * collection, a rules deploy and a second TTL policy.\n *\n * - `rateLimits/sendRate_{windowStartMs}` — one document per hour, holding\n * the count. Carries `expiresAt`, so the TTL policy sweeps it.\n * - `rateLimits/sendRateConfig` — the ceiling. **Carries NO `expiresAt`, and\n * must never be given one**, or the TTL policy that serves the counters\n * would quietly delete the configuration and the platform would silently\n * revert to the compiled-in default. `emailSendRateConfigWrite` is the one\n * writer and `email-send-rate.spec.ts` asserts the field is absent.\n *\n * The window document does NOT carry `lastAtMs`. AGL-1693's rate-limiter\n * health probe queries this collection with `where('lastAtMs', '>=', cutoff)`;\n * a per-hour document in that range would compete with the degradation\n * markers the probe exists to find. It uses `sentAtMs`, which keeps the two\n * queries disjoint at the index level — the same reasoning, and the same\n * mistake avoided, as `refusedAtMs` on the signup markers.\n *\n * ## Failure posture: OPEN\n *\n * If the counter or the config cannot be read, every send is granted. That is\n * the opposite of `consumeRateLimit`'s fail-closed-on-contention rule, and the\n * difference is what the two controls protect. A brute-force limiter that\n * fails open lets an attacker in; this one failing open lets a campaign out.\n * The cost of failing CLOSED here is refusing a paying customer's campaign\n * because of an unrelated Firestore blip — and, worse, refusing a bulk sweep\n * that a customer's month depends on. Neither is worth the hour of ramp it\n * would buy back, and the operator can see it: a degraded grant is reported\n * and surfaced on /admin/emails.\n */\n\nimport {\n type EmailSendGovernorRequest,\n type EmailSendGovernorVerdict,\n type EmailSendPriority,\n type EmailSendRateConfig,\n type EmailSendRateVerdict,\n EMAIL_SEND_RATE_WINDOW_MS,\n emailSendRateVerdict,\n emailSendRateWindowStartMs,\n normalizeEmailSendRateConfig,\n orgHourlyCampaignCeiling,\n setEmailSendGovernor,\n} from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from './firebase-admin'\nimport { RATE_LIMIT_COLLECTION } from './rate-limit-store'\n\n/** Document id of the live ceiling. NEVER written with `expiresAt`. */\nexport const EMAIL_SEND_RATE_CONFIG_DOC = 'sendRateConfig'\n\n/** Id prefix for the per-hour counters. */\nexport const EMAIL_SEND_RATE_WINDOW_PREFIX = 'sendRate_'\n\n/** The counter document id for a window. */\nexport function emailSendRateWindowDocId(windowStartMs: number): string {\n return `${EMAIL_SEND_RATE_WINDOW_PREFIX}${windowStartMs}`\n}\n\n/**\n * Config cache TTL.\n *\n * 15s, the same number and the same reasoning as the platform lockdown doc:\n * this sits on a path that every outbound message crosses, so a per-send read\n * would be one Firestore read per email platform-wide, and 15s is the\n * worst-case lag between staff moving the ramp and a warm process honouring\n * it. A ramp is measured in hours; fifteen seconds is free.\n */\nconst CONFIG_TTL_MS = 15_000\n\nlet configCache: { at: number; config: EmailSendRateConfig } | undefined\nlet configPending: Promise<EmailSendRateConfig> | undefined\n\n/** Drop the in-process config cache — called by the console after a write. */\nexport function invalidateEmailSendRateConfigCache(): void {\n configCache = undefined\n configPending = undefined\n}\n\n/**\n * The live ceiling. Falls back to the compiled-in default on any read error,\n * which is the fail-open posture stated above: an unreachable config must not\n * be read as a ceiling of zero.\n */\nexport async function readEmailSendRateConfig(options?: {\n firestore?: any\n now?: number\n}): Promise<EmailSendRateConfig> {\n const now = options?.now ?? Date.now()\n if (!options?.firestore && configCache && now - configCache.at < CONFIG_TTL_MS) {\n return configCache.config\n }\n const load = async (): Promise<EmailSendRateConfig> => {\n let config = normalizeEmailSendRateConfig(null)\n try {\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(EMAIL_SEND_RATE_CONFIG_DOC)\n .get()\n config = normalizeEmailSendRateConfig(\n snapshot.exists ? (snapshot.data() as Partial<EmailSendRateConfig>) : null,\n )\n } catch {\n // Fail open: an unreachable config is an outage, not a ceiling of zero.\n }\n return config\n }\n // An injected firestore is a test or a one-off read; never cached, so a\n // spec cannot poison the process cache for the next one.\n if (options?.firestore) return load()\n if (!configPending) {\n configPending = load()\n .then((config) => {\n configCache = { at: now, config }\n return config\n })\n .finally(() => {\n configPending = undefined\n })\n }\n return configPending\n}\n\nexport interface ConsumeEmailSendBudgetOptions {\n priority: EmailSendPriority\n /** Messages this send would add. */\n count: number\n now?: number\n firestore?: any\n /** Injectable for tests; otherwise read (and cached) from Firestore. */\n config?: EmailSendRateConfig\n}\n\nexport interface ConsumeEmailSendBudgetResult extends EmailSendRateVerdict {\n windowStartMs: number\n /** True when the counter was unreachable and this failed open. */\n degraded: boolean\n}\n\n/**\n * Counts `count` messages against the current hour, globally, and answers\n * whether they may go.\n *\n * The transaction reads the counter and writes an ABSOLUTE value derived from\n * that read — not `FieldValue.increment` — because the read is the authority\n * for the decision. Firestore aborts and re-runs the callback when a\n * document it read has moved, so two concurrent sweeps cannot both see the\n * same headroom and both take it. An increment would be atomic on the number\n * and useless for the decision, which is exactly the read-then-write shape\n * AGL-2267 is about one surface over.\n *\n * A refused send writes NOTHING. A caller that will retry next hour must not\n * have spent budget on being told no.\n */\nexport async function consumeEmailSendBudget(\n options: ConsumeEmailSendBudgetOptions,\n): Promise<ConsumeEmailSendBudgetResult> {\n const now = options.now ?? Date.now()\n const windowStartMs = emailSendRateWindowStartMs(now)\n const resetMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS\n const config =\n options.config ??\n (await readEmailSendRateConfig({ firestore: options.firestore, now }))\n\n const fallback = (): ConsumeEmailSendBudgetResult => ({\n ...emailSendRateVerdict({\n priority: options.priority,\n used: 0,\n count: options.count,\n ceiling: config.perHour,\n enabled: false,\n windowStartMs,\n }),\n windowStartMs,\n degraded: true,\n })\n\n try {\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(emailSendRateWindowDocId(windowStartMs))\n\n const verdict = await firestore.runTransaction(async (tx: any) => {\n const snapshot = await tx.get(ref)\n const used = Number((snapshot.exists ? snapshot.get('count') : 0) ?? 0)\n const decision = emailSendRateVerdict({\n priority: options.priority,\n used,\n count: options.count,\n ceiling: config.perHour,\n enabled: config.enabled,\n windowStartMs,\n })\n if (!decision.allowed) return decision\n tx.set(\n ref,\n {\n count: decision.used + Math.max(0, Math.floor(Number(options.count) || 0)),\n windowStartMs,\n // NOT `lastAtMs` — see the storage note at the top of this file.\n sentAtMs: now,\n // The TTL field the counters in this collection already use. Two\n // windows of retention so the console can show the hour just gone.\n expiresAt: new Date(resetMs + EMAIL_SEND_RATE_WINDOW_MS),\n },\n { merge: true },\n )\n return decision\n })\n\n return { ...(verdict as EmailSendRateVerdict), windowStartMs, degraded: false }\n } catch (error) {\n console.error('[send-rate] counter unavailable — allowing', error)\n return fallback()\n }\n}\n\n/**\n * What the current hour looks like. Read-only, for the staff console.\n *\n * Returns `used: 0` rather than throwing when the window document does not\n * exist, which is the ordinary state of a quiet hour.\n */\nexport async function readEmailSendRateWindow(options?: {\n firestore?: any\n now?: number\n}): Promise<{ windowStartMs: number; resetMs: number; used: number }> {\n const now = options?.now ?? Date.now()\n const windowStartMs = emailSendRateWindowStartMs(now)\n const resetMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS\n try {\n const firestore = options?.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(emailSendRateWindowDocId(windowStartMs))\n .get()\n const used = Number((snapshot.exists ? snapshot.get('count') : 0) ?? 0)\n return {\n windowStartMs,\n resetMs,\n used: Number.isFinite(used) && used > 0 ? Math.floor(used) : 0,\n }\n } catch {\n return { windowStartMs, resetMs, used: 0 }\n }\n}\n\n/** Id prefix for the per-org, per-hour campaign counters. */\nexport const EMAIL_ORG_SEND_RATE_WINDOW_PREFIX = 'sendRateOrg_'\n\n/**\n * The per-org counter document id for a window.\n *\n * A separate document per org rather than a map field on the platform window,\n * so two orgs sending in the same hour contend on their own documents instead\n * of serialising on one. The platform window is already a single hot document\n * and adding N org fields to it would make every campaign in the hour a write\n * conflict with every other.\n */\nexport function emailOrgSendRateWindowDocId(\n windowStartMs: number,\n orgId: string,\n): string {\n return `${EMAIL_ORG_SEND_RATE_WINDOW_PREFIX}${windowStartMs}_${orgId}`\n}\n\n/** The current hour for one org. Read-only, for a usage surface. */\nexport async function readOrgEmailSendWindow(options: {\n orgId: string\n firestore?: any\n now?: number\n}): Promise<{ windowStartMs: number; resetMs: number; used: number }> {\n const now = options.now ?? Date.now()\n const windowStartMs = emailSendRateWindowStartMs(now)\n const resetMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS\n if (!options.orgId) return { windowStartMs, resetMs, used: 0 }\n try {\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const snapshot = await firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(emailOrgSendRateWindowDocId(windowStartMs, options.orgId))\n .get()\n const used = Number((snapshot.exists ? snapshot.get('count') : 0) ?? 0)\n return {\n windowStartMs,\n resetMs,\n used: Number.isFinite(used) && used > 0 ? Math.floor(used) : 0,\n }\n } catch {\n return { windowStartMs, resetMs, used: 0 }\n }\n}\n\n/** The answer to a per-org hourly claim. Every field is a stated number. */\nexport interface OrgEmailSendClaimResult {\n allowed: boolean\n /** Count in this org's window BEFORE this send. */\n used: number\n /** What this org may send in an hour. */\n ceiling: number\n /** Headroom after this send, floored at 0. */\n remaining: number\n /** When the window rolls and a deferred campaign may go. */\n retryAtMs: number\n /** True when the counter was unreachable and this failed open. */\n degraded: boolean\n}\n\n/**\n * THE PER-ORG SHARE OF THE PLATFORM HOUR.\n *\n * The platform governor bounds total volume; it does not bound how much of\n * that total ONE tenant may take. Without this, a single org with a large\n * audience occupies the whole hour and every other customer's campaigns are\n * refused by a ceiling they did nothing to reach — one tenant denying service\n * to the rest, on a limit they cannot see.\n *\n * The ceiling is derived, not configured: `orgHourlyCampaignCeiling` is a\n * share of whatever the live platform ceiling currently is, so a staff ramp\n * moves both together and the two can never drift into contradiction. See\n * `send-ceilings.ts` for the arithmetic and the relations it maintains.\n *\n * **Campaigns only.** This function is not on `sendEmail`'s path and is called\n * from the campaign sender alone. A transactional message can never reach it,\n * which is the same boundary `emailSendRateVerdict` enforces one layer down\n * and for the same reason: a password reset refused by a throttle converts a\n * reputation risk into an outage on somebody else's business.\n *\n * ## Fails OPEN\n *\n * An unreachable counter grants the send, matching `consumeEmailSendBudget`.\n * A refusal produced by a Firestore blip is a refused campaign for a paying\n * customer, and the hour of pacing it buys back is not worth it.\n *\n * ## Claimed, not reconciled\n *\n * The claim is taken for the whole batch and never refunded, unlike the\n * monthly reservation. The window is one hour and TTL-swept, so an\n * undelivered remainder costs the org the rest of that hour and nothing\n * after it — where an unreconciled MONTHLY claim would cost the rest of the\n * month, which is why that one is reconciled and this one is not. The error\n * is in the direction that can only ever pace mail more, never let more out.\n */\nexport async function claimOrgEmailSendBudget(options: {\n orgId: string\n /** Messages this campaign would send. */\n count: number\n /** The live platform ceiling; the org share is derived from it. */\n platformPerHour: number\n /** False parks the control, exactly as the platform governor's flag does. */\n enabled?: boolean\n now?: number\n firestore?: any\n}): Promise<OrgEmailSendClaimResult> {\n const now = options.now ?? Date.now()\n const windowStartMs = emailSendRateWindowStartMs(now)\n const retryAtMs = windowStartMs + EMAIL_SEND_RATE_WINDOW_MS\n const ceiling = orgHourlyCampaignCeiling(options.platformPerHour)\n const count = Math.max(0, Math.floor(Number(options.count) || 0))\n\n const granted = (used: number, degraded: boolean): OrgEmailSendClaimResult => ({\n allowed: true,\n used,\n ceiling,\n remaining: Math.max(0, ceiling - (used + count)),\n retryAtMs,\n degraded,\n })\n\n // A parked control still reports the ceiling, so a surface reading this\n // shows a real number rather than blanking while the control is off.\n if (options.enabled === false) return granted(0, false)\n // No org means the counter has nowhere to live. The monthly reservation\n // refuses in this case because an unattributable campaign must not send\n // unbounded; here the monthly claim has already made that decision, so\n // failing open costs nothing it has not already been charged for.\n if (!options.orgId) return granted(0, true)\n\n try {\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const ref = firestore\n .collection(RATE_LIMIT_COLLECTION)\n .doc(emailOrgSendRateWindowDocId(windowStartMs, options.orgId))\n\n return await firestore.runTransaction(async (tx: any) => {\n const snapshot = await tx.get(ref)\n const rawUsed = Number((snapshot.exists ? snapshot.get('count') : 0) ?? 0)\n // A corrupt or negative counter must not read as headroom.\n const used =\n Number.isFinite(rawUsed) && rawUsed > 0 ? Math.floor(rawUsed) : 0\n if (used + count > ceiling) {\n // A refused claim writes NOTHING. A campaign that will be retried\n // next hour must not have spent budget on being told no.\n return {\n allowed: false,\n used,\n ceiling,\n remaining: Math.max(0, ceiling - used),\n retryAtMs,\n degraded: false,\n }\n }\n // An absolute write from this transaction's own read, not\n // `FieldValue.increment` — the read is the authority for the decision,\n // and an increment would be atomic on the number while proving nothing\n // about the value the decision was made from.\n tx.set(\n ref,\n {\n count: used + count,\n windowStartMs,\n orgId: options.orgId,\n // NOT `lastAtMs` — the rate-limiter health probe queries this\n // collection on that field and a per-hour document in its range\n // would compete with the markers it exists to find.\n sentAtMs: now,\n expiresAt: new Date(retryAtMs + EMAIL_SEND_RATE_WINDOW_MS),\n },\n { merge: true },\n )\n return granted(used, false)\n })\n } catch (error) {\n console.error('[send-rate] org window unavailable — allowing', error)\n return granted(0, true)\n }\n}\n\n/**\n * The document the console writes when staff move the ramp.\n *\n * Returned rather than written so the route owns the write (and the audit row\n * beside it), and so a spec can assert the SHAPE without a Firestore. The one\n * thing it must never contain is `expiresAt`.\n */\nexport function emailSendRateConfigWrite(input: {\n perHour: number\n enabled: boolean\n actorEmail?: string | null\n note?: string\n now?: number\n}): Partial<EmailSendRateConfig> {\n const normalized = normalizeEmailSendRateConfig({\n perHour: input.perHour,\n enabled: input.enabled,\n note: input.note,\n updatedAtMs: input.now ?? Date.now(),\n updatedByEmail: input.actorEmail ?? null,\n })\n return {\n perHour: normalized.perHour,\n enabled: normalized.enabled,\n note: normalized.note,\n updatedAtMs: normalized.updatedAtMs,\n updatedByEmail: normalized.updatedByEmail,\n }\n}\n\n/**\n * Puts the durable governor on `sendEmail`'s path.\n *\n * **Called at module load**, from the bottom of this file, so that importing\n * `@aglyn/tenant-data-admin` anywhere is enough — every server surface in the\n * product already imports that barrel, and `export *` forces this module to\n * evaluate. The alternative, an `installEmailSendGovernor()` call at each\n * server entrypoint, is the 37-places-to-remember shape this codebase already\n * rejected once for the `context` tag.\n *\n * Idempotent: installing twice replaces the same closure with an equivalent\n * one, and the closure holds no state — the state is the Firestore document.\n */\nexport function installEmailSendGovernor(): void {\n setEmailSendGovernor(\n async (\n request: EmailSendGovernorRequest,\n ): Promise<EmailSendGovernorVerdict> => {\n const result = await consumeEmailSendBudget({\n priority: request.priority,\n count: request.count,\n })\n return {\n allowed: result.allowed,\n ceiling: result.ceiling,\n used: result.used,\n remaining: result.remaining,\n retryAtMs: result.retryAtMs,\n degraded: result.degraded,\n }\n },\n )\n}\n\ninstallEmailSendGovernor()\n"],"names":["EMAIL_SEND_RATE_WINDOW_MS","emailSendRateVerdict","emailSendRateWindowStartMs","normalizeEmailSendRateConfig","orgHourlyCampaignCeiling","setEmailSendGovernor","firebaseAdmin","RATE_LIMIT_COLLECTION","EMAIL_SEND_RATE_CONFIG_DOC","EMAIL_SEND_RATE_WINDOW_PREFIX","emailSendRateWindowDocId","windowStartMs","CONFIG_TTL_MS","configCache","configPending","invalidateEmailSendRateConfigCache","undefined","readEmailSendRateConfig","options","now","Date","firestore","at","config","load","app","snapshot","collection","doc","get","exists","data","then","finally","consumeEmailSendBudget","resetMs","fallback","priority","used","count","ceiling","perHour","enabled","degraded","ref","verdict","runTransaction","tx","Number","decision","allowed","set","Math","max","floor","sentAtMs","expiresAt","merge","error","console","readEmailSendRateWindow","isFinite","EMAIL_ORG_SEND_RATE_WINDOW_PREFIX","emailOrgSendRateWindowDocId","orgId","readOrgEmailSendWindow","claimOrgEmailSendBudget","retryAtMs","platformPerHour","granted","remaining","rawUsed","emailSendRateConfigWrite","input","normalized","note","updatedAtMs","updatedByEmail","actorEmail","installEmailSendGovernor","request","result"],"mappings":"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"}
|