@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,715 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* Self-serve enterprise SSO provisioning (AGL-1210).
|
|
19
|
+
*
|
|
20
|
+
* Before this, `tenantManager()` appeared six times in the repo and every one
|
|
21
|
+
* was `authForTenant(...)` — authenticating against a pool somebody had made by
|
|
22
|
+
* hand in the Firebase console. There was no `createTenant` and no
|
|
23
|
+
* `createProviderConfig` anywhere, so onboarding an SSO org meant a person
|
|
24
|
+
* clicking through GCIP and then writing `tenantId`/`providerId` onto the org
|
|
25
|
+
* doc. This module is the write half that removes that step.
|
|
26
|
+
*
|
|
27
|
+
* ## Domain verification is the security gate, not a formality
|
|
28
|
+
*
|
|
29
|
+
* `ssoDomains/{domain}` routes every sign-in for a domain to whichever org
|
|
30
|
+
* claims it. Letting an org self-assert that claim would be an
|
|
31
|
+
* account-takeover vector: claim `competitor.com`, and their employees' sign-in
|
|
32
|
+
* attempts arrive at your IdP. `domainVerified` used to be staff-attested,
|
|
33
|
+
* which is exactly the human step self-serve removes — so the attestation has
|
|
34
|
+
* to be replaced by proof, not deleted.
|
|
35
|
+
*
|
|
36
|
+
* The proof is a DNS TXT record at `_aglyn-challenge.<domain>` carrying a token
|
|
37
|
+
* we generated for THAT org and THAT domain. Only someone who controls the
|
|
38
|
+
* domain's zone can publish it, and a token issued to one org is worthless to
|
|
39
|
+
* another. Nothing reaches `sso.domains` — the array sign-in actually consults
|
|
40
|
+
* — until a lookup has seen the record.
|
|
41
|
+
*
|
|
42
|
+
* DNS resolution mirrors `/api/domains/verify` (AGL-734): pinned public
|
|
43
|
+
* resolvers rather than the runtime's default, because a stale zone once made
|
|
44
|
+
* Vercel's own resolver return NXDOMAIN for records every public resolver could
|
|
45
|
+
* see. The fallback is deliberately narrow — a genuine "no such record" answer
|
|
46
|
+
* must stay a failure, and only an unreachable resolver falls through.
|
|
47
|
+
*/ import { promises as dns, Resolver as CallbackResolver } from "dns";
|
|
48
|
+
import { randomBytes } from "crypto";
|
|
49
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
50
|
+
const auth = ()=>firebaseAdmin.app().auth();
|
|
51
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
52
|
+
/** Subdomain the challenge record is published at. */ export const SSO_CHALLENGE_PREFIX = '_aglyn-challenge';
|
|
53
|
+
/** Prefix on the TXT value, so an unrelated record cannot pass by accident. */ export const SSO_TXT_PREFIX = 'aglyn-domain-verification=';
|
|
54
|
+
const DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\.[a-z0-9-]{1,63})+$/;
|
|
55
|
+
/** Same pinned resolvers as the custom-domain check — see the header note. */ const PUBLIC_RESOLVERS = [
|
|
56
|
+
'1.1.1.1',
|
|
57
|
+
'8.8.8.8'
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* Domains nobody may claim for SSO, however well they prove ownership.
|
|
61
|
+
*
|
|
62
|
+
* A consumer mailbox provider is shared by millions of unrelated people, so
|
|
63
|
+
* "proves control of the zone" and "is entitled to govern every account on it"
|
|
64
|
+
* come apart completely. Verifying `gmail.com` is possible for exactly one
|
|
65
|
+
* party, and routing every Gmail user's sign-in to their IdP is not a thing
|
|
66
|
+
* that party should be able to do either.
|
|
67
|
+
*/ const PUBLIC_EMAIL_DOMAINS = new Set([
|
|
68
|
+
'gmail.com',
|
|
69
|
+
'googlemail.com',
|
|
70
|
+
'outlook.com',
|
|
71
|
+
'hotmail.com',
|
|
72
|
+
'live.com',
|
|
73
|
+
'msn.com',
|
|
74
|
+
'yahoo.com',
|
|
75
|
+
'ymail.com',
|
|
76
|
+
'aol.com',
|
|
77
|
+
'icloud.com',
|
|
78
|
+
'me.com',
|
|
79
|
+
'mac.com',
|
|
80
|
+
'proton.me',
|
|
81
|
+
'protonmail.com',
|
|
82
|
+
'pm.me',
|
|
83
|
+
'gmx.com',
|
|
84
|
+
'gmx.net',
|
|
85
|
+
'mail.com',
|
|
86
|
+
'zoho.com',
|
|
87
|
+
'yandex.com',
|
|
88
|
+
'fastmail.com',
|
|
89
|
+
'hey.com',
|
|
90
|
+
'qq.com',
|
|
91
|
+
'163.com',
|
|
92
|
+
'126.com'
|
|
93
|
+
]);
|
|
94
|
+
export function normalizeSsoDomain(input) {
|
|
95
|
+
const raw = String(input != null ? input : '').trim().toLowerCase();
|
|
96
|
+
const at = raw.lastIndexOf('@');
|
|
97
|
+
return (at >= 0 ? raw.slice(at + 1) : raw).replace(/^@+/, '').replace(/\.$/, '');
|
|
98
|
+
}
|
|
99
|
+
export function validateSsoDomain(input) {
|
|
100
|
+
const domain = normalizeSsoDomain(input);
|
|
101
|
+
if (!domain || !DOMAIN_PATTERN.test(domain)) {
|
|
102
|
+
return {
|
|
103
|
+
domain: null,
|
|
104
|
+
error: 'Enter a valid domain, for example acme.com'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (PUBLIC_EMAIL_DOMAINS.has(domain)) {
|
|
108
|
+
return {
|
|
109
|
+
domain: null,
|
|
110
|
+
error: 'Public email domains cannot be used for SSO — they are shared by ' + 'people outside your organization. Use a domain your organization owns.'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
domain,
|
|
115
|
+
error: null
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* TXT records at the challenge host, lowercased and unwrapped.
|
|
120
|
+
*
|
|
121
|
+
* A TXT answer arrives as an array of string CHUNKS per record (DNS splits
|
|
122
|
+
* strings over 255 bytes), so each record is joined before comparison —
|
|
123
|
+
* otherwise a long token would never match.
|
|
124
|
+
*/ export async function resolveChallengeTxt(domain) {
|
|
125
|
+
const host = `${SSO_CHALLENGE_PREFIX}.${domain}`;
|
|
126
|
+
const flatten = (records)=>records.map((chunks)=>chunks.join('').trim());
|
|
127
|
+
try {
|
|
128
|
+
const resolver = new CallbackResolver();
|
|
129
|
+
resolver.setServers(PUBLIC_RESOLVERS);
|
|
130
|
+
const records = await new Promise((resolve, reject)=>{
|
|
131
|
+
resolver.resolveTxt(host, (error, addresses)=>error ? reject(error) : resolve(addresses));
|
|
132
|
+
});
|
|
133
|
+
return flatten(records);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// ENOTFOUND/ENODATA are real answers — the name has no TXT record. Only an
|
|
136
|
+
// unreachable resolver falls through, so a genuine miss stays a failure.
|
|
137
|
+
const code = error == null ? void 0 : error.code;
|
|
138
|
+
if (code === 'ENOTFOUND' || code === 'ENODATA' || code === 'NXDOMAIN') {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return flatten(await dns.resolveTxt(host));
|
|
143
|
+
} catch (unused) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The exact TXT value an org must publish for a domain. */ export function challengeValue(token) {
|
|
149
|
+
return `${SSO_TXT_PREFIX}${token}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Does this set of TXT records prove control of the zone for THIS token?
|
|
153
|
+
*
|
|
154
|
+
* Extracted so the comparison that domain verification rests on is reachable
|
|
155
|
+
* from a test without standing up Firestore. It is deliberately an exact match
|
|
156
|
+
* on a whole record: a substring test would accept
|
|
157
|
+
* `aglyn-domain-verification=<token>-and-more`, and a prefix test would accept
|
|
158
|
+
* any record that merely starts the same way. Neither proves anything.
|
|
159
|
+
*/ export function recordsProveOwnership(records, token) {
|
|
160
|
+
if (!token) return false;
|
|
161
|
+
return records.includes(challengeValue(token));
|
|
162
|
+
}
|
|
163
|
+
/*==========================================
|
|
164
|
+
* PERIODIC RE-VERIFICATION (AGL-1210, the residual the 2026-08-23 audit named).
|
|
165
|
+
*
|
|
166
|
+
* A domain is proven ONCE, when an admin presses Verify, and then routes that
|
|
167
|
+
* domain's sign-ins forever. A company that lets its domain lapse — or sells
|
|
168
|
+
* it — keeps the routing until a human revokes it. The audit called that
|
|
169
|
+
* "the account-takeover shape in slow motion", and it is the last residual on
|
|
170
|
+
* an otherwise strong proof.
|
|
171
|
+
*
|
|
172
|
+
* ## Why this REPORTS and never revokes
|
|
173
|
+
*
|
|
174
|
+
* The obvious fix — re-run the lookup, drop the domain when it fails — is a
|
|
175
|
+
* worse bug than the one it closes. `resolveChallengeTxt` cannot tell "the
|
|
176
|
+
* record is gone" from "the resolver did not answer", because BOTH return an
|
|
177
|
+
* empty array. So a resolver blip during the sweep looks exactly like every
|
|
178
|
+
* customer simultaneously deleting their TXT record, and an automated revoke
|
|
179
|
+
* would log out every enterprise on the platform at once. That is the
|
|
180
|
+
* `reverify-plugin-versions` argument — a lint that can stop a plugin in every
|
|
181
|
+
* workspace is a kill switch with no human in it — and the lockdown argument,
|
|
182
|
+
* where an unreachable Firestore is an outage rather than a verdict.
|
|
183
|
+
*
|
|
184
|
+
* So this half adds two things and deliberately nothing else:
|
|
185
|
+
*
|
|
186
|
+
* 1. {@link probeChallengeTxt} — a THIRD state. `unreachable` is not
|
|
187
|
+
* evidence, and is never counted as a failure.
|
|
188
|
+
* 2. {@link assessDomainDrift} — consecutive CONCLUSIVE failures over a
|
|
189
|
+
* minimum wall-clock age before a domain is even reported as drifted.
|
|
190
|
+
*
|
|
191
|
+
* Revocation stays exactly where it was: {@link revokeDomain}, an explicit act
|
|
192
|
+
* by a person. Detection with a human decision beats an automated action that
|
|
193
|
+
* can log out a paying customer.
|
|
194
|
+
*
|
|
195
|
+
* ## Why the risk tolerates a slow answer
|
|
196
|
+
*
|
|
197
|
+
* The threat needs a domain to lapse, clear ~30 days of registrar grace and
|
|
198
|
+
* ~30 days of redemption, drop, and be re-bought. That is months, not days.
|
|
199
|
+
* And the first harm is not silent credential theft: routing still points at
|
|
200
|
+
* the ORIGINAL org's IdP, where the new owner's staff have no account. The
|
|
201
|
+
* concrete harm is that the new legitimate owner cannot claim their own domain
|
|
202
|
+
* (`issueDomainClaim` refuses a domain another org already routes) and their
|
|
203
|
+
* users are misdirected. Weeks of grace cost that risk almost nothing, and buy
|
|
204
|
+
* immunity from the resolver blip that would otherwise be self-inflicted.
|
|
205
|
+
*=========================================*/ /**
|
|
206
|
+
* The pure half of re-verification — the probe STATES and the decision made
|
|
207
|
+
* from them — lives in `sso-drift-logic.ts` and is re-exported here so the
|
|
208
|
+
* public surface is one module. It has no imports at all, which is what lets
|
|
209
|
+
* the console route's spec run the REAL decision against faked I/O.
|
|
210
|
+
*/ export { assessDomainDrift, SSO_DRIFT_FAILURES_BEFORE_REPORT, SSO_DRIFT_MIN_AGE_MS } from "./sso-drift-logic.js";
|
|
211
|
+
/**
|
|
212
|
+
* Re-check a claim's TXT record, distinguishing silence from absence.
|
|
213
|
+
*
|
|
214
|
+
* Same pinned public resolvers and same fallback as {@link resolveChallengeTxt}
|
|
215
|
+
* — this is not a second opinion about DNS, it is the same lookup reporting one
|
|
216
|
+
* more bit. NXDOMAIN/ENOTFOUND/ENODATA are ANSWERS ("that name has no TXT
|
|
217
|
+
* record") and stay conclusive; anything else from both the pinned resolvers
|
|
218
|
+
* and the runtime fallback is `unreachable`.
|
|
219
|
+
*
|
|
220
|
+
* Never throws: a sweep across every org must not stop at the first bad zone.
|
|
221
|
+
*/ export async function probeChallengeTxt(domain, token) {
|
|
222
|
+
const host = `${SSO_CHALLENGE_PREFIX}.${domain}`;
|
|
223
|
+
const flatten = (records)=>records.map((chunks)=>chunks.join('').trim());
|
|
224
|
+
const settle = (records)=>({
|
|
225
|
+
status: recordsProveOwnership(records, token) ? 'proven' : 'missing',
|
|
226
|
+
records: records.slice(0, 5)
|
|
227
|
+
});
|
|
228
|
+
/** Codes that are a real answer rather than a failure to get one. */ const conclusive = (code)=>code === 'ENOTFOUND' || code === 'ENODATA' || code === 'NXDOMAIN';
|
|
229
|
+
try {
|
|
230
|
+
const resolver = new CallbackResolver();
|
|
231
|
+
resolver.setServers(PUBLIC_RESOLVERS);
|
|
232
|
+
const records = await new Promise((resolve, reject)=>{
|
|
233
|
+
resolver.resolveTxt(host, (error, addresses)=>error ? reject(error) : resolve(addresses));
|
|
234
|
+
});
|
|
235
|
+
return settle(flatten(records));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const code = error == null ? void 0 : error.code;
|
|
238
|
+
if (conclusive(code)) return settle([]);
|
|
239
|
+
try {
|
|
240
|
+
return settle(flatten(await dns.resolveTxt(host)));
|
|
241
|
+
} catch (fallbackError) {
|
|
242
|
+
const fallbackCode = fallbackError == null ? void 0 : fallbackError.code;
|
|
243
|
+
// The pinned resolvers gave us nothing usable. Only the fallback's own
|
|
244
|
+
// answer can still make this conclusive; otherwise we genuinely do not
|
|
245
|
+
// know, and saying so is the entire feature.
|
|
246
|
+
if (conclusive(fallbackCode)) return settle([]);
|
|
247
|
+
return {
|
|
248
|
+
status: 'unreachable',
|
|
249
|
+
records: []
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const claimRef = (orgId, domain)=>firestore().collection('orgs').doc(orgId).collection('ssoDomains').doc(domain);
|
|
255
|
+
/**
|
|
256
|
+
* Start (or re-read) a claim on a domain. Idempotent: an existing claim keeps
|
|
257
|
+
* its token, because reissuing would invalidate a record the customer may have
|
|
258
|
+
* already published and turn a working setup into a mysterious failure.
|
|
259
|
+
*/ export async function issueDomainClaim(orgId, rawDomain) {
|
|
260
|
+
const { domain, error } = validateSsoDomain(rawDomain);
|
|
261
|
+
if (!domain) return {
|
|
262
|
+
claim: null,
|
|
263
|
+
error: error != null ? error : 'Invalid domain'
|
|
264
|
+
};
|
|
265
|
+
// One domain, one org. Without this a second org could hold a live claim on
|
|
266
|
+
// a domain already governed elsewhere and race it at verification time.
|
|
267
|
+
const existing = await firestore().collection('ssoDomains').doc(domain).get();
|
|
268
|
+
if (existing.exists && existing.get('orgId') !== orgId) {
|
|
269
|
+
return {
|
|
270
|
+
claim: null,
|
|
271
|
+
error: 'That domain is already verified by another organization.'
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const ref = claimRef(orgId, domain);
|
|
275
|
+
const snapshot = await ref.get();
|
|
276
|
+
const stored = snapshot.exists ? snapshot.get('token') : undefined;
|
|
277
|
+
const token = stored || randomBytes(24).toString('base64url');
|
|
278
|
+
if (!snapshot.exists) {
|
|
279
|
+
await ref.set({
|
|
280
|
+
domain,
|
|
281
|
+
token,
|
|
282
|
+
verified: false,
|
|
283
|
+
createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
284
|
+
});
|
|
285
|
+
} else if (!stored) {
|
|
286
|
+
// A claim document that EXISTS but carries NO token is a shape AGL-1887
|
|
287
|
+
// introduced: {@link attestSsoDomain} writes the attestation marker and
|
|
288
|
+
// nothing else, because a staff attestation has no DNS challenge behind
|
|
289
|
+
// it. Before self-serve, every claim document was created here and so
|
|
290
|
+
// always had a token — `exists` and `has a token` were the same fact.
|
|
291
|
+
//
|
|
292
|
+
// They are not any more, and conflating them broke the upgrade path for
|
|
293
|
+
// exactly the orgs AGL-1887 exists to serve. `exists` alone decided
|
|
294
|
+
// whether to persist, so "Set up DNS proof" on an attested domain minted
|
|
295
|
+
// a token, showed the admin a TXT record built from it, and stored it
|
|
296
|
+
// NOWHERE — a different value on every click. `verifyDomainClaim` re-reads
|
|
297
|
+
// `snapshot.get('token')`, finds `undefined`, and could never match the
|
|
298
|
+
// record they had just published. An attested domain could never become a
|
|
299
|
+
// verified one.
|
|
300
|
+
//
|
|
301
|
+
// MERGE, and `verified` is deliberately NOT written: the attestation has
|
|
302
|
+
// to survive being upgraded, and a claim already midway through DNS is not
|
|
303
|
+
// this branch's to demote.
|
|
304
|
+
await ref.set({
|
|
305
|
+
domain,
|
|
306
|
+
token
|
|
307
|
+
}, {
|
|
308
|
+
merge: true
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
error: null,
|
|
313
|
+
claim: {
|
|
314
|
+
domain,
|
|
315
|
+
token,
|
|
316
|
+
verified: Boolean(snapshot.get('verified')),
|
|
317
|
+
recordHost: `${SSO_CHALLENGE_PREFIX}.${domain}`,
|
|
318
|
+
recordValue: challengeValue(token),
|
|
319
|
+
lastRecords: snapshot.get('lastRecords')
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Check a claim against live DNS and, on success, promote the domain into
|
|
325
|
+
* `sso.domains` — the array that sign-in consults.
|
|
326
|
+
*
|
|
327
|
+
* Failure is recorded but never destructive: a transient DNS outage must not
|
|
328
|
+
* silently un-govern a domain mid-flight. Removal is `revokeDomain`, which is
|
|
329
|
+
* an explicit act.
|
|
330
|
+
*/ export async function verifyDomainClaim(orgId, rawDomain) {
|
|
331
|
+
const { domain, error } = validateSsoDomain(rawDomain);
|
|
332
|
+
if (!domain) return {
|
|
333
|
+
result: null,
|
|
334
|
+
error: error != null ? error : 'Invalid domain'
|
|
335
|
+
};
|
|
336
|
+
const ref = claimRef(orgId, domain);
|
|
337
|
+
const snapshot = await ref.get();
|
|
338
|
+
if (!snapshot.exists) {
|
|
339
|
+
return {
|
|
340
|
+
result: null,
|
|
341
|
+
error: 'No claim on that domain — add it first.'
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const token = snapshot.get('token');
|
|
345
|
+
const expected = challengeValue(token);
|
|
346
|
+
const records = await resolveChallengeTxt(domain);
|
|
347
|
+
const verified = recordsProveOwnership(records, token);
|
|
348
|
+
await ref.set(_extends({
|
|
349
|
+
verified,
|
|
350
|
+
lastCheckedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
351
|
+
}, verified ? {
|
|
352
|
+
verifiedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
|
|
353
|
+
lastRecords: firebaseAdmin.firestore.FieldValue.delete()
|
|
354
|
+
} : {
|
|
355
|
+
lastRecords: records.slice(0, 5)
|
|
356
|
+
}), {
|
|
357
|
+
merge: true
|
|
358
|
+
});
|
|
359
|
+
if (verified) {
|
|
360
|
+
await firestore().collection('orgs').doc(orgId).set({
|
|
361
|
+
sso: {
|
|
362
|
+
domains: firebaseAdmin.firestore.FieldValue.arrayUnion(domain),
|
|
363
|
+
domainVerified: true
|
|
364
|
+
}
|
|
365
|
+
}, {
|
|
366
|
+
merge: true
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
error: null,
|
|
371
|
+
result: {
|
|
372
|
+
domain,
|
|
373
|
+
verified,
|
|
374
|
+
records,
|
|
375
|
+
expected
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Drop a domain: out of `sso.domains`, and its public routing doc deactivated.
|
|
381
|
+
*
|
|
382
|
+
* Order matters. The public doc goes first, because it is the one an attacker
|
|
383
|
+
* would benefit from: while it says `active`, sign-ins for the domain still
|
|
384
|
+
* route to this org's IdP regardless of what the org doc says.
|
|
385
|
+
*/ export async function revokeDomain(orgId, rawDomain) {
|
|
386
|
+
const domain = normalizeSsoDomain(rawDomain);
|
|
387
|
+
if (!domain) return;
|
|
388
|
+
const publicRef = firestore().collection('ssoDomains').doc(domain);
|
|
389
|
+
const publicSnapshot = await publicRef.get();
|
|
390
|
+
if (publicSnapshot.exists && publicSnapshot.get('orgId') === orgId) {
|
|
391
|
+
await publicRef.set({
|
|
392
|
+
active: false
|
|
393
|
+
}, {
|
|
394
|
+
merge: true
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
await firestore().collection('orgs').doc(orgId).set({
|
|
398
|
+
sso: {
|
|
399
|
+
domains: firebaseAdmin.firestore.FieldValue.arrayRemove(domain)
|
|
400
|
+
}
|
|
401
|
+
}, {
|
|
402
|
+
merge: true
|
|
403
|
+
});
|
|
404
|
+
await claimRef(orgId, domain).delete().catch(()=>undefined);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Thrown by `ssoServiceMetadata()` when this deployment has no auth origin at
|
|
408
|
+
* all (AGL-2020).
|
|
409
|
+
*
|
|
410
|
+
* Deliberately names the variables and NOT a value: handing the operator
|
|
411
|
+
* a `*.firebaseapp.com` example here is how they end up pasting somebody
|
|
412
|
+
* else's project id into their IdP. `sso-provisioning.spec.ts` asserts this
|
|
413
|
+
* string mentions no Aglyn host.
|
|
414
|
+
*/ export const SSO_AUTH_ORIGIN_UNCONFIGURED_MESSAGE = 'Single sign-on is not configured for this deployment: no auth origin ' + 'could be resolved. Set NEXT_PUBLIC_FIREBASE_PROJECT_ID — or one of ' + 'NEXT_PUBLIC_FIREBASE_AUTH_HANDLER_HOST, NEXT_PUBLIC_WORKSPACE_DOMAIN or ' + 'NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN — and rebuild.';
|
|
415
|
+
/**
|
|
416
|
+
* A HOST, never a URL.
|
|
417
|
+
*
|
|
418
|
+
* These strings are compared byte for byte — by GCIP against the assertion's
|
|
419
|
+
* Audience, and by the IdP against the Reply URL it posts to. An operator who
|
|
420
|
+
* pastes `https://auth.aglyn.com/` into the env var would otherwise produce
|
|
421
|
+
* `https://https://auth.aglyn.com//__/auth/handler` and reject every
|
|
422
|
+
* assertion, which presents as "SSO stopped working" with nothing in the
|
|
423
|
+
* config that looks wrong.
|
|
424
|
+
*/ function normalizeAuthHost(value) {
|
|
425
|
+
return String(value != null ? value : '').trim().replace(/^https?:\/\//i, '').replace(/\/.*$/, '').toLowerCase();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The auth origin this deployment actually serves (AGL-1381).
|
|
429
|
+
*
|
|
430
|
+
* Mirrors `resolveFirebaseAuthDomain()` in the client's `firebase-config`: on a
|
|
431
|
+
* workspace deployment the whole OAuth/SAML handshake is funnelled through one
|
|
432
|
+
* dedicated same-site origin, `auth.<workspaceDomain>`, which reverse-proxies
|
|
433
|
+
* Firebase's `/__/auth/*` helpers (AGL-462). The IdP has to post the assertion
|
|
434
|
+
* to THAT origin — the one holding the pending-redirect state — or browser
|
|
435
|
+
* storage partitioning severs the two halves of the flow.
|
|
436
|
+
*
|
|
437
|
+
* The order is load-bearing:
|
|
438
|
+
*
|
|
439
|
+
* 1. `NEXT_PUBLIC_FIREBASE_AUTH_HANDLER_HOST` — an explicit override wins.
|
|
440
|
+
* 2. `auth.<NEXT_PUBLIC_WORKSPACE_DOMAIN>` — the deployed workspace origin.
|
|
441
|
+
* Derived only when the variable is actually set, never defaulted: falling
|
|
442
|
+
* back to `auth.aglyn.com` would point a self-hosted install at OUR auth
|
|
443
|
+
* origin, and it would also make the `firebaseapp.com` arm unreachable.
|
|
444
|
+
* It sits ABOVE `AUTH_DOMAIN` because production sets both, and the
|
|
445
|
+
* branded host is the one production serves.
|
|
446
|
+
* 3–4. the project's `*.firebaseapp.com` domain — localhost, previews that
|
|
447
|
+
* are not on the workspace domain, the emulator, and self-host installs
|
|
448
|
+
* that have not stood up an auth subdomain.
|
|
449
|
+
*
|
|
450
|
+
* When all four are absent this THROWS (AGL-2020). It used to default the
|
|
451
|
+
* project id to `aglyn-main` — ours — which is the anti-pattern AGL-1919
|
|
452
|
+
* exists to prevent, and it was silent in both directions that matter:
|
|
453
|
+
* `provisionSsoPool()` wrote our origin into the operator's OWN GCIP provider
|
|
454
|
+
* as `rpEntityId`/`callbackURL`, and the console printed it as the Reply URL
|
|
455
|
+
* for their customer to paste into their IdP. A third party's SAML assertions
|
|
456
|
+
* would then be posted at infrastructure the operator does not run.
|
|
457
|
+
*
|
|
458
|
+
* Throwing rather than degrading is deliberate, and this is the one place in
|
|
459
|
+
* the self-host audit where "run with the feature off" is not available: there
|
|
460
|
+
* is no empty value to fall back to. `authDomain: ''` emits
|
|
461
|
+
* `https:///__/auth/handler`, which GCIP accepts on write and which then
|
|
462
|
+
* rejects every assertion afterwards with nothing in the config that looks
|
|
463
|
+
* wrong — the same "looks configured, works never" shape as an App Check
|
|
464
|
+
* provider built on `undefined`. SSO is opt-in and deliberately configured, so
|
|
465
|
+
* an operator reaching this has asked for it; a named error telling them which
|
|
466
|
+
* variable to set is strictly better than either wrong origin.
|
|
467
|
+
*
|
|
468
|
+
* The READ path degrades instead of 500ing: `/api/orgs/sso?action=status`
|
|
469
|
+
* catches this and returns `metadata: null` with the message, so the settings
|
|
470
|
+
* page renders the reason rather than a wrong instruction. The WRITE path
|
|
471
|
+
* (`provisionSsoPool`) lets it fly.
|
|
472
|
+
*
|
|
473
|
+
* `entityId` carries the scheme. It is not cosmetic: live GCIP for the first
|
|
474
|
+
* SSO org holds `https://auth.aglyn.com`, and Google Workspace is already
|
|
475
|
+
* sending that exact Audience. Emitting the bare host here would be written
|
|
476
|
+
* over the working config by the next `provisionSsoPool()` save and lock out
|
|
477
|
+
* every SSO-only user in the org. `sso-provisioning.spec.ts` pins both strings
|
|
478
|
+
* character for character for that reason.
|
|
479
|
+
*/ export function ssoServiceMetadata() {
|
|
480
|
+
var _process_env_NEXT_PUBLIC_FIREBASE_PROJECT_ID;
|
|
481
|
+
const workspaceDomain = normalizeAuthHost(process.env['NEXT_PUBLIC_WORKSPACE_DOMAIN']);
|
|
482
|
+
const projectId = String((_process_env_NEXT_PUBLIC_FIREBASE_PROJECT_ID = process.env['NEXT_PUBLIC_FIREBASE_PROJECT_ID']) != null ? _process_env_NEXT_PUBLIC_FIREBASE_PROJECT_ID : '').trim();
|
|
483
|
+
const authDomain = normalizeAuthHost(process.env['NEXT_PUBLIC_FIREBASE_AUTH_HANDLER_HOST']) || (workspaceDomain ? `auth.${workspaceDomain}` : '') || normalizeAuthHost(process.env['NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN']) || (projectId ? `${projectId}.firebaseapp.com` : '');
|
|
484
|
+
if (!authDomain) throw new Error(SSO_AUTH_ORIGIN_UNCONFIGURED_MESSAGE);
|
|
485
|
+
return {
|
|
486
|
+
authDomain,
|
|
487
|
+
acsUrl: `https://${authDomain}/__/auth/handler`,
|
|
488
|
+
entityId: `https://${authDomain}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* GCIP display names are constrained (4–20 chars, must start with a letter,
|
|
493
|
+
* letters/digits/hyphens only). An org slug can violate every one of those, so
|
|
494
|
+
* derive rather than pass through — a rejected display name would surface as an
|
|
495
|
+
* opaque provisioning failure at the worst moment.
|
|
496
|
+
*/ export function poolDisplayName(orgSlug, orgId) {
|
|
497
|
+
const base = `${orgSlug || orgId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '').replace(/^[^a-z]+/, '');
|
|
498
|
+
const padded = (base || 'org').slice(0, 20);
|
|
499
|
+
return padded.length >= 4 ? padded : `${padded}-org`.slice(0, 20);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Create (or update) the org's GCIP pool and its SAML provider.
|
|
503
|
+
*
|
|
504
|
+
* Idempotent by construction: an org that already has a `tenantId` reuses it,
|
|
505
|
+
* and a provider that already exists is UPDATED rather than recreated. Creating
|
|
506
|
+
* a second pool would strand every user in the first one — they are separate
|
|
507
|
+
* user namespaces, so "just make a new one" silently locks out everybody who
|
|
508
|
+
* has already signed in.
|
|
509
|
+
*/ export async function provisionSsoPool(options) {
|
|
510
|
+
const { orgId, orgSlug, existingTenantId, existingProviderId, idp } = options;
|
|
511
|
+
const manager = auth().tenantManager();
|
|
512
|
+
const metadata = ssoServiceMetadata();
|
|
513
|
+
let tenantId = existingTenantId;
|
|
514
|
+
let createdPool = false;
|
|
515
|
+
if (!tenantId) {
|
|
516
|
+
const tenant = await manager.createTenant({
|
|
517
|
+
displayName: poolDisplayName(orgSlug, orgId),
|
|
518
|
+
emailSignInConfig: {
|
|
519
|
+
enabled: false,
|
|
520
|
+
passwordRequired: false
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
tenantId = tenant.tenantId;
|
|
524
|
+
createdPool = true;
|
|
525
|
+
}
|
|
526
|
+
const tenantAuth = manager.authForTenant(tenantId);
|
|
527
|
+
const providerId = existingProviderId || `saml.${poolDisplayName(orgSlug, orgId)}`;
|
|
528
|
+
const config = {
|
|
529
|
+
displayName: options.displayName || 'Single sign-on',
|
|
530
|
+
enabled: true,
|
|
531
|
+
idpEntityId: idp.entityId,
|
|
532
|
+
ssoURL: idp.ssoUrl,
|
|
533
|
+
x509Certificates: idp.certificates,
|
|
534
|
+
rpEntityId: metadata.entityId,
|
|
535
|
+
callbackURL: metadata.acsUrl
|
|
536
|
+
};
|
|
537
|
+
let createdProvider = false;
|
|
538
|
+
try {
|
|
539
|
+
await tenantAuth.getProviderConfig(providerId);
|
|
540
|
+
await tenantAuth.updateProviderConfig(providerId, config);
|
|
541
|
+
} catch (unused) {
|
|
542
|
+
await tenantAuth.createProviderConfig(_extends({
|
|
543
|
+
providerId
|
|
544
|
+
}, config));
|
|
545
|
+
createdProvider = true;
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
tenantId,
|
|
549
|
+
providerId,
|
|
550
|
+
createdPool,
|
|
551
|
+
createdProvider
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Is this claim a STAFF ATTESTATION that the org owns the domain? (AGL-1887)
|
|
556
|
+
*
|
|
557
|
+
* The second of exactly two ways a domain can be proven, and the one that
|
|
558
|
+
* exists for orgs onboarded before self-serve. Their domain was written onto
|
|
559
|
+
* `sso.domains` by a person who checked, with no claim document behind it — so
|
|
560
|
+
* `unpublishSsoDomains` deactivated their routing happily and
|
|
561
|
+
* `publishSsoDomains` then refused to bring it back. Off worked, on did not,
|
|
562
|
+
* and for `aglyn-org` the owner's only credential lives inside the pool that
|
|
563
|
+
* stops answering (AGL-1888).
|
|
564
|
+
*
|
|
565
|
+
* A POSITIVE MARKER, never an absence. The rejected alternative was to treat
|
|
566
|
+
* "a routing doc already exists naming this org" as permission to re-publish,
|
|
567
|
+
* which makes `unpublish` non-final and would let a domain whose claim was
|
|
568
|
+
* revoked come back. What is trusted here is a field a staff member wrote on
|
|
569
|
+
* purpose, and it says WHO, so the trust is attributable.
|
|
570
|
+
*
|
|
571
|
+
* WHY THIS IS NOT A CLIENT-REACHABLE HOLE, which is the only question that
|
|
572
|
+
* matters about it:
|
|
573
|
+
*
|
|
574
|
+
* - `orgs/{orgId}/ssoDomains/{domain}` is `allow read, write: if false` in
|
|
575
|
+
* `cloud/firebase-firestore.rules` — deny-all for every client, reads
|
|
576
|
+
* included. Only the Admin SDK, which bypasses rules, can write here.
|
|
577
|
+
* - No customer-reachable route writes this field. `issueDomainClaim` is the
|
|
578
|
+
* only claim writer on the org's own path and it writes `domain`, `token`,
|
|
579
|
+
* `verified: false` and `createdAt` — never `attestedBy`.
|
|
580
|
+
* - {@link attestSsoDomain} is the only writer, and it is staff-only by
|
|
581
|
+
* construction: it lives here, server-side, and takes the attesting uid.
|
|
582
|
+
*
|
|
583
|
+
* That is why AGL-1887's proposed rules change is NOT part of this: the
|
|
584
|
+
* subcollection is already deny-all, and adding a rule permitting a narrower
|
|
585
|
+
* write than "none" would loosen it, not tighten it.
|
|
586
|
+
*
|
|
587
|
+
* A non-empty STRING, checked strictly. `true`, `1` or `{}` are not an
|
|
588
|
+
* attestation by anybody, and a field that can be satisfied by any truthy
|
|
589
|
+
* value is one a future careless write can satisfy by accident.
|
|
590
|
+
*/ export function isStaffAttestedClaim(attestedBy) {
|
|
591
|
+
return typeof attestedBy === 'string' && attestedBy.trim().length > 0;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Record that staff have verified, out of band, that an org owns a domain
|
|
595
|
+
* (AGL-1887).
|
|
596
|
+
*
|
|
597
|
+
* This is the write half of the attestation {@link isStaffAttestedClaim}
|
|
598
|
+
* reads, and it exists so there is exactly ONE way the marker can be created
|
|
599
|
+
* and it is a named function with an actor on it — rather than a field somebody
|
|
600
|
+
* sets by hand in the Firebase console with no record of who or why.
|
|
601
|
+
*
|
|
602
|
+
* `verified` is left FALSE and untouched. The two facts are different: DNS
|
|
603
|
+
* verification is a proof we re-ran ourselves, an attestation is a person
|
|
604
|
+
* vouching. Collapsing them would make an attested domain indistinguishable
|
|
605
|
+
* from a DNS-proven one in the data, and the `verified` flag is what
|
|
606
|
+
* `verifyDomainClaim` owns.
|
|
607
|
+
*
|
|
608
|
+
* Not exposed through `/api/orgs/sso`, deliberately. An org admin proving
|
|
609
|
+
* their own domain has DNS for that; an attestation is us saying we checked,
|
|
610
|
+
* and a customer-reachable path that wrote it would be a customer-reachable
|
|
611
|
+
* path to publishing any domain.
|
|
612
|
+
*/ export async function attestSsoDomain(options) {
|
|
613
|
+
var _options_note;
|
|
614
|
+
const { domain, error } = validateSsoDomain(options.domain);
|
|
615
|
+
if (!domain) return {
|
|
616
|
+
domain: null,
|
|
617
|
+
error: error != null ? error : 'Invalid domain'
|
|
618
|
+
};
|
|
619
|
+
if (!isStaffAttestedClaim(options.attestedByUid)) {
|
|
620
|
+
return {
|
|
621
|
+
domain: null,
|
|
622
|
+
error: 'An attestation needs the staff uid making it.'
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
// Same uniqueness rule `issueDomainClaim` enforces. An attestation must not
|
|
626
|
+
// be a way around "one domain, one org" — if another org already holds live
|
|
627
|
+
// routing for this domain, that conflict is resolved by a human before
|
|
628
|
+
// anything is written, not by whoever attests last.
|
|
629
|
+
const existing = await firestore().collection('ssoDomains').doc(domain).get();
|
|
630
|
+
if (existing.exists && existing.get('orgId') !== options.orgId) {
|
|
631
|
+
return {
|
|
632
|
+
domain: null,
|
|
633
|
+
error: 'That domain is already verified by another organization.'
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
await claimRef(options.orgId, domain).set({
|
|
637
|
+
domain,
|
|
638
|
+
attestedBy: options.attestedByUid.trim(),
|
|
639
|
+
attestedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
|
|
640
|
+
attestationNote: ((_options_note = options.note) == null ? void 0 : _options_note.trim()) || null
|
|
641
|
+
}, // MERGE, so an existing claim keeps its token and its `verified` state. A
|
|
642
|
+
// fresh document is created for the pre-self-serve orgs that have none,
|
|
643
|
+
// which is the whole point, but an attestation must never reset a claim
|
|
644
|
+
// the org is midway through proving by DNS.
|
|
645
|
+
{
|
|
646
|
+
merge: true
|
|
647
|
+
});
|
|
648
|
+
return {
|
|
649
|
+
domain,
|
|
650
|
+
error: null
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Publish the public routing docs — the last step, and the one that actually
|
|
655
|
+
* turns SSO on for a domain.
|
|
656
|
+
*
|
|
657
|
+
* Only PROVEN domains are written — DNS-verified, or staff-attested per
|
|
658
|
+
* {@link isStaffAttestedClaim}. This function is the single writer of
|
|
659
|
+
* `ssoDomains`, so the invariant "a live routing doc implies proven ownership"
|
|
660
|
+
* is enforceable by reading one place.
|
|
661
|
+
*/ export async function publishSsoDomains(options) {
|
|
662
|
+
const { orgId, tenantId, providerId, protocol, displayName } = options;
|
|
663
|
+
const published = [];
|
|
664
|
+
const batch = firestore().batch();
|
|
665
|
+
for (const raw of options.domains){
|
|
666
|
+
const domain = normalizeSsoDomain(raw);
|
|
667
|
+
if (!domain) continue;
|
|
668
|
+
// Re-read the claim rather than trusting the caller's list: this is the
|
|
669
|
+
// boundary where an unverified domain would become live, so the check
|
|
670
|
+
// belongs here and not only at the call site.
|
|
671
|
+
//
|
|
672
|
+
// TWO ways to be proven, and no third (AGL-1887). `verified === true` is
|
|
673
|
+
// DNS, re-checked by us; `attestedBy` is a named staff member vouching for
|
|
674
|
+
// an org onboarded before self-serve existed. Both are POSITIVE markers on
|
|
675
|
+
// the org's own claim document, which no client can write.
|
|
676
|
+
//
|
|
677
|
+
// What this must never become is an unconditional accept, or an inference
|
|
678
|
+
// from the routing doc already existing. That check is what stops an org
|
|
679
|
+
// publishing routing for a domain it does not own and intercepting another
|
|
680
|
+
// company's sign-ins — see `sso-publish-gate.emulator.spec.ts`, which
|
|
681
|
+
// exercises every refusal branch, and `sso-attested-restore.spec.ts`,
|
|
682
|
+
// which pins the boundary this loosening must not cross.
|
|
683
|
+
const claim = await claimRef(orgId, domain).get();
|
|
684
|
+
if (!claim.exists) continue;
|
|
685
|
+
const proven = claim.get('verified') === true || isStaffAttestedClaim(claim.get('attestedBy'));
|
|
686
|
+
if (!proven) continue;
|
|
687
|
+
batch.set(firestore().collection('ssoDomains').doc(domain), {
|
|
688
|
+
orgId,
|
|
689
|
+
tenantId,
|
|
690
|
+
providerId,
|
|
691
|
+
protocol,
|
|
692
|
+
displayName: displayName != null ? displayName : null,
|
|
693
|
+
active: true,
|
|
694
|
+
updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
695
|
+
}, {
|
|
696
|
+
merge: true
|
|
697
|
+
});
|
|
698
|
+
published.push(domain);
|
|
699
|
+
}
|
|
700
|
+
await batch.commit();
|
|
701
|
+
return published;
|
|
702
|
+
}
|
|
703
|
+
/** Deactivate every routing doc this org owns (disable, not delete). */ export async function unpublishSsoDomains(orgId) {
|
|
704
|
+
const owned = await firestore().collection('ssoDomains').where('orgId', '==', orgId).get();
|
|
705
|
+
if (owned.empty) return;
|
|
706
|
+
const batch = firestore().batch();
|
|
707
|
+
owned.docs.forEach((doc)=>batch.set(doc.ref, {
|
|
708
|
+
active: false
|
|
709
|
+
}, {
|
|
710
|
+
merge: true
|
|
711
|
+
}));
|
|
712
|
+
await batch.commit();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
//# sourceMappingURL=sso-provisioning.js.map
|