@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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* EVERY ADDRESS AN ACCOUNT HOLDS — the one answer, for every consumer.
|
|
18
|
+
*
|
|
19
|
+
* ## Why this exists
|
|
20
|
+
*
|
|
21
|
+
* Two stores in this product are keyed by ADDRESS rather than by uid:
|
|
22
|
+
* `emailDeliveries/{sha256(address)}` (the per-recipient delivery log) and
|
|
23
|
+
* `emailSuppressions/{sha256(address)}`. Everything that reached them passed
|
|
24
|
+
* ONE address — the Firebase Auth record's current `email` — and an account
|
|
25
|
+
* is not one address:
|
|
26
|
+
*
|
|
27
|
+
* - A changed primary leaves the old address holding the mail. The new hash
|
|
28
|
+
* names an empty document, so the staff card renders a blank table for a
|
|
29
|
+
* person we demonstrably emailed. That card's own copy warns that reading
|
|
30
|
+
* a blank table as "we never emailed them" is how staff mislead a
|
|
31
|
+
* customer; before this module, an email change made the card do exactly
|
|
32
|
+
* that.
|
|
33
|
+
* - A federated provider carries its own address. `providerData[].email` can
|
|
34
|
+
* differ from the primary and is a real mailbox that has received real
|
|
35
|
+
* mail.
|
|
36
|
+
* - `users/{uid}/emails/{address}` (AGL-2486) holds confirmed aliases and
|
|
37
|
+
* every address this account has been moved OFF of.
|
|
38
|
+
*
|
|
39
|
+
* Erasure inherited the same single-address assumption, which is the sharp
|
|
40
|
+
* version of the bug: a prior or secondary address kept its full delivery
|
|
41
|
+
* history — recipient address, subjects, open and click times — after an
|
|
42
|
+
* erasure request had been honoured and reported complete.
|
|
43
|
+
*
|
|
44
|
+
* ## One resolver, not four call sites
|
|
45
|
+
*
|
|
46
|
+
* The delivery-history card, erasure, the audit subject resolution and the
|
|
47
|
+
* suppression lookups all need the same rule, and four copies of it would
|
|
48
|
+
* drift apart the way the dead-status set did. So the rule lives here and
|
|
49
|
+
* every consumer calls in. A caller that wants "the addresses" must not
|
|
50
|
+
* re-derive them from a record it happens to be holding.
|
|
51
|
+
*
|
|
52
|
+
* ## Why `verifiedAccountEmails` is not this function
|
|
53
|
+
*
|
|
54
|
+
* `account-emails.ts` already answers a question that reads like this one,
|
|
55
|
+
* and reusing it here would be a correctness bug in both directions. It is
|
|
56
|
+
* built for matching an INVITATION, and its three defining choices are all
|
|
57
|
+
* right for that and wrong for this:
|
|
58
|
+
*
|
|
59
|
+
* - **It returns VERIFIED addresses only.** Correct for an invitation: an
|
|
60
|
+
* unverified address must not match one, or adding an address becomes a
|
|
61
|
+
* way into somebody else's organization. Wrong here, because unverified
|
|
62
|
+
* addresses are precisely where the mail is — a verification message is by
|
|
63
|
+
* definition sent to an address that is not yet verified, and it is the
|
|
64
|
+
* single most common row in the delivery log. Filtering it out would make
|
|
65
|
+
* the card miss the mail a staffer is most often asked about, and would
|
|
66
|
+
* make erasure leave it behind.
|
|
67
|
+
* - **It reads the subcollection alone.** The primary lives in the Firebase
|
|
68
|
+
* Auth record and is mirrored into `users/{uid}/emails` only LAZILY, on
|
|
69
|
+
* the account's first read of its own settings. An account that has never
|
|
70
|
+
* opened that page has an empty subcollection and a real primary, so this
|
|
71
|
+
* would return nothing at all for it.
|
|
72
|
+
* - **It `slice`s to `MAX_ACCOUNT_EMAILS`.** A silent truncation is
|
|
73
|
+
* tolerable when the cost is an invitation not matching. It is the worst
|
|
74
|
+
* available outcome for erasure, which would report success over addresses
|
|
75
|
+
* it never swept.
|
|
76
|
+
*
|
|
77
|
+
* So this module composes over that one's STORAGE — same subcollection, same
|
|
78
|
+
* index, and the reverse lookup is delegated to `findAccountByVerifiedAlias`
|
|
79
|
+
* rather than re-read — and adds the two sources it does not have.
|
|
80
|
+
*
|
|
81
|
+
* ⛔ Do not "simplify" this into `verifiedAccountEmails`. The three
|
|
82
|
+
* differences above are each a defect if collapsed.
|
|
83
|
+
*
|
|
84
|
+
* ## THE UNIQUENESS INDEX ONCE HAD A HOLE, AND IT STILL HAS A TAIL
|
|
85
|
+
*
|
|
86
|
+
* `emailIdentityIndex` is what stops two accounts holding one address, and it
|
|
87
|
+
* was claimed in exactly two places: the primary backfill in
|
|
88
|
+
* `listAccountEmails`, and `confirmAccountEmail`. Neither read `providerData`
|
|
89
|
+
* — so an address supplied by a federated provider never reached the index,
|
|
90
|
+
* and two accounts could hold one address without the guard ever seeing them.
|
|
91
|
+
* The live shape of it is ordinary: an account whose Google provider address
|
|
92
|
+
* is another account's primary.
|
|
93
|
+
*
|
|
94
|
+
* `registerProviderAddresses` now claims for provider-asserted addresses at
|
|
95
|
+
* the session mint, so this closes going forward and backfills each account
|
|
96
|
+
* on its next sign-in. Two things follow that this module must keep saying:
|
|
97
|
+
*
|
|
98
|
+
* - **The tail is still open.** An account that has not signed in since is
|
|
99
|
+
* still unregistered, and a collision that already exists is not repaired
|
|
100
|
+
* by a claim that correctly refuses. `tools/scripts/audit-provider-address-claims.mjs`
|
|
101
|
+
* reports both, read-only — resolving a collision means deciding about two
|
|
102
|
+
* real accounts, which is not a decision code makes.
|
|
103
|
+
* - **A refused claim leaves an address held and unindexed.** That is the
|
|
104
|
+
* intended outcome, not a failure, so the reverse direction below is no
|
|
105
|
+
* more complete than it was: it still cannot enumerate provider holders.
|
|
106
|
+
*
|
|
107
|
+
* The registration writes an account-email row and nothing else. It cannot
|
|
108
|
+
* reach SSO provisioning — `sso-jit` reads the address the IdP asserted at
|
|
109
|
+
* sign-in and never consults this store, and
|
|
110
|
+
* `apps/console/specs/account-emails-never-reach-sso.spec.ts` fails if that
|
|
111
|
+
* stops being true. That guard is what makes the write safe.
|
|
112
|
+
*
|
|
113
|
+
* ## The direction that works, and the one that cannot
|
|
114
|
+
*
|
|
115
|
+
* FORWARD (uid → addresses) is complete: all three sources are readable from
|
|
116
|
+
* the uid. {@link resolveAccountAddresses} is that direction.
|
|
117
|
+
*
|
|
118
|
+
* REVERSE (address → uid) is STRUCTURALLY INCOMPLETE and no amount of care
|
|
119
|
+
* here fixes it. Firebase Auth can look an address up as a primary
|
|
120
|
+
* (`getUserByEmail`) and `emailIdentityIndex` answers for confirmed aliases,
|
|
121
|
+
* but there is no query for "which accounts carry this as a provider
|
|
122
|
+
* address" — that field is only readable once you already have the record.
|
|
123
|
+
* So {@link findAccountsHoldingAddress} can prove an address is AMBIGUOUS and
|
|
124
|
+
* can never prove it is unique, and its callers are written to treat a single
|
|
125
|
+
* answer as "no reason to think otherwise" rather than as a fact.
|
|
126
|
+
*
|
|
127
|
+
* That distinction is the whole reason `subjectUid` is omitted rather than
|
|
128
|
+
* guessed: naming one customer on another customer's data access is worse
|
|
129
|
+
* than naming nobody.
|
|
130
|
+
*/ import { createHash } from "crypto";
|
|
131
|
+
import { normalizeAccountEmail } from "@aglyn/aglyn/app-utils/account-emails";
|
|
132
|
+
import { ACCOUNT_EMAILS_SUBCOLLECTION, findAccountByVerifiedAlias } from "./account-emails.js";
|
|
133
|
+
import { findUserByEmailAcrossPools } from "./auth-pools.js";
|
|
134
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
135
|
+
const defaultFirestore = ()=>firebaseAdmin.app().firestore();
|
|
136
|
+
/** `sha256` of the normalized address. */ export function accountAddressKey(address) {
|
|
137
|
+
return createHash('sha256').update(address).digest('hex');
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Every address `uid` holds, from all three sources.
|
|
141
|
+
*
|
|
142
|
+
* `record` is passed in rather than fetched: every caller already has the
|
|
143
|
+
* Auth record in hand, and re-fetching it would add a pooled lookup to a page
|
|
144
|
+
* that has already paid for one.
|
|
145
|
+
*
|
|
146
|
+
* Best-effort per source. A Firestore outage must not make the delivery card
|
|
147
|
+
* fall back to the single-address behaviour SILENTLY — it reports
|
|
148
|
+
* `incomplete` instead, and the callers that destroy data refuse on it.
|
|
149
|
+
*
|
|
150
|
+
* @param detectShared consult the reverse direction for each non-primary
|
|
151
|
+
* address. One extra lookup per address; skip it where the answer is
|
|
152
|
+
* not used.
|
|
153
|
+
*/ export async function resolveAccountAddresses(options) {
|
|
154
|
+
var _options_firestore, _ref, _ref1, _ref2;
|
|
155
|
+
const { uid, record } = options;
|
|
156
|
+
const db = (_options_firestore = options.firestore) != null ? _options_firestore : defaultFirestore();
|
|
157
|
+
const found = new Map();
|
|
158
|
+
/** Addresses whose provider-asserted claim was refused. */ const conflicted = new Set();
|
|
159
|
+
let incomplete = false;
|
|
160
|
+
const add = (input, source)=>{
|
|
161
|
+
var _found_get;
|
|
162
|
+
const address = normalizeAccountEmail(input);
|
|
163
|
+
if (address === null) return;
|
|
164
|
+
const sources = (_found_get = found.get(address)) != null ? _found_get : new Set();
|
|
165
|
+
sources.add(source);
|
|
166
|
+
found.set(address, sources);
|
|
167
|
+
};
|
|
168
|
+
const primary = normalizeAccountEmail((_ref = record == null ? void 0 : record.email) != null ? _ref : null);
|
|
169
|
+
add((_ref1 = record == null ? void 0 : record.email) != null ? _ref1 : null, 'primary');
|
|
170
|
+
for (const provider of (_ref2 = record == null ? void 0 : record.providerData) != null ? _ref2 : []){
|
|
171
|
+
var _ref3;
|
|
172
|
+
add((_ref3 = provider == null ? void 0 : provider.email) != null ? _ref3 : null, 'provider');
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const stored = await db.collection('users').doc(uid).collection(ACCOUNT_EMAILS_SUBCOLLECTION).get();
|
|
176
|
+
for (const doc of stored.docs){
|
|
177
|
+
var _doc_get;
|
|
178
|
+
const address = normalizeAccountEmail((_doc_get = doc.get('address')) != null ? _doc_get : doc.id);
|
|
179
|
+
add(address, 'stored');
|
|
180
|
+
if (address !== null && doc.get('indexConflict') === true) {
|
|
181
|
+
conflicted.add(address);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// The subcollection is where a FORMER primary lives, so losing it is
|
|
186
|
+
// precisely how the orphaned-history bug comes back. Never silent.
|
|
187
|
+
console.error('[account-addresses] stored addresses unreadable', uid, error);
|
|
188
|
+
incomplete = true;
|
|
189
|
+
}
|
|
190
|
+
// Primary first: it is the address staff recognise, and a card that led
|
|
191
|
+
// with an alias would read as the wrong account.
|
|
192
|
+
const ordered = [
|
|
193
|
+
...found.keys()
|
|
194
|
+
].sort((a, b)=>{
|
|
195
|
+
if (a === primary) return -1;
|
|
196
|
+
if (b === primary) return 1;
|
|
197
|
+
return a.localeCompare(b);
|
|
198
|
+
});
|
|
199
|
+
const addresses = [];
|
|
200
|
+
for (const address of ordered){
|
|
201
|
+
var _found_get;
|
|
202
|
+
let shared = false;
|
|
203
|
+
if (options.detectShared === true) {
|
|
204
|
+
try {
|
|
205
|
+
const holders = await findAccountsHoldingAddress(address, {
|
|
206
|
+
firestore: db
|
|
207
|
+
});
|
|
208
|
+
shared = holders.uids.some((holder)=>holder !== uid);
|
|
209
|
+
} catch (unused) {
|
|
210
|
+
// Not `incomplete`: a failed sharing probe under-reports a note on a
|
|
211
|
+
// card. It does not shorten the address list, which is the thing
|
|
212
|
+
// erasure depends on.
|
|
213
|
+
shared = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
addresses.push({
|
|
217
|
+
address,
|
|
218
|
+
sources: [
|
|
219
|
+
...(_found_get = found.get(address)) != null ? _found_get : []
|
|
220
|
+
],
|
|
221
|
+
key: accountAddressKey(address),
|
|
222
|
+
shared,
|
|
223
|
+
indexConflict: conflicted.has(address)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
uid,
|
|
228
|
+
primary,
|
|
229
|
+
addresses,
|
|
230
|
+
incomplete
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Just the addresses, for a caller that needs nothing else. */ export function addressList(set) {
|
|
234
|
+
return set.addresses.map((entry)=>entry.address);
|
|
235
|
+
}
|
|
236
|
+
/** Just the hashed keys, for a query against a store keyed by them. */ export function addressKeys(set) {
|
|
237
|
+
return set.addresses.map((entry)=>entry.key);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Which accounts hold `address`, as far as can be determined.
|
|
241
|
+
*
|
|
242
|
+
* ## What it can and cannot see
|
|
243
|
+
*
|
|
244
|
+
* It consults the two sources an address can be looked up IN:
|
|
245
|
+
*
|
|
246
|
+
* - Firebase Auth, across pools — the account holding it as its PRIMARY.
|
|
247
|
+
* - `emailIdentityIndex/{address}` — the account holding it as a CONFIRMED
|
|
248
|
+
* alias.
|
|
249
|
+
*
|
|
250
|
+
* It cannot see a `providerData` holder, because Firebase Auth offers no
|
|
251
|
+
* lookup by provider address; that field is readable only from a record you
|
|
252
|
+
* already have. This is not a gap that can be closed by trying harder, and
|
|
253
|
+
* the live shape of it is ordinary: an account whose Google provider address
|
|
254
|
+
* is another account's primary.
|
|
255
|
+
*
|
|
256
|
+
* So the result is sound in ONE direction only. `ambiguous: true` is proof
|
|
257
|
+
* that more than one account holds the address. `ambiguous: false` is the
|
|
258
|
+
* absence of evidence, never evidence of absence — a caller must not treat a
|
|
259
|
+
* single uid as "this address belongs to this person".
|
|
260
|
+
*/ export async function findAccountsHoldingAddress(input, options) {
|
|
261
|
+
var _ref;
|
|
262
|
+
const address = normalizeAccountEmail(input);
|
|
263
|
+
if (address === null) return {
|
|
264
|
+
address: '',
|
|
265
|
+
uids: [],
|
|
266
|
+
ambiguous: false
|
|
267
|
+
};
|
|
268
|
+
const db = (_ref = options == null ? void 0 : options.firestore) != null ? _ref : defaultFirestore();
|
|
269
|
+
const uids = new Set();
|
|
270
|
+
try {
|
|
271
|
+
var _pooled_record;
|
|
272
|
+
const pooled = await findUserByEmailAcrossPools(address);
|
|
273
|
+
if (pooled == null ? void 0 : (_pooled_record = pooled.record) == null ? void 0 : _pooled_record.uid) uids.add(pooled.record.uid);
|
|
274
|
+
} catch (unused) {
|
|
275
|
+
// A pool that would not answer leaves the set smaller, which under-reports
|
|
276
|
+
// sharing rather than inventing it.
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
// `account-emails.ts`'s own reverse lookup, not a second read of its
|
|
280
|
+
// index. That function owns what the index means — including that an
|
|
281
|
+
// unverified row is absent from it by construction — and a private copy
|
|
282
|
+
// of the read here would be a second place to keep that in step.
|
|
283
|
+
const alias = await findAccountByVerifiedAlias(address);
|
|
284
|
+
if (alias == null ? void 0 : alias.uid) uids.add(alias.uid);
|
|
285
|
+
} catch (unused) {
|
|
286
|
+
// Same reasoning.
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
address,
|
|
290
|
+
uids: [
|
|
291
|
+
...uids
|
|
292
|
+
],
|
|
293
|
+
ambiguous: uids.size > 1
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The single account an address can be attributed to, or null.
|
|
298
|
+
*
|
|
299
|
+
* Null for BOTH "nobody" and "more than one", deliberately. The caller writes
|
|
300
|
+
* an audit subject with it, and a row naming the wrong customer is worse than
|
|
301
|
+
* a row naming none: the collection exists to answer "who at Aglyn read my
|
|
302
|
+
* data", and an answer that points at an innocent account is not a weaker
|
|
303
|
+
* answer, it is a false one.
|
|
304
|
+
*/ export async function attributableAccountForAddress(input, options) {
|
|
305
|
+
var _holders_uids_;
|
|
306
|
+
const holders = await findAccountsHoldingAddress(input, options);
|
|
307
|
+
if (holders.ambiguous) return null;
|
|
308
|
+
return (_holders_uids_ = holders.uids[0]) != null ? _holders_uids_ : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
//# sourceMappingURL=account-addresses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/account-addresses.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * EVERY ADDRESS AN ACCOUNT HOLDS — the one answer, for every consumer.\n *\n * ## Why this exists\n *\n * Two stores in this product are keyed by ADDRESS rather than by uid:\n * `emailDeliveries/{sha256(address)}` (the per-recipient delivery log) and\n * `emailSuppressions/{sha256(address)}`. Everything that reached them passed\n * ONE address — the Firebase Auth record's current `email` — and an account\n * is not one address:\n *\n * - A changed primary leaves the old address holding the mail. The new hash\n * names an empty document, so the staff card renders a blank table for a\n * person we demonstrably emailed. That card's own copy warns that reading\n * a blank table as \"we never emailed them\" is how staff mislead a\n * customer; before this module, an email change made the card do exactly\n * that.\n * - A federated provider carries its own address. `providerData[].email` can\n * differ from the primary and is a real mailbox that has received real\n * mail.\n * - `users/{uid}/emails/{address}` (AGL-2486) holds confirmed aliases and\n * every address this account has been moved OFF of.\n *\n * Erasure inherited the same single-address assumption, which is the sharp\n * version of the bug: a prior or secondary address kept its full delivery\n * history — recipient address, subjects, open and click times — after an\n * erasure request had been honoured and reported complete.\n *\n * ## One resolver, not four call sites\n *\n * The delivery-history card, erasure, the audit subject resolution and the\n * suppression lookups all need the same rule, and four copies of it would\n * drift apart the way the dead-status set did. So the rule lives here and\n * every consumer calls in. A caller that wants \"the addresses\" must not\n * re-derive them from a record it happens to be holding.\n *\n * ## Why `verifiedAccountEmails` is not this function\n *\n * `account-emails.ts` already answers a question that reads like this one,\n * and reusing it here would be a correctness bug in both directions. It is\n * built for matching an INVITATION, and its three defining choices are all\n * right for that and wrong for this:\n *\n * - **It returns VERIFIED addresses only.** Correct for an invitation: an\n * unverified address must not match one, or adding an address becomes a\n * way into somebody else's organization. Wrong here, because unverified\n * addresses are precisely where the mail is — a verification message is by\n * definition sent to an address that is not yet verified, and it is the\n * single most common row in the delivery log. Filtering it out would make\n * the card miss the mail a staffer is most often asked about, and would\n * make erasure leave it behind.\n * - **It reads the subcollection alone.** The primary lives in the Firebase\n * Auth record and is mirrored into `users/{uid}/emails` only LAZILY, on\n * the account's first read of its own settings. An account that has never\n * opened that page has an empty subcollection and a real primary, so this\n * would return nothing at all for it.\n * - **It `slice`s to `MAX_ACCOUNT_EMAILS`.** A silent truncation is\n * tolerable when the cost is an invitation not matching. It is the worst\n * available outcome for erasure, which would report success over addresses\n * it never swept.\n *\n * So this module composes over that one's STORAGE — same subcollection, same\n * index, and the reverse lookup is delegated to `findAccountByVerifiedAlias`\n * rather than re-read — and adds the two sources it does not have.\n *\n * ⛔ Do not \"simplify\" this into `verifiedAccountEmails`. The three\n * differences above are each a defect if collapsed.\n *\n * ## THE UNIQUENESS INDEX ONCE HAD A HOLE, AND IT STILL HAS A TAIL\n *\n * `emailIdentityIndex` is what stops two accounts holding one address, and it\n * was claimed in exactly two places: the primary backfill in\n * `listAccountEmails`, and `confirmAccountEmail`. Neither read `providerData`\n * — so an address supplied by a federated provider never reached the index,\n * and two accounts could hold one address without the guard ever seeing them.\n * The live shape of it is ordinary: an account whose Google provider address\n * is another account's primary.\n *\n * `registerProviderAddresses` now claims for provider-asserted addresses at\n * the session mint, so this closes going forward and backfills each account\n * on its next sign-in. Two things follow that this module must keep saying:\n *\n * - **The tail is still open.** An account that has not signed in since is\n * still unregistered, and a collision that already exists is not repaired\n * by a claim that correctly refuses. `tools/scripts/audit-provider-address-claims.mjs`\n * reports both, read-only — resolving a collision means deciding about two\n * real accounts, which is not a decision code makes.\n * - **A refused claim leaves an address held and unindexed.** That is the\n * intended outcome, not a failure, so the reverse direction below is no\n * more complete than it was: it still cannot enumerate provider holders.\n *\n * The registration writes an account-email row and nothing else. It cannot\n * reach SSO provisioning — `sso-jit` reads the address the IdP asserted at\n * sign-in and never consults this store, and\n * `apps/console/specs/account-emails-never-reach-sso.spec.ts` fails if that\n * stops being true. That guard is what makes the write safe.\n *\n * ## The direction that works, and the one that cannot\n *\n * FORWARD (uid → addresses) is complete: all three sources are readable from\n * the uid. {@link resolveAccountAddresses} is that direction.\n *\n * REVERSE (address → uid) is STRUCTURALLY INCOMPLETE and no amount of care\n * here fixes it. Firebase Auth can look an address up as a primary\n * (`getUserByEmail`) and `emailIdentityIndex` answers for confirmed aliases,\n * but there is no query for \"which accounts carry this as a provider\n * address\" — that field is only readable once you already have the record.\n * So {@link findAccountsHoldingAddress} can prove an address is AMBIGUOUS and\n * can never prove it is unique, and its callers are written to treat a single\n * answer as \"no reason to think otherwise\" rather than as a fact.\n *\n * That distinction is the whole reason `subjectUid` is omitted rather than\n * guessed: naming one customer on another customer's data access is worse\n * than naming nobody.\n */\n\nimport { createHash } from 'crypto'\nimport { normalizeAccountEmail } from '@aglyn/aglyn/app-utils/account-emails'\nimport {\n ACCOUNT_EMAILS_SUBCOLLECTION,\n findAccountByVerifiedAlias,\n} from './account-emails'\nimport { findUserByEmailAcrossPools } from './auth-pools'\nimport firebaseAdmin from './firebase-admin'\n\nconst defaultFirestore = () => firebaseAdmin.app().firestore()\n\n/** Where an address came from. An address can have more than one. */\nexport type AccountAddressSource =\n /** The Firebase Auth record's `email` — what a token carries today. */\n | 'primary'\n /** A federated provider's own address, from `providerData[].email`. */\n | 'provider'\n /** A row in `users/{uid}/emails` — a confirmed alias, or a former primary. */\n | 'stored'\n\n/** One address, with everything a consumer needs to decide what to do. */\nexport interface AccountAddress {\n /** Lowercased and trimmed. The form both hashed stores key on. */\n address: string\n /** Every source that named it, so a card can say WHY it is listed. */\n sources: AccountAddressSource[]\n /**\n * `sha256(address)` — the SAME derivation `emailSuppressionKey` uses, so a\n * consumer never has to re-derive it and the two can never disagree about\n * which document describes which person.\n */\n key: string\n /**\n * Another account holds this address too.\n *\n * ⚠️ `false` means \"nothing found\", NOT \"nobody else holds it\" — see the\n * reverse-direction note in this module's header.\n */\n shared: boolean\n /**\n * A provider asserted this address for this account and the uniqueness\n * claim was REFUSED, because another account already held it.\n *\n * Carried onto the staff surface rather than left in a log line. The\n * refusal is the whole mechanism: silently skipping the claim is what let\n * the live collision exist unrecorded, so an account carrying a provider\n * address it does not own has to be visible on its own page.\n */\n indexConflict: boolean\n}\n\n/**\n * ## How big this can get, and why nothing here slices\n *\n * `MAX_ACCOUNT_EMAILS` is 5 and caps the SUBCOLLECTION only; the primary and\n * the provider addresses are additional, so the set can exceed 5. It stays\n * far under Firestore's 30-element `in` limit in every realistic shape, but\n * \"far under\" is not a guarantee and a truncated erasure is the worst outcome\n * available in this whole area — it reports success over addresses it never\n * swept.\n *\n * So the rule is: consumers ITERATE this list, they do not slice it, and any\n * consumer feeding it to an `in` query CHUNKS. No function in this module\n * truncates its result.\n */\nexport interface AccountAddressSet {\n uid: string\n /** The Auth record's current address, or null for an addressless account. */\n primary: string | null\n /** Primary first, then the rest in a stable order. */\n addresses: AccountAddress[]\n /**\n * A source threw and the set may be SHORT.\n *\n * Load-bearing for erasure: erasing \"every address\" from an incomplete set\n * is not erasing every address, and the caller has to be able to say so\n * rather than report a complete erasure it did not perform.\n */\n incomplete: boolean\n}\n\n/** `sha256` of the normalized address. */\nexport function accountAddressKey(address: string): string {\n return createHash('sha256').update(address).digest('hex')\n}\n\n/**\n * Every address `uid` holds, from all three sources.\n *\n * `record` is passed in rather than fetched: every caller already has the\n * Auth record in hand, and re-fetching it would add a pooled lookup to a page\n * that has already paid for one.\n *\n * Best-effort per source. A Firestore outage must not make the delivery card\n * fall back to the single-address behaviour SILENTLY — it reports\n * `incomplete` instead, and the callers that destroy data refuse on it.\n *\n * @param detectShared consult the reverse direction for each non-primary\n * address. One extra lookup per address; skip it where the answer is\n * not used.\n */\nexport async function resolveAccountAddresses(options: {\n uid: string\n /** The Auth record — `email` and `providerData` are read from it. */\n record?: {\n email?: string | null\n providerData?: readonly { email?: string | null }[]\n } | null\n detectShared?: boolean\n firestore?: any\n}): Promise<AccountAddressSet> {\n const { uid, record } = options\n const db = options.firestore ?? defaultFirestore()\n const found = new Map<string, Set<AccountAddressSource>>()\n /** Addresses whose provider-asserted claim was refused. */\n const conflicted = new Set<string>()\n let incomplete = false\n\n const add = (input: unknown, source: AccountAddressSource) => {\n const address = normalizeAccountEmail(input)\n if (address === null) return\n const sources = found.get(address) ?? new Set<AccountAddressSource>()\n sources.add(source)\n found.set(address, sources)\n }\n\n const primary = normalizeAccountEmail(record?.email ?? null)\n add(record?.email ?? null, 'primary')\n for (const provider of record?.providerData ?? []) {\n add(provider?.email ?? null, 'provider')\n }\n\n try {\n const stored = await db\n .collection('users')\n .doc(uid)\n .collection(ACCOUNT_EMAILS_SUBCOLLECTION)\n .get()\n for (const doc of stored.docs) {\n const address = normalizeAccountEmail(doc.get('address') ?? doc.id)\n add(address, 'stored')\n if (address !== null && doc.get('indexConflict') === true) {\n conflicted.add(address)\n }\n }\n } catch (error) {\n // The subcollection is where a FORMER primary lives, so losing it is\n // precisely how the orphaned-history bug comes back. Never silent.\n console.error('[account-addresses] stored addresses unreadable', uid, error)\n incomplete = true\n }\n\n // Primary first: it is the address staff recognise, and a card that led\n // with an alias would read as the wrong account.\n const ordered = [...found.keys()].sort((a, b) => {\n if (a === primary) return -1\n if (b === primary) return 1\n return a.localeCompare(b)\n })\n\n const addresses: AccountAddress[] = []\n for (const address of ordered) {\n let shared = false\n if (options.detectShared === true) {\n try {\n const holders = await findAccountsHoldingAddress(address, {\n firestore: db,\n })\n shared = holders.uids.some((holder) => holder !== uid)\n } catch {\n // Not `incomplete`: a failed sharing probe under-reports a note on a\n // card. It does not shorten the address list, which is the thing\n // erasure depends on.\n shared = false\n }\n }\n addresses.push({\n address,\n sources: [...(found.get(address) ?? [])],\n key: accountAddressKey(address),\n shared,\n indexConflict: conflicted.has(address),\n })\n }\n\n return { uid, primary, addresses, incomplete }\n}\n\n/** Just the addresses, for a caller that needs nothing else. */\nexport function addressList(set: AccountAddressSet): string[] {\n return set.addresses.map((entry) => entry.address)\n}\n\n/** Just the hashed keys, for a query against a store keyed by them. */\nexport function addressKeys(set: AccountAddressSet): string[] {\n return set.addresses.map((entry) => entry.key)\n}\n\nexport interface AddressHolders {\n address: string\n /** Every uid found to hold it. May be SHORT — see below. */\n uids: string[]\n /** More than one distinct account was found. */\n ambiguous: boolean\n}\n\n/**\n * Which accounts hold `address`, as far as can be determined.\n *\n * ## What it can and cannot see\n *\n * It consults the two sources an address can be looked up IN:\n *\n * - Firebase Auth, across pools — the account holding it as its PRIMARY.\n * - `emailIdentityIndex/{address}` — the account holding it as a CONFIRMED\n * alias.\n *\n * It cannot see a `providerData` holder, because Firebase Auth offers no\n * lookup by provider address; that field is readable only from a record you\n * already have. This is not a gap that can be closed by trying harder, and\n * the live shape of it is ordinary: an account whose Google provider address\n * is another account's primary.\n *\n * So the result is sound in ONE direction only. `ambiguous: true` is proof\n * that more than one account holds the address. `ambiguous: false` is the\n * absence of evidence, never evidence of absence — a caller must not treat a\n * single uid as \"this address belongs to this person\".\n */\nexport async function findAccountsHoldingAddress(\n input: unknown,\n options?: { firestore?: any },\n): Promise<AddressHolders> {\n const address = normalizeAccountEmail(input)\n if (address === null) return { address: '', uids: [], ambiguous: false }\n const db = options?.firestore ?? defaultFirestore()\n const uids = new Set<string>()\n\n try {\n const pooled = await findUserByEmailAcrossPools(address)\n if (pooled?.record?.uid) uids.add(pooled.record.uid)\n } catch {\n // A pool that would not answer leaves the set smaller, which under-reports\n // sharing rather than inventing it.\n }\n\n try {\n // `account-emails.ts`'s own reverse lookup, not a second read of its\n // index. That function owns what the index means — including that an\n // unverified row is absent from it by construction — and a private copy\n // of the read here would be a second place to keep that in step.\n const alias = await findAccountByVerifiedAlias(address)\n if (alias?.uid) uids.add(alias.uid)\n } catch {\n // Same reasoning.\n }\n\n return { address, uids: [...uids], ambiguous: uids.size > 1 }\n}\n\n/**\n * The single account an address can be attributed to, or null.\n *\n * Null for BOTH \"nobody\" and \"more than one\", deliberately. The caller writes\n * an audit subject with it, and a row naming the wrong customer is worse than\n * a row naming none: the collection exists to answer \"who at Aglyn read my\n * data\", and an answer that points at an innocent account is not a weaker\n * answer, it is a false one.\n */\nexport async function attributableAccountForAddress(\n input: unknown,\n options?: { firestore?: any },\n): Promise<string | null> {\n const holders = await findAccountsHoldingAddress(input, options)\n if (holders.ambiguous) return null\n return holders.uids[0] ?? null\n}\n"],"names":["createHash","normalizeAccountEmail","ACCOUNT_EMAILS_SUBCOLLECTION","findAccountByVerifiedAlias","findUserByEmailAcrossPools","firebaseAdmin","defaultFirestore","app","firestore","accountAddressKey","address","update","digest","resolveAccountAddresses","options","uid","record","db","found","Map","conflicted","Set","incomplete","add","input","source","sources","get","set","primary","email","provider","providerData","stored","collection","doc","docs","id","error","console","ordered","keys","sort","a","b","localeCompare","addresses","shared","detectShared","holders","findAccountsHoldingAddress","uids","some","holder","push","key","indexConflict","has","addressList","map","entry","addressKeys","ambiguous","pooled","alias","size","attributableAccountForAddress"],"mappings":"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"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type AccountEmail, type PrimaryChangeVerdict } from '@aglyn/aglyn/app-utils/account-emails';
|
|
18
|
+
export declare const ACCOUNT_EMAILS_SUBCOLLECTION = "emails";
|
|
19
|
+
export declare const EMAIL_IDENTITY_INDEX_COLLECTION = "emailIdentityIndex";
|
|
20
|
+
export declare const EMAIL_VERIFICATIONS_COLLECTION = "emailVerifications";
|
|
21
|
+
/**
|
|
22
|
+
* Long enough that a confirmation link survives a mail client's queue and a
|
|
23
|
+
* user who reads mail in the evening; short enough that a link sitting in a
|
|
24
|
+
* forwarded thread stops working. Firebase's own action links use one day.
|
|
25
|
+
*/
|
|
26
|
+
export declare const EMAIL_VERIFICATION_TTL_MS: number;
|
|
27
|
+
export interface AccountEmailRow extends AccountEmail {
|
|
28
|
+
/** Null until the round-trip completes. ISO string for the API surface. */
|
|
29
|
+
verifiedAt: string | null;
|
|
30
|
+
}
|
|
31
|
+
/** Every refusal this module can produce, as a stable machine-readable code. */
|
|
32
|
+
export type AccountEmailRefusal = 'invalid-address' | 'cap-reached' | 'already-on-this-account' | 'claimed-by-another-account' | 'unknown-address' | 'cannot-remove-primary' | 'last-verified-address' | 'token-invalid' | 'token-expired' | PrimaryChangeVerdict;
|
|
33
|
+
export interface AccountEmailResult {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
refusal: AccountEmailRefusal | null;
|
|
36
|
+
message: string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read the account's addresses, seeding the primary row from Firebase Auth on
|
|
40
|
+
* first read.
|
|
41
|
+
*
|
|
42
|
+
* THE BACKFILL IS NOT OPTIONAL. Every account that exists today predates this
|
|
43
|
+
* subcollection, so without the seed an established user opens the card and
|
|
44
|
+
* is told they have no addresses — while `decoded.email` says otherwise — and
|
|
45
|
+
* then "you cannot remove your last verified address" would be enforced
|
|
46
|
+
* against an empty set. The Firebase Auth record is the authority for the
|
|
47
|
+
* primary, so the seed is a mirror rather than a new fact.
|
|
48
|
+
*
|
|
49
|
+
* `authEmailVerified` is carried through honestly: an account that has never
|
|
50
|
+
* confirmed its own sign-up address gets an UNVERIFIED primary row, which is
|
|
51
|
+
* the truth and which the card then offers to fix.
|
|
52
|
+
*/
|
|
53
|
+
export declare function listAccountEmails(uid: string, authEmail: string | null | undefined, authEmailVerified: boolean): Promise<AccountEmailRow[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Stage a new address and return the secret for its confirmation link.
|
|
56
|
+
*
|
|
57
|
+
* The address is stored UNVERIFIED and does nothing until the round-trip
|
|
58
|
+
* completes: it is not a sign-in identifier, it receives no account mail, and
|
|
59
|
+
* — importantly — it does NOT claim the uniqueness index. Claiming on `add`
|
|
60
|
+
* would make this endpoint a squatting tool: anyone could park an address
|
|
61
|
+
* they do not own and lock its real owner out of adding it. The index is
|
|
62
|
+
* claimed by {@link confirmAccountEmail}, so the first account to prove
|
|
63
|
+
* delivery wins, and proving delivery is the only way to win.
|
|
64
|
+
*
|
|
65
|
+
* The pre-flight `addressTakenByAnother` here is a courtesy, not the guard —
|
|
66
|
+
* it turns the common case into an immediate, honest error instead of a
|
|
67
|
+
* confirmation email that fails at the end. The real check runs again inside
|
|
68
|
+
* the confirmation transaction, because anything can change in between.
|
|
69
|
+
*/
|
|
70
|
+
export declare function addAccountEmail(uid: string, input: unknown): Promise<AccountEmailResult & {
|
|
71
|
+
address: string | null;
|
|
72
|
+
secret: string | null;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Mint a single-use confirmation secret and store only its digest.
|
|
76
|
+
*
|
|
77
|
+
* The returned string is `{tokenId}.{secret}`; the caller puts it in the
|
|
78
|
+
* emailed link and never persists it. A reader of the database therefore
|
|
79
|
+
* cannot confirm anybody's address — they hold a SHA-256 of the half that
|
|
80
|
+
* matters.
|
|
81
|
+
*/
|
|
82
|
+
export declare function issueVerificationToken(uid: string, address: string): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Complete the round-trip: the bearer of this link can receive mail at the
|
|
85
|
+
* address, so the address becomes verified and the account claims it.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately NOT authenticated against the current session. The person
|
|
88
|
+
* clicking the link is whoever opened the mailbox, and requiring them to be
|
|
89
|
+
* signed in as the right account first is how confirmation links strand
|
|
90
|
+
* people who read mail on a different device. The token itself carries the
|
|
91
|
+
* uid, and it can do nothing except verify the one address it was minted for.
|
|
92
|
+
*
|
|
93
|
+
* The uniqueness claim happens HERE and inside the same logical step as the
|
|
94
|
+
* flag flip, so two accounts racing on one address cannot both end up
|
|
95
|
+
* verified.
|
|
96
|
+
*/
|
|
97
|
+
export declare function confirmAccountEmail(token: unknown): Promise<AccountEmailResult & {
|
|
98
|
+
address: string | null;
|
|
99
|
+
uid: string | null;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Remove an address, freeing its index entry.
|
|
103
|
+
*
|
|
104
|
+
* The index entry is released so the address can be used again — on this
|
|
105
|
+
* account or another. Leaving it behind would make removal a way to burn an
|
|
106
|
+
* address permanently, including somebody else's if they ever wanted it.
|
|
107
|
+
*/
|
|
108
|
+
export declare function removeAccountEmail(uid: string, input: unknown): Promise<AccountEmailResult>;
|
|
109
|
+
export interface SetPrimaryContext {
|
|
110
|
+
/** `decoded.firebase?.tenant` — the pool the caller signed in through. */
|
|
111
|
+
tenantId: string | null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Re-designate the primary address.
|
|
115
|
+
*
|
|
116
|
+
* THE MOST SECURITY-SENSITIVE WRITE IN THIS MODULE, because the primary IS
|
|
117
|
+
* the Firebase Auth record's email, which is what `decoded.email` carries,
|
|
118
|
+
* which is what `evaluateSsoDomainPolicy` reads at the session mint and what
|
|
119
|
+
* the invite-accept comparison matches on. Changing it changes an
|
|
120
|
+
* authorization input — so it goes through `evaluatePrimaryChange` first, and
|
|
121
|
+
* that function refuses to move a primary off a domain the deployment
|
|
122
|
+
* requires SSO for. See its docstring for why that refusal ignores the
|
|
123
|
+
* enforcement switch.
|
|
124
|
+
*
|
|
125
|
+
* The Auth record is updated in the account's OWN pool via `authForPool`: a
|
|
126
|
+
* project-level `updateUser` cannot see a GCIP tenant account, so the narrow
|
|
127
|
+
* call would silently fail for exactly the enterprise users this policy is
|
|
128
|
+
* about.
|
|
129
|
+
*
|
|
130
|
+
* `emailVerified: true` is set alongside the address, and it is honest — the
|
|
131
|
+
* only addresses that can reach this point are ones this module verified by
|
|
132
|
+
* an emailed round-trip. Omitting it would flip the account to unverified and
|
|
133
|
+
* lock it out of every route behind `emailUnverifiedResponse()`.
|
|
134
|
+
*/
|
|
135
|
+
export declare function setPrimaryAccountEmail(uid: string, input: unknown, context: SetPrimaryContext): Promise<AccountEmailResult>;
|
|
136
|
+
/**
|
|
137
|
+
* The account that holds `address` as a VERIFIED alias, or null.
|
|
138
|
+
*
|
|
139
|
+
* ## Read this before calling it
|
|
140
|
+
*
|
|
141
|
+
* This is the one lookup that maps an arbitrary address to an account, and it
|
|
142
|
+
* exists for exactly two callers: resolving a sign-in identifier, and
|
|
143
|
+
* matching an org invitation. Both are safe for the same reason — neither
|
|
144
|
+
* GRANTS anything on the strength of the address:
|
|
145
|
+
*
|
|
146
|
+
* - a sign-in identifier still has to be followed by the account's password;
|
|
147
|
+
* - an invitation is an explicit grant the ORG made, and this only decides
|
|
148
|
+
* which of the recipient's mailboxes it may arrive at.
|
|
149
|
+
*
|
|
150
|
+
* It must never be wired into an SSO path. `sso-jit` resolves the org from
|
|
151
|
+
* the GCIP tenant on a re-verified token and matches the domain the IdP
|
|
152
|
+
* asserted; giving it this function would let a user add an address and be
|
|
153
|
+
* provisioned into the matching organization, which is the escalation this
|
|
154
|
+
* whole feature was designed around.
|
|
155
|
+
*
|
|
156
|
+
* Returns null for an unverified row by construction: the index is only ever
|
|
157
|
+
* written by the confirmation path.
|
|
158
|
+
*/
|
|
159
|
+
export declare function findAccountByVerifiedAlias(input: unknown): Promise<{
|
|
160
|
+
uid: string;
|
|
161
|
+
address: string;
|
|
162
|
+
} | null>;
|
|
163
|
+
/** What one {@link registerProviderAddresses} pass did. */
|
|
164
|
+
export interface ProviderAddressRegistration {
|
|
165
|
+
/** Addresses newly claimed for this account. */
|
|
166
|
+
claimed: string[];
|
|
167
|
+
/** Addresses another account already holds — recorded, never taken. */
|
|
168
|
+
conflicted: string[];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Register the addresses a federated provider asserts for this account.
|
|
172
|
+
*
|
|
173
|
+
* Called from the session mint — the one place every interactive sign-in
|
|
174
|
+
* passes through with a verified token — and BEST-EFFORT by contract.
|
|
175
|
+
*
|
|
176
|
+
* ## Three rules, in priority order
|
|
177
|
+
*
|
|
178
|
+
* 1. **Sign-in must not depend on this.** It is bookkeeping. A person locked
|
|
179
|
+
* out because an index write failed is a worse outcome than the collision
|
|
180
|
+
* it was preventing, so every failure here is swallowed and logged, and
|
|
181
|
+
* the caller runs it off the critical path.
|
|
182
|
+
* 2. **A claim never takes an entry another account holds.**
|
|
183
|
+
* {@link claimIndexEntry} already refuses; what was missing is that the
|
|
184
|
+
* refusal went nowhere. A conflict now lands on the row as
|
|
185
|
+
* `indexConflict`, which the staff account page reads — silently skipping
|
|
186
|
+
* is exactly what produced the live collision.
|
|
187
|
+
* 3. **A conflicted address is stored UNVERIFIED.** This is the conservative
|
|
188
|
+
* half and it matters: `verifiedAccountEmails` feeds invitation matching,
|
|
189
|
+
* so marking a contested address verified on both accounts would make one
|
|
190
|
+
* invitation match two people. Unverified, the row is a record that the
|
|
191
|
+
* address exists on this account and grants nothing.
|
|
192
|
+
*
|
|
193
|
+
* ⛔ It does not merge, reassign or disable anything. Two real accounts
|
|
194
|
+
* sharing an address is a human decision — the row makes it visible and stops
|
|
195
|
+
* there.
|
|
196
|
+
*
|
|
197
|
+
* ⚠️ Registering here CANNOT reach SSO provisioning. `sso-jit` reads the
|
|
198
|
+
* address the IdP asserted at sign-in and never consults this store;
|
|
199
|
+
* `apps/console/specs/account-emails-never-reach-sso.spec.ts` fails if that
|
|
200
|
+
* stops being true. That guard is what makes this safe to write at all.
|
|
201
|
+
*/
|
|
202
|
+
export declare function registerProviderAddresses(uid: string, record: {
|
|
203
|
+
email?: string | null;
|
|
204
|
+
providerData?: readonly {
|
|
205
|
+
providerId?: string | null;
|
|
206
|
+
email?: string | null;
|
|
207
|
+
}[];
|
|
208
|
+
} | null): Promise<ProviderAddressRegistration>;
|
|
209
|
+
/**
|
|
210
|
+
* Every VERIFIED address on an account, for matching an invitation.
|
|
211
|
+
*
|
|
212
|
+
* Bounded by {@link MAX_ACCOUNT_EMAILS}, which is what makes it safe to feed
|
|
213
|
+
* straight into a Firestore `in` query (limit 30).
|
|
214
|
+
*
|
|
215
|
+
* ⛔ NOT the answer to "every address this account holds" — it is verified-only,
|
|
216
|
+
* subcollection-only and silently truncating, all correct here and all wrong
|
|
217
|
+
* there. `account-addresses.ts` is that resolver and explains why at length.
|
|
218
|
+
*/
|
|
219
|
+
export declare function verifiedAccountEmails(uid: string): Promise<string[]>;
|