@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,90 @@
|
|
|
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
|
+
* Published ceiling for `webSettings.allowedDomains`, quoted rather than
|
|
19
|
+
* measured: *"Each key supports a maximum of 250 domains."* An occupancy
|
|
20
|
+
* reading is NOT a limit — this repo already spent two weeks treating a
|
|
21
|
+
* snapshot of nine entries as a hard ceiling. Re-probe occupancy, never the
|
|
22
|
+
* limit.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_ALLOWED_DOMAINS = 250;
|
|
25
|
+
export type RecaptchaAllowlistOutcome =
|
|
26
|
+
/** We wrote the entry and re-read the key to confirm it. */
|
|
27
|
+
'listed'
|
|
28
|
+
/** The exact name was already on the key; nothing was written. */
|
|
29
|
+
| 'already-listed'
|
|
30
|
+
/** We removed the entry and re-read the key to confirm it. */
|
|
31
|
+
| 'removed'
|
|
32
|
+
/** Nothing to remove — the exact name was not on the key. */
|
|
33
|
+
| 'absent'
|
|
34
|
+
/**
|
|
35
|
+
* This deployment runs no App Check reCAPTCHA provider, so there is no
|
|
36
|
+
* allowlist to maintain and nothing is gated. Self-host, and local dev.
|
|
37
|
+
*/
|
|
38
|
+
| 'unenforced'
|
|
39
|
+
/** The key is at {@link MAX_ALLOWED_DOMAINS}. A commercial event, not a bug. */
|
|
40
|
+
| 'full'
|
|
41
|
+
/** Anything else: misconfiguration, IAM, transport, or a suspicious write. */
|
|
42
|
+
| 'failed';
|
|
43
|
+
export interface RecaptchaAllowlistResult {
|
|
44
|
+
outcome: RecaptchaAllowlistOutcome;
|
|
45
|
+
domain: string;
|
|
46
|
+
/** Human-readable cause. Always set when the outcome is not a success. */
|
|
47
|
+
detail?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Live `allowedDomains` after the operation, when we got far enough to read
|
|
50
|
+
* them. Occupancy is worth surfacing precisely because it must never be
|
|
51
|
+
* quoted from memory.
|
|
52
|
+
*/
|
|
53
|
+
allowedDomains?: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Does this outcome mean the origin can attest?
|
|
57
|
+
*
|
|
58
|
+
* The single decision point, so that no caller can invent a looser one. Note
|
|
59
|
+
* what is NOT here: `full` and `failed` both leave a domain that will 401 its
|
|
60
|
+
* customer at sign-in, and `full` is the easier of the two to mistake for a
|
|
61
|
+
* warning.
|
|
62
|
+
*/
|
|
63
|
+
export declare function allowlistSatisfied(outcome: RecaptchaAllowlistOutcome): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Put a custom console domain on the key, so a browser there can attest.
|
|
66
|
+
*
|
|
67
|
+
* Called by `activateConsoleDomain` for the **serving** name only. Redirect
|
|
68
|
+
* twins (`www.acme.com` → `acme.com`) never execute the console, never load
|
|
69
|
+
* reCAPTCHA, and would spend a slot of the 250 for nothing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function allowConsoleOrigin(input: string): Promise<RecaptchaAllowlistResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Take a domain back off the key when its claim is released.
|
|
74
|
+
*
|
|
75
|
+
* Removes the EXACT entry and nothing else. A filter that matched subdomains
|
|
76
|
+
* would take `aglyn.com` off the key while releasing `console.aglyn.com` —
|
|
77
|
+
* and the entry it deleted is the one every Aglyn origin attests against.
|
|
78
|
+
*/
|
|
79
|
+
export declare function reclaimConsoleOrigin(input: string): Promise<RecaptchaAllowlistResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Read-only occupancy, for an operator asking "how many of the 250 are gone?"
|
|
82
|
+
*
|
|
83
|
+
* Deliberately returns the live list rather than a count: a count is what got
|
|
84
|
+
* quoted as a ceiling for two weeks.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readConsoleOriginAllowlist(): Promise<{
|
|
87
|
+
domains: string[] | null;
|
|
88
|
+
limit: number;
|
|
89
|
+
detail?: string;
|
|
90
|
+
}>;
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright 2026 Aglyn LLC
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/ /**
|
|
19
|
+
* The App Check reCAPTCHA allowlist, written from the product (AGL-1378).
|
|
20
|
+
*
|
|
21
|
+
* ## Why this file has to exist
|
|
22
|
+
*
|
|
23
|
+
* The console reads Firestore **client-side**, and App Check gates those reads
|
|
24
|
+
* with a reCAPTCHA token that the key itself refuses to mint on an origin it
|
|
25
|
+
* has never heard of. Measured against the live key on 2026-08-23, with both
|
|
26
|
+
* controls, by requesting `recaptcha/api2/anchor` with a base64 `co` origin:
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* https://app.aglyn.com:443 ACCEPTED (listed)
|
|
30
|
+
* https://never-registered-9f3x.aglyn.com:443 ACCEPTED (subtree of a listed entry)
|
|
31
|
+
* https://console.acme-agency.example:443 REJECTED "Invalid domain for site key"
|
|
32
|
+
* https://console.northwind-coffee.com:443 REJECTED "Invalid domain for site key"
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* A rejected solve means `initializeAppCheck` → `getToken` fails with
|
|
36
|
+
* `appCheck/recaptcha-error`, and the very next Identity Platform call is
|
|
37
|
+
* refused `401 UNAUTHENTICATED — "Firebase App Check token is invalid"`
|
|
38
|
+
* **before** token validation (`docs/design/agl-1099a-poc-findings.md` §2).
|
|
39
|
+
* So a custom console domain that is attached and routed but not allowlisted
|
|
40
|
+
* renders a console that can never sign anyone in — and the symptom the
|
|
41
|
+
* customer reports is "Missing or insufficient permissions", which is the same
|
|
42
|
+
* string a Security Rules verdict produces. That is the misdiagnosis this
|
|
43
|
+
* whole area keeps generating; the fix is to never let the state exist.
|
|
44
|
+
*
|
|
45
|
+
* ## Why it was believed impossible, and why that is stale
|
|
46
|
+
*
|
|
47
|
+
* AGL-1378 concluded the key was a classic v3 key with no management API. The
|
|
48
|
+
* key was auto-migrated into a Google-created project, and every probe asked
|
|
49
|
+
* `aglyn-main` — where the reCAPTCHA API genuinely is disabled — rather than
|
|
50
|
+
* the project the key actually lives in. Measured 2026-08-23:
|
|
51
|
+
* `recaptchaenterprise.googleapis.com` is enabled there, two accounts we
|
|
52
|
+
* control are `roles/owner`, and `projects.keys.patch` on
|
|
53
|
+
* `webSettings.allowedDomains` answers `200`.
|
|
54
|
+
*
|
|
55
|
+
* ## The two hazards this code is shaped around
|
|
56
|
+
*
|
|
57
|
+
* 1. **A patch that drops sibling fields.** `webSettings` also carries
|
|
58
|
+
* `allowAllDomains`, `integrationType`, `allowAmpTraffic` and
|
|
59
|
+
* `challengeSecurityPreference`; a whole-object write that omitted
|
|
60
|
+
* `allowAllDomains` would read as `false`… or a future default. So the mask
|
|
61
|
+
* is `webSettings.allowedDomains` — the narrowest path the API accepts —
|
|
62
|
+
* **and** the response is re-read afterwards: every domain that was there
|
|
63
|
+
* before must still be there, and `allowAllDomains` must still be `false`.
|
|
64
|
+
* A write that silently widened the key to every origin is a worse outcome
|
|
65
|
+
* than the domain never being listed.
|
|
66
|
+
* 2. **Silent success.** Every outcome here is explicit, and
|
|
67
|
+
* {@link allowlistSatisfied} is the ONLY place that decides which ones mean
|
|
68
|
+
* "the customer can actually sign in". `activateConsoleDomain` refuses to
|
|
69
|
+
* mark a claim `active` unless it says yes.
|
|
70
|
+
*
|
|
71
|
+
* ## Exact names, never subtree cover
|
|
72
|
+
*
|
|
73
|
+
* A listed entry covers its whole subtree but never its parents (proved
|
|
74
|
+
* 2026-08-10). It is therefore tempting to skip the write when some existing
|
|
75
|
+
* entry already covers the new name. Deliberately not done: if org A holds
|
|
76
|
+
* `acme.com` and org B later claims `console.acme.com`, B would be silently
|
|
77
|
+
* covered by A's entry — and A detaching would break B with no trace linking
|
|
78
|
+
* the two. One entry per claimed apex is what the documented 250-domain
|
|
79
|
+
* ceiling is denominated in anyway (250 distinct customer apexes, occupancy 5
|
|
80
|
+
* on 2026-08-23), so exactness costs nothing we were not already counting.
|
|
81
|
+
*/ import { getApp } from "firebase-admin/app";
|
|
82
|
+
const RECAPTCHA_API = 'https://recaptchaenterprise.googleapis.com/v1';
|
|
83
|
+
/**
|
|
84
|
+
* Published ceiling for `webSettings.allowedDomains`, quoted rather than
|
|
85
|
+
* measured: *"Each key supports a maximum of 250 domains."* An occupancy
|
|
86
|
+
* reading is NOT a limit — this repo already spent two weeks treating a
|
|
87
|
+
* snapshot of nine entries as a hard ceiling. Re-probe occupancy, never the
|
|
88
|
+
* limit.
|
|
89
|
+
*/ export const MAX_ALLOWED_DOMAINS = 250;
|
|
90
|
+
/**
|
|
91
|
+
* Ceiling on the whole exchange, matching `workspace-domains.ts`.
|
|
92
|
+
*
|
|
93
|
+
* This one is awaited by an attach the operator is watching, so an
|
|
94
|
+
* unresponsive Google API must lose quickly rather than hang the activation
|
|
95
|
+
* until the platform kills the function.
|
|
96
|
+
*/ const REQUEST_TIMEOUT_MS = 8000;
|
|
97
|
+
/**
|
|
98
|
+
* Does this outcome mean the origin can attest?
|
|
99
|
+
*
|
|
100
|
+
* The single decision point, so that no caller can invent a looser one. Note
|
|
101
|
+
* what is NOT here: `full` and `failed` both leave a domain that will 401 its
|
|
102
|
+
* customer at sign-in, and `full` is the easier of the two to mistake for a
|
|
103
|
+
* warning.
|
|
104
|
+
*/ export function allowlistSatisfied(outcome) {
|
|
105
|
+
return outcome === 'listed' || outcome === 'already-listed' || outcome === 'removed' || outcome === 'absent' || outcome === 'unenforced';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `unenforced` is the ONLY safe way to skip, and it is gated on the client's
|
|
109
|
+
* own site key rather than on this file's credentials.
|
|
110
|
+
*
|
|
111
|
+
* The failure this shape exists to prevent: a production deployment that runs
|
|
112
|
+
* App Check but has no `RECAPTCHA_ADMIN_KEY_NAME` would, under a
|
|
113
|
+
* "missing config → skip" rule, report every custom domain ready while listing
|
|
114
|
+
* none of them. Configuration absence must therefore mean two different things
|
|
115
|
+
* depending on whether App Check is running at all — and the tell is
|
|
116
|
+
* `NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY`, which is exactly what
|
|
117
|
+
* `new ReCaptchaV3Provider(...)` is handed.
|
|
118
|
+
*
|
|
119
|
+
* The admin key name must also END with that site key. A resource name
|
|
120
|
+
* pointing at a different key would be written happily and attest nothing —
|
|
121
|
+
* the one misconfiguration whose symptom is indistinguishable from success.
|
|
122
|
+
*/ function config() {
|
|
123
|
+
var _process_env_NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY, _process_env_RECAPTCHA_ADMIN_KEY_NAME;
|
|
124
|
+
const siteKey = String((_process_env_NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY) != null ? _process_env_NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY : '').trim();
|
|
125
|
+
const keyName = String((_process_env_RECAPTCHA_ADMIN_KEY_NAME = process.env.RECAPTCHA_ADMIN_KEY_NAME) != null ? _process_env_RECAPTCHA_ADMIN_KEY_NAME : '').trim();
|
|
126
|
+
if (!siteKey) return {
|
|
127
|
+
kind: 'unenforced'
|
|
128
|
+
};
|
|
129
|
+
if (!keyName) {
|
|
130
|
+
return {
|
|
131
|
+
kind: 'misconfigured',
|
|
132
|
+
detail: 'App Check runs here (NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY is set) but RECAPTCHA_ADMIN_KEY_NAME is not, so the allowlist cannot be written'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!/^projects\/[^/]+\/keys\/[^/]+$/.test(keyName)) {
|
|
136
|
+
return {
|
|
137
|
+
kind: 'misconfigured',
|
|
138
|
+
detail: `RECAPTCHA_ADMIN_KEY_NAME must be projects/{project}/keys/{siteKey}, got "${keyName}"`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (!keyName.endsWith(`/${siteKey}`)) {
|
|
142
|
+
return {
|
|
143
|
+
kind: 'misconfigured',
|
|
144
|
+
detail: 'RECAPTCHA_ADMIN_KEY_NAME names a different key than NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY — writing it would allowlist a key nothing uses'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
kind: 'ready',
|
|
149
|
+
keyName
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* An OAuth token for the same service account firebase-admin already runs as.
|
|
154
|
+
*
|
|
155
|
+
* Not a second credential: `cert()`'s `getAccessToken()` requests
|
|
156
|
+
* `https://www.googleapis.com/auth/cloud-platform` among its scopes, which is
|
|
157
|
+
* the scope `recaptchaenterprise.googleapis.com` asks for. The account needs
|
|
158
|
+
* `recaptchaenterprise.keys.get` + `.update` on the project the KEY lives in —
|
|
159
|
+
* which is not `aglyn-main`; the key was migrated into its own project and
|
|
160
|
+
* that is the whole reason this was thought unautomatable.
|
|
161
|
+
*/ async function accessToken() {
|
|
162
|
+
const credential = getApp().options.credential;
|
|
163
|
+
if (!credential) throw new Error('firebase-admin app has no credential');
|
|
164
|
+
const token = await credential.getAccessToken();
|
|
165
|
+
const value = token == null ? void 0 : token.access_token;
|
|
166
|
+
if (!value) throw new Error('credential returned no access token');
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
async function call(url, init) {
|
|
170
|
+
var _rest_headers;
|
|
171
|
+
const { token } = init, rest = _object_without_properties_loose(init, [
|
|
172
|
+
"token"
|
|
173
|
+
]);
|
|
174
|
+
const response = await fetch(url, _extends({}, rest, {
|
|
175
|
+
headers: _extends({}, (_rest_headers = rest.headers) != null ? _rest_headers : {}, {
|
|
176
|
+
Authorization: `Bearer ${token}`,
|
|
177
|
+
'Content-Type': 'application/json'
|
|
178
|
+
}),
|
|
179
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
180
|
+
}));
|
|
181
|
+
const body = await response.json().catch(()=>({}));
|
|
182
|
+
return {
|
|
183
|
+
ok: response.ok,
|
|
184
|
+
status: response.status,
|
|
185
|
+
body
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function domainsOf(key) {
|
|
189
|
+
var _key_webSettings;
|
|
190
|
+
const domains = key == null ? void 0 : (_key_webSettings = key.webSettings) == null ? void 0 : _key_webSettings.allowedDomains;
|
|
191
|
+
return Array.isArray(domains) ? domains.map((entry)=>String(entry)) : [];
|
|
192
|
+
}
|
|
193
|
+
function normalize(input) {
|
|
194
|
+
return String(input != null ? input : '').trim().toLowerCase().replace(/\.$/, '');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Read the key, apply `mutate` to its domains, write back, and prove the write.
|
|
198
|
+
*
|
|
199
|
+
* `mutate` returning `null` means "no write is needed" and is a success.
|
|
200
|
+
*/ async function rewrite(domain, mutate) {
|
|
201
|
+
var _after_webSettings;
|
|
202
|
+
const settings = config();
|
|
203
|
+
if (settings.kind === 'unenforced') return {
|
|
204
|
+
outcome: 'unenforced',
|
|
205
|
+
domain
|
|
206
|
+
};
|
|
207
|
+
if (settings.kind === 'misconfigured') {
|
|
208
|
+
return {
|
|
209
|
+
outcome: 'failed',
|
|
210
|
+
domain,
|
|
211
|
+
detail: settings.detail
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
let token;
|
|
215
|
+
try {
|
|
216
|
+
token = await accessToken();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return {
|
|
219
|
+
outcome: 'failed',
|
|
220
|
+
domain,
|
|
221
|
+
detail: `credential: ${String(error)}`
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const url = `${RECAPTCHA_API}/${settings.keyName}`;
|
|
225
|
+
let before;
|
|
226
|
+
try {
|
|
227
|
+
const read = await call(url, {
|
|
228
|
+
method: 'GET',
|
|
229
|
+
token
|
|
230
|
+
});
|
|
231
|
+
if (!read.ok) {
|
|
232
|
+
var _ref;
|
|
233
|
+
var _read_body_error, _read_body;
|
|
234
|
+
return {
|
|
235
|
+
outcome: 'failed',
|
|
236
|
+
domain,
|
|
237
|
+
detail: `read ${read.status}: ${(_ref = (_read_body = read.body) == null ? void 0 : (_read_body_error = _read_body.error) == null ? void 0 : _read_body_error.message) != null ? _ref : 'unknown'}`
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
before = read.body;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
outcome: 'failed',
|
|
244
|
+
domain,
|
|
245
|
+
detail: `read: ${String(error)}`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const current = domainsOf(before);
|
|
249
|
+
const plan = mutate(current);
|
|
250
|
+
if ('skip' in plan) {
|
|
251
|
+
return {
|
|
252
|
+
outcome: plan.skip,
|
|
253
|
+
domain,
|
|
254
|
+
detail: plan.detail,
|
|
255
|
+
allowedDomains: current
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
let after;
|
|
259
|
+
try {
|
|
260
|
+
const written = await call(`${url}?updateMask=webSettings.allowedDomains`, {
|
|
261
|
+
method: 'PATCH',
|
|
262
|
+
token,
|
|
263
|
+
body: JSON.stringify({
|
|
264
|
+
webSettings: {
|
|
265
|
+
allowedDomains: plan.next
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
if (!written.ok) {
|
|
270
|
+
var _ref1;
|
|
271
|
+
var _written_body_error, _written_body;
|
|
272
|
+
return {
|
|
273
|
+
outcome: 'failed',
|
|
274
|
+
domain,
|
|
275
|
+
detail: `write ${written.status}: ${(_ref1 = (_written_body = written.body) == null ? void 0 : (_written_body_error = _written_body.error) == null ? void 0 : _written_body_error.message) != null ? _ref1 : 'unknown'}`,
|
|
276
|
+
allowedDomains: current
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
after = written.body;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
outcome: 'failed',
|
|
283
|
+
domain,
|
|
284
|
+
detail: `write: ${String(error)}`,
|
|
285
|
+
allowedDomains: current
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Hazard 1. The response is the authoritative post-state, so believe it
|
|
289
|
+
// rather than the request body. A mask that the API interpreted more
|
|
290
|
+
// broadly than intended shows up here as a missing sibling field or a
|
|
291
|
+
// vanished entry — and `allowAllDomains: true` would be a key open to every
|
|
292
|
+
// origin on the internet, which must never be a side effect of adding one.
|
|
293
|
+
const applied = domainsOf(after);
|
|
294
|
+
const lost = plan.next.filter((entry)=>!applied.includes(entry));
|
|
295
|
+
if (lost.length > 0) {
|
|
296
|
+
return {
|
|
297
|
+
outcome: 'failed',
|
|
298
|
+
domain,
|
|
299
|
+
detail: `the key did not keep every domain the write sent (missing: ${lost.join(', ')})`,
|
|
300
|
+
allowedDomains: applied
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if ((after == null ? void 0 : (_after_webSettings = after.webSettings) == null ? void 0 : _after_webSettings.allowAllDomains) === true) {
|
|
304
|
+
return {
|
|
305
|
+
outcome: 'failed',
|
|
306
|
+
domain,
|
|
307
|
+
detail: 'the write left allowAllDomains true — the key would accept every origin',
|
|
308
|
+
allowedDomains: applied
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
outcome: plan.outcome,
|
|
313
|
+
domain,
|
|
314
|
+
allowedDomains: applied
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Put a custom console domain on the key, so a browser there can attest.
|
|
319
|
+
*
|
|
320
|
+
* Called by `activateConsoleDomain` for the **serving** name only. Redirect
|
|
321
|
+
* twins (`www.acme.com` → `acme.com`) never execute the console, never load
|
|
322
|
+
* reCAPTCHA, and would spend a slot of the 250 for nothing.
|
|
323
|
+
*/ export async function allowConsoleOrigin(input) {
|
|
324
|
+
const domain = normalize(input);
|
|
325
|
+
if (!domain) return {
|
|
326
|
+
outcome: 'failed',
|
|
327
|
+
domain: '',
|
|
328
|
+
detail: 'No domain given'
|
|
329
|
+
};
|
|
330
|
+
return rewrite(domain, (current)=>{
|
|
331
|
+
if (current.includes(domain)) return {
|
|
332
|
+
skip: 'already-listed'
|
|
333
|
+
};
|
|
334
|
+
if (current.length >= MAX_ALLOWED_DOMAINS) {
|
|
335
|
+
return {
|
|
336
|
+
skip: 'full',
|
|
337
|
+
detail: `the key holds ${current.length} of ${MAX_ALLOWED_DOMAINS} domains`
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
next: [
|
|
342
|
+
...current,
|
|
343
|
+
domain
|
|
344
|
+
],
|
|
345
|
+
outcome: 'listed'
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Take a domain back off the key when its claim is released.
|
|
351
|
+
*
|
|
352
|
+
* Removes the EXACT entry and nothing else. A filter that matched subdomains
|
|
353
|
+
* would take `aglyn.com` off the key while releasing `console.aglyn.com` —
|
|
354
|
+
* and the entry it deleted is the one every Aglyn origin attests against.
|
|
355
|
+
*/ export async function reclaimConsoleOrigin(input) {
|
|
356
|
+
const domain = normalize(input);
|
|
357
|
+
if (!domain) return {
|
|
358
|
+
outcome: 'failed',
|
|
359
|
+
domain: '',
|
|
360
|
+
detail: 'No domain given'
|
|
361
|
+
};
|
|
362
|
+
return rewrite(domain, (current)=>{
|
|
363
|
+
if (!current.includes(domain)) return {
|
|
364
|
+
skip: 'absent'
|
|
365
|
+
};
|
|
366
|
+
const next = current.filter((entry)=>entry !== domain);
|
|
367
|
+
if (next.length !== current.length - 1) {
|
|
368
|
+
return {
|
|
369
|
+
skip: 'failed',
|
|
370
|
+
detail: `removing ${domain} would have removed ${current.length - next.length} entries`
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
next,
|
|
375
|
+
outcome: 'removed'
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Read-only occupancy, for an operator asking "how many of the 250 are gone?"
|
|
381
|
+
*
|
|
382
|
+
* Deliberately returns the live list rather than a count: a count is what got
|
|
383
|
+
* quoted as a ceiling for two weeks.
|
|
384
|
+
*/ export async function readConsoleOriginAllowlist() {
|
|
385
|
+
var _result_allowedDomains;
|
|
386
|
+
const result = await rewrite('', ()=>({
|
|
387
|
+
skip: 'absent'
|
|
388
|
+
}));
|
|
389
|
+
if (result.outcome === 'unenforced') {
|
|
390
|
+
return {
|
|
391
|
+
domains: null,
|
|
392
|
+
limit: MAX_ALLOWED_DOMAINS,
|
|
393
|
+
detail: 'App Check is not configured here'
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (result.outcome === 'failed') {
|
|
397
|
+
return {
|
|
398
|
+
domains: null,
|
|
399
|
+
limit: MAX_ALLOWED_DOMAINS,
|
|
400
|
+
detail: result.detail
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
domains: (_result_allowedDomains = result.allowedDomains) != null ? _result_allowedDomains : [],
|
|
405
|
+
limit: MAX_ALLOWED_DOMAINS
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
//# sourceMappingURL=recaptcha-allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/recaptcha-allowlist.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 * The App Check reCAPTCHA allowlist, written from the product (AGL-1378).\n *\n * ## Why this file has to exist\n *\n * The console reads Firestore **client-side**, and App Check gates those reads\n * with a reCAPTCHA token that the key itself refuses to mint on an origin it\n * has never heard of. Measured against the live key on 2026-08-23, with both\n * controls, by requesting `recaptcha/api2/anchor` with a base64 `co` origin:\n *\n * ```\n * https://app.aglyn.com:443 ACCEPTED (listed)\n * https://never-registered-9f3x.aglyn.com:443 ACCEPTED (subtree of a listed entry)\n * https://console.acme-agency.example:443 REJECTED \"Invalid domain for site key\"\n * https://console.northwind-coffee.com:443 REJECTED \"Invalid domain for site key\"\n * ```\n *\n * A rejected solve means `initializeAppCheck` → `getToken` fails with\n * `appCheck/recaptcha-error`, and the very next Identity Platform call is\n * refused `401 UNAUTHENTICATED — \"Firebase App Check token is invalid\"`\n * **before** token validation (`docs/design/agl-1099a-poc-findings.md` §2).\n * So a custom console domain that is attached and routed but not allowlisted\n * renders a console that can never sign anyone in — and the symptom the\n * customer reports is \"Missing or insufficient permissions\", which is the same\n * string a Security Rules verdict produces. That is the misdiagnosis this\n * whole area keeps generating; the fix is to never let the state exist.\n *\n * ## Why it was believed impossible, and why that is stale\n *\n * AGL-1378 concluded the key was a classic v3 key with no management API. The\n * key was auto-migrated into a Google-created project, and every probe asked\n * `aglyn-main` — where the reCAPTCHA API genuinely is disabled — rather than\n * the project the key actually lives in. Measured 2026-08-23:\n * `recaptchaenterprise.googleapis.com` is enabled there, two accounts we\n * control are `roles/owner`, and `projects.keys.patch` on\n * `webSettings.allowedDomains` answers `200`.\n *\n * ## The two hazards this code is shaped around\n *\n * 1. **A patch that drops sibling fields.** `webSettings` also carries\n * `allowAllDomains`, `integrationType`, `allowAmpTraffic` and\n * `challengeSecurityPreference`; a whole-object write that omitted\n * `allowAllDomains` would read as `false`… or a future default. So the mask\n * is `webSettings.allowedDomains` — the narrowest path the API accepts —\n * **and** the response is re-read afterwards: every domain that was there\n * before must still be there, and `allowAllDomains` must still be `false`.\n * A write that silently widened the key to every origin is a worse outcome\n * than the domain never being listed.\n * 2. **Silent success.** Every outcome here is explicit, and\n * {@link allowlistSatisfied} is the ONLY place that decides which ones mean\n * \"the customer can actually sign in\". `activateConsoleDomain` refuses to\n * mark a claim `active` unless it says yes.\n *\n * ## Exact names, never subtree cover\n *\n * A listed entry covers its whole subtree but never its parents (proved\n * 2026-08-10). It is therefore tempting to skip the write when some existing\n * entry already covers the new name. Deliberately not done: if org A holds\n * `acme.com` and org B later claims `console.acme.com`, B would be silently\n * covered by A's entry — and A detaching would break B with no trace linking\n * the two. One entry per claimed apex is what the documented 250-domain\n * ceiling is denominated in anyway (250 distinct customer apexes, occupancy 5\n * on 2026-08-23), so exactness costs nothing we were not already counting.\n */\n\nimport { getApp } from 'firebase-admin/app'\n\nconst RECAPTCHA_API = 'https://recaptchaenterprise.googleapis.com/v1'\n\n/**\n * Published ceiling for `webSettings.allowedDomains`, quoted rather than\n * measured: *\"Each key supports a maximum of 250 domains.\"* An occupancy\n * reading is NOT a limit — this repo already spent two weeks treating a\n * snapshot of nine entries as a hard ceiling. Re-probe occupancy, never the\n * limit.\n */\nexport const MAX_ALLOWED_DOMAINS = 250\n\n/**\n * Ceiling on the whole exchange, matching `workspace-domains.ts`.\n *\n * This one is awaited by an attach the operator is watching, so an\n * unresponsive Google API must lose quickly rather than hang the activation\n * until the platform kills the function.\n */\nconst REQUEST_TIMEOUT_MS = 8_000\n\nexport type RecaptchaAllowlistOutcome =\n /** We wrote the entry and re-read the key to confirm it. */\n | 'listed'\n /** The exact name was already on the key; nothing was written. */\n | 'already-listed'\n /** We removed the entry and re-read the key to confirm it. */\n | 'removed'\n /** Nothing to remove — the exact name was not on the key. */\n | 'absent'\n /**\n * This deployment runs no App Check reCAPTCHA provider, so there is no\n * allowlist to maintain and nothing is gated. Self-host, and local dev.\n */\n | 'unenforced'\n /** The key is at {@link MAX_ALLOWED_DOMAINS}. A commercial event, not a bug. */\n | 'full'\n /** Anything else: misconfiguration, IAM, transport, or a suspicious write. */\n | 'failed'\n\nexport interface RecaptchaAllowlistResult {\n outcome: RecaptchaAllowlistOutcome\n domain: string\n /** Human-readable cause. Always set when the outcome is not a success. */\n detail?: string\n /**\n * Live `allowedDomains` after the operation, when we got far enough to read\n * them. Occupancy is worth surfacing precisely because it must never be\n * quoted from memory.\n */\n allowedDomains?: string[]\n}\n\n/**\n * Does this outcome mean the origin can attest?\n *\n * The single decision point, so that no caller can invent a looser one. Note\n * what is NOT here: `full` and `failed` both leave a domain that will 401 its\n * customer at sign-in, and `full` is the easier of the two to mistake for a\n * warning.\n */\nexport function allowlistSatisfied(outcome: RecaptchaAllowlistOutcome): boolean {\n return (\n outcome === 'listed' ||\n outcome === 'already-listed' ||\n outcome === 'removed' ||\n outcome === 'absent' ||\n outcome === 'unenforced'\n )\n}\n\ntype Config =\n | { kind: 'unenforced' }\n | { kind: 'misconfigured'; detail: string }\n | { kind: 'ready'; keyName: string }\n\n/**\n * `unenforced` is the ONLY safe way to skip, and it is gated on the client's\n * own site key rather than on this file's credentials.\n *\n * The failure this shape exists to prevent: a production deployment that runs\n * App Check but has no `RECAPTCHA_ADMIN_KEY_NAME` would, under a\n * \"missing config → skip\" rule, report every custom domain ready while listing\n * none of them. Configuration absence must therefore mean two different things\n * depending on whether App Check is running at all — and the tell is\n * `NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY`, which is exactly what\n * `new ReCaptchaV3Provider(...)` is handed.\n *\n * The admin key name must also END with that site key. A resource name\n * pointing at a different key would be written happily and attest nothing —\n * the one misconfiguration whose symptom is indistinguishable from success.\n */\nfunction config(): Config {\n const siteKey = String(process.env.NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY ?? '').trim()\n const keyName = String(process.env.RECAPTCHA_ADMIN_KEY_NAME ?? '').trim()\n if (!siteKey) return { kind: 'unenforced' }\n if (!keyName) {\n return {\n kind: 'misconfigured',\n detail:\n 'App Check runs here (NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY is set) but RECAPTCHA_ADMIN_KEY_NAME is not, so the allowlist cannot be written',\n }\n }\n if (!/^projects\\/[^/]+\\/keys\\/[^/]+$/.test(keyName)) {\n return {\n kind: 'misconfigured',\n detail: `RECAPTCHA_ADMIN_KEY_NAME must be projects/{project}/keys/{siteKey}, got \"${keyName}\"`,\n }\n }\n if (!keyName.endsWith(`/${siteKey}`)) {\n return {\n kind: 'misconfigured',\n detail:\n 'RECAPTCHA_ADMIN_KEY_NAME names a different key than NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY — writing it would allowlist a key nothing uses',\n }\n }\n return { kind: 'ready', keyName }\n}\n\n/**\n * An OAuth token for the same service account firebase-admin already runs as.\n *\n * Not a second credential: `cert()`'s `getAccessToken()` requests\n * `https://www.googleapis.com/auth/cloud-platform` among its scopes, which is\n * the scope `recaptchaenterprise.googleapis.com` asks for. The account needs\n * `recaptchaenterprise.keys.get` + `.update` on the project the KEY lives in —\n * which is not `aglyn-main`; the key was migrated into its own project and\n * that is the whole reason this was thought unautomatable.\n */\nasync function accessToken(): Promise<string> {\n const credential = getApp().options.credential\n if (!credential) throw new Error('firebase-admin app has no credential')\n const token = await credential.getAccessToken()\n const value = token?.access_token\n if (!value) throw new Error('credential returned no access token')\n return value\n}\n\nasync function call(\n url: string,\n init: RequestInit & { token: string },\n): Promise<{ ok: boolean; status: number; body: any }> {\n const { token, ...rest } = init\n const response = await fetch(url, {\n ...rest,\n headers: {\n ...(rest.headers ?? {}),\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n const body = await response.json().catch(() => ({}))\n return { ok: response.ok, status: response.status, body }\n}\n\nfunction domainsOf(key: any): string[] {\n const domains = key?.webSettings?.allowedDomains\n return Array.isArray(domains) ? domains.map((entry: unknown) => String(entry)) : []\n}\n\nfunction normalize(input: string): string {\n return String(input ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.$/, '')\n}\n\n/**\n * Read the key, apply `mutate` to its domains, write back, and prove the write.\n *\n * `mutate` returning `null` means \"no write is needed\" and is a success.\n */\nasync function rewrite(\n domain: string,\n mutate: (\n current: string[],\n ) =>\n | { next: string[]; outcome: RecaptchaAllowlistOutcome }\n | { skip: RecaptchaAllowlistOutcome; detail?: string },\n): Promise<RecaptchaAllowlistResult> {\n const settings = config()\n if (settings.kind === 'unenforced') return { outcome: 'unenforced', domain }\n if (settings.kind === 'misconfigured') {\n return { outcome: 'failed', domain, detail: settings.detail }\n }\n\n let token: string\n try {\n token = await accessToken()\n } catch (error) {\n return { outcome: 'failed', domain, detail: `credential: ${String(error)}` }\n }\n\n const url = `${RECAPTCHA_API}/${settings.keyName}`\n let before: any\n try {\n const read = await call(url, { method: 'GET', token })\n if (!read.ok) {\n return {\n outcome: 'failed',\n domain,\n detail: `read ${read.status}: ${read.body?.error?.message ?? 'unknown'}`,\n }\n }\n before = read.body\n } catch (error) {\n return { outcome: 'failed', domain, detail: `read: ${String(error)}` }\n }\n\n const current = domainsOf(before)\n const plan = mutate(current)\n if ('skip' in plan) {\n return {\n outcome: plan.skip,\n domain,\n detail: plan.detail,\n allowedDomains: current,\n }\n }\n\n let after: any\n try {\n const written = await call(`${url}?updateMask=webSettings.allowedDomains`, {\n method: 'PATCH',\n token,\n body: JSON.stringify({ webSettings: { allowedDomains: plan.next } }),\n })\n if (!written.ok) {\n return {\n outcome: 'failed',\n domain,\n detail: `write ${written.status}: ${written.body?.error?.message ?? 'unknown'}`,\n allowedDomains: current,\n }\n }\n after = written.body\n } catch (error) {\n return { outcome: 'failed', domain, detail: `write: ${String(error)}`, allowedDomains: current }\n }\n\n // Hazard 1. The response is the authoritative post-state, so believe it\n // rather than the request body. A mask that the API interpreted more\n // broadly than intended shows up here as a missing sibling field or a\n // vanished entry — and `allowAllDomains: true` would be a key open to every\n // origin on the internet, which must never be a side effect of adding one.\n const applied = domainsOf(after)\n const lost = plan.next.filter((entry) => !applied.includes(entry))\n if (lost.length > 0) {\n return {\n outcome: 'failed',\n domain,\n detail: `the key did not keep every domain the write sent (missing: ${lost.join(', ')})`,\n allowedDomains: applied,\n }\n }\n if (after?.webSettings?.allowAllDomains === true) {\n return {\n outcome: 'failed',\n domain,\n detail: 'the write left allowAllDomains true — the key would accept every origin',\n allowedDomains: applied,\n }\n }\n\n return { outcome: plan.outcome, domain, allowedDomains: applied }\n}\n\n/**\n * Put a custom console domain on the key, so a browser there can attest.\n *\n * Called by `activateConsoleDomain` for the **serving** name only. Redirect\n * twins (`www.acme.com` → `acme.com`) never execute the console, never load\n * reCAPTCHA, and would spend a slot of the 250 for nothing.\n */\nexport async function allowConsoleOrigin(\n input: string,\n): Promise<RecaptchaAllowlistResult> {\n const domain = normalize(input)\n if (!domain) return { outcome: 'failed', domain: '', detail: 'No domain given' }\n return rewrite(domain, (current) => {\n if (current.includes(domain)) return { skip: 'already-listed' as const }\n if (current.length >= MAX_ALLOWED_DOMAINS) {\n return {\n skip: 'full' as const,\n detail: `the key holds ${current.length} of ${MAX_ALLOWED_DOMAINS} domains`,\n }\n }\n return { next: [...current, domain], outcome: 'listed' as const }\n })\n}\n\n/**\n * Take a domain back off the key when its claim is released.\n *\n * Removes the EXACT entry and nothing else. A filter that matched subdomains\n * would take `aglyn.com` off the key while releasing `console.aglyn.com` —\n * and the entry it deleted is the one every Aglyn origin attests against.\n */\nexport async function reclaimConsoleOrigin(\n input: string,\n): Promise<RecaptchaAllowlistResult> {\n const domain = normalize(input)\n if (!domain) return { outcome: 'failed', domain: '', detail: 'No domain given' }\n return rewrite(domain, (current) => {\n if (!current.includes(domain)) return { skip: 'absent' as const }\n const next = current.filter((entry) => entry !== domain)\n if (next.length !== current.length - 1) {\n return {\n skip: 'failed' as const,\n detail: `removing ${domain} would have removed ${current.length - next.length} entries`,\n }\n }\n return { next, outcome: 'removed' as const }\n })\n}\n\n/**\n * Read-only occupancy, for an operator asking \"how many of the 250 are gone?\"\n *\n * Deliberately returns the live list rather than a count: a count is what got\n * quoted as a ceiling for two weeks.\n */\nexport async function readConsoleOriginAllowlist(): Promise<{\n domains: string[] | null\n limit: number\n detail?: string\n}> {\n const result = await rewrite('', () => ({ skip: 'absent' as const }))\n if (result.outcome === 'unenforced') {\n return { domains: null, limit: MAX_ALLOWED_DOMAINS, detail: 'App Check is not configured here' }\n }\n if (result.outcome === 'failed') {\n return { domains: null, limit: MAX_ALLOWED_DOMAINS, detail: result.detail }\n }\n return { domains: result.allowedDomains ?? [], limit: MAX_ALLOWED_DOMAINS }\n}\n"],"names":["getApp","RECAPTCHA_API","MAX_ALLOWED_DOMAINS","REQUEST_TIMEOUT_MS","allowlistSatisfied","outcome","config","process","siteKey","String","env","NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY","trim","keyName","RECAPTCHA_ADMIN_KEY_NAME","kind","detail","test","endsWith","accessToken","credential","options","Error","token","getAccessToken","value","access_token","call","url","init","rest","response","fetch","headers","Authorization","signal","AbortSignal","timeout","body","json","catch","ok","status","domainsOf","key","domains","webSettings","allowedDomains","Array","isArray","map","entry","normalize","input","toLowerCase","replace","rewrite","domain","mutate","after","settings","error","before","read","method","message","current","plan","skip","written","JSON","stringify","next","applied","lost","filter","includes","length","join","allowAllDomains","allowConsoleOrigin","reclaimConsoleOrigin","readConsoleOriginAllowlist","result","limit"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type OrgPlan, type OrgReleaseFlagOverrides, type ReleaseFlagKey, type ReleaseFlagValue } from '@aglyn/aglyn/server';
|
|
18
|
+
export declare function getServerReleaseFlagValues(): Promise<Record<ReleaseFlagKey, ReleaseFlagValue>>;
|
|
19
|
+
/**
|
|
20
|
+
* Per-org release-flag overrides (AGL-1635), read off `orgs/{orgId}`.
|
|
21
|
+
*
|
|
22
|
+
* Cached on the same TTL as the template read above, and for the same
|
|
23
|
+
* reason: these are consulted on every published page render and every API
|
|
24
|
+
* dispatch, so an uncached read would add a Firestore round trip to the hot
|
|
25
|
+
* path. A grant therefore takes up to a minute to land, matching how long a
|
|
26
|
+
* flag flip already takes — one propagation story, not two.
|
|
27
|
+
*
|
|
28
|
+
* Keyed by orgId because a subject may be a uid or a hostId, and only an
|
|
29
|
+
* org can carry overrides. Failure is silent and yields `{}`: a missing org
|
|
30
|
+
* doc or a denied read must gate exactly as "no override set", never as an
|
|
31
|
+
* error that takes a published page down.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* What one org read answers about release flags: the staff overrides, and
|
|
35
|
+
* the org's EFFECTIVE plan for tier targeting (AGL-2486).
|
|
36
|
+
*
|
|
37
|
+
* `plan` is null when the read did not produce an org document — missing,
|
|
38
|
+
* denied, or no Firestore at all. Deliberately not `'free'`: an org whose
|
|
39
|
+
* doc could not be read has an UNKNOWN tier, and
|
|
40
|
+
* `resolveEffectivePlan(undefined)` answers `'free'`, which would hand a
|
|
41
|
+
* Free-targeted flag to every org the read failed for. The two cases have to
|
|
42
|
+
* stay distinguishable at the boundary, because the evaluator treats an
|
|
43
|
+
* unknown plan as a refusal and a known `'free'` as a match.
|
|
44
|
+
*/
|
|
45
|
+
interface OrgReleaseFlagTargeting {
|
|
46
|
+
overrides: OrgReleaseFlagOverrides;
|
|
47
|
+
plan: OrgPlan | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Both halves of the per-org answer from ONE cached document read.
|
|
51
|
+
*
|
|
52
|
+
* Sharing the read is the point: `plan` and `releaseFlags` live on the same
|
|
53
|
+
* org document, this runs on every published page render and every API
|
|
54
|
+
* dispatch, and fetching the tier separately would have doubled the hot-path
|
|
55
|
+
* Firestore traffic to learn something already in hand.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getOrgReleaseFlagTargeting(orgId: string | null | undefined): Promise<OrgReleaseFlagTargeting>;
|
|
58
|
+
export declare function getOrgReleaseFlagOverrides(orgId: string | null | undefined): Promise<OrgReleaseFlagOverrides>;
|
|
59
|
+
/**
|
|
60
|
+
* The verdict for ONE flag and ONE org, overrides applied (AGL-1635).
|
|
61
|
+
*
|
|
62
|
+
* The surfaces that gate a single flag server-side — the tenant admin bar
|
|
63
|
+
* slot, edit-context redemption, edit-access minting — resolve through here
|
|
64
|
+
* rather than reading `getServerReleaseFlagValues()` and calling
|
|
65
|
+
* `isReleaseFlagOn` themselves, so a per-org grant reaches all of them at
|
|
66
|
+
* once. A gate that kept the old two-step would silently ignore overrides,
|
|
67
|
+
* which is the failure this issue exists to fix.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isServerReleaseFlagOnForOrg(flagKey: ReleaseFlagKey, orgId: string | null | undefined): Promise<boolean>;
|
|
70
|
+
/** Test seam: drops both caches so a spec can change a verdict. */
|
|
71
|
+
export declare function __resetReleaseFlagCaches(): void;
|
|
72
|
+
/**
|
|
73
|
+
* The server half of the plugin release gate (AGL-422): subtracts
|
|
74
|
+
* flagged-off first-party plugins from an effective set. The staff bypass is
|
|
75
|
+
* only paid for when something was actually subtracted AND the request
|
|
76
|
+
* carries a bearer token — the common all-flags-on path never verifies.
|
|
77
|
+
*
|
|
78
|
+
* `orgId` is the ONLY subject: it selects the per-org overrides (AGL-1635)
|
|
79
|
+
* and it is the rollout bucket. AGL-1635 took a separate `subjectId` here
|
|
80
|
+
* because callers fell back to a hostId when the org lookup missed, and a
|
|
81
|
+
* hostId must never be read as an org id. AGL-1656 removed that fallback
|
|
82
|
+
* instead of accommodating it — a hostId and an orgId hash to different
|
|
83
|
+
* buckets, so a mid-rollout flag could land the published site and the
|
|
84
|
+
* console on opposite sides of the same percentage for the same workspace.
|
|
85
|
+
* With one subject the two cannot disagree by construction, and there is no
|
|
86
|
+
* longer a parameter through which a non-org subject could be reintroduced.
|
|
87
|
+
*/
|
|
88
|
+
export declare function filterEnabledPluginsByReleaseFlags(pluginIds: readonly string[], options: {
|
|
89
|
+
/**
|
|
90
|
+
* The org this request belongs to: rollout bucket AND override key.
|
|
91
|
+
* Null when no org could be resolved — a subject-less request gets the
|
|
92
|
+
* fully-enabled flags only, never a partial rollout.
|
|
93
|
+
*/
|
|
94
|
+
orgId?: string | null;
|
|
95
|
+
/** Raw Authorization header, when the caller has one (API dispatch). */
|
|
96
|
+
authorization?: string | null;
|
|
97
|
+
/**
|
|
98
|
+
* The account a TOKENLESS request acts for, as its route proved it
|
|
99
|
+
* (AGL-2978) — see `PluginApiRequestSubject.uid`. Consulted for the staff
|
|
100
|
+
* preview only, and only when the request carries no bearer token: a
|
|
101
|
+
* request with a token is the token's, whatever else it names.
|
|
102
|
+
*/
|
|
103
|
+
subjectUid?: string | null;
|
|
104
|
+
}): Promise<string[]>;
|
|
105
|
+
export {};
|