@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,643 @@
|
|
|
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
|
+
* Server half of the lockdown system (AGL-1501): the verdict every
|
|
19
|
+
* enforcement chokepoint asks for. The pure shape/precedence logic lives in
|
|
20
|
+
* `@aglyn/aglyn` `app-utils/lockdown.ts`; this module adds the Admin-SDK
|
|
21
|
+
* reads and the two properties only the server can hold:
|
|
22
|
+
*
|
|
23
|
+
* **The un-panic invariant.** A verified `staff` claim bypasses EVERY scope,
|
|
24
|
+
* unconditionally, before any read happens. A platform-wide lockdown must
|
|
25
|
+
* never lock out the staff who can lift it — a panic button that panics its
|
|
26
|
+
* own operator is worse than none. Nothing may be added to
|
|
27
|
+
* `getLockdownVerdict` above the staff check.
|
|
28
|
+
*
|
|
29
|
+
* **Fail-open on infrastructure error.** If the `lockdowns/platform` read
|
|
30
|
+
* throws (Firestore outage, emulator without the collection), the verdict is
|
|
31
|
+
* "not locked": an infrastructure blip must not weld the whole platform
|
|
32
|
+
* shut. The same posture as the sanctions gate on an absent geo signal
|
|
33
|
+
* (AGL-1492) and the release-flag reader on a missing template.
|
|
34
|
+
*
|
|
35
|
+
* **…except for takedowns (AGL-1621).** One class of lock inverts that cost
|
|
36
|
+
* function: a legal or abuse takedown must keep holding when Firestore is
|
|
37
|
+
* unreachable, because "we served it, our database was down" answers no
|
|
38
|
+
* court order. The class is an EXPLICIT stored field (`enforcement`), never
|
|
39
|
+
* inferred from scope or reason, and absent means `standard` — so every
|
|
40
|
+
* lock that exists today, and every lock an operator does not classify,
|
|
41
|
+
* keeps failing open exactly as before. See the takedown ledger below for
|
|
42
|
+
* how a classification survives the read that loses it.
|
|
43
|
+
*/ import { featureLockdownDocId, orgFeatureLockdownDocId, isLockdownActive, isTakedownLockdown, lockdownFeatureStaffBypass, LOCKDOWNS_COLLECTION, lockdownBlocks, lockdownIntentForMethod, lockdownNotice, lockdownRetryAfterSeconds, domainLockdownDocId, normalizeHostLockdown, normalizeLockdownDoc, normalizeOrgLockdown, PLATFORM_LOCKDOWN_DOC_ID, resolveLockdown, userLockdownDocId } from "@aglyn/aglyn/server";
|
|
44
|
+
import { getApp } from "firebase-admin/app";
|
|
45
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
46
|
+
/**
|
|
47
|
+
* Platform-doc TTL. Short on purpose: this is the panic path, and 15s is the
|
|
48
|
+
* worst-case lag between staff pressing the button and a warm server process
|
|
49
|
+
* refusing its next request. Per-request reads would be ~1 Firestore read
|
|
50
|
+
* per API call platform-wide, which AGL-1302 showed is real money at scale.
|
|
51
|
+
*/ const PLATFORM_TTL_MS = 15000;
|
|
52
|
+
/**
|
|
53
|
+
* ===================== THE TAKEDOWN LEDGER (AGL-1621) =====================
|
|
54
|
+
*
|
|
55
|
+
* Lockdown fails open, and that stays true for ordinary locks: an
|
|
56
|
+
* unreachable Firestore is an outage, not a lockdown, and a blip must never
|
|
57
|
+
* weld every customer site shut. But a legal or abuse TAKEDOWN has the
|
|
58
|
+
* opposite cost function — "we served the infringing content because our
|
|
59
|
+
* database was down" is not an answer to a court order. So takedown-class
|
|
60
|
+
* locks (`enforcement: 'takedown'`) hold through a failed read, and
|
|
61
|
+
* everything else keeps today's behaviour exactly.
|
|
62
|
+
*
|
|
63
|
+
* ## The ordering problem this exists to solve
|
|
64
|
+
*
|
|
65
|
+
* The classification is stored ON the lock document. When the read throws
|
|
66
|
+
* there IS no document, so the class cannot be consulted at failure time —
|
|
67
|
+
* the obvious implementation ("if the lock is a takedown, fail closed") is
|
|
68
|
+
* unimplementable as written, because the failure is precisely the loss of
|
|
69
|
+
* the thing it wants to branch on.
|
|
70
|
+
*
|
|
71
|
+
* The only honest source is what this process last SUCCESSFULLY read. So
|
|
72
|
+
* every successful read records its verdict here: a takedown is remembered,
|
|
73
|
+
* and anything else RETIRES the entry. A subsequent failed read consults
|
|
74
|
+
* the ledger instead of falling to null.
|
|
75
|
+
*
|
|
76
|
+
* ## What this does and does not guarantee — stated, not implied
|
|
77
|
+
*
|
|
78
|
+
* HOLDS: a takedown that a process has already observed keeps being
|
|
79
|
+
* enforced by that process for as long as Firestore is unreachable. That is
|
|
80
|
+
* the real incident shape — a takedown placed hours or days ago, then a
|
|
81
|
+
* Firestore blip — and it is what the decision asks for.
|
|
82
|
+
*
|
|
83
|
+
* DOES NOT HOLD: a COLD process that has never once read the takedown has
|
|
84
|
+
* nothing to remember and fails open, like everything else. Closing that
|
|
85
|
+
* gap needs a second carrier more available than Firestore (edge config or
|
|
86
|
+
* similar), which is a different and much larger change; pretending
|
|
87
|
+
* otherwise here would be worse than naming it. A takedown placed DURING a
|
|
88
|
+
* total Firestore outage is likewise not enforceable — the write itself
|
|
89
|
+
* cannot land.
|
|
90
|
+
*
|
|
91
|
+
* ## Two things it deliberately does not do
|
|
92
|
+
*
|
|
93
|
+
* It is NOT cleared by `invalidate*LockdownCache`. Those run after a staff
|
|
94
|
+
* write, and clearing the ledger there would mean a lock-then-outage
|
|
95
|
+
* sequence forgot the takedown at the exact moment it mattered. The next
|
|
96
|
+
* successful read is what retires an entry, and a lift IS such a read.
|
|
97
|
+
*
|
|
98
|
+
* It does not outlive the lock's own expiry: a held takedown is still
|
|
99
|
+
* checked against `isLockdownActive`, so a `untilMs` that has passed
|
|
100
|
+
* releases on schedule even while Firestore is down. A dead-man expiry must
|
|
101
|
+
* not become un-liftable by being classified.
|
|
102
|
+
*/ const takedownLedger = new Map();
|
|
103
|
+
/**
|
|
104
|
+
* Bounded like the user and domain caches, and for the same reason: two of
|
|
105
|
+
* the four keyed readers take their key from caller-controlled input.
|
|
106
|
+
* Eviction is least-recently-USED.
|
|
107
|
+
*/ export const TAKEDOWN_LEDGER_MAX = 5000;
|
|
108
|
+
/**
|
|
109
|
+
* Record what a SUCCESSFUL read saw. A takedown is remembered; anything
|
|
110
|
+
* else — no lock, an ordinary lock, or a lock whose class this build cannot
|
|
111
|
+
* interpret — retires the entry, which is how a lift takes effect.
|
|
112
|
+
*/ function rememberTakedown(key, state) {
|
|
113
|
+
if (!state || !isTakedownLockdown(state)) {
|
|
114
|
+
takedownLedger.delete(key);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Re-insert to refresh recency before the LRU trim below.
|
|
118
|
+
takedownLedger.delete(key);
|
|
119
|
+
takedownLedger.set(key, state);
|
|
120
|
+
while(takedownLedger.size > TAKEDOWN_LEDGER_MAX){
|
|
121
|
+
const oldest = takedownLedger.keys().next().value;
|
|
122
|
+
if (oldest === undefined) break;
|
|
123
|
+
takedownLedger.delete(oldest);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What a FAILED read should serve: a remembered, still-active takedown, or
|
|
128
|
+
* null — which is the shipped fail-open answer and therefore the default
|
|
129
|
+
* for every lock that is not an observed takedown.
|
|
130
|
+
*/ function heldTakedown(key, nowMs) {
|
|
131
|
+
const state = takedownLedger.get(key);
|
|
132
|
+
if (!state) return null;
|
|
133
|
+
if (!isLockdownActive(state, nowMs)) {
|
|
134
|
+
takedownLedger.delete(key);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Forget every remembered takedown. NOT part of the panic path and not
|
|
141
|
+
* called by the admin route — this exists for process-boundary and test
|
|
142
|
+
* setup, where a ledger surviving between cases would make one test's lock
|
|
143
|
+
* another test's fail-closed verdict.
|
|
144
|
+
*/ export function resetTakedownLedger() {
|
|
145
|
+
takedownLedger.clear();
|
|
146
|
+
}
|
|
147
|
+
/** Ledger keys. Disjoint by construction — the doc-id prefixes already are. */ const PLATFORM_LEDGER_KEY = 'platform';
|
|
148
|
+
const featureLedgerKey = (feature)=>featureLockdownDocId(feature);
|
|
149
|
+
const userLedgerKey = (uid)=>userLockdownDocId(uid);
|
|
150
|
+
const domainLedgerKey = (hostname)=>domainLockdownDocId(hostname);
|
|
151
|
+
/**
|
|
152
|
+
* ORG and HOST scope (AGL-1881). These two locks have no document in
|
|
153
|
+
* `lockdowns/*` — they live as the `suspended*` family ON the org and host
|
|
154
|
+
* documents — so they never passed through a ledgered reader, and
|
|
155
|
+
* `getSiteLockdown` answered `null` flat when its reads threw. The route
|
|
156
|
+
* persists `suspendedEnforcement: 'takedown'` for both scopes all the same,
|
|
157
|
+
* from a control that promises the lock "keeps holding if Aglyn cannot reach
|
|
158
|
+
* the database". A court-ordered host takedown plus a partial Firestore
|
|
159
|
+
* outage therefore served the infringing site for the duration of the
|
|
160
|
+
* outage, which is the precise failure the mode exists to prevent.
|
|
161
|
+
*
|
|
162
|
+
* Keyed by HOST ID, both of them, including the org one. Every reader on
|
|
163
|
+
* this path is host-addressed, and on a failed read the org id is exactly as
|
|
164
|
+
* unreadable as the lock — `getOrgForHost` is the read that just threw. So
|
|
165
|
+
* "the org lock as seen through this host" is the only thing a failed read
|
|
166
|
+
* can look up, and it matches the ledger's existing warm-process semantics:
|
|
167
|
+
* an org takedown holds for each site a process has already read once.
|
|
168
|
+
*
|
|
169
|
+
* Distinct prefixes keep them disjoint from the four doc-id keys above
|
|
170
|
+
* (`platform`, `feature--`, `user--`, `domain--`).
|
|
171
|
+
*/ const orgForHostLedgerKey = (hostId)=>`org--for-host--${hostId}`;
|
|
172
|
+
const hostLedgerKey = (hostId)=>`host--${hostId}`;
|
|
173
|
+
/**
|
|
174
|
+
* Record what a SUCCESSFUL site read saw, per scope. Same contract as the
|
|
175
|
+
* four `lockdowns/*` readers: a takedown is remembered, and anything else —
|
|
176
|
+
* no lock, an ordinary lock, a takedown DOWNGRADED to one — retires the
|
|
177
|
+
* entry, which is how a lift takes effect.
|
|
178
|
+
*/ export function rememberSiteTakedown(hostId, states) {
|
|
179
|
+
rememberTakedown(orgForHostLedgerKey(hostId), states.org);
|
|
180
|
+
rememberTakedown(hostLedgerKey(hostId), states.host);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* What a FAILED site read may still serve: the remembered, still-active
|
|
184
|
+
* takedowns at org and host scope, or nulls — which is the shipped fail-open
|
|
185
|
+
* answer and therefore the default for every lock that is not an observed
|
|
186
|
+
* takedown.
|
|
187
|
+
*/ export function heldSiteTakedown(hostId, nowMs) {
|
|
188
|
+
return {
|
|
189
|
+
org: heldTakedown(orgForHostLedgerKey(hostId), nowMs),
|
|
190
|
+
host: heldTakedown(hostLedgerKey(hostId), nowMs)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
let platformCache;
|
|
194
|
+
let platformPending;
|
|
195
|
+
/**
|
|
196
|
+
* Drop the in-process platform cache. Called by /api/admin/lockdown after a
|
|
197
|
+
* platform write so the process that took the action serves fresh verdicts
|
|
198
|
+
* immediately; other processes converge within PLATFORM_TTL_MS.
|
|
199
|
+
*/ export function invalidatePlatformLockdownCache() {
|
|
200
|
+
platformCache = undefined;
|
|
201
|
+
platformPending = undefined;
|
|
202
|
+
}
|
|
203
|
+
/** `lockdowns/platform`, normalized; null = not locked (incl. on error). */ export async function getPlatformLockdown() {
|
|
204
|
+
if (platformCache && Date.now() - platformCache.at < PLATFORM_TTL_MS) {
|
|
205
|
+
return platformCache.state;
|
|
206
|
+
}
|
|
207
|
+
if (!platformPending) {
|
|
208
|
+
platformPending = (async ()=>{
|
|
209
|
+
let state;
|
|
210
|
+
try {
|
|
211
|
+
const snapshot = await firebaseAdmin.app().firestore().collection(LOCKDOWNS_COLLECTION).doc(PLATFORM_LOCKDOWN_DOC_ID).get();
|
|
212
|
+
state = snapshot.exists ? normalizeLockdownDoc(snapshot.data(), 'platform') : null;
|
|
213
|
+
rememberTakedown(PLATFORM_LEDGER_KEY, state);
|
|
214
|
+
} catch (unused) {
|
|
215
|
+
// Fail open: an unreachable Firestore is an outage, not a lockdown
|
|
216
|
+
// — UNLESS this process has already seen a takedown here, which is
|
|
217
|
+
// the one class that must survive the read failing (AGL-1621).
|
|
218
|
+
state = heldTakedown(PLATFORM_LEDGER_KEY, Date.now());
|
|
219
|
+
}
|
|
220
|
+
platformCache = {
|
|
221
|
+
at: Date.now(),
|
|
222
|
+
state
|
|
223
|
+
};
|
|
224
|
+
return state;
|
|
225
|
+
})().finally(()=>{
|
|
226
|
+
platformPending = undefined;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return platformPending;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Feature-doc cache (AGL-1510): same TTL and the same reasoning as the
|
|
233
|
+
* platform doc — feature gates sit on hot chokepoints (the session mint,
|
|
234
|
+
* every media ingress, the plugin dispatcher), and 15s is the worst-case
|
|
235
|
+
* lag between staff flipping a feature switch and a warm process enforcing
|
|
236
|
+
* it. One map for all keys; the admin route invalidates after every write.
|
|
237
|
+
*/ /**
|
|
238
|
+
* Keyed by DOCUMENT ID rather than by feature key since AGL-2927, so the
|
|
239
|
+
* platform-wide `feature--{key}` entry and each workspace's
|
|
240
|
+
* `feature--{key}--org--{orgId}` entry share one map, one TTL and one
|
|
241
|
+
* invalidation — the admin route's write clears both kinds at once.
|
|
242
|
+
*/ const featureCache = new Map();
|
|
243
|
+
const featurePending = new Map();
|
|
244
|
+
/** Drop the in-process feature cache (all keys) after an admin write. */ export function invalidateFeatureLockdownCache() {
|
|
245
|
+
featureCache.clear();
|
|
246
|
+
featurePending.clear();
|
|
247
|
+
}
|
|
248
|
+
/** One feature-scope carrier, by document id — see `getFeatureLockdown`. */ async function readFeatureLockdownDoc(docId) {
|
|
249
|
+
const cached = featureCache.get(docId);
|
|
250
|
+
if (cached && Date.now() - cached.at < PLATFORM_TTL_MS) return cached.state;
|
|
251
|
+
let pending = featurePending.get(docId);
|
|
252
|
+
if (!pending) {
|
|
253
|
+
pending = (async ()=>{
|
|
254
|
+
let state;
|
|
255
|
+
try {
|
|
256
|
+
const snapshot = await firebaseAdmin.app().firestore().collection(LOCKDOWNS_COLLECTION).doc(docId).get();
|
|
257
|
+
state = snapshot.exists ? normalizeLockdownDoc(snapshot.data(), 'feature') : null;
|
|
258
|
+
rememberTakedown(docId, state);
|
|
259
|
+
} catch (unused) {
|
|
260
|
+
// Fail open, matching the platform read: an unreachable Firestore is
|
|
261
|
+
// an outage, not a feature lockdown. A takedown-class feature lock
|
|
262
|
+
// this process has already observed still holds (AGL-1621).
|
|
263
|
+
state = heldTakedown(docId, Date.now());
|
|
264
|
+
}
|
|
265
|
+
featureCache.set(docId, {
|
|
266
|
+
at: Date.now(),
|
|
267
|
+
state
|
|
268
|
+
});
|
|
269
|
+
return state;
|
|
270
|
+
})().finally(()=>{
|
|
271
|
+
featurePending.delete(docId);
|
|
272
|
+
});
|
|
273
|
+
featurePending.set(docId, pending);
|
|
274
|
+
}
|
|
275
|
+
return pending;
|
|
276
|
+
}
|
|
277
|
+
/** `lockdowns/feature--{key}`, normalized; null = not locked (incl. error). */ export async function getFeatureLockdown(feature) {
|
|
278
|
+
return readFeatureLockdownDoc(featureLedgerKey(feature));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* `lockdowns/feature--{key}--org--{orgId}` (AGL-2927): the same capability
|
|
282
|
+
* paused for ONE workspace. Same cache, same TTL, same fail-open posture as
|
|
283
|
+
* the platform-wide document; null = not paused for that org.
|
|
284
|
+
*/ export async function getOrgFeatureLockdown(feature, orgId) {
|
|
285
|
+
return readFeatureLockdownDoc(orgFeatureLockdownDocId(feature, orgId));
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The feature verdict and refusal in one call (AGL-1510) — the one-line
|
|
289
|
+
* wiring a feature chokepoint carries:
|
|
290
|
+
*
|
|
291
|
+
* const locked = await featureLockdownRefusal({ feature: 'checkout', staff })
|
|
292
|
+
* if (locked) return locked
|
|
293
|
+
*
|
|
294
|
+
* Composition, not ranking: a PLATFORM lock implies every feature, so it is
|
|
295
|
+
* checked first (TTL-cached — routes that already ran the scope verdict pay
|
|
296
|
+
* no extra read), and the platform-scope staff bypass there is UNCHANGED
|
|
297
|
+
* and unconditional. The feature doc is checked second, and its staff
|
|
298
|
+
* bypass is per-feature (`lockdownFeatureStaffBypass`): granted where a
|
|
299
|
+
* staff action aids incident response (uploads, installs, ai-assist),
|
|
300
|
+
* withheld where it would BE the incident (checkout — a staff checkout
|
|
301
|
+
* session is still a real charge). A feature lock implies nothing about the
|
|
302
|
+
* org/host/user scopes — those stay with `lockdownRefusal` at the routes
|
|
303
|
+
* that carry it.
|
|
304
|
+
*
|
|
305
|
+
* A door that names the org it serves passes `orgId` (AGL-2927), and the
|
|
306
|
+
* workspace-scoped carrier for the same feature is checked third, under the
|
|
307
|
+
* same per-feature staff bypass: the staff AI pause is a spend stop on one
|
|
308
|
+
* customer, and a staff call made to verify the pause is ours to spend. A
|
|
309
|
+
* door that names no org — the plugin dispatcher, which gates by path
|
|
310
|
+
* before the body is read — sees the platform-wide document only, which is
|
|
311
|
+
* what it saw before.
|
|
312
|
+
*/ export async function featureLockdownRefusal(options) {
|
|
313
|
+
var _options_nowMs;
|
|
314
|
+
var _options_orgId;
|
|
315
|
+
const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
|
|
316
|
+
const platform = await getPlatformLockdown();
|
|
317
|
+
if (isLockdownActive(platform, nowMs)) {
|
|
318
|
+
// The un-panic invariant at the platform scope, unchanged.
|
|
319
|
+
if (options.staff === true) return null;
|
|
320
|
+
return lockdownJsonResponse(platform);
|
|
321
|
+
}
|
|
322
|
+
const bypass = options.staff === true && lockdownFeatureStaffBypass(options.feature);
|
|
323
|
+
const state = await getFeatureLockdown(options.feature);
|
|
324
|
+
if (isLockdownActive(state, nowMs)) {
|
|
325
|
+
return bypass ? null : lockdownJsonResponse(state);
|
|
326
|
+
}
|
|
327
|
+
const orgId = (_options_orgId = options.orgId) == null ? void 0 : _options_orgId.trim();
|
|
328
|
+
if (!orgId) return null;
|
|
329
|
+
const paused = await getOrgFeatureLockdown(options.feature, orgId);
|
|
330
|
+
if (!isLockdownActive(paused, nowMs)) return null;
|
|
331
|
+
return bypass ? null : lockdownJsonResponse(paused);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* User-doc cache (AGL-1522): the last per-call read on the verdict path.
|
|
335
|
+
* AGL-1506 wired the verdict into ~36 routes plus the session mint/exchange,
|
|
336
|
+
* and every call carrying a `uid` paid one Firestore get on
|
|
337
|
+
* `lockdowns/user--{uid}` — an active console editor generated one read per
|
|
338
|
+
* wired mutation, against the AGL-1302 read budget. Same TTL, same pending
|
|
339
|
+
* dedupe, same fail-open posture as the platform and feature caches above —
|
|
340
|
+
* ONE cache pattern in this module, not three implementations.
|
|
341
|
+
*
|
|
342
|
+
* The staleness tradeoff, stated: a user locked mid-window keeps passing
|
|
343
|
+
* this check for up to PLATFORM_TTL_MS (15s) plus the caller's polling
|
|
344
|
+
* cadence — on every process EXCEPT the one that took the action, which
|
|
345
|
+
* refuses immediately because /api/admin/lockdown invalidates this uid's
|
|
346
|
+
* entry after the write (the platform/feature invalidation hook, mirrored).
|
|
347
|
+
* That bound is acceptable here for the same reason `media/sign` omits the
|
|
348
|
+
* user scope entirely: the hard kill never rode this read. A user lock also
|
|
349
|
+
* disables the Auth account and revokes refresh tokens, so the ID token
|
|
350
|
+
* stops refreshing and the session cookie dies at its next
|
|
351
|
+
* `verifySessionCookie(…, true)` — the cache can only delay the DISTINCT
|
|
352
|
+
* 423 body, not access itself, and 15s matches the platform scope's stated
|
|
353
|
+
* worst case and the drill's measured flip times.
|
|
354
|
+
*
|
|
355
|
+
* Bounded so a scan of uids cannot balloon a warm process: past
|
|
356
|
+
* USER_LOCKDOWN_CACHE_MAX entries the least-recently-USED is evicted (a
|
|
357
|
+
* cache hit refreshes recency, so active sessions survive a scan).
|
|
358
|
+
*/ export const USER_LOCKDOWN_CACHE_MAX = 5000;
|
|
359
|
+
const userCache = new Map();
|
|
360
|
+
const userPending = new Map();
|
|
361
|
+
/**
|
|
362
|
+
* Drop one uid's cached verdict (after a user lock/unlock write), or the
|
|
363
|
+
* whole cache when called bare. The acting process serves a fresh verdict
|
|
364
|
+
* immediately; other processes converge within PLATFORM_TTL_MS.
|
|
365
|
+
*/ export function invalidateUserLockdownCache(uid) {
|
|
366
|
+
if (uid === undefined) {
|
|
367
|
+
userCache.clear();
|
|
368
|
+
userPending.clear();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
userCache.delete(uid);
|
|
372
|
+
userPending.delete(uid);
|
|
373
|
+
}
|
|
374
|
+
/** `lockdowns/user--{uid}`, normalized; null = not locked (incl. on error). */ export async function getUserLockdown(uid) {
|
|
375
|
+
const cached = userCache.get(uid);
|
|
376
|
+
if (cached && Date.now() - cached.at < PLATFORM_TTL_MS) {
|
|
377
|
+
// Re-insert to refresh recency: eviction is by least-recently-used, and
|
|
378
|
+
// an ACTIVE session must not be the one a uid scan pushes out.
|
|
379
|
+
userCache.delete(uid);
|
|
380
|
+
userCache.set(uid, cached);
|
|
381
|
+
return cached.state;
|
|
382
|
+
}
|
|
383
|
+
let pending = userPending.get(uid);
|
|
384
|
+
if (!pending) {
|
|
385
|
+
pending = (async ()=>{
|
|
386
|
+
let state;
|
|
387
|
+
try {
|
|
388
|
+
const snapshot = await firebaseAdmin.app().firestore().collection(LOCKDOWNS_COLLECTION).doc(userLockdownDocId(uid)).get();
|
|
389
|
+
state = snapshot.exists ? normalizeLockdownDoc(snapshot.data(), 'user') : null;
|
|
390
|
+
rememberTakedown(userLedgerKey(uid), state);
|
|
391
|
+
} catch (unused) {
|
|
392
|
+
// Fail open, matching the platform and feature reads: an unreachable
|
|
393
|
+
// Firestore is an outage, not a lockdown. An observed takedown-class
|
|
394
|
+
// user lock holds through it (AGL-1621).
|
|
395
|
+
state = heldTakedown(userLedgerKey(uid), Date.now());
|
|
396
|
+
}
|
|
397
|
+
userCache.delete(uid);
|
|
398
|
+
userCache.set(uid, {
|
|
399
|
+
at: Date.now(),
|
|
400
|
+
state
|
|
401
|
+
});
|
|
402
|
+
while(userCache.size > USER_LOCKDOWN_CACHE_MAX){
|
|
403
|
+
const oldest = userCache.keys().next().value;
|
|
404
|
+
if (oldest === undefined) break;
|
|
405
|
+
userCache.delete(oldest);
|
|
406
|
+
}
|
|
407
|
+
return state;
|
|
408
|
+
})().finally(()=>{
|
|
409
|
+
userPending.delete(uid);
|
|
410
|
+
});
|
|
411
|
+
userPending.set(uid, pending);
|
|
412
|
+
}
|
|
413
|
+
return pending;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* DOMAIN-lock cache (AGL-1513), same shape and same TTL as the user cache.
|
|
417
|
+
*
|
|
418
|
+
* Bounded for the same reason and one more: the key is a HOSTNAME taken from
|
|
419
|
+
* the request, so an unbounded map here would be attacker-keyed by anyone who
|
|
420
|
+
* can point a DNS record at us — which, for the hijack incident this scope
|
|
421
|
+
* exists for, is the adversary by definition.
|
|
422
|
+
*/ export const DOMAIN_LOCKDOWN_CACHE_MAX = 5000;
|
|
423
|
+
const domainCache = new Map();
|
|
424
|
+
const domainPending = new Map();
|
|
425
|
+
/**
|
|
426
|
+
* Drop one hostname's cached verdict (after a domain lock/unlock write), or
|
|
427
|
+
* the whole cache when called bare.
|
|
428
|
+
*/ export function invalidateDomainLockdownCache(hostname) {
|
|
429
|
+
if (hostname === undefined) {
|
|
430
|
+
domainCache.clear();
|
|
431
|
+
domainPending.clear();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const key = hostname.trim().toLowerCase();
|
|
435
|
+
domainCache.delete(key);
|
|
436
|
+
domainPending.delete(key);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* `lockdowns/domain--{hostname}`, normalized; null = not locked (incl. on
|
|
440
|
+
* error), matching every other reader here — an unreachable Firestore is an
|
|
441
|
+
* outage, not a lockdown.
|
|
442
|
+
*/ export async function getDomainLockdown(hostname) {
|
|
443
|
+
const key = String(hostname != null ? hostname : '').trim().toLowerCase();
|
|
444
|
+
if (!key) return null;
|
|
445
|
+
const cached = domainCache.get(key);
|
|
446
|
+
if (cached && Date.now() - cached.at < PLATFORM_TTL_MS) {
|
|
447
|
+
// Re-insert to refresh recency, like the user cache: eviction is
|
|
448
|
+
// least-recently-USED, and a domain under active dispute is exactly the
|
|
449
|
+
// one a scan must not push out.
|
|
450
|
+
domainCache.delete(key);
|
|
451
|
+
domainCache.set(key, cached);
|
|
452
|
+
return cached.state;
|
|
453
|
+
}
|
|
454
|
+
let pending = domainPending.get(key);
|
|
455
|
+
if (!pending) {
|
|
456
|
+
pending = (async ()=>{
|
|
457
|
+
let state;
|
|
458
|
+
try {
|
|
459
|
+
const snapshot = await firebaseAdmin.app().firestore().collection(LOCKDOWNS_COLLECTION).doc(domainLockdownDocId(key)).get();
|
|
460
|
+
state = snapshot.exists ? normalizeLockdownDoc(snapshot.data(), 'domain') : null;
|
|
461
|
+
rememberTakedown(domainLedgerKey(key), state);
|
|
462
|
+
} catch (unused) {
|
|
463
|
+
// Fail open, as above — and, as above, an observed takedown holds.
|
|
464
|
+
// This is the scope where that matters most: AGL-1513's hijack and
|
|
465
|
+
// dispute incidents are takedowns almost by definition.
|
|
466
|
+
state = heldTakedown(domainLedgerKey(key), Date.now());
|
|
467
|
+
}
|
|
468
|
+
domainCache.delete(key);
|
|
469
|
+
domainCache.set(key, {
|
|
470
|
+
at: Date.now(),
|
|
471
|
+
state
|
|
472
|
+
});
|
|
473
|
+
while(domainCache.size > DOMAIN_LOCKDOWN_CACHE_MAX){
|
|
474
|
+
const oldest = domainCache.keys().next().value;
|
|
475
|
+
if (oldest === undefined) break;
|
|
476
|
+
domainCache.delete(oldest);
|
|
477
|
+
}
|
|
478
|
+
return state;
|
|
479
|
+
})().finally(()=>{
|
|
480
|
+
domainPending.delete(key);
|
|
481
|
+
});
|
|
482
|
+
domainPending.set(key, pending);
|
|
483
|
+
}
|
|
484
|
+
return pending;
|
|
485
|
+
}
|
|
486
|
+
/** The declared or derived intent of a verdict request; `write` by default. */ function verdictIntent(options) {
|
|
487
|
+
if (options.intent) return options.intent;
|
|
488
|
+
if (options.request) return lockdownIntentForMethod(options.request.method);
|
|
489
|
+
return 'write';
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The one verdict: the lockdown that REFUSES this caller's request, or null.
|
|
493
|
+
* Precedence platform > org > host > user, strictness before width (from
|
|
494
|
+
* `resolveLockdown`).
|
|
495
|
+
*
|
|
496
|
+
* "Refuses this request" rather than "covers this caller" since AGL-1511: a
|
|
497
|
+
* read-only lock is active but does not refuse a read, and returning it
|
|
498
|
+
* anyway would make every wired chokepoint 423 a GET — a read-only mode that
|
|
499
|
+
* behaves exactly like a full one. The active state is still what
|
|
500
|
+
* `resolveLockdown` chose, so the staff probe and the notice copy see the
|
|
501
|
+
* real lock; only the ANSWER is intent-aware.
|
|
502
|
+
*/ export async function getLockdownVerdict(options) {
|
|
503
|
+
var _options_nowMs;
|
|
504
|
+
// THE UN-PANIC INVARIANT. Staff are never locked out, by any scope, ever
|
|
505
|
+
// — they are the only ones who can lift a lockdown. Keep this line first:
|
|
506
|
+
// everything below it may read Firestore, and a staff verdict must not
|
|
507
|
+
// depend on any read succeeding.
|
|
508
|
+
//
|
|
509
|
+
// This line is also the whole of AGL-1511's "staff writes bypass
|
|
510
|
+
// read-only": staff bypass every scope AND every mode, unconditionally,
|
|
511
|
+
// and read-only exists precisely so staff can work while the world reads.
|
|
512
|
+
// A mode-aware staff rule below would be a second bypass to keep correct.
|
|
513
|
+
if (options.staff === true) return null;
|
|
514
|
+
const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
|
|
515
|
+
const [platform, user] = await Promise.all([
|
|
516
|
+
getPlatformLockdown(),
|
|
517
|
+
options.uid ? getUserLockdown(options.uid) : Promise.resolve(null)
|
|
518
|
+
]);
|
|
519
|
+
const state = resolveLockdown({
|
|
520
|
+
platform,
|
|
521
|
+
org: normalizeOrgLockdown(options.org),
|
|
522
|
+
host: normalizeHostLockdown(options.host),
|
|
523
|
+
user
|
|
524
|
+
}, nowMs);
|
|
525
|
+
return lockdownBlocks(state, verdictIntent(options)) ? state : null;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The refusal an API route returns for a locked caller: **423 Locked** with
|
|
529
|
+
* a machine-readable body, so an API consumer sees "suspended: billing" and
|
|
530
|
+
* not a mystery 403. Carries only the sanitized, user-facing subset — never
|
|
531
|
+
* the actor uid or staff rationale.
|
|
532
|
+
*/ /**
|
|
533
|
+
* Verdict and refusal in one call — the two-line wiring every org-scoped
|
|
534
|
+
* API route carries (AGL-1506):
|
|
535
|
+
*
|
|
536
|
+
* const locked = await lockdownRefusal({ staff, uid, org, host })
|
|
537
|
+
* if (locked) return locked
|
|
538
|
+
*
|
|
539
|
+
* Null means "not locked, proceed". One mechanism: this delegates to
|
|
540
|
+
* `getLockdownVerdict` (so the un-panic invariant and fail-open posture are
|
|
541
|
+
* inherited, never re-implemented) and to `lockdownJsonResponse` for the
|
|
542
|
+
* distinct 423 body. Routes with a richer flow (the session mint clears
|
|
543
|
+
* cookies on refusal) keep calling the two halves directly.
|
|
544
|
+
*/ export async function lockdownRefusal(options) {
|
|
545
|
+
const state = await getLockdownVerdict(options);
|
|
546
|
+
return state ? lockdownJsonResponse(state, {
|
|
547
|
+
notice: options.notice
|
|
548
|
+
}) : null;
|
|
549
|
+
}
|
|
550
|
+
export function lockdownJsonResponse(state, options) {
|
|
551
|
+
var _ref;
|
|
552
|
+
const notice = (_ref = options == null ? void 0 : options.notice) != null ? _ref : lockdownNotice(state);
|
|
553
|
+
const retryAfter = lockdownRetryAfterSeconds(state, Date.now());
|
|
554
|
+
return Response.json(_extends({
|
|
555
|
+
error: 'locked',
|
|
556
|
+
scope: state.scope
|
|
557
|
+
}, state.mode ? {
|
|
558
|
+
mode: state.mode
|
|
559
|
+
} : {}, state.feature ? {
|
|
560
|
+
feature: state.feature
|
|
561
|
+
} : {}, {
|
|
562
|
+
reason: state.reason,
|
|
563
|
+
title: notice.title,
|
|
564
|
+
message: notice.body
|
|
565
|
+
}, notice.contact ? {
|
|
566
|
+
contact: notice.contact
|
|
567
|
+
} : {}, typeof state.untilMs === 'number' ? {
|
|
568
|
+
untilMs: state.untilMs
|
|
569
|
+
} : {}), {
|
|
570
|
+
status: 423,
|
|
571
|
+
headers: _extends({
|
|
572
|
+
'Cache-Control': 'no-store'
|
|
573
|
+
}, retryAfter ? {
|
|
574
|
+
'Retry-After': String(retryAfter)
|
|
575
|
+
} : {})
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/** Bounded: the panic page must render even when this call is the slow one. */ const IDENTITY_CONFIG_TIMEOUT_MS = 4000;
|
|
579
|
+
export async function readSignupsCreationTriggerStatus() {
|
|
580
|
+
let token;
|
|
581
|
+
let projectId;
|
|
582
|
+
try {
|
|
583
|
+
var _app_options_projectId;
|
|
584
|
+
var _this, _app_options_credential;
|
|
585
|
+
// `getApp()`, not `firebaseAdmin.app()`: the local shim narrows the app
|
|
586
|
+
// to its four accessors and drops `options`, which is where the
|
|
587
|
+
// credential and the project id live. Same acquisition path as the
|
|
588
|
+
// error-beacon target (`client-error-report.ts`).
|
|
589
|
+
const app = getApp();
|
|
590
|
+
projectId = (_app_options_projectId = app.options.projectId) != null ? _app_options_projectId : process.env['NEXT_PUBLIC_FIREBASE_PROJECT_ID'];
|
|
591
|
+
token = (_this = await ((_app_options_credential = app.options.credential) == null ? void 0 : _app_options_credential.getAccessToken())) == null ? void 0 : _this.access_token;
|
|
592
|
+
} catch (unused) {
|
|
593
|
+
token = undefined;
|
|
594
|
+
}
|
|
595
|
+
if (!token || !projectId) {
|
|
596
|
+
return {
|
|
597
|
+
status: 'unknown',
|
|
598
|
+
reason: 'No admin credential or project id on this deployment, so the ' + 'Identity Platform config could not be read.'
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const controller = new AbortController();
|
|
602
|
+
const timer = setTimeout(()=>controller.abort(), IDENTITY_CONFIG_TIMEOUT_MS);
|
|
603
|
+
try {
|
|
604
|
+
var _trigger_functionUri, _trigger_updateTime;
|
|
605
|
+
var _payload_blockingFunctions_triggers, _payload_blockingFunctions;
|
|
606
|
+
const response = await fetch(`https://identitytoolkit.googleapis.com/admin/v2/projects/${encodeURIComponent(projectId)}/config`, {
|
|
607
|
+
headers: {
|
|
608
|
+
Authorization: `Bearer ${token}`
|
|
609
|
+
},
|
|
610
|
+
signal: controller.signal
|
|
611
|
+
});
|
|
612
|
+
if (!response.ok) {
|
|
613
|
+
return {
|
|
614
|
+
status: 'unknown',
|
|
615
|
+
reason: `Identity Platform config read returned ${response.status}.`
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const payload = await response.json();
|
|
619
|
+
// The trigger key Identity Platform uses is `beforeCreate`; the Firebase
|
|
620
|
+
// SDK's `beforeUserCreated` is what registers it. Absent means the valve
|
|
621
|
+
// is not deployed OR was deployed and never registered — indistinguishable
|
|
622
|
+
// from here, and both mean the same thing to an operator: creation is not
|
|
623
|
+
// being refused.
|
|
624
|
+
const trigger = (_payload_blockingFunctions = payload.blockingFunctions) == null ? void 0 : (_payload_blockingFunctions_triggers = _payload_blockingFunctions.triggers) == null ? void 0 : _payload_blockingFunctions_triggers['beforeCreate'];
|
|
625
|
+
if (!trigger) return {
|
|
626
|
+
status: 'absent'
|
|
627
|
+
};
|
|
628
|
+
return {
|
|
629
|
+
status: 'armed',
|
|
630
|
+
functionUri: (_trigger_functionUri = trigger.functionUri) != null ? _trigger_functionUri : null,
|
|
631
|
+
updateTime: (_trigger_updateTime = trigger.updateTime) != null ? _trigger_updateTime : null
|
|
632
|
+
};
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return {
|
|
635
|
+
status: 'unknown',
|
|
636
|
+
reason: (error == null ? void 0 : error.name) === 'AbortError' ? 'Identity Platform config read timed out.' : 'Identity Platform config could not be read.'
|
|
637
|
+
};
|
|
638
|
+
} finally{
|
|
639
|
+
clearTimeout(timer);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
//# sourceMappingURL=lockdown.js.map
|