@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,276 @@
|
|
|
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
|
+
* Durable CSP-violation aggregates (AGL-1799).
|
|
19
|
+
*
|
|
20
|
+
* The collectors were delivering reports into a sink that forgets: the Vercel
|
|
21
|
+
* runtime log retains ~60 minutes and there is no log drain, so AGL-1702's
|
|
22
|
+
* "a week of signed-in traffic" and AGL-1726's "a business week of visitor
|
|
23
|
+
* traffic" were gated on evidence that structurally could not exist. This
|
|
24
|
+
* module is the durable half — the log lines stay exactly as they were (they
|
|
25
|
+
* are still the right tool for the last hour), and every accepted violation
|
|
26
|
+
* ALSO compounds into a Firestore counter that holds for the retention window.
|
|
27
|
+
*
|
|
28
|
+
* ## Shape: counters, never report bodies
|
|
29
|
+
*
|
|
30
|
+
* One document per (UTC day x app x directive x disposition x blocked origin),
|
|
31
|
+
* with `FieldValue.increment` — NOT one document per report. AGL-523 made a
|
|
32
|
+
* deliberate choice not to write report bodies to Firestore from an
|
|
33
|
+
* unauthenticated route, and AGL-1799 argued the distinction explicitly: "a
|
|
34
|
+
* counter with a bounded key space is a different proposition from storing
|
|
35
|
+
* report bodies". The bound is what this module is careful about, because
|
|
36
|
+
* every key ingredient except the day is attacker-influenced:
|
|
37
|
+
*
|
|
38
|
+
* - the DIRECTIVE is normalized into a closed set (a real browser can only
|
|
39
|
+
* ever send a spec directive; anything else lands in `other` rather than
|
|
40
|
+
* minting a document per invented directive);
|
|
41
|
+
* - the ORIGIN is reduced to the host of `blocked-uri` (or its bare keyword
|
|
42
|
+
* — `inline`, `eval`, `data`), sanitized to a fixed character set, and
|
|
43
|
+
* capped: at most {@link MAX_DISTINCT_ORIGINS_PER_DAY} distinct origins per
|
|
44
|
+
* (day x app x directive) may mint documents per instance per day. Beyond
|
|
45
|
+
* that, new origins are dropped while known ones keep counting — a flood
|
|
46
|
+
* of unique URLs (the AGL-1769 lesson: never let request content mint
|
|
47
|
+
* unbounded documents) saturates the cap instead of the collection;
|
|
48
|
+
* - WRITES are budgeted per instance per minute, so a POST flood cannot turn
|
|
49
|
+
* an unauthenticated beacon into a Firestore write bill even against
|
|
50
|
+
* already-minted documents.
|
|
51
|
+
*
|
|
52
|
+
* Every cap here is per instance, like the tenant collector's `keyStore`: the
|
|
53
|
+
* fleet-wide worst case is (instances x cap), which is bounded and small,
|
|
54
|
+
* and an instance recycle merely re-counts an origin against a fresh cap —
|
|
55
|
+
* the merge write is idempotent about which document it lands in.
|
|
56
|
+
*
|
|
57
|
+
* ## Ids are attacker-derived, so they go through `isDocumentId`
|
|
58
|
+
*
|
|
59
|
+
* Key parts are sanitized to `[a-z0-9.:_-]` and joined with `|`, then the
|
|
60
|
+
* whole id is checked with {@link isDocumentId} (the AGL-1771 predicate —
|
|
61
|
+
* imported from its leaf, `./document-id`, which imports nothing). Belt and
|
|
62
|
+
* braces: the sanitizer already removes `/` and the joiner makes a reserved
|
|
63
|
+
* `__x__` form impossible, but the predicate is the contract and it is
|
|
64
|
+
* cheaper to check than to argue.
|
|
65
|
+
*
|
|
66
|
+
* ## Retention is TTL, not a sweep
|
|
67
|
+
*
|
|
68
|
+
* `expiresAt` (a Timestamp — TTL refuses numbers) is set
|
|
69
|
+
* {@link CSP_AGGREGATE_RETENTION_DAYS} days past the day bucket. The TTL
|
|
70
|
+
* policy is a gcloud action plus a `fieldOverrides` entry in
|
|
71
|
+
* `cloud/firebase-firestore.indexes.json` — see
|
|
72
|
+
* docs/FIRESTORE_MANUAL_CONFIG.md (AGL-1793): the entry ships with this
|
|
73
|
+
* module and the gcloud enable is owed at deploy time.
|
|
74
|
+
*
|
|
75
|
+
* ## Fail-soft, like everything on this path
|
|
76
|
+
*
|
|
77
|
+
* A collector must never turn an observed problem into a served one. Every
|
|
78
|
+
* failure here — no credential, Firestore down, a hostile id that survives
|
|
79
|
+
* sanitization — drops the aggregate (one `console.error` line, tagged) and
|
|
80
|
+
* returns. The log lines the collectors already emit are unaffected.
|
|
81
|
+
*
|
|
82
|
+
* No client ever reads or writes this collection: there is no rules `match`
|
|
83
|
+
* for it, and Firestore denies what nothing matches, so the Admin SDK is the
|
|
84
|
+
* only path in. The staff read-back is `/api/admin/csp-reports` on the
|
|
85
|
+
* console.
|
|
86
|
+
*/ import { FieldValue } from "firebase-admin/firestore";
|
|
87
|
+
import { isDocumentId } from "./document-id.js";
|
|
88
|
+
/** Top-level collection of counter documents. Server-only; no rules match. */ export const CSP_AGGREGATE_COLLECTION = 'cspViolationDaily';
|
|
89
|
+
/**
|
|
90
|
+
* Long enough that "a week of traffic" (AGL-1702 condition 1, AGL-1726
|
|
91
|
+
* condition 3) is readable with room to be read late, short enough that the
|
|
92
|
+
* collection stays a working set rather than an archive.
|
|
93
|
+
*/ export const CSP_AGGREGATE_RETENTION_DAYS = 60;
|
|
94
|
+
/**
|
|
95
|
+
* Distinct blocked origins that may mint documents per (day x app x
|
|
96
|
+
* directive), per instance. A legitimate policy violates against a handful of
|
|
97
|
+
* hosts; thirty is generous headroom for a real regression while keeping a
|
|
98
|
+
* flood of attacker-invented origins from minting unbounded documents.
|
|
99
|
+
*/ export const MAX_DISTINCT_ORIGINS_PER_DAY = 30;
|
|
100
|
+
/**
|
|
101
|
+
* Aggregate writes allowed per instance per minute. Distinct-origin capping
|
|
102
|
+
* bounds document COUNT; this bounds write OPS, which are billed even when
|
|
103
|
+
* they land on existing documents. Sized far above what real browsers on one
|
|
104
|
+
* instance produce and far below what makes a flood expensive.
|
|
105
|
+
*/ const WRITE_BUDGET_PER_MINUTE = 120;
|
|
106
|
+
/**
|
|
107
|
+
* The directives a browser can actually report, per the CSP3 registry. An
|
|
108
|
+
* unrecognized directive is a non-browser caller (or a future spec revision)
|
|
109
|
+
* and shares one bucket rather than minting its own key space.
|
|
110
|
+
*/ const KNOWN_DIRECTIVES = new Set([
|
|
111
|
+
'base-uri',
|
|
112
|
+
'block-all-mixed-content',
|
|
113
|
+
'child-src',
|
|
114
|
+
'connect-src',
|
|
115
|
+
'default-src',
|
|
116
|
+
'font-src',
|
|
117
|
+
'form-action',
|
|
118
|
+
'frame-ancestors',
|
|
119
|
+
'frame-src',
|
|
120
|
+
'img-src',
|
|
121
|
+
'manifest-src',
|
|
122
|
+
'media-src',
|
|
123
|
+
'object-src',
|
|
124
|
+
'prefetch-src',
|
|
125
|
+
'script-src',
|
|
126
|
+
'script-src-attr',
|
|
127
|
+
'script-src-elem',
|
|
128
|
+
'style-src',
|
|
129
|
+
'style-src-attr',
|
|
130
|
+
'style-src-elem',
|
|
131
|
+
'upgrade-insecure-requests',
|
|
132
|
+
'worker-src'
|
|
133
|
+
]);
|
|
134
|
+
/**
|
|
135
|
+
* Per-instance mint tracker: (day|app|directive) to the origin|disposition
|
|
136
|
+
* pairs that have been allowed to mint documents. Cleared wholesale at the
|
|
137
|
+
* cap, like the tenant collector's `keyStore`: the failure mode of clearing
|
|
138
|
+
* is a brief widening of the mint cap, strictly better than an unbounded map
|
|
139
|
+
* fed by attacker-chosen keys.
|
|
140
|
+
*/ const mintTracker = new Map();
|
|
141
|
+
const MAX_TRACKED_GROUPS = 512;
|
|
142
|
+
/** Per-instance write budget window. */ let budgetWindowStartMs = 0;
|
|
143
|
+
let budgetUsed = 0;
|
|
144
|
+
/** Test seam: module-scope state would otherwise leak across cases. */ export function resetCspAggregateStateForTests() {
|
|
145
|
+
mintTracker.clear();
|
|
146
|
+
budgetWindowStartMs = 0;
|
|
147
|
+
budgetUsed = 0;
|
|
148
|
+
}
|
|
149
|
+
/** One id/field component: lowercase, fixed charset, never empty. */ function sanitizePart(value, max) {
|
|
150
|
+
const cleaned = value.toLowerCase().replace(/[^a-z0-9.:_-]+/g, '-').slice(0, max);
|
|
151
|
+
return cleaned || 'unknown';
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `blocked-uri` reduced to the coarsest thing worth counting: the HOST for a
|
|
155
|
+
* URL, the bare keyword (`inline`, `eval`, `data`, ...) otherwise. Paths are
|
|
156
|
+
* deliberately dropped — twelve chunk URLs off one CDN are one origin-level
|
|
157
|
+
* fact (the AGL-1779 shape), and a path would both explode the key space and
|
|
158
|
+
* hand the id to whoever controls the URL.
|
|
159
|
+
*/ export function cspBlockedOrigin(blockedUri) {
|
|
160
|
+
if (!blockedUri) return 'none';
|
|
161
|
+
try {
|
|
162
|
+
return sanitizePart(new URL(blockedUri).host, 120);
|
|
163
|
+
} catch (unused) {
|
|
164
|
+
return sanitizePart(blockedUri, 40);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** UTC day bucket, `YYYY-MM-DD`. */ export function cspAggregateDay(nowMs) {
|
|
168
|
+
return new Date(nowMs).toISOString().slice(0, 10);
|
|
169
|
+
}
|
|
170
|
+
function normalizeDirective(directive) {
|
|
171
|
+
return KNOWN_DIRECTIVES.has(directive) ? directive : 'other';
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Compound `violations` into the daily counters. Never throws; returns how
|
|
175
|
+
* many documents were written (0 on any failure, budget exhaustion included).
|
|
176
|
+
*/ export async function recordCspViolations(violations, options) {
|
|
177
|
+
try {
|
|
178
|
+
var _options_now, _options_firestore;
|
|
179
|
+
if (!violations.length) return 0;
|
|
180
|
+
const now = (_options_now = options.now) != null ? _options_now : Date.now();
|
|
181
|
+
const day = cspAggregateDay(now);
|
|
182
|
+
const dayStartMs = Date.parse(`${day}T00:00:00.000Z`);
|
|
183
|
+
// Group first: N occurrences of one key in a batch are ONE write carrying
|
|
184
|
+
// increment(N), so a page that repeats itself costs one op, not N.
|
|
185
|
+
const grouped = new Map();
|
|
186
|
+
for (const violation of violations){
|
|
187
|
+
const directive = normalizeDirective(violation.effectiveDirective);
|
|
188
|
+
const origin = cspBlockedOrigin(violation.blockedUri);
|
|
189
|
+
const disposition = sanitizePart(violation.disposition || 'report', 16);
|
|
190
|
+
const id = [
|
|
191
|
+
day,
|
|
192
|
+
options.app,
|
|
193
|
+
sanitizePart(directive, 64),
|
|
194
|
+
disposition,
|
|
195
|
+
origin
|
|
196
|
+
].join('|');
|
|
197
|
+
// Belt and braces — the AGL-1771 contract for anything handed to
|
|
198
|
+
// `.doc()`. The sanitizer above should make this unreachable.
|
|
199
|
+
if (!isDocumentId(id)) continue;
|
|
200
|
+
const existing = grouped.get(id);
|
|
201
|
+
if (existing) {
|
|
202
|
+
existing.n += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Distinct-origin cap, per (day x app x directive). Known pairs keep
|
|
206
|
+
// counting; new pairs beyond the cap are dropped — bounded documents
|
|
207
|
+
// is the property, not perfect counts.
|
|
208
|
+
const groupKey = `${day}|${options.app}|${directive}`;
|
|
209
|
+
if (mintTracker.size > MAX_TRACKED_GROUPS) mintTracker.clear();
|
|
210
|
+
let minted = mintTracker.get(groupKey);
|
|
211
|
+
if (!minted) {
|
|
212
|
+
minted = new Set();
|
|
213
|
+
mintTracker.set(groupKey, minted);
|
|
214
|
+
}
|
|
215
|
+
const pair = `${disposition}|${origin}`;
|
|
216
|
+
if (!minted.has(pair)) {
|
|
217
|
+
if (minted.size >= MAX_DISTINCT_ORIGINS_PER_DAY) continue;
|
|
218
|
+
minted.add(pair);
|
|
219
|
+
}
|
|
220
|
+
grouped.set(id, {
|
|
221
|
+
n: 1,
|
|
222
|
+
directive,
|
|
223
|
+
origin,
|
|
224
|
+
violation
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (!grouped.size) return 0;
|
|
228
|
+
// Write budget: fixed one-minute window per instance.
|
|
229
|
+
if (now - budgetWindowStartMs >= 60000) {
|
|
230
|
+
budgetWindowStartMs = now;
|
|
231
|
+
budgetUsed = 0;
|
|
232
|
+
}
|
|
233
|
+
const room = WRITE_BUDGET_PER_MINUTE - budgetUsed;
|
|
234
|
+
if (room <= 0) return 0;
|
|
235
|
+
const entries = [
|
|
236
|
+
...grouped.entries()
|
|
237
|
+
].slice(0, room);
|
|
238
|
+
budgetUsed += entries.length;
|
|
239
|
+
const firestore = (_options_firestore = options.firestore) != null ? _options_firestore : // Lazily resolved so this module stays inert at load: route specs and
|
|
240
|
+
// this module's own spec inject a fake, and nothing initializes
|
|
241
|
+
// firebase-admin just by importing the barrel.
|
|
242
|
+
(await import("./firebase-admin.js")).firebaseAdmin.app().firestore();
|
|
243
|
+
await Promise.all(entries.map(([id, { n, directive, origin, violation }])=>firestore.collection(CSP_AGGREGATE_COLLECTION).doc(id).set(_extends({
|
|
244
|
+
day,
|
|
245
|
+
app: options.app,
|
|
246
|
+
directive,
|
|
247
|
+
origin,
|
|
248
|
+
disposition: sanitizePart(violation.disposition || 'report', 16),
|
|
249
|
+
count: FieldValue.increment(n),
|
|
250
|
+
lastSeenMs: now,
|
|
251
|
+
// Samples, not keys: enough to find the surface, no power to
|
|
252
|
+
// mint documents. Already clamped by the parser; clamped again
|
|
253
|
+
// because this module's contract should not depend on that.
|
|
254
|
+
lastPath: violation.documentPath.slice(0, 200)
|
|
255
|
+
}, options.site ? {
|
|
256
|
+
lastSite: options.site.slice(0, 253)
|
|
257
|
+
} : {}, {
|
|
258
|
+
// Timestamp-typed via Date — Firestore TTL refuses numbers.
|
|
259
|
+
expiresAt: new Date(dayStartMs + CSP_AGGREGATE_RETENTION_DAYS * 86400000)
|
|
260
|
+
}), {
|
|
261
|
+
merge: true
|
|
262
|
+
})));
|
|
263
|
+
return entries.length;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
// Fail-soft: the aggregate is an observer, and `console.error` rather
|
|
266
|
+
// than `console.warn` on purpose — the collectors' own log lines are
|
|
267
|
+
// warns, and their specs count warns.
|
|
268
|
+
console.error(JSON.stringify({
|
|
269
|
+
tag: 'AGL-1799:csp-aggregate',
|
|
270
|
+
error: String(error).slice(0, 200)
|
|
271
|
+
}));
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//# sourceMappingURL=csp-aggregate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/csp-aggregate.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 * Durable CSP-violation aggregates (AGL-1799).\n *\n * The collectors were delivering reports into a sink that forgets: the Vercel\n * runtime log retains ~60 minutes and there is no log drain, so AGL-1702's\n * \"a week of signed-in traffic\" and AGL-1726's \"a business week of visitor\n * traffic\" were gated on evidence that structurally could not exist. This\n * module is the durable half — the log lines stay exactly as they were (they\n * are still the right tool for the last hour), and every accepted violation\n * ALSO compounds into a Firestore counter that holds for the retention window.\n *\n * ## Shape: counters, never report bodies\n *\n * One document per (UTC day x app x directive x disposition x blocked origin),\n * with `FieldValue.increment` — NOT one document per report. AGL-523 made a\n * deliberate choice not to write report bodies to Firestore from an\n * unauthenticated route, and AGL-1799 argued the distinction explicitly: \"a\n * counter with a bounded key space is a different proposition from storing\n * report bodies\". The bound is what this module is careful about, because\n * every key ingredient except the day is attacker-influenced:\n *\n * - the DIRECTIVE is normalized into a closed set (a real browser can only\n * ever send a spec directive; anything else lands in `other` rather than\n * minting a document per invented directive);\n * - the ORIGIN is reduced to the host of `blocked-uri` (or its bare keyword\n * — `inline`, `eval`, `data`), sanitized to a fixed character set, and\n * capped: at most {@link MAX_DISTINCT_ORIGINS_PER_DAY} distinct origins per\n * (day x app x directive) may mint documents per instance per day. Beyond\n * that, new origins are dropped while known ones keep counting — a flood\n * of unique URLs (the AGL-1769 lesson: never let request content mint\n * unbounded documents) saturates the cap instead of the collection;\n * - WRITES are budgeted per instance per minute, so a POST flood cannot turn\n * an unauthenticated beacon into a Firestore write bill even against\n * already-minted documents.\n *\n * Every cap here is per instance, like the tenant collector's `keyStore`: the\n * fleet-wide worst case is (instances x cap), which is bounded and small,\n * and an instance recycle merely re-counts an origin against a fresh cap —\n * the merge write is idempotent about which document it lands in.\n *\n * ## Ids are attacker-derived, so they go through `isDocumentId`\n *\n * Key parts are sanitized to `[a-z0-9.:_-]` and joined with `|`, then the\n * whole id is checked with {@link isDocumentId} (the AGL-1771 predicate —\n * imported from its leaf, `./document-id`, which imports nothing). Belt and\n * braces: the sanitizer already removes `/` and the joiner makes a reserved\n * `__x__` form impossible, but the predicate is the contract and it is\n * cheaper to check than to argue.\n *\n * ## Retention is TTL, not a sweep\n *\n * `expiresAt` (a Timestamp — TTL refuses numbers) is set\n * {@link CSP_AGGREGATE_RETENTION_DAYS} days past the day bucket. The TTL\n * policy is a gcloud action plus a `fieldOverrides` entry in\n * `cloud/firebase-firestore.indexes.json` — see\n * docs/FIRESTORE_MANUAL_CONFIG.md (AGL-1793): the entry ships with this\n * module and the gcloud enable is owed at deploy time.\n *\n * ## Fail-soft, like everything on this path\n *\n * A collector must never turn an observed problem into a served one. Every\n * failure here — no credential, Firestore down, a hostile id that survives\n * sanitization — drops the aggregate (one `console.error` line, tagged) and\n * returns. The log lines the collectors already emit are unaffected.\n *\n * No client ever reads or writes this collection: there is no rules `match`\n * for it, and Firestore denies what nothing matches, so the Admin SDK is the\n * only path in. The staff read-back is `/api/admin/csp-reports` on the\n * console.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { CspViolation } from '@aglyn/aglyn/app-utils/csp-report'\nimport { isDocumentId } from './document-id'\n\n/** Top-level collection of counter documents. Server-only; no rules match. */\nexport const CSP_AGGREGATE_COLLECTION = 'cspViolationDaily'\n\n/**\n * Long enough that \"a week of traffic\" (AGL-1702 condition 1, AGL-1726\n * condition 3) is readable with room to be read late, short enough that the\n * collection stays a working set rather than an archive.\n */\nexport const CSP_AGGREGATE_RETENTION_DAYS = 60\n\n/**\n * Distinct blocked origins that may mint documents per (day x app x\n * directive), per instance. A legitimate policy violates against a handful of\n * hosts; thirty is generous headroom for a real regression while keeping a\n * flood of attacker-invented origins from minting unbounded documents.\n */\nexport const MAX_DISTINCT_ORIGINS_PER_DAY = 30\n\n/**\n * Aggregate writes allowed per instance per minute. Distinct-origin capping\n * bounds document COUNT; this bounds write OPS, which are billed even when\n * they land on existing documents. Sized far above what real browsers on one\n * instance produce and far below what makes a flood expensive.\n */\nconst WRITE_BUDGET_PER_MINUTE = 120\n\n/**\n * The directives a browser can actually report, per the CSP3 registry. An\n * unrecognized directive is a non-browser caller (or a future spec revision)\n * and shares one bucket rather than minting its own key space.\n */\nconst KNOWN_DIRECTIVES = new Set([\n 'base-uri',\n 'block-all-mixed-content',\n 'child-src',\n 'connect-src',\n 'default-src',\n 'font-src',\n 'form-action',\n 'frame-ancestors',\n 'frame-src',\n 'img-src',\n 'manifest-src',\n 'media-src',\n 'object-src',\n 'prefetch-src',\n 'script-src',\n 'script-src-attr',\n 'script-src-elem',\n 'style-src',\n 'style-src-attr',\n 'style-src-elem',\n 'upgrade-insecure-requests',\n 'worker-src',\n])\n\n/**\n * Per-instance mint tracker: (day|app|directive) to the origin|disposition\n * pairs that have been allowed to mint documents. Cleared wholesale at the\n * cap, like the tenant collector's `keyStore`: the failure mode of clearing\n * is a brief widening of the mint cap, strictly better than an unbounded map\n * fed by attacker-chosen keys.\n */\nconst mintTracker = new Map<string, Set<string>>()\nconst MAX_TRACKED_GROUPS = 512\n\n/** Per-instance write budget window. */\nlet budgetWindowStartMs = 0\nlet budgetUsed = 0\n\n/** Test seam: module-scope state would otherwise leak across cases. */\nexport function resetCspAggregateStateForTests(): void {\n mintTracker.clear()\n budgetWindowStartMs = 0\n budgetUsed = 0\n}\n\n/** One id/field component: lowercase, fixed charset, never empty. */\nfunction sanitizePart(value: string, max: number): string {\n const cleaned = value\n .toLowerCase()\n .replace(/[^a-z0-9.:_-]+/g, '-')\n .slice(0, max)\n return cleaned || 'unknown'\n}\n\n/**\n * `blocked-uri` reduced to the coarsest thing worth counting: the HOST for a\n * URL, the bare keyword (`inline`, `eval`, `data`, ...) otherwise. Paths are\n * deliberately dropped — twelve chunk URLs off one CDN are one origin-level\n * fact (the AGL-1779 shape), and a path would both explode the key space and\n * hand the id to whoever controls the URL.\n */\nexport function cspBlockedOrigin(blockedUri: string): string {\n if (!blockedUri) return 'none'\n try {\n return sanitizePart(new URL(blockedUri).host, 120)\n } catch {\n return sanitizePart(blockedUri, 40)\n }\n}\n\n/** UTC day bucket, `YYYY-MM-DD`. */\nexport function cspAggregateDay(nowMs: number): string {\n return new Date(nowMs).toISOString().slice(0, 10)\n}\n\nfunction normalizeDirective(directive: string): string {\n return KNOWN_DIRECTIVES.has(directive) ? directive : 'other'\n}\n\nexport interface RecordCspViolationsOptions {\n /** Which collector this came through — part of the counter key. */\n app: 'console' | 'tenant'\n /**\n * The reporting site (tenant only), REQUEST-derived and already clamped by\n * the caller. Stored as a sample field, never in the id: distinct sites\n * must not multiply the key space.\n */\n site?: string\n now?: number\n /** Injectable for tests; defaults to the Admin SDK's Firestore. */\n firestore?: any\n}\n\n/**\n * Compound `violations` into the daily counters. Never throws; returns how\n * many documents were written (0 on any failure, budget exhaustion included).\n */\nexport async function recordCspViolations(\n violations: readonly CspViolation[],\n options: RecordCspViolationsOptions,\n): Promise<number> {\n try {\n if (!violations.length) return 0\n const now = options.now ?? Date.now()\n const day = cspAggregateDay(now)\n const dayStartMs = Date.parse(`${day}T00:00:00.000Z`)\n\n // Group first: N occurrences of one key in a batch are ONE write carrying\n // increment(N), so a page that repeats itself costs one op, not N.\n const grouped = new Map<\n string,\n { n: number; directive: string; origin: string; violation: CspViolation }\n >()\n for (const violation of violations) {\n const directive = normalizeDirective(violation.effectiveDirective)\n const origin = cspBlockedOrigin(violation.blockedUri)\n const disposition = sanitizePart(violation.disposition || 'report', 16)\n const id = [\n day,\n options.app,\n sanitizePart(directive, 64),\n disposition,\n origin,\n ].join('|')\n // Belt and braces — the AGL-1771 contract for anything handed to\n // `.doc()`. The sanitizer above should make this unreachable.\n if (!isDocumentId(id)) continue\n\n const existing = grouped.get(id)\n if (existing) {\n existing.n += 1\n continue\n }\n\n // Distinct-origin cap, per (day x app x directive). Known pairs keep\n // counting; new pairs beyond the cap are dropped — bounded documents\n // is the property, not perfect counts.\n const groupKey = `${day}|${options.app}|${directive}`\n if (mintTracker.size > MAX_TRACKED_GROUPS) mintTracker.clear()\n let minted = mintTracker.get(groupKey)\n if (!minted) {\n minted = new Set()\n mintTracker.set(groupKey, minted)\n }\n const pair = `${disposition}|${origin}`\n if (!minted.has(pair)) {\n if (minted.size >= MAX_DISTINCT_ORIGINS_PER_DAY) continue\n minted.add(pair)\n }\n\n grouped.set(id, { n: 1, directive, origin, violation })\n }\n if (!grouped.size) return 0\n\n // Write budget: fixed one-minute window per instance.\n if (now - budgetWindowStartMs >= 60_000) {\n budgetWindowStartMs = now\n budgetUsed = 0\n }\n const room = WRITE_BUDGET_PER_MINUTE - budgetUsed\n if (room <= 0) return 0\n const entries = [...grouped.entries()].slice(0, room)\n budgetUsed += entries.length\n\n const firestore =\n options.firestore ??\n // Lazily resolved so this module stays inert at load: route specs and\n // this module's own spec inject a fake, and nothing initializes\n // firebase-admin just by importing the barrel.\n (await import('./firebase-admin')).firebaseAdmin.app().firestore()\n\n await Promise.all(\n entries.map(([id, { n, directive, origin, violation }]) =>\n firestore\n .collection(CSP_AGGREGATE_COLLECTION)\n .doc(id)\n .set(\n {\n day,\n app: options.app,\n directive,\n origin,\n disposition: sanitizePart(violation.disposition || 'report', 16),\n count: FieldValue.increment(n),\n lastSeenMs: now,\n // Samples, not keys: enough to find the surface, no power to\n // mint documents. Already clamped by the parser; clamped again\n // because this module's contract should not depend on that.\n lastPath: violation.documentPath.slice(0, 200),\n ...(options.site ? { lastSite: options.site.slice(0, 253) } : {}),\n // Timestamp-typed via Date — Firestore TTL refuses numbers.\n expiresAt: new Date(\n dayStartMs + CSP_AGGREGATE_RETENTION_DAYS * 86_400_000,\n ),\n },\n { merge: true },\n ),\n ),\n )\n return entries.length\n } catch (error) {\n // Fail-soft: the aggregate is an observer, and `console.error` rather\n // than `console.warn` on purpose — the collectors' own log lines are\n // warns, and their specs count warns.\n console.error(\n JSON.stringify({\n tag: 'AGL-1799:csp-aggregate',\n error: String(error).slice(0, 200),\n }),\n )\n return 0\n }\n}\n"],"names":["FieldValue","isDocumentId","CSP_AGGREGATE_COLLECTION","CSP_AGGREGATE_RETENTION_DAYS","MAX_DISTINCT_ORIGINS_PER_DAY","WRITE_BUDGET_PER_MINUTE","KNOWN_DIRECTIVES","Set","mintTracker","Map","MAX_TRACKED_GROUPS","budgetWindowStartMs","budgetUsed","resetCspAggregateStateForTests","clear","sanitizePart","value","max","cleaned","toLowerCase","replace","slice","cspBlockedOrigin","blockedUri","URL","host","cspAggregateDay","nowMs","Date","toISOString","normalizeDirective","directive","has","recordCspViolations","violations","options","length","now","day","dayStartMs","parse","grouped","violation","effectiveDirective","origin","disposition","id","app","join","existing","get","n","groupKey","size","minted","set","pair","add","room","entries","firestore","firebaseAdmin","Promise","all","map","collection","doc","count","increment","lastSeenMs","lastPath","documentPath","site","lastSite","expiresAt","merge","error","console","JSON","stringify","tag","String"],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* `dataStorageMbPerOrg`, answered once for every path that writes dataset
|
|
19
|
+
* bytes (AGL-2253).
|
|
20
|
+
*
|
|
21
|
+
* AGL-2163 wired `checkDataStorageQuota().allowed` into `/api/orgs/datasets`
|
|
22
|
+
* and stopped there, so the console route refused what the REST API and the
|
|
23
|
+
* public form path went on accepting. The measurement and the shape logic
|
|
24
|
+
* lived inside that route file, which is exactly why the other two writers
|
|
25
|
+
* never got it — a gate that only one caller can reach is a gate the next
|
|
26
|
+
* caller is guaranteed to miss.
|
|
27
|
+
*
|
|
28
|
+
* The VERDICT lives here; the REFUSAL does not. `/api/orgs/datasets` answers
|
|
29
|
+
* a console `{ error }` 403, `/v1` answers an `ApiErrors.planRequired` body
|
|
30
|
+
* with a machine-readable `code`, and the tenant form path answers nothing at
|
|
31
|
+
* all — it drops the dataset row and still accepts the submission. Three
|
|
32
|
+
* correct, incompatible renderings of one decision.
|
|
33
|
+
*
|
|
34
|
+
* ## What it costs a paying customer: NOTHING
|
|
35
|
+
*
|
|
36
|
+
* Every plan with an `extraDataGbMonthlyUsd` rate resolves to
|
|
37
|
+
* `'never-blocks'` — the overage bills, exactly as `checkDataStorageQuota`'s
|
|
38
|
+
* docblock promises — so the metered case returns `null` without a single
|
|
39
|
+
* read and with no possibility of a refusal. Only the other two shapes cost
|
|
40
|
+
* anything, and one of them costs no read either.
|
|
41
|
+
*/
|
|
42
|
+
export interface DataStorageRefusal {
|
|
43
|
+
/** The included band the write would exceed, MB. */
|
|
44
|
+
includedMb: number;
|
|
45
|
+
/**
|
|
46
|
+
* `'always'` — the band is zero and the plan meters nothing, so no
|
|
47
|
+
* measurement could change the answer and none was taken.
|
|
48
|
+
* `'measured'` — a finite non-zero band on a plan with no rate, decided
|
|
49
|
+
* against the monthly rollup.
|
|
50
|
+
*/
|
|
51
|
+
basis: 'always' | 'measured';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Null when the write may proceed; a refusal when dataset bytes are not
|
|
55
|
+
* included on this org's plan.
|
|
56
|
+
*
|
|
57
|
+
* The `'measure'` shape reads `orgs/{orgId}/usage/{YYYY-MM}.dataStorageMb`
|
|
58
|
+
* rather than re-summing the org's datasets: `report-usage`'s
|
|
59
|
+
* `orgDatasetBytes` is O(datasets) reads with two aggregate queries EACH,
|
|
60
|
+
* which is not a per-record-write cost. The reading is therefore up to a
|
|
61
|
+
* month stale, and that is stated rather than hidden — it can only ever
|
|
62
|
+
* UNDER-refuse, never refuse a write it should have allowed. The only orgs
|
|
63
|
+
* that shape can reach are ones staff configured by hand
|
|
64
|
+
* (`entitlementOverrides.dataStorageMbPerOrg` on a plan that meters nothing).
|
|
65
|
+
*/
|
|
66
|
+
export declare function dataStorageRefusal(org: unknown, orgRef: FirebaseFirestore.DocumentReference, now?: Date): Promise<DataStorageRefusal | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { checkDataStorageQuota, dataStorageEnforcementShape } from "@aglyn/aglyn/server";
|
|
17
|
+
/**
|
|
18
|
+
* Null when the write may proceed; a refusal when dataset bytes are not
|
|
19
|
+
* included on this org's plan.
|
|
20
|
+
*
|
|
21
|
+
* The `'measure'` shape reads `orgs/{orgId}/usage/{YYYY-MM}.dataStorageMb`
|
|
22
|
+
* rather than re-summing the org's datasets: `report-usage`'s
|
|
23
|
+
* `orgDatasetBytes` is O(datasets) reads with two aggregate queries EACH,
|
|
24
|
+
* which is not a per-record-write cost. The reading is therefore up to a
|
|
25
|
+
* month stale, and that is stated rather than hidden — it can only ever
|
|
26
|
+
* UNDER-refuse, never refuse a write it should have allowed. The only orgs
|
|
27
|
+
* that shape can reach are ones staff configured by hand
|
|
28
|
+
* (`entitlementOverrides.dataStorageMbPerOrg` on a plan that meters nothing).
|
|
29
|
+
*/ export async function dataStorageRefusal(org, orgRef, now = new Date()) {
|
|
30
|
+
var _usage_get;
|
|
31
|
+
const shape = dataStorageEnforcementShape(org);
|
|
32
|
+
if (shape === 'never-blocks') return null;
|
|
33
|
+
if (shape === 'always-blocks') {
|
|
34
|
+
return {
|
|
35
|
+
includedMb: checkDataStorageQuota(org, 0).includedMb,
|
|
36
|
+
basis: 'always'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const usage = await orgRef.collection('usage').doc(now.toISOString().slice(0, 7)).get();
|
|
40
|
+
const quota = checkDataStorageQuota(org, Number((_usage_get = usage.get('dataStorageMb')) != null ? _usage_get : 0));
|
|
41
|
+
if (quota.allowed) return null;
|
|
42
|
+
return {
|
|
43
|
+
includedMb: quota.includedMb,
|
|
44
|
+
basis: 'measured'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=data-storage-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/data-storage-gate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkDataStorageQuota,\n dataStorageEnforcementShape,\n} from '@aglyn/aglyn/server'\n\n/**\n * `dataStorageMbPerOrg`, answered once for every path that writes dataset\n * bytes (AGL-2253).\n *\n * AGL-2163 wired `checkDataStorageQuota().allowed` into `/api/orgs/datasets`\n * and stopped there, so the console route refused what the REST API and the\n * public form path went on accepting. The measurement and the shape logic\n * lived inside that route file, which is exactly why the other two writers\n * never got it — a gate that only one caller can reach is a gate the next\n * caller is guaranteed to miss.\n *\n * The VERDICT lives here; the REFUSAL does not. `/api/orgs/datasets` answers\n * a console `{ error }` 403, `/v1` answers an `ApiErrors.planRequired` body\n * with a machine-readable `code`, and the tenant form path answers nothing at\n * all — it drops the dataset row and still accepts the submission. Three\n * correct, incompatible renderings of one decision.\n *\n * ## What it costs a paying customer: NOTHING\n *\n * Every plan with an `extraDataGbMonthlyUsd` rate resolves to\n * `'never-blocks'` — the overage bills, exactly as `checkDataStorageQuota`'s\n * docblock promises — so the metered case returns `null` without a single\n * read and with no possibility of a refusal. Only the other two shapes cost\n * anything, and one of them costs no read either.\n */\nexport interface DataStorageRefusal {\n /** The included band the write would exceed, MB. */\n includedMb: number\n /**\n * `'always'` — the band is zero and the plan meters nothing, so no\n * measurement could change the answer and none was taken.\n * `'measured'` — a finite non-zero band on a plan with no rate, decided\n * against the monthly rollup.\n */\n basis: 'always' | 'measured'\n}\n\n/**\n * Null when the write may proceed; a refusal when dataset bytes are not\n * included on this org's plan.\n *\n * The `'measure'` shape reads `orgs/{orgId}/usage/{YYYY-MM}.dataStorageMb`\n * rather than re-summing the org's datasets: `report-usage`'s\n * `orgDatasetBytes` is O(datasets) reads with two aggregate queries EACH,\n * which is not a per-record-write cost. The reading is therefore up to a\n * month stale, and that is stated rather than hidden — it can only ever\n * UNDER-refuse, never refuse a write it should have allowed. The only orgs\n * that shape can reach are ones staff configured by hand\n * (`entitlementOverrides.dataStorageMbPerOrg` on a plan that meters nothing).\n */\nexport async function dataStorageRefusal(\n org: unknown,\n orgRef: FirebaseFirestore.DocumentReference,\n now: Date = new Date(),\n): Promise<DataStorageRefusal | null> {\n const shape = dataStorageEnforcementShape(org as never)\n if (shape === 'never-blocks') return null\n if (shape === 'always-blocks') {\n return {\n includedMb: checkDataStorageQuota(org as never, 0).includedMb,\n basis: 'always',\n }\n }\n const usage = await orgRef\n .collection('usage')\n .doc(now.toISOString().slice(0, 7))\n .get()\n const quota = checkDataStorageQuota(\n org as never,\n Number(usage.get('dataStorageMb') ?? 0),\n )\n if (quota.allowed) return null\n return { includedMb: quota.includedMb, basis: 'measured' }\n}\n"],"names":["checkDataStorageQuota","dataStorageEnforcementShape","dataStorageRefusal","org","orgRef","now","Date","usage","shape","includedMb","basis","collection","doc","toISOString","slice","get","quota","Number","allowed"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,2BAA2B,QACtB,sBAAqB;AAuC5B;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,mBACpBC,GAAY,EACZC,MAA2C,EAC3CC,MAAY,IAAIC,MAAM;QAgBbC;IAdT,MAAMC,QAAQP,4BAA4BE;IAC1C,IAAIK,UAAU,gBAAgB,OAAO;IACrC,IAAIA,UAAU,iBAAiB;QAC7B,OAAO;YACLC,YAAYT,sBAAsBG,KAAc,GAAGM,UAAU;YAC7DC,OAAO;QACT;IACF;IACA,MAAMH,QAAQ,MAAMH,OACjBO,UAAU,CAAC,SACXC,GAAG,CAACP,IAAIQ,WAAW,GAAGC,KAAK,CAAC,GAAG,IAC/BC,GAAG;IACN,MAAMC,QAAQhB,sBACZG,KACAc,QAAOV,aAAAA,MAAMQ,GAAG,CAAC,4BAAVR,aAA8B;IAEvC,IAAIS,MAAME,OAAO,EAAE,OAAO;IAC1B,OAAO;QAAET,YAAYO,MAAMP,UAAU;QAAEC,OAAO;IAAW;AAC3D"}
|
|
@@ -0,0 +1,179 @@
|
|
|
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 { Firestore } from 'firebase-admin/firestore';
|
|
18
|
+
import { type UsageSources } from './live-page-usage';
|
|
19
|
+
/**
|
|
20
|
+
* How many documents per collection the per-site scan reads.
|
|
21
|
+
*
|
|
22
|
+
* The console's publish scan uses the same number for the same reason
|
|
23
|
+
* (AGL-1161): this decides which caches get dropped, so a prefix scan would
|
|
24
|
+
* report a refreshed site and leave real pages serving the old rows. Bounded
|
|
25
|
+
* all the same, and when the bound bites it is SAID rather than absorbed.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DATASET_SCAN_LIMIT = 2000;
|
|
28
|
+
/**
|
|
29
|
+
* How many sites one record write fans out to.
|
|
30
|
+
*
|
|
31
|
+
* `MAX_SCOPE_HOSTS` is the ceiling on a NAMED selection, so only an org-wide
|
|
32
|
+
* dataset can reach this — an org with more sites than this pays a scan per
|
|
33
|
+
* site on every write otherwise, which is a cost the person writing one row
|
|
34
|
+
* did not ask for. Sites past the cap keep the TTL they had before this
|
|
35
|
+
* existed, and the shortfall is returned and logged rather than swallowed.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_DATASET_FANOUT_HOSTS = 30;
|
|
38
|
+
/**
|
|
39
|
+
* How long one site-and-dataset pair waits before announcing again.
|
|
40
|
+
*
|
|
41
|
+
* A 1,000-row import is one announce because the importer announces after its
|
|
42
|
+
* loop, not per row. This covers the other burst — many small writes arriving
|
|
43
|
+
* separately, a form under load or an automation fanning out — where each one
|
|
44
|
+
* would otherwise pay a full scan per site and hand the tenant the same path
|
|
45
|
+
* list again.
|
|
46
|
+
*
|
|
47
|
+
* Leading edge, so the FIRST write of a burst refreshes immediately; what is
|
|
48
|
+
* skipped is a repeat inside the window, and the next write after it announces
|
|
49
|
+
* again. The cost of the skip is bounded by the window: a row written during
|
|
50
|
+
* one is visible no later than the next announce.
|
|
51
|
+
*/
|
|
52
|
+
export declare const DATASET_ANNOUNCE_WINDOW_MS = 2000;
|
|
53
|
+
/** Test seam: the window is process-wide state, so a spec has to reset it. */
|
|
54
|
+
export declare function resetDatasetAnnounceThrottle(): void;
|
|
55
|
+
/** One site's share of a dataset change. */
|
|
56
|
+
export interface DatasetLivePageTarget {
|
|
57
|
+
hostId: string;
|
|
58
|
+
/** The tenant keys its cache on this, never on `hostId`. */
|
|
59
|
+
subdomain: string;
|
|
60
|
+
/** The attached custom domain, whose pages live under a second cache key. */
|
|
61
|
+
cname?: string;
|
|
62
|
+
/** Site-absolute addresses (`/`, `/team`) that repeat over the dataset. */
|
|
63
|
+
paths: string[];
|
|
64
|
+
/** The site held more documents than the scan read, so `paths` is partial. */
|
|
65
|
+
truncated: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface DatasetLivePageScope {
|
|
68
|
+
targets: DatasetLivePageTarget[];
|
|
69
|
+
/**
|
|
70
|
+
* Sites the fan-out cap left out. Zero in the ordinary case, and reported
|
|
71
|
+
* rather than logged because a caller that says "refreshed" while some sites
|
|
72
|
+
* were skipped is the failure this whole arc exists to remove.
|
|
73
|
+
*/
|
|
74
|
+
hostsDropped: number;
|
|
75
|
+
/**
|
|
76
|
+
* Why there is nothing to announce, or `'ok'`.
|
|
77
|
+
*
|
|
78
|
+
* `'not-rendered'` is a success: a dataset no page repeats over has no live
|
|
79
|
+
* page to refresh. It is kept apart from `'error'` for the same reason the
|
|
80
|
+
* revalidate route keeps `not-routed` apart from a refusal — one means there
|
|
81
|
+
* was nothing to do, the other means something was left stale.
|
|
82
|
+
*/
|
|
83
|
+
reason: 'ok' | 'no-dataset' | 'no-sites' | 'not-rendered' | 'error';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every live screen whose rendered output repeats over the dataset, however
|
|
87
|
+
* indirectly.
|
|
88
|
+
*
|
|
89
|
+
* The same three-bucket closure a placed form's pages are found by, for the
|
|
90
|
+
* same reason: a binding is found by SEARCHING node trees rather than by
|
|
91
|
+
* matching a pointer, so it can sit on the screen, in the chrome of the layout
|
|
92
|
+
* the screen renders inside, or inside a reusable component a third component
|
|
93
|
+
* nests. After the first level the fan-out is identical, so it is delegated
|
|
94
|
+
* rather than restated.
|
|
95
|
+
*
|
|
96
|
+
* Pure, and separate from the Firestore read, so the closure is testable
|
|
97
|
+
* without a database.
|
|
98
|
+
*/
|
|
99
|
+
export declare function screenIdsRepeatingDataset(keys: ReadonlySet<string>, sources: UsageSources): string[];
|
|
100
|
+
/**
|
|
101
|
+
* Every live page a change to this dataset's records makes stale, per site.
|
|
102
|
+
*
|
|
103
|
+
* Reads only; announcing is the caller's, because the two runtimes announce
|
|
104
|
+
* differently — the console posts to the tenant over the shared secret, and
|
|
105
|
+
* the tenant drops its own caches in process. Splitting it here is what lets
|
|
106
|
+
* both use one answer to the question of WHICH pages.
|
|
107
|
+
*
|
|
108
|
+
* Never throws. A scope that could not be computed is a scope of nothing, and
|
|
109
|
+
* the TTL underneath is unaffected.
|
|
110
|
+
*/
|
|
111
|
+
export declare function datasetLivePageScope(options: {
|
|
112
|
+
firestore: Firestore;
|
|
113
|
+
orgId: string;
|
|
114
|
+
datasetId: string;
|
|
115
|
+
}): Promise<DatasetLivePageScope>;
|
|
116
|
+
/**
|
|
117
|
+
* How one runtime drops a site's cached pages.
|
|
118
|
+
*
|
|
119
|
+
* The generic seam, registered by the deployment that owns the caches. The
|
|
120
|
+
* tenant registers an in-process drop at boot; the console announces over the
|
|
121
|
+
* shared secret and passes its own dropper at the call site. Core names
|
|
122
|
+
* neither, exactly as it names no repeat source.
|
|
123
|
+
*
|
|
124
|
+
* Returns whether the drop landed. It must never throw: everything above it
|
|
125
|
+
* has already been written.
|
|
126
|
+
*/
|
|
127
|
+
export type LivePageDropper = (target: DatasetLivePageTarget) => Promise<boolean>;
|
|
128
|
+
/**
|
|
129
|
+
* Registers the dropper for this process, replacing any previous one. Returns
|
|
130
|
+
* the unregister, which removes it only if it is still the one registered.
|
|
131
|
+
*
|
|
132
|
+
* Called from a function the platform calls BY NAME at boot, never from a
|
|
133
|
+
* module's top level: a bundler deletes a module imported only for its side
|
|
134
|
+
* effect when its package says it has none (AGL-3025).
|
|
135
|
+
*/
|
|
136
|
+
export declare function registerLivePageDropper(drop: LivePageDropper): () => void;
|
|
137
|
+
export interface DatasetAnnounceResult {
|
|
138
|
+
/** Sites whose cached pages were dropped. */
|
|
139
|
+
sitesRefreshed: number;
|
|
140
|
+
/** Pages named across those sites. */
|
|
141
|
+
pathsRefreshed: number;
|
|
142
|
+
/** Sites the drop was attempted on and refused. */
|
|
143
|
+
sitesRefused: number;
|
|
144
|
+
/** Sites past the fan-out cap, and sites whose scan was truncated. */
|
|
145
|
+
hostsDropped: number;
|
|
146
|
+
scanTruncated: boolean;
|
|
147
|
+
/** `'throttled'` when a drop for this pair already went out just now. */
|
|
148
|
+
reason: DatasetLivePageScope['reason'] | 'throttled' | 'no-dropper';
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Refresh every live page that shows this dataset's records.
|
|
152
|
+
*
|
|
153
|
+
* The one call a write path makes. BEST EFFORT, ALWAYS: it never throws and
|
|
154
|
+
* never rejects, so a caller can `await` it inside the try that already guards
|
|
155
|
+
* the write without any chance of turning a stored record into a failed one.
|
|
156
|
+
*
|
|
157
|
+
* `drop` overrides the registered dropper for callers that hold their own —
|
|
158
|
+
* the console, whose announce is an authenticated request rather than a local
|
|
159
|
+
* cache call.
|
|
160
|
+
*/
|
|
161
|
+
export declare function announceDatasetRecordChange(options: {
|
|
162
|
+
firestore: Firestore;
|
|
163
|
+
orgId: string;
|
|
164
|
+
datasetId: string;
|
|
165
|
+
drop?: LivePageDropper;
|
|
166
|
+
}): Promise<DatasetAnnounceResult>;
|
|
167
|
+
/**
|
|
168
|
+
* One sentence for a refresh that did not cover every page, or `null` when it
|
|
169
|
+
* did.
|
|
170
|
+
*
|
|
171
|
+
* Beside the call rather than at each surface, so the console cannot word this
|
|
172
|
+
* differently in two places — the same reason `describeRevalidateShortfall`
|
|
173
|
+
* lives beside `revalidateLivePages`. `lead` names the act that succeeded,
|
|
174
|
+
* because only that word differs between surfaces: a record is SAVED, and
|
|
175
|
+
* telling someone who fixed a typo that they published it is a worse error
|
|
176
|
+
* than the staleness the sentence is about.
|
|
177
|
+
*/
|
|
178
|
+
export declare function describeDatasetAnnounceShortfall(result: DatasetAnnounceResult | null, lead?: string): string | null;
|
|
179
|
+
export default announceDatasetRecordChange;
|