@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,144 @@
|
|
|
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 { AglynOrgBilling } from '@aglyn/aglyn/foundation';
|
|
18
|
+
import { type CrmActivityLink, type CrmEmailQuotaResult, type CrmRecordsQuotaResult } from '@aglyn/aglyn/server';
|
|
19
|
+
import { type EmailRampVerdict } from '@aglyn/shared-util-email';
|
|
20
|
+
/**
|
|
21
|
+
* The CRM RECORDS band's measurement (AGL-2611): the three collections it
|
|
22
|
+
* counts, and their sum.
|
|
23
|
+
*
|
|
24
|
+
* One shape for every server that asks, because the band is enforced at six
|
|
25
|
+
* doors — contact capture, the console's company and deal drawers (through
|
|
26
|
+
* the client twin of this read), the CSV import, the lead conversion and the
|
|
27
|
+
* two REST creates — and the monthly rollup bills from the same three
|
|
28
|
+
* aggregates. A door that counted contacts alone would admit a company on a
|
|
29
|
+
* band the rollup had already found full.
|
|
30
|
+
*/
|
|
31
|
+
export interface CrmRecordsCount {
|
|
32
|
+
contactsCount: number;
|
|
33
|
+
companiesCount: number;
|
|
34
|
+
dealsCount: number;
|
|
35
|
+
/** The band's own figure: the three above, summed. */
|
|
36
|
+
crmRecordsCount: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Count the records band for one org: three `count()` aggregates, in
|
|
40
|
+
* parallel, no document reads.
|
|
41
|
+
*
|
|
42
|
+
* `contacts` may be handed in by a caller that already holds the collection
|
|
43
|
+
* reference — `upsertHostContact` resolves it through `orgDataCollection`
|
|
44
|
+
* before it decides anything — so the read is the same one whichever door
|
|
45
|
+
* it came through, and the org reference is needed only for the two
|
|
46
|
+
* collections the hub added.
|
|
47
|
+
*/
|
|
48
|
+
export declare function countCrmRecords(orgRef: FirebaseFirestore.DocumentReference, contacts?: FirebaseFirestore.CollectionReference): Promise<CrmRecordsCount>;
|
|
49
|
+
/**
|
|
50
|
+
* The band's verdict for one org, measured now — the read every create
|
|
51
|
+
* door takes before it writes, so the six doors cannot disagree about
|
|
52
|
+
* whether there is room. `checkCrmRecordsQuota` answers from the org doc
|
|
53
|
+
* alone; this pairs it with the measurement.
|
|
54
|
+
*/
|
|
55
|
+
export declare function crmRecordsQuotaForOrg(org: Partial<AglynOrgBilling> | null | undefined, orgRef: FirebaseFirestore.DocumentReference, contacts?: FirebaseFirestore.CollectionReference): Promise<CrmRecordsQuotaResult & CrmRecordsCount>;
|
|
56
|
+
/**
|
|
57
|
+
* How many activities one record already carries — one aggregate on a
|
|
58
|
+
* single-field equality, which Firestore indexes on its own, so this needs
|
|
59
|
+
* no composite index and cannot go FAILED_PRECONDITION in production.
|
|
60
|
+
*/
|
|
61
|
+
export declare function countCrmActivitiesForRecord(orgRef: FirebaseFirestore.DocumentReference, link: CrmActivityLink): Promise<number>;
|
|
62
|
+
/** Today's counter document — {@link crmEmailUsageRefForDay} keyed by `now`. */
|
|
63
|
+
export declare function crmEmailUsageRef(firestore: FirebaseFirestore.Firestore, orgId: string, now?: Date): FirebaseFirestore.DocumentReference;
|
|
64
|
+
/**
|
|
65
|
+
* How many one-to-one emails the org has sent today, for a meter — never
|
|
66
|
+
* for the cap, which {@link reserveCrmEmailSend} judges inside the
|
|
67
|
+
* transaction that also takes the slot.
|
|
68
|
+
*/
|
|
69
|
+
export declare function crmEmailsSentToday(firestore: FirebaseFirestore.Firestore, orgId: string, now?: Date): Promise<number>;
|
|
70
|
+
/** One slot on one day's counter, held from the reservation to the send. */
|
|
71
|
+
export interface CrmEmailSendReservation {
|
|
72
|
+
orgId: string;
|
|
73
|
+
/**
|
|
74
|
+
* The UTC day the slot was taken on, so a release after midnight gives
|
|
75
|
+
* the slot back to the day that lent it rather than to the new day.
|
|
76
|
+
*/
|
|
77
|
+
day: string;
|
|
78
|
+
}
|
|
79
|
+
export type ReserveCrmEmailSendResult = {
|
|
80
|
+
ok: true;
|
|
81
|
+
reservation: CrmEmailSendReservation;
|
|
82
|
+
quota: CrmEmailQuotaResult;
|
|
83
|
+
} | {
|
|
84
|
+
ok: false;
|
|
85
|
+
quota: CrmEmailQuotaResult;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Claims ONE one-to-one send against today's cap, ATOMICALLY (AGL-2645).
|
|
89
|
+
*
|
|
90
|
+
* The counter used to be read before the send and incremented after
|
|
91
|
+
* delivery, so two reps sending in the same window both passed the same
|
|
92
|
+
* reading and both sent — `CRM_EMAIL_SENDS_PER_MINUTE` past the cap per
|
|
93
|
+
* sender, in exactly the conditions the cap exists for. **A read-then-write
|
|
94
|
+
* cap is not a cap**, the lesson `reserveCampaignEmailSends` learned for
|
|
95
|
+
* campaigns; this is the same shape on the day counter.
|
|
96
|
+
*
|
|
97
|
+
* The transaction reads the counter, judges it with `checkCrmEmailQuota` —
|
|
98
|
+
* the real plan table, `UNLIMITED` on Enterprise, zero on a tier with no
|
|
99
|
+
* suite — and writes an ABSOLUTE value derived from that read, deliberately
|
|
100
|
+
* not `FieldValue.increment`. Firestore aborts and re-runs the callback
|
|
101
|
+
* when a document the transaction read has moved, so a second sender that
|
|
102
|
+
* starts inside the first one's window re-reads the raised figure and is
|
|
103
|
+
* refused. An increment would be atomic on the number and useless for the
|
|
104
|
+
* decision, because the decision is made from a value the write never
|
|
105
|
+
* proves it still held.
|
|
106
|
+
*
|
|
107
|
+
* A refused reservation writes NOTHING. A taken one is the count the cap
|
|
108
|
+
* enforces and the billing page shows, from the moment it is taken: a send
|
|
109
|
+
* the provider then refuses hands the slot back through
|
|
110
|
+
* {@link releaseCrmEmailSend}, and a process that dies between the two
|
|
111
|
+
* leaves the org one send short for the rest of the UTC day — conservative
|
|
112
|
+
* in the direction a cap cares about, and healed at midnight, because each
|
|
113
|
+
* day is its own document.
|
|
114
|
+
*
|
|
115
|
+
* ## The new-sender ramp rides the same read
|
|
116
|
+
*
|
|
117
|
+
* `ramp` narrows the plan's day to the share of it a young workspace has
|
|
118
|
+
* earned, INSIDE the transaction and before the counter is judged — see
|
|
119
|
+
* {@link rampedCrmEmailQuota}. Applying it here rather than in the caller is
|
|
120
|
+
* the whole of what makes it safe: the ceiling and the count it is compared
|
|
121
|
+
* against come out of one read, so the retry Firestore forces on a losing
|
|
122
|
+
* transaction re-derives both. A ramp checked outside would be a second
|
|
123
|
+
* ceiling, decided from a snapshot the write never proves it still held —
|
|
124
|
+
* the defect this reservation was built to remove, one field over.
|
|
125
|
+
*
|
|
126
|
+
* The ramp is optional and an absent one is not a throttle: a caller that
|
|
127
|
+
* did not resolve one gets the plan's day, which is what every surface but
|
|
128
|
+
* the send path wants.
|
|
129
|
+
*/
|
|
130
|
+
export declare function reserveCrmEmailSend(firestore: FirebaseFirestore.Firestore, orgId: string, org: Partial<AglynOrgBilling> | null | undefined, now?: Date, ramp?: EmailRampVerdict | null): Promise<ReserveCrmEmailSendResult>;
|
|
131
|
+
/**
|
|
132
|
+
* Gives back a slot the provider did not use.
|
|
133
|
+
*
|
|
134
|
+
* Also a transaction, and also an absolute write from its own read: a
|
|
135
|
+
* decrement computed from a stale figure would undo a slot another rep took
|
|
136
|
+
* in the meantime, which is the same defect one direction over. Never
|
|
137
|
+
* drives the counter below zero.
|
|
138
|
+
*
|
|
139
|
+
* **Never throws.** This runs on the failure path of a send that was
|
|
140
|
+
* already refused, and the answer the rep gets is the provider's refusal; a
|
|
141
|
+
* bookkeeping failure on top of it is logged, and its cost is one slot the
|
|
142
|
+
* org does not get back until midnight — the safe direction.
|
|
143
|
+
*/
|
|
144
|
+
export declare function releaseCrmEmailSend(firestore: FirebaseFirestore.Firestore, reservation: CrmEmailSendReservation | null | undefined): Promise<void>;
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { checkCrmEmailQuota, checkCrmRecordsQuota, CRM_COLLECTIONS, CRM_EMAIL_USAGE_COLLECTION, crmActivityCeilingLink, crmEmailUsageDayKey } from "@aglyn/aglyn/server";
|
|
18
|
+
import { rampedDailyAllowance } from "@aglyn/shared-util-email";
|
|
19
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
20
|
+
/**
|
|
21
|
+
* Count the records band for one org: three `count()` aggregates, in
|
|
22
|
+
* parallel, no document reads.
|
|
23
|
+
*
|
|
24
|
+
* `contacts` may be handed in by a caller that already holds the collection
|
|
25
|
+
* reference — `upsertHostContact` resolves it through `orgDataCollection`
|
|
26
|
+
* before it decides anything — so the read is the same one whichever door
|
|
27
|
+
* it came through, and the org reference is needed only for the two
|
|
28
|
+
* collections the hub added.
|
|
29
|
+
*/ export async function countCrmRecords(orgRef, contacts = orgRef.collection('contacts')) {
|
|
30
|
+
const [contactsSnap, companiesSnap, dealsSnap] = await Promise.all([
|
|
31
|
+
contacts.count().get(),
|
|
32
|
+
orgRef.collection(CRM_COLLECTIONS.companies).count().get(),
|
|
33
|
+
orgRef.collection(CRM_COLLECTIONS.deals).count().get()
|
|
34
|
+
]);
|
|
35
|
+
const contactsCount = aggregateCount(contactsSnap);
|
|
36
|
+
const companiesCount = aggregateCount(companiesSnap);
|
|
37
|
+
const dealsCount = aggregateCount(dealsSnap);
|
|
38
|
+
return {
|
|
39
|
+
contactsCount,
|
|
40
|
+
companiesCount,
|
|
41
|
+
dealsCount,
|
|
42
|
+
crmRecordsCount: contactsCount + companiesCount + dealsCount
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The band's verdict for one org, measured now — the read every create
|
|
47
|
+
* door takes before it writes, so the six doors cannot disagree about
|
|
48
|
+
* whether there is room. `checkCrmRecordsQuota` answers from the org doc
|
|
49
|
+
* alone; this pairs it with the measurement.
|
|
50
|
+
*/ export async function crmRecordsQuotaForOrg(org, orgRef, contacts) {
|
|
51
|
+
const counts = await countCrmRecords(orgRef, contacts);
|
|
52
|
+
return _extends({}, checkCrmRecordsQuota(org, counts.crmRecordsCount), counts);
|
|
53
|
+
}
|
|
54
|
+
/** `count()` answers through `.data().count`; a missing figure is zero, never NaN. */ function aggregateCount(snapshot) {
|
|
55
|
+
var _ref;
|
|
56
|
+
var _snapshot_data;
|
|
57
|
+
const value = Number((_ref = (_snapshot_data = snapshot.data()) == null ? void 0 : _snapshot_data.count) != null ? _ref : 0);
|
|
58
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* How many activities one record already carries — one aggregate on a
|
|
62
|
+
* single-field equality, which Firestore indexes on its own, so this needs
|
|
63
|
+
* no composite index and cannot go FAILED_PRECONDITION in production.
|
|
64
|
+
*/ export async function countCrmActivitiesForRecord(orgRef, link) {
|
|
65
|
+
const lead = crmActivityCeilingLink(link);
|
|
66
|
+
if (!lead) return 0;
|
|
67
|
+
const snapshot = await orgRef.collection(CRM_COLLECTIONS.activities).where(lead.field, '==', lead.id).count().get();
|
|
68
|
+
return aggregateCount(snapshot);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `orgs/{orgId}/crmEmailUsage/{day}` — one UTC day's one-to-one email
|
|
72
|
+
* counter, the document `checkCrmEmailQuota` is enforced against and the
|
|
73
|
+
* billing page reads.
|
|
74
|
+
*/ function crmEmailUsageRefForDay(firestore, orgId, day) {
|
|
75
|
+
return firestore.collection('orgs').doc(orgId).collection(CRM_EMAIL_USAGE_COLLECTION).doc(day);
|
|
76
|
+
}
|
|
77
|
+
/** Today's counter document — {@link crmEmailUsageRefForDay} keyed by `now`. */ export function crmEmailUsageRef(firestore, orgId, now = new Date()) {
|
|
78
|
+
return crmEmailUsageRefForDay(firestore, orgId, crmEmailUsageDayKey(now));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The counter a day-document holds, clamped.
|
|
82
|
+
*
|
|
83
|
+
* A malformed or absent counter reads as ZERO SENT, which is the permissive
|
|
84
|
+
* direction — deliberately, and the same clamp `checkApiRequestQuota`'s
|
|
85
|
+
* reader applies: a counter that cannot be read must not refuse a rep who
|
|
86
|
+
* has sent nothing, and the cap is a pace rather than a cost.
|
|
87
|
+
*/ function crmEmailCount(snapshot) {
|
|
88
|
+
var _snapshot_get;
|
|
89
|
+
const value = Number((_snapshot_get = snapshot.get('count')) != null ? _snapshot_get : 0);
|
|
90
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* How many one-to-one emails the org has sent today, for a meter — never
|
|
94
|
+
* for the cap, which {@link reserveCrmEmailSend} judges inside the
|
|
95
|
+
* transaction that also takes the slot.
|
|
96
|
+
*/ export async function crmEmailsSentToday(firestore, orgId, now = new Date()) {
|
|
97
|
+
return crmEmailCount(await crmEmailUsageRef(firestore, orgId, now).get());
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The plan's day, narrowed to the share of it a young workspace has earned.
|
|
101
|
+
*
|
|
102
|
+
* The new-sender ramp reaches the one-to-one path here and only here, so the
|
|
103
|
+
* ceiling the transaction below judges against is one value with one origin.
|
|
104
|
+
* The alternative — a full-allowance reservation with a ramp consulted beside
|
|
105
|
+
* it — is two ceilings for one decision, and under two concurrent senders it
|
|
106
|
+
* admits against whichever of them was read first.
|
|
107
|
+
*
|
|
108
|
+
* `included` is the only field the ramp moves; `resetsAt` is the day's, which
|
|
109
|
+
* a share does not change, and `used` is what the transaction read.
|
|
110
|
+
*/ function rampedCrmEmailQuota(plan, ramp) {
|
|
111
|
+
const included = rampedDailyAllowance(plan.included, ramp);
|
|
112
|
+
if (included >= plan.included) return plan;
|
|
113
|
+
return _extends({}, plan, {
|
|
114
|
+
included,
|
|
115
|
+
allowed: plan.used < included,
|
|
116
|
+
remaining: Math.max(0, included - plan.used)
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Claims ONE one-to-one send against today's cap, ATOMICALLY (AGL-2645).
|
|
121
|
+
*
|
|
122
|
+
* The counter used to be read before the send and incremented after
|
|
123
|
+
* delivery, so two reps sending in the same window both passed the same
|
|
124
|
+
* reading and both sent — `CRM_EMAIL_SENDS_PER_MINUTE` past the cap per
|
|
125
|
+
* sender, in exactly the conditions the cap exists for. **A read-then-write
|
|
126
|
+
* cap is not a cap**, the lesson `reserveCampaignEmailSends` learned for
|
|
127
|
+
* campaigns; this is the same shape on the day counter.
|
|
128
|
+
*
|
|
129
|
+
* The transaction reads the counter, judges it with `checkCrmEmailQuota` —
|
|
130
|
+
* the real plan table, `UNLIMITED` on Enterprise, zero on a tier with no
|
|
131
|
+
* suite — and writes an ABSOLUTE value derived from that read, deliberately
|
|
132
|
+
* not `FieldValue.increment`. Firestore aborts and re-runs the callback
|
|
133
|
+
* when a document the transaction read has moved, so a second sender that
|
|
134
|
+
* starts inside the first one's window re-reads the raised figure and is
|
|
135
|
+
* refused. An increment would be atomic on the number and useless for the
|
|
136
|
+
* decision, because the decision is made from a value the write never
|
|
137
|
+
* proves it still held.
|
|
138
|
+
*
|
|
139
|
+
* A refused reservation writes NOTHING. A taken one is the count the cap
|
|
140
|
+
* enforces and the billing page shows, from the moment it is taken: a send
|
|
141
|
+
* the provider then refuses hands the slot back through
|
|
142
|
+
* {@link releaseCrmEmailSend}, and a process that dies between the two
|
|
143
|
+
* leaves the org one send short for the rest of the UTC day — conservative
|
|
144
|
+
* in the direction a cap cares about, and healed at midnight, because each
|
|
145
|
+
* day is its own document.
|
|
146
|
+
*
|
|
147
|
+
* ## The new-sender ramp rides the same read
|
|
148
|
+
*
|
|
149
|
+
* `ramp` narrows the plan's day to the share of it a young workspace has
|
|
150
|
+
* earned, INSIDE the transaction and before the counter is judged — see
|
|
151
|
+
* {@link rampedCrmEmailQuota}. Applying it here rather than in the caller is
|
|
152
|
+
* the whole of what makes it safe: the ceiling and the count it is compared
|
|
153
|
+
* against come out of one read, so the retry Firestore forces on a losing
|
|
154
|
+
* transaction re-derives both. A ramp checked outside would be a second
|
|
155
|
+
* ceiling, decided from a snapshot the write never proves it still held —
|
|
156
|
+
* the defect this reservation was built to remove, one field over.
|
|
157
|
+
*
|
|
158
|
+
* The ramp is optional and an absent one is not a throttle: a caller that
|
|
159
|
+
* did not resolve one gets the plan's day, which is what every surface but
|
|
160
|
+
* the send path wants.
|
|
161
|
+
*/ export async function reserveCrmEmailSend(firestore, orgId, org, now = new Date(), ramp) {
|
|
162
|
+
const day = crmEmailUsageDayKey(now);
|
|
163
|
+
const ref = crmEmailUsageRefForDay(firestore, orgId, day);
|
|
164
|
+
return firestore.runTransaction(async (tx)=>{
|
|
165
|
+
const quota = rampedCrmEmailQuota(checkCrmEmailQuota(org, crmEmailCount(await tx.get(ref)), now), ramp);
|
|
166
|
+
if (!quota.allowed) return {
|
|
167
|
+
ok: false,
|
|
168
|
+
quota
|
|
169
|
+
};
|
|
170
|
+
tx.set(ref, {
|
|
171
|
+
count: quota.used + 1,
|
|
172
|
+
day,
|
|
173
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
174
|
+
}, {
|
|
175
|
+
merge: true
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
reservation: {
|
|
180
|
+
orgId,
|
|
181
|
+
day
|
|
182
|
+
},
|
|
183
|
+
quota
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Gives back a slot the provider did not use.
|
|
189
|
+
*
|
|
190
|
+
* Also a transaction, and also an absolute write from its own read: a
|
|
191
|
+
* decrement computed from a stale figure would undo a slot another rep took
|
|
192
|
+
* in the meantime, which is the same defect one direction over. Never
|
|
193
|
+
* drives the counter below zero.
|
|
194
|
+
*
|
|
195
|
+
* **Never throws.** This runs on the failure path of a send that was
|
|
196
|
+
* already refused, and the answer the rep gets is the provider's refusal; a
|
|
197
|
+
* bookkeeping failure on top of it is logged, and its cost is one slot the
|
|
198
|
+
* org does not get back until midnight — the safe direction.
|
|
199
|
+
*/ export async function releaseCrmEmailSend(firestore, reservation) {
|
|
200
|
+
if (!(reservation == null ? void 0 : reservation.orgId) || !reservation.day) return;
|
|
201
|
+
const ref = crmEmailUsageRefForDay(firestore, reservation.orgId, reservation.day);
|
|
202
|
+
try {
|
|
203
|
+
await firestore.runTransaction(async (tx)=>{
|
|
204
|
+
const used = crmEmailCount(await tx.get(ref));
|
|
205
|
+
tx.set(ref, {
|
|
206
|
+
count: Math.max(0, used - 1),
|
|
207
|
+
day: reservation.day,
|
|
208
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
209
|
+
}, {
|
|
210
|
+
merge: true
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('crm email reservation release failed', error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//# sourceMappingURL=crm-records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/crm-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation'\nimport {\n checkCrmEmailQuota,\n checkCrmRecordsQuota,\n CRM_COLLECTIONS,\n CRM_EMAIL_USAGE_COLLECTION,\n type CrmActivityLink,\n crmActivityCeilingLink,\n type CrmEmailQuotaResult,\n crmEmailUsageDayKey,\n type CrmRecordsQuotaResult,\n} from '@aglyn/aglyn/server'\nimport {\n type EmailRampVerdict,\n rampedDailyAllowance,\n} from '@aglyn/shared-util-email'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * The CRM RECORDS band's measurement (AGL-2611): the three collections it\n * counts, and their sum.\n *\n * One shape for every server that asks, because the band is enforced at six\n * doors — contact capture, the console's company and deal drawers (through\n * the client twin of this read), the CSV import, the lead conversion and the\n * two REST creates — and the monthly rollup bills from the same three\n * aggregates. A door that counted contacts alone would admit a company on a\n * band the rollup had already found full.\n */\nexport interface CrmRecordsCount {\n contactsCount: number\n companiesCount: number\n dealsCount: number\n /** The band's own figure: the three above, summed. */\n crmRecordsCount: number\n}\n\n/**\n * Count the records band for one org: three `count()` aggregates, in\n * parallel, no document reads.\n *\n * `contacts` may be handed in by a caller that already holds the collection\n * reference — `upsertHostContact` resolves it through `orgDataCollection`\n * before it decides anything — so the read is the same one whichever door\n * it came through, and the org reference is needed only for the two\n * collections the hub added.\n */\nexport async function countCrmRecords(\n orgRef: FirebaseFirestore.DocumentReference,\n contacts: FirebaseFirestore.CollectionReference = orgRef.collection('contacts'),\n): Promise<CrmRecordsCount> {\n const [contactsSnap, companiesSnap, dealsSnap] = await Promise.all([\n contacts.count().get(),\n orgRef.collection(CRM_COLLECTIONS.companies).count().get(),\n orgRef.collection(CRM_COLLECTIONS.deals).count().get(),\n ])\n const contactsCount = aggregateCount(contactsSnap)\n const companiesCount = aggregateCount(companiesSnap)\n const dealsCount = aggregateCount(dealsSnap)\n return {\n contactsCount,\n companiesCount,\n dealsCount,\n crmRecordsCount: contactsCount + companiesCount + dealsCount,\n }\n}\n\n/**\n * The band's verdict for one org, measured now — the read every create\n * door takes before it writes, so the six doors cannot disagree about\n * whether there is room. `checkCrmRecordsQuota` answers from the org doc\n * alone; this pairs it with the measurement.\n */\nexport async function crmRecordsQuotaForOrg(\n org: Partial<AglynOrgBilling> | null | undefined,\n orgRef: FirebaseFirestore.DocumentReference,\n contacts?: FirebaseFirestore.CollectionReference,\n): Promise<CrmRecordsQuotaResult & CrmRecordsCount> {\n const counts = await countCrmRecords(orgRef, contacts)\n return { ...checkCrmRecordsQuota(org, counts.crmRecordsCount), ...counts }\n}\n\n/** `count()` answers through `.data().count`; a missing figure is zero, never NaN. */\nfunction aggregateCount(snapshot: { data(): { count?: unknown } }): number {\n const value = Number(snapshot.data()?.count ?? 0)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\n/**\n * How many activities one record already carries — one aggregate on a\n * single-field equality, which Firestore indexes on its own, so this needs\n * no composite index and cannot go FAILED_PRECONDITION in production.\n */\nexport async function countCrmActivitiesForRecord(\n orgRef: FirebaseFirestore.DocumentReference,\n link: CrmActivityLink,\n): Promise<number> {\n const lead = crmActivityCeilingLink(link)\n if (!lead) return 0\n const snapshot = await orgRef\n .collection(CRM_COLLECTIONS.activities)\n .where(lead.field, '==', lead.id)\n .count()\n .get()\n return aggregateCount(snapshot)\n}\n\n/**\n * `orgs/{orgId}/crmEmailUsage/{day}` — one UTC day's one-to-one email\n * counter, the document `checkCrmEmailQuota` is enforced against and the\n * billing page reads.\n */\nfunction crmEmailUsageRefForDay(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n day: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(CRM_EMAIL_USAGE_COLLECTION)\n .doc(day)\n}\n\n/** Today's counter document — {@link crmEmailUsageRefForDay} keyed by `now`. */\nexport function crmEmailUsageRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n now: Date = new Date(),\n): FirebaseFirestore.DocumentReference {\n return crmEmailUsageRefForDay(firestore, orgId, crmEmailUsageDayKey(now))\n}\n\n/**\n * The counter a day-document holds, clamped.\n *\n * A malformed or absent counter reads as ZERO SENT, which is the permissive\n * direction — deliberately, and the same clamp `checkApiRequestQuota`'s\n * reader applies: a counter that cannot be read must not refuse a rep who\n * has sent nothing, and the cap is a pace rather than a cost.\n */\nfunction crmEmailCount(snapshot: { get(field: string): unknown }): number {\n const value = Number(snapshot.get('count') ?? 0)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\n/**\n * How many one-to-one emails the org has sent today, for a meter — never\n * for the cap, which {@link reserveCrmEmailSend} judges inside the\n * transaction that also takes the slot.\n */\nexport async function crmEmailsSentToday(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n now: Date = new Date(),\n): Promise<number> {\n return crmEmailCount(await crmEmailUsageRef(firestore, orgId, now).get())\n}\n\n/** One slot on one day's counter, held from the reservation to the send. */\nexport interface CrmEmailSendReservation {\n orgId: string\n /**\n * The UTC day the slot was taken on, so a release after midnight gives\n * the slot back to the day that lent it rather than to the new day.\n */\n day: string\n}\n\nexport type ReserveCrmEmailSendResult =\n | { ok: true; reservation: CrmEmailSendReservation; quota: CrmEmailQuotaResult }\n | { ok: false; quota: CrmEmailQuotaResult }\n\n/**\n * The plan's day, narrowed to the share of it a young workspace has earned.\n *\n * The new-sender ramp reaches the one-to-one path here and only here, so the\n * ceiling the transaction below judges against is one value with one origin.\n * The alternative — a full-allowance reservation with a ramp consulted beside\n * it — is two ceilings for one decision, and under two concurrent senders it\n * admits against whichever of them was read first.\n *\n * `included` is the only field the ramp moves; `resetsAt` is the day's, which\n * a share does not change, and `used` is what the transaction read.\n */\nfunction rampedCrmEmailQuota(\n plan: CrmEmailQuotaResult,\n ramp: EmailRampVerdict | null | undefined,\n): CrmEmailQuotaResult {\n const included = rampedDailyAllowance(plan.included, ramp)\n if (included >= plan.included) return plan\n return {\n ...plan,\n included,\n allowed: plan.used < included,\n remaining: Math.max(0, included - plan.used),\n }\n}\n\n/**\n * Claims ONE one-to-one send against today's cap, ATOMICALLY (AGL-2645).\n *\n * The counter used to be read before the send and incremented after\n * delivery, so two reps sending in the same window both passed the same\n * reading and both sent — `CRM_EMAIL_SENDS_PER_MINUTE` past the cap per\n * sender, in exactly the conditions the cap exists for. **A read-then-write\n * cap is not a cap**, the lesson `reserveCampaignEmailSends` learned for\n * campaigns; this is the same shape on the day counter.\n *\n * The transaction reads the counter, judges it with `checkCrmEmailQuota` —\n * the real plan table, `UNLIMITED` on Enterprise, zero on a tier with no\n * suite — and writes an ABSOLUTE value derived from that read, deliberately\n * not `FieldValue.increment`. Firestore aborts and re-runs the callback\n * when a document the transaction read has moved, so a second sender that\n * starts inside the first one's window re-reads the raised figure and is\n * refused. An increment would be atomic on the number and useless for the\n * decision, because the decision is made from a value the write never\n * proves it still held.\n *\n * A refused reservation writes NOTHING. A taken one is the count the cap\n * enforces and the billing page shows, from the moment it is taken: a send\n * the provider then refuses hands the slot back through\n * {@link releaseCrmEmailSend}, and a process that dies between the two\n * leaves the org one send short for the rest of the UTC day — conservative\n * in the direction a cap cares about, and healed at midnight, because each\n * day is its own document.\n *\n * ## The new-sender ramp rides the same read\n *\n * `ramp` narrows the plan's day to the share of it a young workspace has\n * earned, INSIDE the transaction and before the counter is judged — see\n * {@link rampedCrmEmailQuota}. Applying it here rather than in the caller is\n * the whole of what makes it safe: the ceiling and the count it is compared\n * against come out of one read, so the retry Firestore forces on a losing\n * transaction re-derives both. A ramp checked outside would be a second\n * ceiling, decided from a snapshot the write never proves it still held —\n * the defect this reservation was built to remove, one field over.\n *\n * The ramp is optional and an absent one is not a throttle: a caller that\n * did not resolve one gets the plan's day, which is what every surface but\n * the send path wants.\n */\nexport async function reserveCrmEmailSend(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n org: Partial<AglynOrgBilling> | null | undefined,\n now: Date = new Date(),\n ramp?: EmailRampVerdict | null,\n): Promise<ReserveCrmEmailSendResult> {\n const day = crmEmailUsageDayKey(now)\n const ref = crmEmailUsageRefForDay(firestore, orgId, day)\n return firestore.runTransaction(async (tx) => {\n const quota = rampedCrmEmailQuota(\n checkCrmEmailQuota(org, crmEmailCount(await tx.get(ref)), now),\n ramp,\n )\n if (!quota.allowed) return { ok: false, quota }\n tx.set(\n ref,\n { count: quota.used + 1, day, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n return { ok: true, reservation: { orgId, day }, quota }\n })\n}\n\n/**\n * Gives back a slot the provider did not use.\n *\n * Also a transaction, and also an absolute write from its own read: a\n * decrement computed from a stale figure would undo a slot another rep took\n * in the meantime, which is the same defect one direction over. Never\n * drives the counter below zero.\n *\n * **Never throws.** This runs on the failure path of a send that was\n * already refused, and the answer the rep gets is the provider's refusal; a\n * bookkeeping failure on top of it is logged, and its cost is one slot the\n * org does not get back until midnight — the safe direction.\n */\nexport async function releaseCrmEmailSend(\n firestore: FirebaseFirestore.Firestore,\n reservation: CrmEmailSendReservation | null | undefined,\n): Promise<void> {\n if (!reservation?.orgId || !reservation.day) return\n const ref = crmEmailUsageRefForDay(firestore, reservation.orgId, reservation.day)\n try {\n await firestore.runTransaction(async (tx) => {\n const used = crmEmailCount(await tx.get(ref))\n tx.set(\n ref,\n {\n count: Math.max(0, used - 1),\n day: reservation.day,\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n })\n } catch (error) {\n console.error('crm email reservation release failed', error)\n }\n}\n"],"names":["checkCrmEmailQuota","checkCrmRecordsQuota","CRM_COLLECTIONS","CRM_EMAIL_USAGE_COLLECTION","crmActivityCeilingLink","crmEmailUsageDayKey","rampedDailyAllowance","FieldValue","countCrmRecords","orgRef","contacts","collection","contactsSnap","companiesSnap","dealsSnap","Promise","all","count","get","companies","deals","contactsCount","aggregateCount","companiesCount","dealsCount","crmRecordsCount","crmRecordsQuotaForOrg","org","counts","snapshot","value","Number","data","isFinite","Math","floor","countCrmActivitiesForRecord","link","lead","activities","where","field","id","crmEmailUsageRefForDay","firestore","orgId","day","doc","crmEmailUsageRef","now","Date","crmEmailCount","crmEmailsSentToday","rampedCrmEmailQuota","plan","ramp","included","allowed","used","remaining","max","reserveCrmEmailSend","ref","runTransaction","tx","quota","ok","set","updatedAt","serverTimestamp","merge","reservation","releaseCrmEmailSend","error","console"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { CspViolation } from '@aglyn/aglyn/app-utils/csp-report';
|
|
18
|
+
/** Top-level collection of counter documents. Server-only; no rules match. */
|
|
19
|
+
export declare const CSP_AGGREGATE_COLLECTION = "cspViolationDaily";
|
|
20
|
+
/**
|
|
21
|
+
* Long enough that "a week of traffic" (AGL-1702 condition 1, AGL-1726
|
|
22
|
+
* condition 3) is readable with room to be read late, short enough that the
|
|
23
|
+
* collection stays a working set rather than an archive.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CSP_AGGREGATE_RETENTION_DAYS = 60;
|
|
26
|
+
/**
|
|
27
|
+
* Distinct blocked origins that may mint documents per (day x app x
|
|
28
|
+
* directive), per instance. A legitimate policy violates against a handful of
|
|
29
|
+
* hosts; thirty is generous headroom for a real regression while keeping a
|
|
30
|
+
* flood of attacker-invented origins from minting unbounded documents.
|
|
31
|
+
*/
|
|
32
|
+
export declare const MAX_DISTINCT_ORIGINS_PER_DAY = 30;
|
|
33
|
+
/** Test seam: module-scope state would otherwise leak across cases. */
|
|
34
|
+
export declare function resetCspAggregateStateForTests(): void;
|
|
35
|
+
/**
|
|
36
|
+
* `blocked-uri` reduced to the coarsest thing worth counting: the HOST for a
|
|
37
|
+
* URL, the bare keyword (`inline`, `eval`, `data`, ...) otherwise. Paths are
|
|
38
|
+
* deliberately dropped — twelve chunk URLs off one CDN are one origin-level
|
|
39
|
+
* fact (the AGL-1779 shape), and a path would both explode the key space and
|
|
40
|
+
* hand the id to whoever controls the URL.
|
|
41
|
+
*/
|
|
42
|
+
export declare function cspBlockedOrigin(blockedUri: string): string;
|
|
43
|
+
/** UTC day bucket, `YYYY-MM-DD`. */
|
|
44
|
+
export declare function cspAggregateDay(nowMs: number): string;
|
|
45
|
+
export interface RecordCspViolationsOptions {
|
|
46
|
+
/** Which collector this came through — part of the counter key. */
|
|
47
|
+
app: 'console' | 'tenant';
|
|
48
|
+
/**
|
|
49
|
+
* The reporting site (tenant only), REQUEST-derived and already clamped by
|
|
50
|
+
* the caller. Stored as a sample field, never in the id: distinct sites
|
|
51
|
+
* must not multiply the key space.
|
|
52
|
+
*/
|
|
53
|
+
site?: string;
|
|
54
|
+
now?: number;
|
|
55
|
+
/** Injectable for tests; defaults to the Admin SDK's Firestore. */
|
|
56
|
+
firestore?: any;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compound `violations` into the daily counters. Never throws; returns how
|
|
60
|
+
* many documents were written (0 on any failure, budget exhaustion included).
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordCspViolations(violations: readonly CspViolation[], options: RecordCspViolationsOptions): Promise<number>;
|