@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,114 @@
|
|
|
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
|
+
*/ import { consumeRateLimit } from "./rate-limit-store.js";
|
|
18
|
+
/**
|
|
19
|
+
* The cooldown on the AUTOMATIC verification send (AGL-2584).
|
|
20
|
+
*
|
|
21
|
+
* `/verify-email` asks for a link every time it mounts, so somebody who leaves
|
|
22
|
+
* the tab and comes back to see whether the mail arrived asks for a second
|
|
23
|
+
* one. Identity Platform throttles link minting per account, ahead of and
|
|
24
|
+
* independently of the route's own 5/hour budget, and that collision is what a
|
|
25
|
+
* returning visitor met: a mail that had genuinely been sent, reported to them
|
|
26
|
+
* as a send failure.
|
|
27
|
+
*
|
|
28
|
+
* Reopening the page is a request to know whether the first mail worked, not a
|
|
29
|
+
* request for another one — so an automatic send is skipped while a link
|
|
30
|
+
* minted for this uid is still young. The explicit "Resend verification email"
|
|
31
|
+
* button does not come through here: someone who says the mail never arrived
|
|
32
|
+
* is asking deliberately, and that affordance keeps the full per-uid budget.
|
|
33
|
+
*
|
|
34
|
+
* ## Why the state is server-side and not a `sentAt` in the browser
|
|
35
|
+
*
|
|
36
|
+
* The throttle this avoids is per ACCOUNT, so the memory of "a link was minted
|
|
37
|
+
* recently" has to be too. A marker held in the browser cannot see a link
|
|
38
|
+
* minted from a phone, a second browser, or a private window, and clearing
|
|
39
|
+
* site data erases it — each of which puts the collision straight back. It
|
|
40
|
+
* would also put the decision on the side of the wire that does not do the
|
|
41
|
+
* minting, which is where it went wrong the first time.
|
|
42
|
+
*
|
|
43
|
+
* ## Why the durable limiter, with a budget of one
|
|
44
|
+
*
|
|
45
|
+
* A cooldown *is* a limit of one per window, and the limiter already solves
|
|
46
|
+
* the parts that are easy to get wrong: it is global rather than per instance
|
|
47
|
+
* (a per-instance `Map` on Vercel is close to no limit at all, AGL-794), it
|
|
48
|
+
* hashes the key out of the document id, and it fails soft when Firestore is
|
|
49
|
+
* unreachable.
|
|
50
|
+
*
|
|
51
|
+
* Its window is fixed rather than rolling, so a link minted near a boundary
|
|
52
|
+
* can be followed by another as soon as the next window opens — the cooldown
|
|
53
|
+
* holds for somewhere between zero and its full length. Where a window does
|
|
54
|
+
* end early the outcome is one extra mint, which the route reports as the
|
|
55
|
+
* throttle it is rather than as a failure.
|
|
56
|
+
*
|
|
57
|
+
* ## It may only suppress on evidence, never on silence
|
|
58
|
+
*
|
|
59
|
+
* The limiter refuses for two unrelated reasons, and only one of them is a
|
|
60
|
+
* counted overage. `contended` means the counter could not be read and
|
|
61
|
+
* written inside its budget, and the budget is spent by any slowness at all —
|
|
62
|
+
* a cold instance paying for Admin SDK startup and two Firestore round trips
|
|
63
|
+
* reaches it without another caller in sight, which on a low-traffic
|
|
64
|
+
* deployment is the ordinary case rather than the rare one.
|
|
65
|
+
*
|
|
66
|
+
* That refusal carries no information about whether a link was recently
|
|
67
|
+
* minted, so it may not be read as one. The asymmetry is the whole argument:
|
|
68
|
+
* a duplicate mail is a nuisance the route already reports correctly, while a
|
|
69
|
+
* suppressed FIRST mail is an account that never receives the link it is
|
|
70
|
+
* waiting for — and because a suppressed send is answered 200, the account
|
|
71
|
+
* holder is told a mail is on its way that nobody ever sent. A cooldown whose
|
|
72
|
+
* purpose is politeness does not get to fail that way, so an unanswered gate
|
|
73
|
+
* sends.
|
|
74
|
+
*/ /**
|
|
75
|
+
* How long an automatic send suppresses the next one. Long enough to cover the
|
|
76
|
+
* round trip this exists for — open the mail app, look, come back — and short
|
|
77
|
+
* enough that someone whose mail was eaten by a spam filter gets a fresh one
|
|
78
|
+
* from a later visit rather than only from the resend button.
|
|
79
|
+
*/ export const VERIFY_EMAIL_AUTO_SEND_COOLDOWN_MS = 10 * 60 * 1000;
|
|
80
|
+
/**
|
|
81
|
+
* Asks whether the page's automatic send may mint a link for `uid`.
|
|
82
|
+
*
|
|
83
|
+
* Keyed on the uid alone, under its own prefix: the cooldown is one account's
|
|
84
|
+
* and never another's, and it counts separately from the per-uid hourly budget
|
|
85
|
+
* so a suppressed automatic send spends nothing a deliberate resend would want.
|
|
86
|
+
*/ export async function consumeVerifyEmailAutoSend(uid, options) {
|
|
87
|
+
var _ref;
|
|
88
|
+
const at = (_ref = options == null ? void 0 : options.now) != null ? _ref : Date.now();
|
|
89
|
+
const result = await consumeRateLimit(`verify-email:auto:${uid}`, _extends({
|
|
90
|
+
limit: 1,
|
|
91
|
+
windowMs: VERIFY_EMAIL_AUTO_SEND_COOLDOWN_MS
|
|
92
|
+
}, (options == null ? void 0 : options.now) === undefined ? {} : {
|
|
93
|
+
now: options.now
|
|
94
|
+
}, (options == null ? void 0 : options.firestore) === undefined ? {} : {
|
|
95
|
+
firestore: options.firestore
|
|
96
|
+
}));
|
|
97
|
+
// A refusal the limiter could not count is silence, not evidence of a
|
|
98
|
+
// recent mail, and this cooldown suppresses only on evidence.
|
|
99
|
+
if (result.contended) {
|
|
100
|
+
return {
|
|
101
|
+
allowed: true,
|
|
102
|
+
retryAfterSeconds: 0,
|
|
103
|
+
degraded: result.degraded
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
allowed: result.allowed,
|
|
108
|
+
retryAfterSeconds: result.allowed ? 0 : Math.max(1, Math.ceil((result.resetMs - at) / 1000)),
|
|
109
|
+
degraded: result.degraded
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export default consumeVerifyEmailAutoSend;
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=verify-email-cooldown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/verify-email-cooldown.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consumeRateLimit } from './rate-limit-store'\n\n/**\n * The cooldown on the AUTOMATIC verification send (AGL-2584).\n *\n * `/verify-email` asks for a link every time it mounts, so somebody who leaves\n * the tab and comes back to see whether the mail arrived asks for a second\n * one. Identity Platform throttles link minting per account, ahead of and\n * independently of the route's own 5/hour budget, and that collision is what a\n * returning visitor met: a mail that had genuinely been sent, reported to them\n * as a send failure.\n *\n * Reopening the page is a request to know whether the first mail worked, not a\n * request for another one — so an automatic send is skipped while a link\n * minted for this uid is still young. The explicit \"Resend verification email\"\n * button does not come through here: someone who says the mail never arrived\n * is asking deliberately, and that affordance keeps the full per-uid budget.\n *\n * ## Why the state is server-side and not a `sentAt` in the browser\n *\n * The throttle this avoids is per ACCOUNT, so the memory of \"a link was minted\n * recently\" has to be too. A marker held in the browser cannot see a link\n * minted from a phone, a second browser, or a private window, and clearing\n * site data erases it — each of which puts the collision straight back. It\n * would also put the decision on the side of the wire that does not do the\n * minting, which is where it went wrong the first time.\n *\n * ## Why the durable limiter, with a budget of one\n *\n * A cooldown *is* a limit of one per window, and the limiter already solves\n * the parts that are easy to get wrong: it is global rather than per instance\n * (a per-instance `Map` on Vercel is close to no limit at all, AGL-794), it\n * hashes the key out of the document id, and it fails soft when Firestore is\n * unreachable.\n *\n * Its window is fixed rather than rolling, so a link minted near a boundary\n * can be followed by another as soon as the next window opens — the cooldown\n * holds for somewhere between zero and its full length. Where a window does\n * end early the outcome is one extra mint, which the route reports as the\n * throttle it is rather than as a failure.\n *\n * ## It may only suppress on evidence, never on silence\n *\n * The limiter refuses for two unrelated reasons, and only one of them is a\n * counted overage. `contended` means the counter could not be read and\n * written inside its budget, and the budget is spent by any slowness at all —\n * a cold instance paying for Admin SDK startup and two Firestore round trips\n * reaches it without another caller in sight, which on a low-traffic\n * deployment is the ordinary case rather than the rare one.\n *\n * That refusal carries no information about whether a link was recently\n * minted, so it may not be read as one. The asymmetry is the whole argument:\n * a duplicate mail is a nuisance the route already reports correctly, while a\n * suppressed FIRST mail is an account that never receives the link it is\n * waiting for — and because a suppressed send is answered 200, the account\n * holder is told a mail is on its way that nobody ever sent. A cooldown whose\n * purpose is politeness does not get to fail that way, so an unanswered gate\n * sends.\n */\n\n/**\n * How long an automatic send suppresses the next one. Long enough to cover the\n * round trip this exists for — open the mail app, look, come back — and short\n * enough that someone whose mail was eaten by a spam filter gets a fresh one\n * from a later visit rather than only from the resend button.\n */\nexport const VERIFY_EMAIL_AUTO_SEND_COOLDOWN_MS = 10 * 60 * 1000\n\nexport interface VerifyEmailAutoSendResult {\n /** True when this mount may mint a link. */\n allowed: boolean\n /** Seconds until the cooldown lifts; 0 when allowed. */\n retryAfterSeconds: number\n /** True when the durable store was unreachable and only a local cap applied. */\n degraded: boolean\n}\n\nexport interface VerifyEmailAutoSendOptions {\n now?: number\n /** Injectable for tests; defaults to the Admin SDK's Firestore. */\n firestore?: unknown\n}\n\n/**\n * Asks whether the page's automatic send may mint a link for `uid`.\n *\n * Keyed on the uid alone, under its own prefix: the cooldown is one account's\n * and never another's, and it counts separately from the per-uid hourly budget\n * so a suppressed automatic send spends nothing a deliberate resend would want.\n */\nexport async function consumeVerifyEmailAutoSend(\n uid: string,\n options?: VerifyEmailAutoSendOptions,\n): Promise<VerifyEmailAutoSendResult> {\n const at = options?.now ?? Date.now()\n const result = await consumeRateLimit(`verify-email:auto:${uid}`, {\n limit: 1,\n windowMs: VERIFY_EMAIL_AUTO_SEND_COOLDOWN_MS,\n ...(options?.now === undefined ? {} : { now: options.now }),\n ...(options?.firestore === undefined\n ? {}\n : { firestore: options.firestore }),\n })\n // A refusal the limiter could not count is silence, not evidence of a\n // recent mail, and this cooldown suppresses only on evidence.\n if (result.contended) {\n return { allowed: true, retryAfterSeconds: 0, degraded: result.degraded }\n }\n return {\n allowed: result.allowed,\n retryAfterSeconds: result.allowed\n ? 0\n : Math.max(1, Math.ceil((result.resetMs - at) / 1000)),\n degraded: result.degraded,\n }\n}\n\nexport default consumeVerifyEmailAutoSend\n"],"names":["consumeRateLimit","VERIFY_EMAIL_AUTO_SEND_COOLDOWN_MS","consumeVerifyEmailAutoSend","uid","options","at","now","Date","result","limit","windowMs","undefined","firestore","contended","allowed","retryAfterSeconds","degraded","Math","max","ceil","resetMs"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,wBAAoB;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwDC,GAED;;;;;CAKC,GACD,OAAO,MAAMC,qCAAqC,KAAK,KAAK,KAAI;AAiBhE;;;;;;CAMC,GACD,OAAO,eAAeC,2BACpBC,GAAW,EACXC,OAAoC;;IAEpC,MAAMC,aAAKD,2BAAAA,QAASE,GAAG,mBAAIC,KAAKD,GAAG;IACnC,MAAME,SAAS,MAAMR,iBAAiB,CAAC,kBAAkB,EAAEG,KAAK,EAAE;QAChEM,OAAO;QACPC,UAAUT;OACNG,CAAAA,2BAAAA,QAASE,GAAG,MAAKK,YAAY,CAAC,IAAI;QAAEL,KAAKF,QAAQE,GAAG;IAAC,GACrDF,CAAAA,2BAAAA,QAASQ,SAAS,MAAKD,YACvB,CAAC,IACD;QAAEC,WAAWR,QAAQQ,SAAS;IAAC;IAErC,sEAAsE;IACtE,8DAA8D;IAC9D,IAAIJ,OAAOK,SAAS,EAAE;QACpB,OAAO;YAAEC,SAAS;YAAMC,mBAAmB;YAAGC,UAAUR,OAAOQ,QAAQ;QAAC;IAC1E;IACA,OAAO;QACLF,SAASN,OAAOM,OAAO;QACvBC,mBAAmBP,OAAOM,OAAO,GAC7B,IACAG,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAAC,AAACX,CAAAA,OAAOY,OAAO,GAAGf,EAAC,IAAK;QAClDW,UAAUR,OAAOQ,QAAQ;IAC3B;AACF;AAEA,eAAed,2BAA0B"}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
export interface VisitorWriteRateLimitOptions {
|
|
18
|
+
/** Dispatcher path, e.g. `commerce/cart`. Decides visitor vs machine. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** The site the write targets; `''` when none resolved (still limited). */
|
|
21
|
+
hostId: string;
|
|
22
|
+
/** Read for its method and its client-address headers. */
|
|
23
|
+
request: {
|
|
24
|
+
method?: string;
|
|
25
|
+
headers?: {
|
|
26
|
+
get?: (name: string) => unknown;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
limit?: number;
|
|
30
|
+
windowMs?: number;
|
|
31
|
+
nowMs?: number;
|
|
32
|
+
/** Injectable for tests; defaults to the Admin SDK's Firestore. */
|
|
33
|
+
firestore?: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Counts one visitor write and returns a 429 once the window is spent.
|
|
37
|
+
*
|
|
38
|
+
* ## Reads are not limited, and that is a decision rather than an omission
|
|
39
|
+
*
|
|
40
|
+
* AGL-1770 scopes this to visitor-facing *writes*, and the cost argument
|
|
41
|
+
* agrees. `RATE_LIMITING.md`'s standing rule is "`checkRateLimit` for volume,
|
|
42
|
+
* `consumeRateLimit` for consequence": each durable call is a Firestore
|
|
43
|
+
* transaction — one read plus one write — which is the right price for a cart
|
|
44
|
+
* write that itself creates a document, and the wrong price for a GET. The
|
|
45
|
+
* dispatcher's read paths "leave before any Firestore read" (the lockdown gate
|
|
46
|
+
* says so), so a durable limiter there would be the ONLY Firestore work in the
|
|
47
|
+
* request — precisely the analytics-beacon anti-pattern the doc names.
|
|
48
|
+
*
|
|
49
|
+
* The residual is real and worth stating: tenant reads are not free (~40
|
|
50
|
+
* Firestore reads per rendered page). It is a different defect with a
|
|
51
|
+
* different remedy — caching and the ISR layer, not a per-request
|
|
52
|
+
* transaction — and it is not this one.
|
|
53
|
+
*
|
|
54
|
+
* ## Fail SOFT, inherited from `consumeRateLimit` and re-argued here
|
|
55
|
+
*
|
|
56
|
+
* A Firestore outage lets traffic through, flagged `degraded`, rather than
|
|
57
|
+
* refusing it. CSRF fails closed; this deliberately does not, for three
|
|
58
|
+
* reasons, the third of which is specific to this limiter:
|
|
59
|
+
*
|
|
60
|
+
* 1. **The blast radius of the strict choice is every storefront at once.**
|
|
61
|
+
* A Firestore blip that 429s every add-to-cart on every site costs
|
|
62
|
+
* merchants sales during an outage they did not cause. The risk it guards
|
|
63
|
+
* is unbounded document creation — storage. Lost revenue is the worse of
|
|
64
|
+
* the two, and it is the one the merchant notices.
|
|
65
|
+
* 2. **The fallback still enforces something.** `consumeRateLimit` degrades to
|
|
66
|
+
* the in-process limiter at the same ceiling, so the cap becomes
|
|
67
|
+
* `limit × warm instances` rather than nothing.
|
|
68
|
+
* 3. **The store and the thing being protected are the same Firestore.** The
|
|
69
|
+
* counter lives in `rateLimits` and the cart document it guards lives in
|
|
70
|
+
* `hosts/…/carts` — one database. If the limiter cannot write, the write it
|
|
71
|
+
* exists to bound is almost certainly failing too. Failing closed would
|
|
72
|
+
* refuse requests that were going to fail anyway, and *additionally* refuse
|
|
73
|
+
* the ones that would have succeeded. It buys nothing.
|
|
74
|
+
*
|
|
75
|
+
* The degradation stays findable: AGL-1679's recovery marker and AGL-1693's
|
|
76
|
+
* `/api/health/rate-limits` already cover every `consumeRateLimit` caller, so
|
|
77
|
+
* this one is watched by construction and needs no record of its own.
|
|
78
|
+
*
|
|
79
|
+
* ## What the caller is told
|
|
80
|
+
*
|
|
81
|
+
* A bare 429 with `Retry-After`. No `X-RateLimit-*` headers and no `degraded`
|
|
82
|
+
* flag, following `forms/submit`: this surface is public and unauthenticated,
|
|
83
|
+
* and both "you have N requests left" and "the global limiter is currently a
|
|
84
|
+
* per-instance one" are sentences an abuser would spend the window on.
|
|
85
|
+
*/
|
|
86
|
+
export declare function visitorWriteRateLimitRefusal(options: VisitorWriteRateLimitOptions): Promise<Response | null>;
|
|
87
|
+
export default visitorWriteRateLimitRefusal;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { isMachinePluginApiPath, lockdownIntentForMethod, VISITOR_WRITE_RATE_LIMIT, VISITOR_WRITE_RATE_WINDOW_MS, visitorWriteRateLimitKey } from "@aglyn/aglyn/server";
|
|
17
|
+
import { consumeRateLimit } from "./rate-limit-store.js";
|
|
18
|
+
import { NO_CLIENT_ADDRESS_BUCKET, readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
|
|
19
|
+
/**
|
|
20
|
+
* The rate-limit refusal a visitor-facing plugin write returns, or null to
|
|
21
|
+
* proceed (AGL-1770):
|
|
22
|
+
*
|
|
23
|
+
* const limited = await visitorWriteRateLimitRefusal({ path, hostId, request })
|
|
24
|
+
* if (limited) return limited
|
|
25
|
+
*
|
|
26
|
+
* Sits beside `visitorWriteRefusal` on purpose — same shape, same chokepoint,
|
|
27
|
+
* same "return it or carry on" contract. The policy (ceiling, window, key,
|
|
28
|
+
* which paths are exempt) is pure and lives in `@aglyn/aglyn/server`; this is
|
|
29
|
+
* only the durable half, which needs Firestore.
|
|
30
|
+
*/ /**
|
|
31
|
+
* The visitor's address, through the shared reader, or the no-address bucket.
|
|
32
|
+
*
|
|
33
|
+
* Kept counting rather than skipped when nothing readable arrives: this is a
|
|
34
|
+
* cost control over unauthenticated writes, and one that stops counting is an
|
|
35
|
+
* open write endpoint. The key already carries the site, so an unreadable
|
|
36
|
+
* address shares one budget per SITE and not one across the deployment.
|
|
37
|
+
*/ function clientIp(request) {
|
|
38
|
+
var _readClientIp;
|
|
39
|
+
const headers = request == null ? void 0 : request.headers;
|
|
40
|
+
if (typeof (headers == null ? void 0 : headers.get) !== 'function') return NO_CLIENT_ADDRESS_BUCKET;
|
|
41
|
+
return (_readClientIp = readClientIp(headers)) != null ? _readClientIp : NO_CLIENT_ADDRESS_BUCKET;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Counts one visitor write and returns a 429 once the window is spent.
|
|
45
|
+
*
|
|
46
|
+
* ## Reads are not limited, and that is a decision rather than an omission
|
|
47
|
+
*
|
|
48
|
+
* AGL-1770 scopes this to visitor-facing *writes*, and the cost argument
|
|
49
|
+
* agrees. `RATE_LIMITING.md`'s standing rule is "`checkRateLimit` for volume,
|
|
50
|
+
* `consumeRateLimit` for consequence": each durable call is a Firestore
|
|
51
|
+
* transaction — one read plus one write — which is the right price for a cart
|
|
52
|
+
* write that itself creates a document, and the wrong price for a GET. The
|
|
53
|
+
* dispatcher's read paths "leave before any Firestore read" (the lockdown gate
|
|
54
|
+
* says so), so a durable limiter there would be the ONLY Firestore work in the
|
|
55
|
+
* request — precisely the analytics-beacon anti-pattern the doc names.
|
|
56
|
+
*
|
|
57
|
+
* The residual is real and worth stating: tenant reads are not free (~40
|
|
58
|
+
* Firestore reads per rendered page). It is a different defect with a
|
|
59
|
+
* different remedy — caching and the ISR layer, not a per-request
|
|
60
|
+
* transaction — and it is not this one.
|
|
61
|
+
*
|
|
62
|
+
* ## Fail SOFT, inherited from `consumeRateLimit` and re-argued here
|
|
63
|
+
*
|
|
64
|
+
* A Firestore outage lets traffic through, flagged `degraded`, rather than
|
|
65
|
+
* refusing it. CSRF fails closed; this deliberately does not, for three
|
|
66
|
+
* reasons, the third of which is specific to this limiter:
|
|
67
|
+
*
|
|
68
|
+
* 1. **The blast radius of the strict choice is every storefront at once.**
|
|
69
|
+
* A Firestore blip that 429s every add-to-cart on every site costs
|
|
70
|
+
* merchants sales during an outage they did not cause. The risk it guards
|
|
71
|
+
* is unbounded document creation — storage. Lost revenue is the worse of
|
|
72
|
+
* the two, and it is the one the merchant notices.
|
|
73
|
+
* 2. **The fallback still enforces something.** `consumeRateLimit` degrades to
|
|
74
|
+
* the in-process limiter at the same ceiling, so the cap becomes
|
|
75
|
+
* `limit × warm instances` rather than nothing.
|
|
76
|
+
* 3. **The store and the thing being protected are the same Firestore.** The
|
|
77
|
+
* counter lives in `rateLimits` and the cart document it guards lives in
|
|
78
|
+
* `hosts/…/carts` — one database. If the limiter cannot write, the write it
|
|
79
|
+
* exists to bound is almost certainly failing too. Failing closed would
|
|
80
|
+
* refuse requests that were going to fail anyway, and *additionally* refuse
|
|
81
|
+
* the ones that would have succeeded. It buys nothing.
|
|
82
|
+
*
|
|
83
|
+
* The degradation stays findable: AGL-1679's recovery marker and AGL-1693's
|
|
84
|
+
* `/api/health/rate-limits` already cover every `consumeRateLimit` caller, so
|
|
85
|
+
* this one is watched by construction and needs no record of its own.
|
|
86
|
+
*
|
|
87
|
+
* ## What the caller is told
|
|
88
|
+
*
|
|
89
|
+
* A bare 429 with `Retry-After`. No `X-RateLimit-*` headers and no `degraded`
|
|
90
|
+
* flag, following `forms/submit`: this surface is public and unauthenticated,
|
|
91
|
+
* and both "you have N requests left" and "the global limiter is currently a
|
|
92
|
+
* per-instance one" are sentences an abuser would spend the window on.
|
|
93
|
+
*/ export async function visitorWriteRateLimitRefusal(options) {
|
|
94
|
+
var _options_nowMs, _options_limit, _options_windowMs;
|
|
95
|
+
var _options_request;
|
|
96
|
+
if (lockdownIntentForMethod((_options_request = options.request) == null ? void 0 : _options_request.method) === 'read') return null;
|
|
97
|
+
if (isMachinePluginApiPath(options.path)) return null;
|
|
98
|
+
const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
|
|
99
|
+
const rate = await consumeRateLimit(visitorWriteRateLimitKey(options.hostId, clientIp(options.request)), {
|
|
100
|
+
limit: (_options_limit = options.limit) != null ? _options_limit : VISITOR_WRITE_RATE_LIMIT,
|
|
101
|
+
windowMs: (_options_windowMs = options.windowMs) != null ? _options_windowMs : VISITOR_WRITE_RATE_WINDOW_MS,
|
|
102
|
+
now: nowMs,
|
|
103
|
+
firestore: options.firestore
|
|
104
|
+
});
|
|
105
|
+
if (rate.allowed) return null;
|
|
106
|
+
return Response.json({
|
|
107
|
+
error: 'Too many requests'
|
|
108
|
+
}, {
|
|
109
|
+
status: 429,
|
|
110
|
+
headers: {
|
|
111
|
+
'Retry-After': String(Math.max(1, Math.ceil((rate.resetMs - nowMs) / 1000)))
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export default visitorWriteRateLimitRefusal;
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=visitor-write-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/visitor-write-rate-limit.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isMachinePluginApiPath,\n lockdownIntentForMethod,\n VISITOR_WRITE_RATE_LIMIT,\n VISITOR_WRITE_RATE_WINDOW_MS,\n visitorWriteRateLimitKey,\n} from '@aglyn/aglyn/server'\nimport { consumeRateLimit } from './rate-limit-store'\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\n\n/**\n * The rate-limit refusal a visitor-facing plugin write returns, or null to\n * proceed (AGL-1770):\n *\n * const limited = await visitorWriteRateLimitRefusal({ path, hostId, request })\n * if (limited) return limited\n *\n * Sits beside `visitorWriteRefusal` on purpose — same shape, same chokepoint,\n * same \"return it or carry on\" contract. The policy (ceiling, window, key,\n * which paths are exempt) is pure and lives in `@aglyn/aglyn/server`; this is\n * only the durable half, which needs Firestore.\n */\n\n/**\n * The visitor's address, through the shared reader, or the no-address bucket.\n *\n * Kept counting rather than skipped when nothing readable arrives: this is a\n * cost control over unauthenticated writes, and one that stops counting is an\n * open write endpoint. The key already carries the site, so an unreadable\n * address shares one budget per SITE and not one across the deployment.\n */\nfunction clientIp(request: { headers?: { get?: (name: string) => unknown } }): string {\n const headers = request?.headers\n if (typeof headers?.get !== 'function') return NO_CLIENT_ADDRESS_BUCKET\n return (\n readClientIp(headers as { get(name: string): string | null }) ??\n NO_CLIENT_ADDRESS_BUCKET\n )\n}\n\nexport interface VisitorWriteRateLimitOptions {\n /** Dispatcher path, e.g. `commerce/cart`. Decides visitor vs machine. */\n path: string\n /** The site the write targets; `''` when none resolved (still limited). */\n hostId: string\n /** Read for its method and its client-address headers. */\n request: { method?: string; headers?: { get?: (name: string) => unknown } }\n limit?: number\n windowMs?: number\n nowMs?: number\n /** Injectable for tests; defaults to the Admin SDK's Firestore. */\n firestore?: unknown\n}\n\n/**\n * Counts one visitor write and returns a 429 once the window is spent.\n *\n * ## Reads are not limited, and that is a decision rather than an omission\n *\n * AGL-1770 scopes this to visitor-facing *writes*, and the cost argument\n * agrees. `RATE_LIMITING.md`'s standing rule is \"`checkRateLimit` for volume,\n * `consumeRateLimit` for consequence\": each durable call is a Firestore\n * transaction — one read plus one write — which is the right price for a cart\n * write that itself creates a document, and the wrong price for a GET. The\n * dispatcher's read paths \"leave before any Firestore read\" (the lockdown gate\n * says so), so a durable limiter there would be the ONLY Firestore work in the\n * request — precisely the analytics-beacon anti-pattern the doc names.\n *\n * The residual is real and worth stating: tenant reads are not free (~40\n * Firestore reads per rendered page). It is a different defect with a\n * different remedy — caching and the ISR layer, not a per-request\n * transaction — and it is not this one.\n *\n * ## Fail SOFT, inherited from `consumeRateLimit` and re-argued here\n *\n * A Firestore outage lets traffic through, flagged `degraded`, rather than\n * refusing it. CSRF fails closed; this deliberately does not, for three\n * reasons, the third of which is specific to this limiter:\n *\n * 1. **The blast radius of the strict choice is every storefront at once.**\n * A Firestore blip that 429s every add-to-cart on every site costs\n * merchants sales during an outage they did not cause. The risk it guards\n * is unbounded document creation — storage. Lost revenue is the worse of\n * the two, and it is the one the merchant notices.\n * 2. **The fallback still enforces something.** `consumeRateLimit` degrades to\n * the in-process limiter at the same ceiling, so the cap becomes\n * `limit × warm instances` rather than nothing.\n * 3. **The store and the thing being protected are the same Firestore.** The\n * counter lives in `rateLimits` and the cart document it guards lives in\n * `hosts/…/carts` — one database. If the limiter cannot write, the write it\n * exists to bound is almost certainly failing too. Failing closed would\n * refuse requests that were going to fail anyway, and *additionally* refuse\n * the ones that would have succeeded. It buys nothing.\n *\n * The degradation stays findable: AGL-1679's recovery marker and AGL-1693's\n * `/api/health/rate-limits` already cover every `consumeRateLimit` caller, so\n * this one is watched by construction and needs no record of its own.\n *\n * ## What the caller is told\n *\n * A bare 429 with `Retry-After`. No `X-RateLimit-*` headers and no `degraded`\n * flag, following `forms/submit`: this surface is public and unauthenticated,\n * and both \"you have N requests left\" and \"the global limiter is currently a\n * per-instance one\" are sentences an abuser would spend the window on.\n */\nexport async function visitorWriteRateLimitRefusal(\n options: VisitorWriteRateLimitOptions,\n): Promise<Response | null> {\n if (lockdownIntentForMethod(options.request?.method) === 'read') return null\n if (isMachinePluginApiPath(options.path)) return null\n\n const nowMs = options.nowMs ?? Date.now()\n const rate = await consumeRateLimit(\n visitorWriteRateLimitKey(options.hostId, clientIp(options.request)),\n {\n limit: options.limit ?? VISITOR_WRITE_RATE_LIMIT,\n windowMs: options.windowMs ?? VISITOR_WRITE_RATE_WINDOW_MS,\n now: nowMs,\n firestore: options.firestore,\n },\n )\n if (rate.allowed) return null\n\n return Response.json(\n { error: 'Too many requests' },\n {\n status: 429,\n headers: {\n 'Retry-After': String(\n Math.max(1, Math.ceil((rate.resetMs - nowMs) / 1000)),\n ),\n },\n },\n )\n}\n\nexport default visitorWriteRateLimitRefusal\n"],"names":["isMachinePluginApiPath","lockdownIntentForMethod","VISITOR_WRITE_RATE_LIMIT","VISITOR_WRITE_RATE_WINDOW_MS","visitorWriteRateLimitKey","consumeRateLimit","NO_CLIENT_ADDRESS_BUCKET","readClientIp","clientIp","request","headers","get","visitorWriteRateLimitRefusal","options","method","path","nowMs","Date","now","rate","hostId","limit","windowMs","firestore","allowed","Response","json","error","status","String","Math","max","ceil","resetMs"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,EACtBC,uBAAuB,EACvBC,wBAAwB,EACxBC,4BAA4B,EAC5BC,wBAAwB,QACnB,sBAAqB;AAC5B,SAASC,gBAAgB,QAAQ,wBAAoB;AACrD,SACEC,wBAAwB,EACxBC,YAAY,QACP,oCAAmC;AAE1C;;;;;;;;;;;CAWC,GAED;;;;;;;CAOC,GACD,SAASC,SAASC,OAA0D;QAIxEF;IAHF,MAAMG,UAAUD,2BAAAA,QAASC,OAAO;IAChC,IAAI,QAAOA,2BAAAA,QAASC,GAAG,MAAK,YAAY,OAAOL;IAC/C,QACEC,gBAAAA,aAAaG,oBAAbH,gBACAD;AAEJ;AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDC,GACD,OAAO,eAAeM,6BACpBC,OAAqC;QAKvBA,gBAIHA,gBACGA;QARcA;IAA5B,IAAIZ,yBAAwBY,mBAAAA,QAAQJ,OAAO,qBAAfI,iBAAiBC,MAAM,MAAM,QAAQ,OAAO;IACxE,IAAId,uBAAuBa,QAAQE,IAAI,GAAG,OAAO;IAEjD,MAAMC,SAAQH,iBAAAA,QAAQG,KAAK,YAAbH,iBAAiBI,KAAKC,GAAG;IACvC,MAAMC,OAAO,MAAMd,iBACjBD,yBAAyBS,QAAQO,MAAM,EAAEZ,SAASK,QAAQJ,OAAO,IACjE;QACEY,KAAK,GAAER,iBAAAA,QAAQQ,KAAK,YAAbR,iBAAiBX;QACxBoB,QAAQ,GAAET,oBAAAA,QAAQS,QAAQ,YAAhBT,oBAAoBV;QAC9Be,KAAKF;QACLO,WAAWV,QAAQU,SAAS;IAC9B;IAEF,IAAIJ,KAAKK,OAAO,EAAE,OAAO;IAEzB,OAAOC,SAASC,IAAI,CAClB;QAAEC,OAAO;IAAoB,GAC7B;QACEC,QAAQ;QACRlB,SAAS;YACP,eAAemB,OACbC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAAC,AAACb,CAAAA,KAAKc,OAAO,GAAGjB,KAAI,IAAK;QAEnD;IACF;AAEJ;AAEA,eAAeJ,6BAA4B"}
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
* Attach and detach names for the console (AGL-1136).
|
|
19
|
+
*
|
|
20
|
+
* Two layers. The bottom one — `attachProjectDomain` / `detachProjectDomain` —
|
|
21
|
+
* takes a fully-qualified name and is what a custom **console** domain uses
|
|
22
|
+
* (AGL-1373). The top one is the workspace-subdomain pair below, which is that
|
|
23
|
+
* primitive with `{slug}.aglyn.com` built for it.
|
|
24
|
+
*
|
|
25
|
+
* The hosting vendor is no longer either of them. Registering a name goes
|
|
26
|
+
* through `domain-provider.ts`, so the same workspace subdomain is created by
|
|
27
|
+
* Vercel's API on Aglyn's own hosting, by a wildcard record on a Docker
|
|
28
|
+
* install, or by whatever an operator put behind the webhook driver. What
|
|
29
|
+
* stays here is everything that is Aglyn's own policy rather than a vendor's:
|
|
30
|
+
* the slug arithmetic, the upload-CORS reconcile, and the three properties
|
|
31
|
+
* below.
|
|
32
|
+
*
|
|
33
|
+
* AGL-1135 removed the `*.aglyn.com` wildcard, because it meant every
|
|
34
|
+
* hostname under the domain served a real console sign-in page —
|
|
35
|
+
* `billing-security-update.aglyn.com` returned 200 with a valid certificate.
|
|
36
|
+
* Removing it made unregistered names 404, and made **registered** names
|
|
37
|
+
* something that has to be registered: a slug only resolves if that exact
|
|
38
|
+
* domain is attached.
|
|
39
|
+
*
|
|
40
|
+
* Nothing did that, so every workspace created after AGL-1135 had a subdomain
|
|
41
|
+
* that 404s. AGL-1115 then made signup provision an org, which turned a latent
|
|
42
|
+
* gap into one every new account hits.
|
|
43
|
+
*
|
|
44
|
+
* ## Three properties, in the order they matter
|
|
45
|
+
*
|
|
46
|
+
* 1. **It can never fail an org.** Every entry point swallows its errors and
|
|
47
|
+
* returns a verdict instead of throwing. The console is path-routed
|
|
48
|
+
* (`app.aglyn.com/{slug}`) and that is the canonical form, so a workspace
|
|
49
|
+
* with no subdomain is fully usable — while an org creation that rolled
|
|
50
|
+
* back because a DNS API was slow would not be.
|
|
51
|
+
* 2. **Unconfigured is a no-op, not an error.** With no provider this reports
|
|
52
|
+
* `skipped`. A deployment that registers names some other way must not log
|
|
53
|
+
* an error per signup forever.
|
|
54
|
+
* 3. **A rename ADDS without removing.** The old slug keeps a tombstone that
|
|
55
|
+
* 308s to the new one (AGL-585/236), and a redirect can only run on a
|
|
56
|
+
* hostname that still resolves. Detaching the old domain would break the
|
|
57
|
+
* very redirect the tombstone exists to serve.
|
|
58
|
+
*/
|
|
59
|
+
import { type DomainAttachOptions, type DomainOutcome, type DomainScope, type ProjectDomainStatus } from './domain-provider';
|
|
60
|
+
import { type UploadCorsRelease, type UploadCorsVerdict } from './upload-cors-reconcile';
|
|
61
|
+
export { domainStateServes, redirectHostname, type ProjectDomainState, type ProjectDomainStatus, type ProjectDomainVerification, } from './domain-provider';
|
|
62
|
+
export type WorkspaceDomainOutcome = DomainOutcome;
|
|
63
|
+
export interface WorkspaceDomainResult {
|
|
64
|
+
outcome: WorkspaceDomainOutcome;
|
|
65
|
+
domain: string;
|
|
66
|
+
detail?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Whether a browser at this name can complete a signed direct-to-GCS upload
|
|
69
|
+
* (AGL-1452).
|
|
70
|
+
*
|
|
71
|
+
* Present only when the attach made the name a SERVING origin — a redirect
|
|
72
|
+
* never becomes one. `permitted: false` carries the exact command a human
|
|
73
|
+
* must run, so the gap is legible here rather than days later as a large
|
|
74
|
+
* upload that fails behind a generic snackbar.
|
|
75
|
+
*/
|
|
76
|
+
uploadCors?: UploadCorsVerdict | null;
|
|
77
|
+
/**
|
|
78
|
+
* What a DETACH did about the origin the name leaves behind (AGL-1452).
|
|
79
|
+
*
|
|
80
|
+
* The attach half without this one only ever grows the allowlist. A stale
|
|
81
|
+
* origin is a standing permission to complete a signed upload, held by a host
|
|
82
|
+
* we no longer serve — and for a white-label console domain, one the customer
|
|
83
|
+
* still controls.
|
|
84
|
+
*/
|
|
85
|
+
uploadCorsRelease?: UploadCorsRelease | null;
|
|
86
|
+
}
|
|
87
|
+
export type ProjectDomainOptions = DomainAttachOptions;
|
|
88
|
+
/**
|
|
89
|
+
* Attach one fully-qualified name to the console.
|
|
90
|
+
*
|
|
91
|
+
* Idempotent by contract: a name already registered comes back as
|
|
92
|
+
* `already-exists`, which is success — the reconcile script and the create
|
|
93
|
+
* path can both run without coordinating.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ Tolerating `already-exists` is only safe while **every** name this
|
|
96
|
+
* function is asked to attach is also claimed in Firestore. Without that, a
|
|
97
|
+
* second org attaches a name someone else already holds, reads it as success,
|
|
98
|
+
* and gets a console that looks healthy while its visitors are served — or
|
|
99
|
+
* redirected — somewhere else. That is the AGL-743 shape, and
|
|
100
|
+
* `console-domains.ts` is where the claim is kept in step.
|
|
101
|
+
*/
|
|
102
|
+
export declare function attachProjectDomain(name: string, options?: ProjectDomainOptions, scope?: DomainScope): Promise<WorkspaceDomainResult>;
|
|
103
|
+
/**
|
|
104
|
+
* The command still owed after a set of attaches, or null when nothing is
|
|
105
|
+
* (AGL-1452).
|
|
106
|
+
*
|
|
107
|
+
* One remedy rather than a list: every verdict names the same bucket and the
|
|
108
|
+
* same command, and a caller that has to render five copies of it renders
|
|
109
|
+
* none.
|
|
110
|
+
*/
|
|
111
|
+
export declare function pendingUploadCorsRemedy(results: WorkspaceDomainResult[]): string | null;
|
|
112
|
+
/** Detach one fully-qualified name from the console. */
|
|
113
|
+
export declare function detachProjectDomain(name: string, scope?: DomainScope): Promise<WorkspaceDomainResult>;
|
|
114
|
+
/**
|
|
115
|
+
* Attach `{slug}.<workspace domain>` so the workspace subdomain resolves.
|
|
116
|
+
*
|
|
117
|
+
* The `!slug` guard is not redundant with the one inside `attachProjectDomain`:
|
|
118
|
+
* an empty slug still produces the well-formed `.aglyn.com`, and on the detach
|
|
119
|
+
* side that is a request to delete the apex.
|
|
120
|
+
*/
|
|
121
|
+
export declare function attachWorkspaceDomain(slug: string): Promise<WorkspaceDomainResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Detach the domain, for org erasure.
|
|
124
|
+
*
|
|
125
|
+
* NOT called on rename: the previous slug keeps serving a 308 to the new one,
|
|
126
|
+
* and a redirect needs a hostname that still resolves.
|
|
127
|
+
*/
|
|
128
|
+
export declare function detachWorkspaceDomain(slug: string): Promise<WorkspaceDomainResult>;
|
|
129
|
+
/**
|
|
130
|
+
* Whether the workspace-domain integration is configured at all.
|
|
131
|
+
*
|
|
132
|
+
* Exposed so a reconcile script can say "not configured" once instead of
|
|
133
|
+
* reporting every workspace as skipped, which reads like a failure.
|
|
134
|
+
*/
|
|
135
|
+
export declare function workspaceDomainsConfigured(): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* What a name is ACTUALLY doing, right now.
|
|
138
|
+
*
|
|
139
|
+
* `attachProjectDomain` answers "did the registration succeed", which is a
|
|
140
|
+
* different question and a much weaker one. A domain can be registered and
|
|
141
|
+
* still serve nothing — ownership unproven, DNS not yet pointed, certificate
|
|
142
|
+
* not yet issued — and all three return a perfectly successful attach.
|
|
143
|
+
*
|
|
144
|
+
* That is the gap this closes (AGL-1913): the site custom-domain wizard
|
|
145
|
+
* reported `attached: true` off the attach alone, so "certificate is still
|
|
146
|
+
* issuing" and "this will never work" rendered as the same green chip, and the
|
|
147
|
+
* only advice the docs could give was to press retry again.
|
|
148
|
+
*
|
|
149
|
+
* `scope` picks which app should be answering for the name — `tenant` for a
|
|
150
|
+
* published site's custom domain, `console` for a workspace subdomain or a
|
|
151
|
+
* white-label console domain. It was a raw project id before the provider
|
|
152
|
+
* seam, which only ever meant anything to one vendor.
|
|
153
|
+
*/
|
|
154
|
+
export declare function projectDomainStatus(name: string, options?: {
|
|
155
|
+
scope?: DomainScope;
|
|
156
|
+
}): Promise<ProjectDomainStatus>;
|