@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,300 @@
|
|
|
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
|
+
* Which live pages a change reaches, for a site's screens, layouts and
|
|
19
|
+
* components.
|
|
20
|
+
*
|
|
21
|
+
* The corpus read and the two closures over it, held here rather than in the
|
|
22
|
+
* console because the console is no longer the only side that needs them. A
|
|
23
|
+
* dataset record is written from the tenant as well — a form submission, an
|
|
24
|
+
* automation step — and those writes make the same pages stale as a console
|
|
25
|
+
* edit does. Two implementations of "which screens render this" is the shape
|
|
26
|
+
* that put AGL-1223 in the tree: one reader handled a node tree's second
|
|
27
|
+
* storage form and the other did not, so half the corpus answered "used
|
|
28
|
+
* nowhere" and a publish dropped no cache.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here is dataset-specific, and nothing is console-specific. The
|
|
31
|
+
* console's `scan-artifact-usage` re-exports these and keeps the parts that
|
|
32
|
+
* are about the console's own "what would I break" copy.
|
|
33
|
+
*/ import { nodesReferenceComponent, decodeStoredNodes } from "@aglyn/aglyn/server";
|
|
34
|
+
/** `displayName`, falling back to a legacy `name`, then the raw id. */ export function usageCandidateLabel(candidate) {
|
|
35
|
+
var _ref, _candidate_displayName;
|
|
36
|
+
return String((_ref = (_candidate_displayName = candidate.displayName) != null ? _candidate_displayName : candidate.name) != null ? _ref : candidate.id);
|
|
37
|
+
}
|
|
38
|
+
/** A soft-deleted document renders nothing, so it depends on nothing. */ export const isLiveUsageCandidate = (candidate)=>!candidate.deletedAt;
|
|
39
|
+
/**
|
|
40
|
+
* Everything that references a reusable component (AGL-703).
|
|
41
|
+
*
|
|
42
|
+
* Three places, because the renderer expands instances in three places:
|
|
43
|
+
* published screen versions, published layout versions, and OTHER component
|
|
44
|
+
* definitions — `composeReusableComponentNodes` grafts nested instances, so
|
|
45
|
+
* a component used only inside another component is genuinely used. Omitting
|
|
46
|
+
* that third scan would report "used nowhere" for it and invite a confident
|
|
47
|
+
* deletion, which is worse than showing nothing at all.
|
|
48
|
+
*/ export function scanComponentUsage(componentId, sources) {
|
|
49
|
+
if (!componentId) return [];
|
|
50
|
+
const dependents = [];
|
|
51
|
+
const collect = (candidates, type)=>{
|
|
52
|
+
for (const candidate of candidates){
|
|
53
|
+
if (!isLiveUsageCandidate(candidate)) continue;
|
|
54
|
+
// A component never counts as using itself, however it nests.
|
|
55
|
+
if (type === 'component' && candidate.id === componentId) continue;
|
|
56
|
+
if (!nodesReferenceComponent(candidate.nodes, componentId)) continue;
|
|
57
|
+
dependents.push(_extends({
|
|
58
|
+
type,
|
|
59
|
+
id: candidate.id,
|
|
60
|
+
name: usageCandidateLabel(candidate),
|
|
61
|
+
// Instances reference by id, so a rename can never break them.
|
|
62
|
+
via: [
|
|
63
|
+
'id'
|
|
64
|
+
]
|
|
65
|
+
}, candidate.versionId ? {
|
|
66
|
+
versionId: candidate.versionId
|
|
67
|
+
} : {}));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
collect(sources.screens, 'screen');
|
|
71
|
+
collect(sources.layouts, 'layout');
|
|
72
|
+
collect(sources.components, 'component');
|
|
73
|
+
return dependents;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Everything rendering inside a layout (AGL-703).
|
|
77
|
+
*
|
|
78
|
+
* Two kinds of dependent, both expressed by the same `layoutId` pointer:
|
|
79
|
+
*
|
|
80
|
+
* - **screens**, which name the layout they render inside;
|
|
81
|
+
* - **other layouts**, since a layout can itself sit inside one. A nested
|
|
82
|
+
* layout is a real dependent — deleting its parent unwraps every screen
|
|
83
|
+
* underneath it — so leaving layouts out would report a parent layout as
|
|
84
|
+
* used only by the screens that name it directly, and none of the ones
|
|
85
|
+
* that reach it through a child.
|
|
86
|
+
*
|
|
87
|
+
* A layout never counts as its own dependent; `canNestLayout` refuses that,
|
|
88
|
+
* and this refuses to report it even if stored data holds one.
|
|
89
|
+
*/ export function scanLayoutUsage(layoutId, screens, layouts = []) {
|
|
90
|
+
if (!layoutId) return [];
|
|
91
|
+
const dependentsOf = (candidates, type)=>candidates.filter((candidate)=>isLiveUsageCandidate(candidate) && candidate.layoutId === layoutId && candidate.id !== layoutId).map((candidate)=>_extends({
|
|
92
|
+
type,
|
|
93
|
+
id: candidate.id,
|
|
94
|
+
name: usageCandidateLabel(candidate),
|
|
95
|
+
via: [
|
|
96
|
+
'id'
|
|
97
|
+
]
|
|
98
|
+
}, candidate.versionId ? {
|
|
99
|
+
versionId: candidate.versionId
|
|
100
|
+
} : {}));
|
|
101
|
+
return [
|
|
102
|
+
...dependentsOf(screens, 'screen'),
|
|
103
|
+
...dependentsOf(layouts, 'layout')
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Every live screen rendered inside `layoutId`, at ANY nesting depth
|
|
108
|
+
* (AGL-1150).
|
|
109
|
+
*
|
|
110
|
+
* `scanLayoutUsage` answers one level. Layouts nest — a screen points at a
|
|
111
|
+
* layout, which can point at a parent layout, and `compose-screen-nodes` walks
|
|
112
|
+
* that whole chain when composing a page. So publishing a layout changes every
|
|
113
|
+
* screen below it, not just the ones bound to it directly, and a cache drop
|
|
114
|
+
* that only handles the direct level leaves the rest showing stale chrome for
|
|
115
|
+
* the full revalidate window.
|
|
116
|
+
*
|
|
117
|
+
* Pure, and separate from the Firestore read, so the nesting behaviour is
|
|
118
|
+
* testable without a database.
|
|
119
|
+
*
|
|
120
|
+
* Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written
|
|
121
|
+
* straight to Firestore is not bound by that, and a cycle here would hang a
|
|
122
|
+
* publish request rather than surface anything.
|
|
123
|
+
*/ export function screenIdsUsingLayoutDeep(layoutId, screens, layouts = []) {
|
|
124
|
+
if (!layoutId) return [];
|
|
125
|
+
const screenIds = new Set();
|
|
126
|
+
const seenLayouts = new Set([
|
|
127
|
+
layoutId
|
|
128
|
+
]);
|
|
129
|
+
let frontier = [
|
|
130
|
+
layoutId
|
|
131
|
+
];
|
|
132
|
+
while(frontier.length){
|
|
133
|
+
const next = [];
|
|
134
|
+
for (const id of frontier){
|
|
135
|
+
for (const dependent of scanLayoutUsage(id, screens, layouts)){
|
|
136
|
+
if (dependent.type === 'screen') {
|
|
137
|
+
screenIds.add(dependent.id);
|
|
138
|
+
} else if (!seenLayouts.has(dependent.id)) {
|
|
139
|
+
seenLayouts.add(dependent.id);
|
|
140
|
+
next.push(dependent.id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
frontier = next;
|
|
145
|
+
}
|
|
146
|
+
return [
|
|
147
|
+
...screenIds
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Every live screen whose rendered output contains `componentId`, however
|
|
152
|
+
* indirectly (AGL-1161).
|
|
153
|
+
*
|
|
154
|
+
* `scanComponentUsage` answers one level and returns three kinds of dependent.
|
|
155
|
+
* Only one of them is a screen, and the other two both reach screens by routes
|
|
156
|
+
* a single-level scan cannot see:
|
|
157
|
+
*
|
|
158
|
+
* - a **component** dependent nests the target inside itself, and that outer
|
|
159
|
+
* component may itself only be used inside a third — so component→component
|
|
160
|
+
* edges have to be followed to a fixed point;
|
|
161
|
+
* - a **layout** dependent puts the component in page chrome, which every
|
|
162
|
+
* screen under that layout renders. Layouts nest, so that is
|
|
163
|
+
* `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.
|
|
164
|
+
*
|
|
165
|
+
* Miss either and a publish reports success while some pages keep serving the
|
|
166
|
+
* old component for the full revalidate window — the failure this whole arc
|
|
167
|
+
* exists to remove, and the one that is hardest to notice because the pages
|
|
168
|
+
* that ARE dropped update instantly.
|
|
169
|
+
*
|
|
170
|
+
* Pure, and separate from the Firestore read, so the closure is testable
|
|
171
|
+
* without a database — the same split `screenIdsUsingLayoutDeep` uses.
|
|
172
|
+
*
|
|
173
|
+
* Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a
|
|
174
|
+
* document written straight to Firestore is not bound by what the editor
|
|
175
|
+
* allows, and a cycle here would hang a publish rather than surface anything.
|
|
176
|
+
*/ export function screenIdsUsingComponentDeep(componentId, sources) {
|
|
177
|
+
if (!componentId) return [];
|
|
178
|
+
const screenIds = new Set();
|
|
179
|
+
const seenComponents = new Set([
|
|
180
|
+
componentId
|
|
181
|
+
]);
|
|
182
|
+
// Layouts are resolved through their own deep walk, so remember which ones
|
|
183
|
+
// have already been expanded: two components in the same layout would
|
|
184
|
+
// otherwise re-walk the whole layout tree once each.
|
|
185
|
+
const seenLayouts = new Set();
|
|
186
|
+
let frontier = [
|
|
187
|
+
componentId
|
|
188
|
+
];
|
|
189
|
+
while(frontier.length){
|
|
190
|
+
const next = [];
|
|
191
|
+
for (const id of frontier){
|
|
192
|
+
for (const dependent of scanComponentUsage(id, sources)){
|
|
193
|
+
if (dependent.type === 'screen') {
|
|
194
|
+
screenIds.add(dependent.id);
|
|
195
|
+
} else if (dependent.type === 'layout') {
|
|
196
|
+
if (seenLayouts.has(dependent.id)) continue;
|
|
197
|
+
seenLayouts.add(dependent.id);
|
|
198
|
+
// The layout itself renders no URL; the screens beneath it do.
|
|
199
|
+
for (const screenId of screenIdsUsingLayoutDeep(dependent.id, sources.screens, sources.layouts)){
|
|
200
|
+
screenIds.add(screenId);
|
|
201
|
+
}
|
|
202
|
+
} else if (!seenComponents.has(dependent.id)) {
|
|
203
|
+
seenComponents.add(dependent.id);
|
|
204
|
+
next.push(dependent.id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
frontier = next;
|
|
209
|
+
}
|
|
210
|
+
return [
|
|
211
|
+
...screenIds
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* One collection's documents, with published nodes attached when the scan
|
|
216
|
+
* needs to search them.
|
|
217
|
+
*
|
|
218
|
+
* `limit` is a real bound, not a guess: it is fetched with one extra document
|
|
219
|
+
* so exceeding it is DETECTED rather than assumed away. A caller that ignores
|
|
220
|
+
* `truncated` is choosing to be wrong quietly.
|
|
221
|
+
*/ export async function readUsageCandidates(hostRef, collectionName, options) {
|
|
222
|
+
const { withNodes, limit } = options;
|
|
223
|
+
// One over the limit: if the extra document comes back, there was more than
|
|
224
|
+
// we are about to look at. Cheaper than a count() and exact.
|
|
225
|
+
const docs = await hostRef.collection(collectionName).limit(limit + 1).get();
|
|
226
|
+
const truncated = docs.size > limit;
|
|
227
|
+
const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs;
|
|
228
|
+
const candidates = await Promise.all(inScope.map(async (docSnapshot)=>{
|
|
229
|
+
const versionId = docSnapshot.get('versionId');
|
|
230
|
+
// Components keep their tree on the document; screens and layouts keep
|
|
231
|
+
// it on the published version.
|
|
232
|
+
//
|
|
233
|
+
// BOTH reads are decoded (AGL-1223). A component document is msgpack
|
|
234
|
+
// for anything promoted since components were compressed and a plain
|
|
235
|
+
// map for everything older, and `decodeStoredNodes` returns a map
|
|
236
|
+
// unchanged — so the branch below cannot be simplified back into a raw
|
|
237
|
+
// read on either side.
|
|
238
|
+
//
|
|
239
|
+
// What rides on this: every consumer walks the value with
|
|
240
|
+
// `Object.values`, which over a `Buffer` yields byte NUMBERS and
|
|
241
|
+
// matches nothing. The answer that produces is "used nowhere" — for
|
|
242
|
+
// `/api/hosts/where-used` an invitation to delete something a live page
|
|
243
|
+
// renders, and for `/api/screens/revalidate` a publish that drops no
|
|
244
|
+
// cache and leaves the old page served.
|
|
245
|
+
const nodes = collectionName === 'components' ? decodeStoredNodes(docSnapshot.get('nodes')) : withNodes && versionId ? await docSnapshot.ref.collection('versions').doc(String(versionId)).get().then((version)=>decodeStoredNodes(version.get('nodes'))).catch(()=>null) : null;
|
|
246
|
+
return _extends({
|
|
247
|
+
id: docSnapshot.id,
|
|
248
|
+
displayName: docSnapshot.get('displayName'),
|
|
249
|
+
name: docSnapshot.get('name'),
|
|
250
|
+
deletedAt: docSnapshot.get('deletedAt'),
|
|
251
|
+
nodes
|
|
252
|
+
}, versionId ? {
|
|
253
|
+
versionId: String(versionId)
|
|
254
|
+
} : {}, docSnapshot.get('layoutId') ? {
|
|
255
|
+
layoutId: String(docSnapshot.get('layoutId'))
|
|
256
|
+
} : {}, docSnapshot.get('parentId') ? {
|
|
257
|
+
parentId: String(docSnapshot.get('parentId'))
|
|
258
|
+
} : {}, collectionName === 'components' && Array.isArray(docSnapshot.get('props')) ? {
|
|
259
|
+
props: docSnapshot.get('props')
|
|
260
|
+
} : {});
|
|
261
|
+
}));
|
|
262
|
+
return {
|
|
263
|
+
candidates,
|
|
264
|
+
truncated
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Every screen, layout and component of a site, with their node trees — the
|
|
269
|
+
* corpus every tree-searching closure above walks.
|
|
270
|
+
*
|
|
271
|
+
* Each collection ONCE, in memory: a query per level would multiply round
|
|
272
|
+
* trips by the nesting depth of the graph. Shared so two scans cannot read
|
|
273
|
+
* different corpora under different bounds, and a change to one cannot quietly
|
|
274
|
+
* narrow the other.
|
|
275
|
+
*/ export async function readUsageSources(hostRef, limit) {
|
|
276
|
+
const [screens, layouts, components] = await Promise.all([
|
|
277
|
+
readUsageCandidates(hostRef, 'screens', {
|
|
278
|
+
withNodes: true,
|
|
279
|
+
limit
|
|
280
|
+
}),
|
|
281
|
+
readUsageCandidates(hostRef, 'layouts', {
|
|
282
|
+
withNodes: true,
|
|
283
|
+
limit
|
|
284
|
+
}),
|
|
285
|
+
readUsageCandidates(hostRef, 'components', {
|
|
286
|
+
withNodes: true,
|
|
287
|
+
limit
|
|
288
|
+
})
|
|
289
|
+
]);
|
|
290
|
+
return {
|
|
291
|
+
candidates: {
|
|
292
|
+
screens: screens.candidates,
|
|
293
|
+
layouts: layouts.candidates,
|
|
294
|
+
components: components.candidates
|
|
295
|
+
},
|
|
296
|
+
truncated: screens.truncated || layouts.truncated || components.truncated
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
//# sourceMappingURL=live-page-usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/live-page-usage.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 * Which live pages a change reaches, for a site's screens, layouts and\n * components.\n *\n * The corpus read and the two closures over it, held here rather than in the\n * console because the console is no longer the only side that needs them. A\n * dataset record is written from the tenant as well — a form submission, an\n * automation step — and those writes make the same pages stale as a console\n * edit does. Two implementations of \"which screens render this\" is the shape\n * that put AGL-1223 in the tree: one reader handled a node tree's second\n * storage form and the other did not, so half the corpus answered \"used\n * nowhere\" and a publish dropped no cache.\n *\n * Nothing here is dataset-specific, and nothing is console-specific. The\n * console's `scan-artifact-usage` re-exports these and keeps the parts that\n * are about the console's own \"what would I break\" copy.\n */\n\nimport {\n nodesReferenceComponent,\n decodeStoredNodes,\n type ReusableComponentProp,\n} from '@aglyn/aglyn/server'\n\n/** A screen/layout/component reduced to what a usage scan needs. */\nexport interface UsageCandidate {\n id: string\n displayName?: string\n /** Legacy field some older documents used instead of `displayName`. */\n name?: string\n deletedAt?: unknown\n /**\n * Node tree to search. For screens and layouts this is the PUBLISHED\n * version's nodes (what visitors see); for components it is the definition\n * tree off the component document, which is what the runtime reads.\n */\n nodes?: Record<string, any> | null\n /** Published version, carried through so the caller can deep-link. */\n versionId?: string\n /** Screens only: the layout they render inside. */\n layoutId?: string\n /** Screens only: the screen they nest under, which is part of their path. */\n parentId?: string\n /**\n * Components only: the properties the definition declares (AGL-1247). A\n * Link property's default renders as a link wherever an instance leaves the\n * property unset, and it is stored here, not in `nodes` (AGL-2846).\n */\n props?: ReadonlyArray<ReusableComponentProp | null | undefined> | null\n}\n\n/** One document that depends on the artifact a scan was asked about. */\nexport interface UsageDependent {\n type: 'screen' | 'layout' | 'component' | 'collection'\n id: string\n name: string\n via: Array<'id' | 'name'>\n versionId?: string\n /**\n * HOW the dependent references the artifact — screens and collection\n * listings (AGL-703, AGL-2806).\n *\n * A component or a layout has exactly one kind of dependent and the noun\n * says everything: an instance, or a binding. A screen has three, and they\n * break in three different ways — a link goes dead, a child moves, a\n * collection loses the page it renders through. Copy that could not tell\n * them apart would have to describe the worst case every time. A collection\n * listing's dependents are all links, and say so.\n */\n relation?: 'link' | 'child' | 'template'\n}\n\n/** The three corpora every closure below walks. */\nexport interface UsageSources {\n screens: UsageCandidate[]\n layouts: UsageCandidate[]\n components: UsageCandidate[]\n}\n\n/** `displayName`, falling back to a legacy `name`, then the raw id. */\nexport function usageCandidateLabel(candidate: UsageCandidate): string {\n return String(candidate.displayName ?? candidate.name ?? candidate.id)\n}\n\n/** A soft-deleted document renders nothing, so it depends on nothing. */\nexport const isLiveUsageCandidate = (candidate: UsageCandidate): boolean =>\n !candidate.deletedAt\n\n/**\n * Everything that references a reusable component (AGL-703).\n *\n * Three places, because the renderer expands instances in three places:\n * published screen versions, published layout versions, and OTHER component\n * definitions — `composeReusableComponentNodes` grafts nested instances, so\n * a component used only inside another component is genuinely used. Omitting\n * that third scan would report \"used nowhere\" for it and invite a confident\n * deletion, which is worse than showing nothing at all.\n */\nexport function scanComponentUsage(\n componentId: string,\n sources: UsageSources,\n): UsageDependent[] {\n if (!componentId) return []\n const dependents: UsageDependent[] = []\n const collect = (\n candidates: UsageCandidate[],\n type: UsageDependent['type'],\n ) => {\n for (const candidate of candidates) {\n if (!isLiveUsageCandidate(candidate)) continue\n // A component never counts as using itself, however it nests.\n if (type === 'component' && candidate.id === componentId) continue\n if (!nodesReferenceComponent(candidate.nodes, componentId)) continue\n dependents.push({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n // Instances reference by id, so a rename can never break them.\n via: ['id'],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n })\n }\n }\n collect(sources.screens, 'screen')\n collect(sources.layouts, 'layout')\n collect(sources.components, 'component')\n return dependents\n}\n\n/**\n * Everything rendering inside a layout (AGL-703).\n *\n * Two kinds of dependent, both expressed by the same `layoutId` pointer:\n *\n * - **screens**, which name the layout they render inside;\n * - **other layouts**, since a layout can itself sit inside one. A nested\n * layout is a real dependent — deleting its parent unwraps every screen\n * underneath it — so leaving layouts out would report a parent layout as\n * used only by the screens that name it directly, and none of the ones\n * that reach it through a child.\n *\n * A layout never counts as its own dependent; `canNestLayout` refuses that,\n * and this refuses to report it even if stored data holds one.\n */\nexport function scanLayoutUsage(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): UsageDependent[] {\n if (!layoutId) return []\n const dependentsOf = (\n candidates: UsageCandidate[],\n type: 'screen' | 'layout',\n ) =>\n candidates\n .filter(\n (candidate) =>\n isLiveUsageCandidate(candidate) &&\n candidate.layoutId === layoutId &&\n candidate.id !== layoutId,\n )\n .map((candidate) => ({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n via: ['id' as const],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n }))\n return [\n ...dependentsOf(screens, 'screen'),\n ...dependentsOf(layouts, 'layout'),\n ]\n}\n\n/**\n * Every live screen rendered inside `layoutId`, at ANY nesting depth\n * (AGL-1150).\n *\n * `scanLayoutUsage` answers one level. Layouts nest — a screen points at a\n * layout, which can point at a parent layout, and `compose-screen-nodes` walks\n * that whole chain when composing a page. So publishing a layout changes every\n * screen below it, not just the ones bound to it directly, and a cache drop\n * that only handles the direct level leaves the rest showing stale chrome for\n * the full revalidate window.\n *\n * Pure, and separate from the Firestore read, so the nesting behaviour is\n * testable without a database.\n *\n * Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written\n * straight to Firestore is not bound by that, and a cycle here would hang a\n * publish request rather than surface anything.\n */\nexport function screenIdsUsingLayoutDeep(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): string[] {\n if (!layoutId) return []\n const screenIds = new Set<string>()\n const seenLayouts = new Set<string>([layoutId])\n let frontier = [layoutId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanLayoutUsage(id, screens, layouts)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (!seenLayouts.has(dependent.id)) {\n seenLayouts.add(dependent.id)\n next.push(dependent.id)\n }\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\n/**\n * Every live screen whose rendered output contains `componentId`, however\n * indirectly (AGL-1161).\n *\n * `scanComponentUsage` answers one level and returns three kinds of dependent.\n * Only one of them is a screen, and the other two both reach screens by routes\n * a single-level scan cannot see:\n *\n * - a **component** dependent nests the target inside itself, and that outer\n * component may itself only be used inside a third — so component→component\n * edges have to be followed to a fixed point;\n * - a **layout** dependent puts the component in page chrome, which every\n * screen under that layout renders. Layouts nest, so that is\n * `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.\n *\n * Miss either and a publish reports success while some pages keep serving the\n * old component for the full revalidate window — the failure this whole arc\n * exists to remove, and the one that is hardest to notice because the pages\n * that ARE dropped update instantly.\n *\n * Pure, and separate from the Firestore read, so the closure is testable\n * without a database — the same split `screenIdsUsingLayoutDeep` uses.\n *\n * Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a\n * document written straight to Firestore is not bound by what the editor\n * allows, and a cycle here would hang a publish rather than surface anything.\n */\nexport function screenIdsUsingComponentDeep(\n componentId: string,\n sources: UsageSources,\n): string[] {\n if (!componentId) return []\n const screenIds = new Set<string>()\n const seenComponents = new Set<string>([componentId])\n // Layouts are resolved through their own deep walk, so remember which ones\n // have already been expanded: two components in the same layout would\n // otherwise re-walk the whole layout tree once each.\n const seenLayouts = new Set<string>()\n let frontier = [componentId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanComponentUsage(id, sources)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (dependent.type === 'layout') {\n if (seenLayouts.has(dependent.id)) continue\n seenLayouts.add(dependent.id)\n // The layout itself renders no URL; the screens beneath it do.\n for (const screenId of screenIdsUsingLayoutDeep(\n dependent.id,\n sources.screens,\n sources.layouts,\n )) {\n screenIds.add(screenId)\n }\n } else if (!seenComponents.has(dependent.id)) {\n seenComponents.add(dependent.id)\n next.push(dependent.id)\n }\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\nexport interface UsageCandidateRead {\n candidates: UsageCandidate[]\n /**\n * The collection held more documents than `limit` allowed, so the scan below\n * it is INCOMPLETE.\n *\n * Returned rather than logged, because the two callers owe the user\n * different things: an advisory \"what would I break\" can show a partial\n * answer and say so, while a cache drop that silently scans a prefix reports\n * a successful publish and leaves real pages stale.\n */\n truncated: boolean\n}\n\n/**\n * One collection's documents, with published nodes attached when the scan\n * needs to search them.\n *\n * `limit` is a real bound, not a guess: it is fetched with one extra document\n * so exceeding it is DETECTED rather than assumed away. A caller that ignores\n * `truncated` is choosing to be wrong quietly.\n */\nexport async function readUsageCandidates(\n hostRef: FirebaseFirestore.DocumentReference,\n collectionName: 'screens' | 'layouts' | 'components',\n options: { withNodes: boolean; limit: number },\n): Promise<UsageCandidateRead> {\n const { withNodes, limit } = options\n // One over the limit: if the extra document comes back, there was more than\n // we are about to look at. Cheaper than a count() and exact.\n const docs = await hostRef.collection(collectionName).limit(limit + 1).get()\n const truncated = docs.size > limit\n const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs\n\n const candidates = await Promise.all(\n inScope.map(async (docSnapshot) => {\n const versionId = docSnapshot.get('versionId')\n // Components keep their tree on the document; screens and layouts keep\n // it on the published version.\n //\n // BOTH reads are decoded (AGL-1223). A component document is msgpack\n // for anything promoted since components were compressed and a plain\n // map for everything older, and `decodeStoredNodes` returns a map\n // unchanged — so the branch below cannot be simplified back into a raw\n // read on either side.\n //\n // What rides on this: every consumer walks the value with\n // `Object.values`, which over a `Buffer` yields byte NUMBERS and\n // matches nothing. The answer that produces is \"used nowhere\" — for\n // `/api/hosts/where-used` an invitation to delete something a live page\n // renders, and for `/api/screens/revalidate` a publish that drops no\n // cache and leaves the old page served.\n const nodes =\n collectionName === 'components'\n ? decodeStoredNodes(docSnapshot.get('nodes'))\n : withNodes && versionId\n ? await docSnapshot.ref\n .collection('versions')\n .doc(String(versionId))\n .get()\n .then((version) => decodeStoredNodes(version.get('nodes')))\n .catch(() => null)\n : null\n return {\n id: docSnapshot.id,\n displayName: docSnapshot.get('displayName'),\n name: docSnapshot.get('name'),\n deletedAt: docSnapshot.get('deletedAt'),\n nodes,\n ...(versionId ? { versionId: String(versionId) } : {}),\n ...(docSnapshot.get('layoutId')\n ? { layoutId: String(docSnapshot.get('layoutId')) }\n : {}),\n // Screens only, and only the screen scan reads it: a child screen is\n // a dependent of its parent, because its PATH is built from it\n // (AGL-703).\n ...(docSnapshot.get('parentId')\n ? { parentId: String(docSnapshot.get('parentId')) }\n : {}),\n // Components only: the declared properties, whose Link defaults render\n // as links wherever an instance leaves one unset (AGL-2846). The same\n // document the tree came from, so it costs no read.\n ...(collectionName === 'components' &&\n Array.isArray(docSnapshot.get('props'))\n ? { props: docSnapshot.get('props') }\n : {}),\n } satisfies UsageCandidate\n }),\n )\n\n return { candidates, truncated }\n}\n\n/**\n * Every screen, layout and component of a site, with their node trees — the\n * corpus every tree-searching closure above walks.\n *\n * Each collection ONCE, in memory: a query per level would multiply round\n * trips by the nesting depth of the graph. Shared so two scans cannot read\n * different corpora under different bounds, and a change to one cannot quietly\n * narrow the other.\n */\nexport async function readUsageSources(\n hostRef: FirebaseFirestore.DocumentReference,\n limit: number,\n): Promise<{ candidates: UsageSources; truncated: boolean }> {\n const [screens, layouts, components] = await Promise.all([\n readUsageCandidates(hostRef, 'screens', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'layouts', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'components', { withNodes: true, limit }),\n ])\n return {\n candidates: {\n screens: screens.candidates,\n layouts: layouts.candidates,\n components: components.candidates,\n },\n truncated: screens.truncated || layouts.truncated || components.truncated,\n }\n}\n"],"names":["nodesReferenceComponent","decodeStoredNodes","usageCandidateLabel","candidate","String","displayName","name","id","isLiveUsageCandidate","deletedAt","scanComponentUsage","componentId","sources","dependents","collect","candidates","type","nodes","push","via","versionId","screens","layouts","components","scanLayoutUsage","layoutId","dependentsOf","filter","map","screenIdsUsingLayoutDeep","screenIds","Set","seenLayouts","frontier","length","next","dependent","add","has","screenIdsUsingComponentDeep","seenComponents","screenId","readUsageCandidates","hostRef","collectionName","options","withNodes","limit","docs","collection","get","truncated","size","inScope","slice","Promise","all","docSnapshot","ref","doc","then","version","catch","parentId","Array","isArray","props","readUsageSources"],"mappings":"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"}
|
|
@@ -0,0 +1,313 @@
|
|
|
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
|
+
* 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
|
+
*/
|
|
44
|
+
import { type LockdownFeatureKey, type LockdownIntent, type LockdownNotice, type LockdownState, normalizeHostLockdown, normalizeOrgLockdown } from '@aglyn/aglyn/server';
|
|
45
|
+
/**
|
|
46
|
+
* Bounded like the user and domain caches, and for the same reason: two of
|
|
47
|
+
* the four keyed readers take their key from caller-controlled input.
|
|
48
|
+
* Eviction is least-recently-USED.
|
|
49
|
+
*/
|
|
50
|
+
export declare const TAKEDOWN_LEDGER_MAX = 5000;
|
|
51
|
+
/**
|
|
52
|
+
* Forget every remembered takedown. NOT part of the panic path and not
|
|
53
|
+
* called by the admin route — this exists for process-boundary and test
|
|
54
|
+
* setup, where a ledger surviving between cases would make one test's lock
|
|
55
|
+
* another test's fail-closed verdict.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resetTakedownLedger(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Record what a SUCCESSFUL site read saw, per scope. Same contract as the
|
|
60
|
+
* four `lockdowns/*` readers: a takedown is remembered, and anything else —
|
|
61
|
+
* no lock, an ordinary lock, a takedown DOWNGRADED to one — retires the
|
|
62
|
+
* entry, which is how a lift takes effect.
|
|
63
|
+
*/
|
|
64
|
+
export declare function rememberSiteTakedown(hostId: string, states: {
|
|
65
|
+
org: LockdownState | null;
|
|
66
|
+
host: LockdownState | null;
|
|
67
|
+
}): void;
|
|
68
|
+
/**
|
|
69
|
+
* What a FAILED site read may still serve: the remembered, still-active
|
|
70
|
+
* takedowns at org and host scope, or nulls — which is the shipped fail-open
|
|
71
|
+
* answer and therefore the default for every lock that is not an observed
|
|
72
|
+
* takedown.
|
|
73
|
+
*/
|
|
74
|
+
export declare function heldSiteTakedown(hostId: string, nowMs: number): {
|
|
75
|
+
org: LockdownState | null;
|
|
76
|
+
host: LockdownState | null;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Drop the in-process platform cache. Called by /api/admin/lockdown after a
|
|
80
|
+
* platform write so the process that took the action serves fresh verdicts
|
|
81
|
+
* immediately; other processes converge within PLATFORM_TTL_MS.
|
|
82
|
+
*/
|
|
83
|
+
export declare function invalidatePlatformLockdownCache(): void;
|
|
84
|
+
/** `lockdowns/platform`, normalized; null = not locked (incl. on error). */
|
|
85
|
+
export declare function getPlatformLockdown(): Promise<LockdownState | null>;
|
|
86
|
+
/** Drop the in-process feature cache (all keys) after an admin write. */
|
|
87
|
+
export declare function invalidateFeatureLockdownCache(): void;
|
|
88
|
+
/** `lockdowns/feature--{key}`, normalized; null = not locked (incl. error). */
|
|
89
|
+
export declare function getFeatureLockdown(feature: LockdownFeatureKey): Promise<LockdownState | null>;
|
|
90
|
+
/**
|
|
91
|
+
* `lockdowns/feature--{key}--org--{orgId}` (AGL-2927): the same capability
|
|
92
|
+
* paused for ONE workspace. Same cache, same TTL, same fail-open posture as
|
|
93
|
+
* the platform-wide document; null = not paused for that org.
|
|
94
|
+
*/
|
|
95
|
+
export declare function getOrgFeatureLockdown(feature: LockdownFeatureKey, orgId: string): Promise<LockdownState | null>;
|
|
96
|
+
/**
|
|
97
|
+
* The feature verdict and refusal in one call (AGL-1510) — the one-line
|
|
98
|
+
* wiring a feature chokepoint carries:
|
|
99
|
+
*
|
|
100
|
+
* const locked = await featureLockdownRefusal({ feature: 'checkout', staff })
|
|
101
|
+
* if (locked) return locked
|
|
102
|
+
*
|
|
103
|
+
* Composition, not ranking: a PLATFORM lock implies every feature, so it is
|
|
104
|
+
* checked first (TTL-cached — routes that already ran the scope verdict pay
|
|
105
|
+
* no extra read), and the platform-scope staff bypass there is UNCHANGED
|
|
106
|
+
* and unconditional. The feature doc is checked second, and its staff
|
|
107
|
+
* bypass is per-feature (`lockdownFeatureStaffBypass`): granted where a
|
|
108
|
+
* staff action aids incident response (uploads, installs, ai-assist),
|
|
109
|
+
* withheld where it would BE the incident (checkout — a staff checkout
|
|
110
|
+
* session is still a real charge). A feature lock implies nothing about the
|
|
111
|
+
* org/host/user scopes — those stay with `lockdownRefusal` at the routes
|
|
112
|
+
* that carry it.
|
|
113
|
+
*
|
|
114
|
+
* A door that names the org it serves passes `orgId` (AGL-2927), and the
|
|
115
|
+
* workspace-scoped carrier for the same feature is checked third, under the
|
|
116
|
+
* same per-feature staff bypass: the staff AI pause is a spend stop on one
|
|
117
|
+
* customer, and a staff call made to verify the pause is ours to spend. A
|
|
118
|
+
* door that names no org — the plugin dispatcher, which gates by path
|
|
119
|
+
* before the body is read — sees the platform-wide document only, which is
|
|
120
|
+
* what it saw before.
|
|
121
|
+
*/
|
|
122
|
+
export declare function featureLockdownRefusal(options: {
|
|
123
|
+
feature: LockdownFeatureKey;
|
|
124
|
+
/** Verified `staff` custom claim — from a VERIFIED token only. */
|
|
125
|
+
staff?: boolean;
|
|
126
|
+
/** The workspace the request is metered against, where the door knows it. */
|
|
127
|
+
orgId?: string | null;
|
|
128
|
+
nowMs?: number;
|
|
129
|
+
}): Promise<Response | null>;
|
|
130
|
+
/**
|
|
131
|
+
* User-doc cache (AGL-1522): the last per-call read on the verdict path.
|
|
132
|
+
* AGL-1506 wired the verdict into ~36 routes plus the session mint/exchange,
|
|
133
|
+
* and every call carrying a `uid` paid one Firestore get on
|
|
134
|
+
* `lockdowns/user--{uid}` — an active console editor generated one read per
|
|
135
|
+
* wired mutation, against the AGL-1302 read budget. Same TTL, same pending
|
|
136
|
+
* dedupe, same fail-open posture as the platform and feature caches above —
|
|
137
|
+
* ONE cache pattern in this module, not three implementations.
|
|
138
|
+
*
|
|
139
|
+
* The staleness tradeoff, stated: a user locked mid-window keeps passing
|
|
140
|
+
* this check for up to PLATFORM_TTL_MS (15s) plus the caller's polling
|
|
141
|
+
* cadence — on every process EXCEPT the one that took the action, which
|
|
142
|
+
* refuses immediately because /api/admin/lockdown invalidates this uid's
|
|
143
|
+
* entry after the write (the platform/feature invalidation hook, mirrored).
|
|
144
|
+
* That bound is acceptable here for the same reason `media/sign` omits the
|
|
145
|
+
* user scope entirely: the hard kill never rode this read. A user lock also
|
|
146
|
+
* disables the Auth account and revokes refresh tokens, so the ID token
|
|
147
|
+
* stops refreshing and the session cookie dies at its next
|
|
148
|
+
* `verifySessionCookie(…, true)` — the cache can only delay the DISTINCT
|
|
149
|
+
* 423 body, not access itself, and 15s matches the platform scope's stated
|
|
150
|
+
* worst case and the drill's measured flip times.
|
|
151
|
+
*
|
|
152
|
+
* Bounded so a scan of uids cannot balloon a warm process: past
|
|
153
|
+
* USER_LOCKDOWN_CACHE_MAX entries the least-recently-USED is evicted (a
|
|
154
|
+
* cache hit refreshes recency, so active sessions survive a scan).
|
|
155
|
+
*/
|
|
156
|
+
export declare const USER_LOCKDOWN_CACHE_MAX = 5000;
|
|
157
|
+
/**
|
|
158
|
+
* Drop one uid's cached verdict (after a user lock/unlock write), or the
|
|
159
|
+
* whole cache when called bare. The acting process serves a fresh verdict
|
|
160
|
+
* immediately; other processes converge within PLATFORM_TTL_MS.
|
|
161
|
+
*/
|
|
162
|
+
export declare function invalidateUserLockdownCache(uid?: string): void;
|
|
163
|
+
/** `lockdowns/user--{uid}`, normalized; null = not locked (incl. on error). */
|
|
164
|
+
export declare function getUserLockdown(uid: string): Promise<LockdownState | null>;
|
|
165
|
+
/**
|
|
166
|
+
* DOMAIN-lock cache (AGL-1513), same shape and same TTL as the user cache.
|
|
167
|
+
*
|
|
168
|
+
* Bounded for the same reason and one more: the key is a HOSTNAME taken from
|
|
169
|
+
* the request, so an unbounded map here would be attacker-keyed by anyone who
|
|
170
|
+
* can point a DNS record at us — which, for the hijack incident this scope
|
|
171
|
+
* exists for, is the adversary by definition.
|
|
172
|
+
*/
|
|
173
|
+
export declare const DOMAIN_LOCKDOWN_CACHE_MAX = 5000;
|
|
174
|
+
/**
|
|
175
|
+
* Drop one hostname's cached verdict (after a domain lock/unlock write), or
|
|
176
|
+
* the whole cache when called bare.
|
|
177
|
+
*/
|
|
178
|
+
export declare function invalidateDomainLockdownCache(hostname?: string): void;
|
|
179
|
+
/**
|
|
180
|
+
* `lockdowns/domain--{hostname}`, normalized; null = not locked (incl. on
|
|
181
|
+
* error), matching every other reader here — an unreachable Firestore is an
|
|
182
|
+
* outage, not a lockdown.
|
|
183
|
+
*/
|
|
184
|
+
export declare function getDomainLockdown(hostname: string): Promise<LockdownState | null>;
|
|
185
|
+
export interface LockdownVerdictOptions {
|
|
186
|
+
/**
|
|
187
|
+
* The verified `staff` custom claim of the caller — from a VERIFIED token
|
|
188
|
+
* only, never from a header or body. True bypasses every scope: this is
|
|
189
|
+
* the un-panic invariant.
|
|
190
|
+
*/
|
|
191
|
+
staff?: boolean;
|
|
192
|
+
/** Verified caller uid; enables the user scope. */
|
|
193
|
+
uid?: string | null;
|
|
194
|
+
/**
|
|
195
|
+
* Already-loaded org doc (or its suspension fields), when the caller has
|
|
196
|
+
* one in hand — org scope costs no extra read. Absent = scope not
|
|
197
|
+
* evaluated.
|
|
198
|
+
*/
|
|
199
|
+
org?: Parameters<typeof normalizeOrgLockdown>[0];
|
|
200
|
+
/** Already-loaded host doc; same contract as `org`. */
|
|
201
|
+
host?: Parameters<typeof normalizeHostLockdown>[0];
|
|
202
|
+
/**
|
|
203
|
+
* READ-ONLY discrimination (AGL-1511). A `read-only` lock refuses writes
|
|
204
|
+
* and passes reads; a `full` lock refuses both, so this changes nothing
|
|
205
|
+
* for every lock written before the mode existed.
|
|
206
|
+
*
|
|
207
|
+
* The request the chokepoint is answering — its METHOD decides. Passing
|
|
208
|
+
* the route's own `request` is the wiring, because a single console
|
|
209
|
+
* handler is usually exported as both GET and POST and the verdict runs
|
|
210
|
+
* before the method branch: derive the intent, never restate it.
|
|
211
|
+
*/
|
|
212
|
+
request?: {
|
|
213
|
+
method?: string;
|
|
214
|
+
} | null;
|
|
215
|
+
/**
|
|
216
|
+
* Explicit intent, overriding `request`. For the routes whose method lies
|
|
217
|
+
* about what they do — a POST that is really a query (`where-used`,
|
|
218
|
+
* `plugin-impact`), or a GET that mutates.
|
|
219
|
+
*
|
|
220
|
+
* Neither given = `write`, the fail-safe: a chokepoint that never declared
|
|
221
|
+
* its intent refuses during a migration rather than letting an
|
|
222
|
+
* unconsidered write race the repair.
|
|
223
|
+
*/
|
|
224
|
+
intent?: LockdownIntent;
|
|
225
|
+
nowMs?: number;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The one verdict: the lockdown that REFUSES this caller's request, or null.
|
|
229
|
+
* Precedence platform > org > host > user, strictness before width (from
|
|
230
|
+
* `resolveLockdown`).
|
|
231
|
+
*
|
|
232
|
+
* "Refuses this request" rather than "covers this caller" since AGL-1511: a
|
|
233
|
+
* read-only lock is active but does not refuse a read, and returning it
|
|
234
|
+
* anyway would make every wired chokepoint 423 a GET — a read-only mode that
|
|
235
|
+
* behaves exactly like a full one. The active state is still what
|
|
236
|
+
* `resolveLockdown` chose, so the staff probe and the notice copy see the
|
|
237
|
+
* real lock; only the ANSWER is intent-aware.
|
|
238
|
+
*/
|
|
239
|
+
export declare function getLockdownVerdict(options: LockdownVerdictOptions): Promise<LockdownState | null>;
|
|
240
|
+
/**
|
|
241
|
+
* The refusal an API route returns for a locked caller: **423 Locked** with
|
|
242
|
+
* a machine-readable body, so an API consumer sees "suspended: billing" and
|
|
243
|
+
* not a mystery 403. Carries only the sanitized, user-facing subset — never
|
|
244
|
+
* the actor uid or staff rationale.
|
|
245
|
+
*/
|
|
246
|
+
/**
|
|
247
|
+
* Verdict and refusal in one call — the two-line wiring every org-scoped
|
|
248
|
+
* API route carries (AGL-1506):
|
|
249
|
+
*
|
|
250
|
+
* const locked = await lockdownRefusal({ staff, uid, org, host })
|
|
251
|
+
* if (locked) return locked
|
|
252
|
+
*
|
|
253
|
+
* Null means "not locked, proceed". One mechanism: this delegates to
|
|
254
|
+
* `getLockdownVerdict` (so the un-panic invariant and fail-open posture are
|
|
255
|
+
* inherited, never re-implemented) and to `lockdownJsonResponse` for the
|
|
256
|
+
* distinct 423 body. Routes with a richer flow (the session mint clears
|
|
257
|
+
* cookies on refusal) keep calling the two halves directly.
|
|
258
|
+
*/
|
|
259
|
+
export declare function lockdownRefusal(options: LockdownVerdictOptions & {
|
|
260
|
+
/** Visitor-facing copy override; see `lockdownJsonResponse`. */
|
|
261
|
+
notice?: LockdownNotice;
|
|
262
|
+
}): Promise<Response | null>;
|
|
263
|
+
export declare function lockdownJsonResponse(state: LockdownState, options?: {
|
|
264
|
+
/**
|
|
265
|
+
* Substitute copy for a surface whose reader is not the account holder
|
|
266
|
+
* — the tenant runtime's visitor-facing pause (AGL-1511). ONE wire
|
|
267
|
+
* shape and ONE writer either way: only the words change, so
|
|
268
|
+
* `parseLockdownRefusal` reads a paused checkout exactly as it reads a
|
|
269
|
+
* locked console.
|
|
270
|
+
*/
|
|
271
|
+
notice?: LockdownNotice;
|
|
272
|
+
}): Response;
|
|
273
|
+
/**
|
|
274
|
+
* IS THE CREATION-LEVEL VALVE ACTUALLY ARMED? (AGL-1531)
|
|
275
|
+
*
|
|
276
|
+
* The signups feature lock refuses the session mint, the acceptance recorder
|
|
277
|
+
* and the signup-page doors from code that ships with every deploy. It also
|
|
278
|
+
* refuses account CREATION — but only through a Firebase Auth
|
|
279
|
+
* `beforeUserCreated` blocking function in `cloud/functions`, which merging
|
|
280
|
+
* does not deploy and deploying does not necessarily register. Identity
|
|
281
|
+
* Platform holds that registration, not this repo.
|
|
282
|
+
*
|
|
283
|
+
* So the staff page had a way to be confidently wrong: the switch reads
|
|
284
|
+
* LOCKED either way, and an operator during a bot wave would have no way to
|
|
285
|
+
* tell "creation is refused" from "creation is still wide open and only
|
|
286
|
+
* sessions are being turned away". This is the read that closes that gap —
|
|
287
|
+
* the panic surface states which of the two it is instead of implying the
|
|
288
|
+
* stronger one.
|
|
289
|
+
*
|
|
290
|
+
* `unknown` is a first-class answer and is NEVER rendered as armed. The
|
|
291
|
+
* probe needs a credential with the identitytoolkit scope and an outbound
|
|
292
|
+
* call; a deployment where either is missing must report that it does not
|
|
293
|
+
* know, because "we could not check" and "it is on" are the two answers an
|
|
294
|
+
* incident cannot afford to have confused.
|
|
295
|
+
*/
|
|
296
|
+
/**
|
|
297
|
+
* A STRING discriminant, not a `boolean | null`. `strictNullChecks` is off
|
|
298
|
+
* repo-wide, so `null` is assignable to every member and a `armed: null` arm
|
|
299
|
+
* cannot narrow — the compiler collapses the union and the "unknown" case
|
|
300
|
+
* silently stops being distinguishable from the other two. Which is the one
|
|
301
|
+
* distinction this type exists to hold.
|
|
302
|
+
*/
|
|
303
|
+
export type SignupsCreationTriggerStatus = {
|
|
304
|
+
status: 'armed';
|
|
305
|
+
functionUri: string | null;
|
|
306
|
+
updateTime: string | null;
|
|
307
|
+
} | {
|
|
308
|
+
status: 'absent';
|
|
309
|
+
} | {
|
|
310
|
+
status: 'unknown';
|
|
311
|
+
reason: string;
|
|
312
|
+
};
|
|
313
|
+
export declare function readSignupsCreationTriggerStatus(): Promise<SignupsCreationTriggerStatus>;
|