@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,640 @@
|
|
|
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
|
+
* The store behind several email addresses per account (AGL-2486).
|
|
19
|
+
*
|
|
20
|
+
* The policy — which address anything is allowed to READ — lives in
|
|
21
|
+
* `@aglyn/aglyn/app-utils/account-emails`, pure and client-importable. This
|
|
22
|
+
* module is the Admin-SDK half: the writes, the uniqueness guard, and the
|
|
23
|
+
* verification round-trip.
|
|
24
|
+
*
|
|
25
|
+
* ## Storage, and why every bit of it is server-only
|
|
26
|
+
*
|
|
27
|
+
* - `users/{uid}/emails/{address}` — the rows. SERVER-WRITE ONLY. This is
|
|
28
|
+
* not a style choice: `users/{uid}` itself is `allow read, write` for its
|
|
29
|
+
* owner under the rules, with no field validation whatsoever, so anything
|
|
30
|
+
* stored ON that document is a field its owner can set to whatever they
|
|
31
|
+
* like. An owner-writable `verified: true` is the entire feature defeated
|
|
32
|
+
* — claim `ceo@acme.com`, flip the bit, and you hold a sign-in identifier
|
|
33
|
+
* for an address you have never been able to receive mail at. So the rows
|
|
34
|
+
* live in a subcollection with `allow write: if false`, exactly like
|
|
35
|
+
* `passkeys` and `legalAcceptances`, and for exactly the same reason:
|
|
36
|
+
* a client-writable credential store is an auth bypass.
|
|
37
|
+
* - `emailIdentityIndex/{address}` — `{ uid }`, deny-all. THE uniqueness
|
|
38
|
+
* guard. Two accounts holding the same verified address makes the sign-in
|
|
39
|
+
* identifier ambiguous, which is an account-takeover vector, not a
|
|
40
|
+
* cosmetic clash. Modelled on `passkeyCredentialIndex`.
|
|
41
|
+
* - `emailVerifications/{tokenId}` — `{ uid, address, digest, expiresAt }`,
|
|
42
|
+
* deny-all, single-use. Only the DIGEST is stored, so a database read does
|
|
43
|
+
* not yield a working confirmation link. Modelled on
|
|
44
|
+
* `webauthnChallenges`.
|
|
45
|
+
*
|
|
46
|
+
* ## What is deliberately NOT here
|
|
47
|
+
*
|
|
48
|
+
* There is no `domainsFor(uid)`, no `emailsInDomain(domain)`, and no lookup
|
|
49
|
+
* that answers "which orgs could this account belong to". Adding one would be
|
|
50
|
+
* the whole hazard: `sso-jit` grants org membership from a verified email's
|
|
51
|
+
* domain, and a helper that handed it this account's OTHER addresses would
|
|
52
|
+
* turn "add an address" into "join an organization". The SSO paths read the
|
|
53
|
+
* address the IdP asserted at sign-in and nothing else — see
|
|
54
|
+
* `apps/console/specs/account-emails-never-reach-sso.spec.ts`, which fails if
|
|
55
|
+
* that stops being true.
|
|
56
|
+
*/ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
57
|
+
import { MAX_ACCOUNT_EMAILS, evaluatePrimaryChange, canRemoveAccountEmail, normalizeAccountEmail } from "@aglyn/aglyn/app-utils/account-emails";
|
|
58
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
59
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
60
|
+
import { authForPool, findUserByEmailAcrossPools } from "./auth-pools.js";
|
|
61
|
+
import { ssoDomainEnforcementEnabled, ssoRequiredDomains } from "./sso-domain-policy.js";
|
|
62
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
63
|
+
export const ACCOUNT_EMAILS_SUBCOLLECTION = 'emails';
|
|
64
|
+
export const EMAIL_IDENTITY_INDEX_COLLECTION = 'emailIdentityIndex';
|
|
65
|
+
export const EMAIL_VERIFICATIONS_COLLECTION = 'emailVerifications';
|
|
66
|
+
/**
|
|
67
|
+
* Long enough that a confirmation link survives a mail client's queue and a
|
|
68
|
+
* user who reads mail in the evening; short enough that a link sitting in a
|
|
69
|
+
* forwarded thread stops working. Firebase's own action links use one day.
|
|
70
|
+
*/ export const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
71
|
+
const ok = ()=>({
|
|
72
|
+
ok: true,
|
|
73
|
+
refusal: null,
|
|
74
|
+
message: null
|
|
75
|
+
});
|
|
76
|
+
const refuse = (refusal, message)=>({
|
|
77
|
+
ok: false,
|
|
78
|
+
refusal,
|
|
79
|
+
message
|
|
80
|
+
});
|
|
81
|
+
function emailsRef(uid) {
|
|
82
|
+
return firestore().collection('users').doc(uid).collection(ACCOUNT_EMAILS_SUBCOLLECTION);
|
|
83
|
+
}
|
|
84
|
+
function indexRef(address) {
|
|
85
|
+
return firestore().collection(EMAIL_IDENTITY_INDEX_COLLECTION).doc(address);
|
|
86
|
+
}
|
|
87
|
+
function toRow(data) {
|
|
88
|
+
const verifiedAt = data.verifiedAt;
|
|
89
|
+
return {
|
|
90
|
+
address: data.address,
|
|
91
|
+
verified: data.verified === true,
|
|
92
|
+
primary: data.primary === true,
|
|
93
|
+
verifiedAt: verifiedAt !== undefined && verifiedAt !== null && typeof verifiedAt.toDate === 'function' ? verifiedAt.toDate().toISOString() : null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the account's addresses, seeding the primary row from Firebase Auth on
|
|
98
|
+
* first read.
|
|
99
|
+
*
|
|
100
|
+
* THE BACKFILL IS NOT OPTIONAL. Every account that exists today predates this
|
|
101
|
+
* subcollection, so without the seed an established user opens the card and
|
|
102
|
+
* is told they have no addresses — while `decoded.email` says otherwise — and
|
|
103
|
+
* then "you cannot remove your last verified address" would be enforced
|
|
104
|
+
* against an empty set. The Firebase Auth record is the authority for the
|
|
105
|
+
* primary, so the seed is a mirror rather than a new fact.
|
|
106
|
+
*
|
|
107
|
+
* `authEmailVerified` is carried through honestly: an account that has never
|
|
108
|
+
* confirmed its own sign-up address gets an UNVERIFIED primary row, which is
|
|
109
|
+
* the truth and which the card then offers to fix.
|
|
110
|
+
*/ export async function listAccountEmails(uid, authEmail, authEmailVerified) {
|
|
111
|
+
const snapshot = await emailsRef(uid).get();
|
|
112
|
+
const rows = snapshot.docs.map((doc)=>toRow(doc.data()));
|
|
113
|
+
const primaryAddress = normalizeAccountEmail(authEmail);
|
|
114
|
+
if (primaryAddress === null) return rows;
|
|
115
|
+
const known = rows.find((row)=>row.address === primaryAddress);
|
|
116
|
+
if (known !== undefined) {
|
|
117
|
+
// The Auth record is the authority for WHICH address is primary. If they
|
|
118
|
+
// ever disagree — a staff email change through the admin console, say —
|
|
119
|
+
// Auth wins and the mirror is corrected, rather than the card showing a
|
|
120
|
+
// primary the token does not carry.
|
|
121
|
+
if (known.primary !== true) {
|
|
122
|
+
await reconcilePrimaryFlag(uid, primaryAddress);
|
|
123
|
+
return listAccountEmails(uid, authEmail, authEmailVerified);
|
|
124
|
+
}
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
await emailsRef(uid).doc(primaryAddress).set(_extends({
|
|
128
|
+
address: primaryAddress,
|
|
129
|
+
verified: authEmailVerified === true,
|
|
130
|
+
primary: true,
|
|
131
|
+
createdAt: FieldValue.serverTimestamp()
|
|
132
|
+
}, authEmailVerified === true ? {
|
|
133
|
+
verifiedAt: FieldValue.serverTimestamp()
|
|
134
|
+
} : {}), {
|
|
135
|
+
merge: true
|
|
136
|
+
});
|
|
137
|
+
/*
|
|
138
|
+
* CLEAR THE FLAG OFF WHATEVER USED TO HOLD IT.
|
|
139
|
+
*
|
|
140
|
+
* This branch runs when Auth's address is not among the stored rows — which
|
|
141
|
+
* is exactly what a staff email change produces, since it writes the Auth
|
|
142
|
+
* record directly. Setting the new row primary without clearing the old one
|
|
143
|
+
* left TWO rows flagged primary, and every reader that takes the first match
|
|
144
|
+
* then answers with whichever Firestore returned first.
|
|
145
|
+
*
|
|
146
|
+
* Only when there were pre-existing rows: a first-ever seed has nothing to
|
|
147
|
+
* reconcile and must not pay for a second read and a batch commit.
|
|
148
|
+
*/ if (rows.length > 0) await reconcilePrimaryFlag(uid, primaryAddress);
|
|
149
|
+
// Claim the index for it too, but never steal one: an address already
|
|
150
|
+
// indexed to somebody else is a real conflict that a backfill must not
|
|
151
|
+
// paper over. Best-effort — a failed claim must not stop someone reading
|
|
152
|
+
// their own settings page.
|
|
153
|
+
if (authEmailVerified === true) {
|
|
154
|
+
try {
|
|
155
|
+
await claimIndexEntry(primaryAddress, uid);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('[account-emails] primary index backfill failed', error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
...rows,
|
|
162
|
+
{
|
|
163
|
+
address: primaryAddress,
|
|
164
|
+
verified: authEmailVerified === true,
|
|
165
|
+
primary: true,
|
|
166
|
+
verifiedAt: null
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
async function reconcilePrimaryFlag(uid, primaryAddress) {
|
|
171
|
+
const snapshot = await emailsRef(uid).get();
|
|
172
|
+
const batch = firestore().batch();
|
|
173
|
+
for (const doc of snapshot.docs){
|
|
174
|
+
const shouldBePrimary = doc.id === primaryAddress;
|
|
175
|
+
if (doc.get('primary') === true !== shouldBePrimary) {
|
|
176
|
+
batch.set(doc.ref, {
|
|
177
|
+
primary: shouldBePrimary
|
|
178
|
+
}, {
|
|
179
|
+
merge: true
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await batch.commit();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Claim `address` for `uid` in the uniqueness index, transactionally.
|
|
187
|
+
*
|
|
188
|
+
* Returns false when another account already holds it. Re-claiming your own
|
|
189
|
+
* is a no-op success, which is what makes the whole flow idempotent.
|
|
190
|
+
*/ async function claimIndexEntry(address, uid) {
|
|
191
|
+
return firestore().runTransaction(async (transaction)=>{
|
|
192
|
+
const ref = indexRef(address);
|
|
193
|
+
const existing = await transaction.get(ref);
|
|
194
|
+
if (existing.exists) {
|
|
195
|
+
const owner = existing.get('uid');
|
|
196
|
+
if (owner !== uid) return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
transaction.set(ref, {
|
|
200
|
+
uid,
|
|
201
|
+
address,
|
|
202
|
+
claimedAt: FieldValue.serverTimestamp()
|
|
203
|
+
});
|
|
204
|
+
return true;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Is `address` spoken for by an account other than `uid`?
|
|
209
|
+
*
|
|
210
|
+
* Consults BOTH the index and Firebase Auth, and the Auth half is the
|
|
211
|
+
* load-bearing one. The index only knows about addresses that have been
|
|
212
|
+
* through this feature; every account that existed before it has its primary
|
|
213
|
+
* in the Auth record and nowhere else. Checking the index alone would let a
|
|
214
|
+
* brand-new account claim an established user's sign-up address, which is the
|
|
215
|
+
* exact ambiguity the uniqueness rule exists to prevent.
|
|
216
|
+
*
|
|
217
|
+
* `findUserByEmailAcrossPools` rather than a project-pool `getUserByEmail`:
|
|
218
|
+
* an SSO user lives in their org's GCIP tenant and is invisible to
|
|
219
|
+
* project-level lookups (AGL-1122), so the narrow call would report an
|
|
220
|
+
* enterprise customer's address as free.
|
|
221
|
+
*/ async function addressTakenByAnother(address, uid) {
|
|
222
|
+
const indexed = await indexRef(address).get();
|
|
223
|
+
if (indexed.exists && indexed.get('uid') !== uid) return true;
|
|
224
|
+
const pooled = await findUserByEmailAcrossPools(address);
|
|
225
|
+
if (pooled !== null && pooled !== undefined && pooled.record.uid !== uid) return true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Stage a new address and return the secret for its confirmation link.
|
|
230
|
+
*
|
|
231
|
+
* The address is stored UNVERIFIED and does nothing until the round-trip
|
|
232
|
+
* completes: it is not a sign-in identifier, it receives no account mail, and
|
|
233
|
+
* — importantly — it does NOT claim the uniqueness index. Claiming on `add`
|
|
234
|
+
* would make this endpoint a squatting tool: anyone could park an address
|
|
235
|
+
* they do not own and lock its real owner out of adding it. The index is
|
|
236
|
+
* claimed by {@link confirmAccountEmail}, so the first account to prove
|
|
237
|
+
* delivery wins, and proving delivery is the only way to win.
|
|
238
|
+
*
|
|
239
|
+
* The pre-flight `addressTakenByAnother` here is a courtesy, not the guard —
|
|
240
|
+
* it turns the common case into an immediate, honest error instead of a
|
|
241
|
+
* confirmation email that fails at the end. The real check runs again inside
|
|
242
|
+
* the confirmation transaction, because anything can change in between.
|
|
243
|
+
*/ export async function addAccountEmail(uid, input) {
|
|
244
|
+
const address = normalizeAccountEmail(input);
|
|
245
|
+
if (address === null) {
|
|
246
|
+
return _extends({}, refuse('invalid-address', 'Enter a valid email address.'), {
|
|
247
|
+
address: null,
|
|
248
|
+
secret: null
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const existing = await emailsRef(uid).get();
|
|
252
|
+
if (existing.docs.some((doc)=>doc.id === address)) {
|
|
253
|
+
return _extends({}, refuse('already-on-this-account', 'That address is already on this account.'), {
|
|
254
|
+
address,
|
|
255
|
+
secret: null
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// THE CAP (see MAX_ACCOUNT_EMAILS). Counted from a server read immediately
|
|
259
|
+
// before the write — a number read anywhere else is a number that can be
|
|
260
|
+
// stale by the time it is trusted.
|
|
261
|
+
if (existing.size >= MAX_ACCOUNT_EMAILS) {
|
|
262
|
+
return _extends({}, refuse('cap-reached', `An account can hold ${MAX_ACCOUNT_EMAILS} email addresses. ` + 'Remove one before adding another.'), {
|
|
263
|
+
address,
|
|
264
|
+
secret: null
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (await addressTakenByAnother(address, uid)) {
|
|
268
|
+
return _extends({}, refuse('claimed-by-another-account', 'That address is already in use on another account.'), {
|
|
269
|
+
address,
|
|
270
|
+
secret: null
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
await emailsRef(uid).doc(address).set({
|
|
274
|
+
address,
|
|
275
|
+
verified: false,
|
|
276
|
+
primary: false,
|
|
277
|
+
createdAt: FieldValue.serverTimestamp()
|
|
278
|
+
}, {
|
|
279
|
+
merge: true
|
|
280
|
+
});
|
|
281
|
+
const secret = await issueVerificationToken(uid, address);
|
|
282
|
+
return _extends({}, ok(), {
|
|
283
|
+
address,
|
|
284
|
+
secret
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Mint a single-use confirmation secret and store only its digest.
|
|
289
|
+
*
|
|
290
|
+
* The returned string is `{tokenId}.{secret}`; the caller puts it in the
|
|
291
|
+
* emailed link and never persists it. A reader of the database therefore
|
|
292
|
+
* cannot confirm anybody's address — they hold a SHA-256 of the half that
|
|
293
|
+
* matters.
|
|
294
|
+
*/ export async function issueVerificationToken(uid, address) {
|
|
295
|
+
const tokenId = randomBytes(16).toString('hex');
|
|
296
|
+
const secret = randomBytes(32).toString('hex');
|
|
297
|
+
await firestore().collection(EMAIL_VERIFICATIONS_COLLECTION).doc(tokenId).set({
|
|
298
|
+
uid,
|
|
299
|
+
address,
|
|
300
|
+
digest: createHash('sha256').update(secret).digest('hex'),
|
|
301
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
302
|
+
expiresAt: Date.now() + EMAIL_VERIFICATION_TTL_MS
|
|
303
|
+
});
|
|
304
|
+
return `${tokenId}.${secret}`;
|
|
305
|
+
}
|
|
306
|
+
/** Constant-time compare of two hex digests of equal length. */ function digestsMatch(a, b) {
|
|
307
|
+
if (typeof a !== 'string' || a.length !== b.length) return false;
|
|
308
|
+
return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Complete the round-trip: the bearer of this link can receive mail at the
|
|
312
|
+
* address, so the address becomes verified and the account claims it.
|
|
313
|
+
*
|
|
314
|
+
* Deliberately NOT authenticated against the current session. The person
|
|
315
|
+
* clicking the link is whoever opened the mailbox, and requiring them to be
|
|
316
|
+
* signed in as the right account first is how confirmation links strand
|
|
317
|
+
* people who read mail on a different device. The token itself carries the
|
|
318
|
+
* uid, and it can do nothing except verify the one address it was minted for.
|
|
319
|
+
*
|
|
320
|
+
* The uniqueness claim happens HERE and inside the same logical step as the
|
|
321
|
+
* flag flip, so two accounts racing on one address cannot both end up
|
|
322
|
+
* verified.
|
|
323
|
+
*/ export async function confirmAccountEmail(token) {
|
|
324
|
+
var _snapshot_get, _snapshot_get1, _snapshot_get2;
|
|
325
|
+
const raw = String(token != null ? token : '');
|
|
326
|
+
const separator = raw.indexOf('.');
|
|
327
|
+
if (separator <= 0) {
|
|
328
|
+
return _extends({}, refuse('token-invalid', 'That confirmation link is not valid.'), {
|
|
329
|
+
address: null,
|
|
330
|
+
uid: null
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const tokenId = raw.slice(0, separator);
|
|
334
|
+
const secret = raw.slice(separator + 1);
|
|
335
|
+
if (!/^[a-f0-9]{32}$/.test(tokenId) || !/^[a-f0-9]{64}$/.test(secret)) {
|
|
336
|
+
return _extends({}, refuse('token-invalid', 'That confirmation link is not valid.'), {
|
|
337
|
+
address: null,
|
|
338
|
+
uid: null
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const tokenRef = firestore().collection(EMAIL_VERIFICATIONS_COLLECTION).doc(tokenId);
|
|
342
|
+
const snapshot = await tokenRef.get();
|
|
343
|
+
if (!snapshot.exists) {
|
|
344
|
+
return _extends({}, refuse('token-invalid', 'That confirmation link has already been used.'), {
|
|
345
|
+
address: null,
|
|
346
|
+
uid: null
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (!digestsMatch(snapshot.get('digest'), createHash('sha256').update(secret).digest('hex'))) {
|
|
350
|
+
return _extends({}, refuse('token-invalid', 'That confirmation link is not valid.'), {
|
|
351
|
+
address: null,
|
|
352
|
+
uid: null
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const expiresAt = Number((_snapshot_get = snapshot.get('expiresAt')) != null ? _snapshot_get : 0);
|
|
356
|
+
if (!Number.isFinite(expiresAt) || expiresAt < Date.now()) {
|
|
357
|
+
await tokenRef.delete();
|
|
358
|
+
return _extends({}, refuse('token-expired', 'That confirmation link has expired — send a new one.'), {
|
|
359
|
+
address: null,
|
|
360
|
+
uid: null
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const uid = String((_snapshot_get1 = snapshot.get('uid')) != null ? _snapshot_get1 : '');
|
|
364
|
+
const address = String((_snapshot_get2 = snapshot.get('address')) != null ? _snapshot_get2 : '');
|
|
365
|
+
if (!uid || !address) {
|
|
366
|
+
await tokenRef.delete();
|
|
367
|
+
return _extends({}, refuse('token-invalid', 'That confirmation link is not valid.'), {
|
|
368
|
+
address: null,
|
|
369
|
+
uid: null
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
// Single-use: burned before the claim, so a link cannot be replayed even if
|
|
373
|
+
// the claim below fails.
|
|
374
|
+
await tokenRef.delete();
|
|
375
|
+
if (await addressTakenByAnother(address, uid)) {
|
|
376
|
+
return _extends({}, refuse('claimed-by-another-account', 'That address has since been confirmed on another account.'), {
|
|
377
|
+
address,
|
|
378
|
+
uid
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const claimed = await claimIndexEntry(address, uid);
|
|
382
|
+
if (!claimed) {
|
|
383
|
+
return _extends({}, refuse('claimed-by-another-account', 'That address has since been confirmed on another account.'), {
|
|
384
|
+
address,
|
|
385
|
+
uid
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
await emailsRef(uid).doc(address).set({
|
|
389
|
+
address,
|
|
390
|
+
verified: true,
|
|
391
|
+
verifiedAt: FieldValue.serverTimestamp()
|
|
392
|
+
}, {
|
|
393
|
+
merge: true
|
|
394
|
+
});
|
|
395
|
+
return _extends({}, ok(), {
|
|
396
|
+
address,
|
|
397
|
+
uid
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Remove an address, freeing its index entry.
|
|
402
|
+
*
|
|
403
|
+
* The index entry is released so the address can be used again — on this
|
|
404
|
+
* account or another. Leaving it behind would make removal a way to burn an
|
|
405
|
+
* address permanently, including somebody else's if they ever wanted it.
|
|
406
|
+
*/ export async function removeAccountEmail(uid, input) {
|
|
407
|
+
const address = normalizeAccountEmail(input);
|
|
408
|
+
if (address === null) return refuse('invalid-address', 'Enter a valid email address.');
|
|
409
|
+
const snapshot = await emailsRef(uid).get();
|
|
410
|
+
const rows = snapshot.docs.map((doc)=>{
|
|
411
|
+
var _doc_get;
|
|
412
|
+
return {
|
|
413
|
+
address: String((_doc_get = doc.get('address')) != null ? _doc_get : doc.id),
|
|
414
|
+
verified: doc.get('verified') === true,
|
|
415
|
+
primary: doc.get('primary') === true
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
const verdict = canRemoveAccountEmail(address, rows);
|
|
419
|
+
if (!verdict.allowed) {
|
|
420
|
+
var _verdict_message;
|
|
421
|
+
const row = rows.find((entry)=>entry.address === address);
|
|
422
|
+
// Distinct codes so the card can say WHICH floor it hit — "make another
|
|
423
|
+
// address primary first" and "this is your only confirmed address" are
|
|
424
|
+
// different instructions, and a single code would collapse them into one
|
|
425
|
+
// unhelpful sentence.
|
|
426
|
+
const code = row === undefined ? 'unknown-address' : row.primary === true ? 'cannot-remove-primary' : 'last-verified-address';
|
|
427
|
+
return refuse(code, String((_verdict_message = verdict.message) != null ? _verdict_message : 'That address cannot be removed.'));
|
|
428
|
+
}
|
|
429
|
+
await emailsRef(uid).doc(address).delete();
|
|
430
|
+
const indexed = await indexRef(address).get();
|
|
431
|
+
if (indexed.exists && indexed.get('uid') === uid) {
|
|
432
|
+
await indexRef(address).delete();
|
|
433
|
+
}
|
|
434
|
+
return ok();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Re-designate the primary address.
|
|
438
|
+
*
|
|
439
|
+
* THE MOST SECURITY-SENSITIVE WRITE IN THIS MODULE, because the primary IS
|
|
440
|
+
* the Firebase Auth record's email, which is what `decoded.email` carries,
|
|
441
|
+
* which is what `evaluateSsoDomainPolicy` reads at the session mint and what
|
|
442
|
+
* the invite-accept comparison matches on. Changing it changes an
|
|
443
|
+
* authorization input — so it goes through `evaluatePrimaryChange` first, and
|
|
444
|
+
* that function refuses to move a primary off a domain the deployment
|
|
445
|
+
* requires SSO for. See its docstring for why that refusal ignores the
|
|
446
|
+
* enforcement switch.
|
|
447
|
+
*
|
|
448
|
+
* The Auth record is updated in the account's OWN pool via `authForPool`: a
|
|
449
|
+
* project-level `updateUser` cannot see a GCIP tenant account, so the narrow
|
|
450
|
+
* call would silently fail for exactly the enterprise users this policy is
|
|
451
|
+
* about.
|
|
452
|
+
*
|
|
453
|
+
* `emailVerified: true` is set alongside the address, and it is honest — the
|
|
454
|
+
* only addresses that can reach this point are ones this module verified by
|
|
455
|
+
* an emailed round-trip. Omitting it would flip the account to unverified and
|
|
456
|
+
* lock it out of every route behind `emailUnverifiedResponse()`.
|
|
457
|
+
*/ export async function setPrimaryAccountEmail(uid, input, context) {
|
|
458
|
+
var _rows_find, _rows_find1, _context_tenantId;
|
|
459
|
+
const address = normalizeAccountEmail(input);
|
|
460
|
+
if (address === null) return refuse('invalid-address', 'Enter a valid email address.');
|
|
461
|
+
const snapshot = await emailsRef(uid).get();
|
|
462
|
+
const rows = snapshot.docs.map((doc)=>{
|
|
463
|
+
var _doc_get;
|
|
464
|
+
return {
|
|
465
|
+
address: String((_doc_get = doc.get('address')) != null ? _doc_get : doc.id),
|
|
466
|
+
verified: doc.get('verified') === true,
|
|
467
|
+
primary: doc.get('primary') === true
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
const current = (_rows_find = rows.find((row)=>row.primary === true)) != null ? _rows_find : null;
|
|
471
|
+
const next = (_rows_find1 = rows.find((row)=>row.address === address)) != null ? _rows_find1 : null;
|
|
472
|
+
const decision = evaluatePrimaryChange({
|
|
473
|
+
current,
|
|
474
|
+
next,
|
|
475
|
+
requiredDomains: ssoRequiredDomains(),
|
|
476
|
+
tenantId: (_context_tenantId = context.tenantId) != null ? _context_tenantId : null,
|
|
477
|
+
enforcementEnabled: ssoDomainEnforcementEnabled()
|
|
478
|
+
});
|
|
479
|
+
if (!decision.allowed) {
|
|
480
|
+
var _decision_message;
|
|
481
|
+
return refuse(decision.verdict, String((_decision_message = decision.message) != null ? _decision_message : 'That change is not allowed.'));
|
|
482
|
+
}
|
|
483
|
+
await authForPool(context.tenantId).updateUser(uid, {
|
|
484
|
+
email: address,
|
|
485
|
+
emailVerified: true
|
|
486
|
+
});
|
|
487
|
+
await reconcilePrimaryFlag(uid, address);
|
|
488
|
+
return ok();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The account that holds `address` as a VERIFIED alias, or null.
|
|
492
|
+
*
|
|
493
|
+
* ## Read this before calling it
|
|
494
|
+
*
|
|
495
|
+
* This is the one lookup that maps an arbitrary address to an account, and it
|
|
496
|
+
* exists for exactly two callers: resolving a sign-in identifier, and
|
|
497
|
+
* matching an org invitation. Both are safe for the same reason — neither
|
|
498
|
+
* GRANTS anything on the strength of the address:
|
|
499
|
+
*
|
|
500
|
+
* - a sign-in identifier still has to be followed by the account's password;
|
|
501
|
+
* - an invitation is an explicit grant the ORG made, and this only decides
|
|
502
|
+
* which of the recipient's mailboxes it may arrive at.
|
|
503
|
+
*
|
|
504
|
+
* It must never be wired into an SSO path. `sso-jit` resolves the org from
|
|
505
|
+
* the GCIP tenant on a re-verified token and matches the domain the IdP
|
|
506
|
+
* asserted; giving it this function would let a user add an address and be
|
|
507
|
+
* provisioned into the matching organization, which is the escalation this
|
|
508
|
+
* whole feature was designed around.
|
|
509
|
+
*
|
|
510
|
+
* Returns null for an unverified row by construction: the index is only ever
|
|
511
|
+
* written by the confirmation path.
|
|
512
|
+
*/ export async function findAccountByVerifiedAlias(input) {
|
|
513
|
+
var _indexed_get;
|
|
514
|
+
const address = normalizeAccountEmail(input);
|
|
515
|
+
if (address === null) return null;
|
|
516
|
+
const indexed = await indexRef(address).get();
|
|
517
|
+
if (!indexed.exists) return null;
|
|
518
|
+
const uid = String((_indexed_get = indexed.get('uid')) != null ? _indexed_get : '');
|
|
519
|
+
if (!uid) return null;
|
|
520
|
+
return {
|
|
521
|
+
uid,
|
|
522
|
+
address
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Providers whose asserted address counts as proven.
|
|
527
|
+
*
|
|
528
|
+
* `password` is excluded because its address is the primary, already handled,
|
|
529
|
+
* and its verification state lives on the Auth record rather than in the
|
|
530
|
+
* provider entry. Everything else here is a federated IdP that has itself
|
|
531
|
+
* established control of the mailbox — the same standard
|
|
532
|
+
* {@link confirmAccountEmail} applies, met by a different party.
|
|
533
|
+
*/ function isFederatedProvider(providerId) {
|
|
534
|
+
const id = String(providerId != null ? providerId : '');
|
|
535
|
+
return id !== '' && id !== 'password' && id !== 'phone' && id !== 'anonymous';
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Register the addresses a federated provider asserts for this account.
|
|
539
|
+
*
|
|
540
|
+
* Called from the session mint — the one place every interactive sign-in
|
|
541
|
+
* passes through with a verified token — and BEST-EFFORT by contract.
|
|
542
|
+
*
|
|
543
|
+
* ## Three rules, in priority order
|
|
544
|
+
*
|
|
545
|
+
* 1. **Sign-in must not depend on this.** It is bookkeeping. A person locked
|
|
546
|
+
* out because an index write failed is a worse outcome than the collision
|
|
547
|
+
* it was preventing, so every failure here is swallowed and logged, and
|
|
548
|
+
* the caller runs it off the critical path.
|
|
549
|
+
* 2. **A claim never takes an entry another account holds.**
|
|
550
|
+
* {@link claimIndexEntry} already refuses; what was missing is that the
|
|
551
|
+
* refusal went nowhere. A conflict now lands on the row as
|
|
552
|
+
* `indexConflict`, which the staff account page reads — silently skipping
|
|
553
|
+
* is exactly what produced the live collision.
|
|
554
|
+
* 3. **A conflicted address is stored UNVERIFIED.** This is the conservative
|
|
555
|
+
* half and it matters: `verifiedAccountEmails` feeds invitation matching,
|
|
556
|
+
* so marking a contested address verified on both accounts would make one
|
|
557
|
+
* invitation match two people. Unverified, the row is a record that the
|
|
558
|
+
* address exists on this account and grants nothing.
|
|
559
|
+
*
|
|
560
|
+
* ⛔ It does not merge, reassign or disable anything. Two real accounts
|
|
561
|
+
* sharing an address is a human decision — the row makes it visible and stops
|
|
562
|
+
* there.
|
|
563
|
+
*
|
|
564
|
+
* ⚠️ Registering here CANNOT reach SSO provisioning. `sso-jit` reads the
|
|
565
|
+
* address the IdP asserted at sign-in and never consults this store;
|
|
566
|
+
* `apps/console/specs/account-emails-never-reach-sso.spec.ts` fails if that
|
|
567
|
+
* stops being true. That guard is what makes this safe to write at all.
|
|
568
|
+
*/ export async function registerProviderAddresses(uid, record) {
|
|
569
|
+
var _ref, _ref1;
|
|
570
|
+
const claimed = [];
|
|
571
|
+
const conflicted = [];
|
|
572
|
+
const primary = normalizeAccountEmail((_ref = record == null ? void 0 : record.email) != null ? _ref : null);
|
|
573
|
+
const candidates = new Set();
|
|
574
|
+
for (const provider of (_ref1 = record == null ? void 0 : record.providerData) != null ? _ref1 : []){
|
|
575
|
+
var _ref2;
|
|
576
|
+
if (!isFederatedProvider(provider == null ? void 0 : provider.providerId)) continue;
|
|
577
|
+
const address = normalizeAccountEmail((_ref2 = provider == null ? void 0 : provider.email) != null ? _ref2 : null);
|
|
578
|
+
// The primary is registered by `listAccountEmails`' own backfill, on the
|
|
579
|
+
// Auth record's authority. Re-doing it here would race that seed.
|
|
580
|
+
if (address === null || address === primary) continue;
|
|
581
|
+
candidates.add(address);
|
|
582
|
+
}
|
|
583
|
+
for (const address of candidates){
|
|
584
|
+
try {
|
|
585
|
+
const won = await claimIndexEntry(address, uid);
|
|
586
|
+
if (won) claimed.push(address);
|
|
587
|
+
else conflicted.push(address);
|
|
588
|
+
await emailsRef(uid).doc(address).set(_extends({
|
|
589
|
+
address,
|
|
590
|
+
// Only when the claim was won — see rule 3.
|
|
591
|
+
verified: won,
|
|
592
|
+
primary: false,
|
|
593
|
+
source: 'provider'
|
|
594
|
+
}, won ? {
|
|
595
|
+
verifiedAt: FieldValue.serverTimestamp(),
|
|
596
|
+
indexConflict: false
|
|
597
|
+
} : {
|
|
598
|
+
indexConflict: true,
|
|
599
|
+
indexConflictAtMs: Date.now()
|
|
600
|
+
}, {
|
|
601
|
+
createdAt: FieldValue.serverTimestamp()
|
|
602
|
+
}), {
|
|
603
|
+
merge: true
|
|
604
|
+
});
|
|
605
|
+
} catch (error) {
|
|
606
|
+
// Rule 1. Never rethrow: the caller is a sign-in.
|
|
607
|
+
console.error('[account-emails] provider address registration failed', uid, error);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (conflicted.length) {
|
|
611
|
+
// Loud, per rule 2 — a conflict that only ever landed in a document would
|
|
612
|
+
// be discovered by whoever happened to open the right page.
|
|
613
|
+
console.error('[account-emails] provider address already claimed by another account', JSON.stringify({
|
|
614
|
+
uid,
|
|
615
|
+
conflicted: conflicted.length
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
claimed,
|
|
620
|
+
conflicted
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Every VERIFIED address on an account, for matching an invitation.
|
|
625
|
+
*
|
|
626
|
+
* Bounded by {@link MAX_ACCOUNT_EMAILS}, which is what makes it safe to feed
|
|
627
|
+
* straight into a Firestore `in` query (limit 30).
|
|
628
|
+
*
|
|
629
|
+
* ⛔ NOT the answer to "every address this account holds" — it is verified-only,
|
|
630
|
+
* subcollection-only and silently truncating, all correct here and all wrong
|
|
631
|
+
* there. `account-addresses.ts` is that resolver and explains why at length.
|
|
632
|
+
*/ export async function verifiedAccountEmails(uid) {
|
|
633
|
+
const snapshot = await emailsRef(uid).get();
|
|
634
|
+
return snapshot.docs.filter((doc)=>doc.get('verified') === true).map((doc)=>{
|
|
635
|
+
var _doc_get;
|
|
636
|
+
return String((_doc_get = doc.get('address')) != null ? _doc_get : doc.id);
|
|
637
|
+
}).slice(0, MAX_ACCOUNT_EMAILS);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
//# sourceMappingURL=account-emails.js.map
|