@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,451 @@
|
|
|
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
|
+
* THE PER-SITE SENDING DOMAIN — the claim, and nothing else.
|
|
18
|
+
*
|
|
19
|
+
* Provisioning a site's sending domain is two writes to two vendors and one
|
|
20
|
+
* write of our own. This module owns only the third: the LABEL CLAIM and the
|
|
21
|
+
* record that says a domain has been asked for. It calls no vendor, reads no
|
|
22
|
+
* credential, and is safe for the tenant runtime to import — which matters,
|
|
23
|
+
* because the tenant runtime is where a site first discovers it needs to send.
|
|
24
|
+
*
|
|
25
|
+
* The vendor half lives in the console (`provision-sending-domain.ts`), for
|
|
26
|
+
* the same structural reason `sending-domain-provider.ts` does: the keys that
|
|
27
|
+
* create a Resend domain and write our DNS zone must not be reachable from a
|
|
28
|
+
* process that serves published sites.
|
|
29
|
+
*
|
|
30
|
+
* ## Request here, issue there
|
|
31
|
+
*
|
|
32
|
+
* That split is what makes provisioning ON DEMAND possible at all.
|
|
33
|
+
* {@link requestHostSendingDomain} is a cheap Firestore write with no
|
|
34
|
+
* credential behind it; the console-side sweep picks the claim up and does the
|
|
35
|
+
* vendor work, and the re-check sweep then moves the domain to `verified` on
|
|
36
|
+
* its own.
|
|
37
|
+
*
|
|
38
|
+
* So a site is never blocked waiting on a credential it cannot hold, and the
|
|
39
|
+
* credential is never held by a process that should not have it.
|
|
40
|
+
*
|
|
41
|
+
* ## NOTHING HERE RUNS WITHOUT SOMEBODY ASKING
|
|
42
|
+
*
|
|
43
|
+
* A dedicated subdomain is the one shape in the sending model that draws on a
|
|
44
|
+
* resource we cannot buy our way out of at scale: a slot in the provider's
|
|
45
|
+
* account-wide domain allowance, three records in our own zone, and a
|
|
46
|
+
* permanent place in the re-verification sweep. The shared pool is flat at any
|
|
47
|
+
* number of sites, and a domain the CUSTOMER owns costs our zone nothing at
|
|
48
|
+
* all — so this is the only one that has to be rationed, and the only one a
|
|
49
|
+
* ceiling can bind.
|
|
50
|
+
*
|
|
51
|
+
* It used to be claimed automatically at three moments: site creation, the
|
|
52
|
+
* billing webhook's upgrade transition, and a sweep that walked hosts looking
|
|
53
|
+
* for entitled sites without one. All three made the count grow with paying
|
|
54
|
+
* customers rather than with anybody's decision, and a merchant who never
|
|
55
|
+
* wanted an Aglyn-branded sending name still spent a slot on one.
|
|
56
|
+
*
|
|
57
|
+
* The function is named for a REQUEST rather than for an invariant on purpose.
|
|
58
|
+
* `ensure…` is an invitation to call it defensively — "just make sure it
|
|
59
|
+
* exists" — which is exactly how the three automatic callers came to be, and
|
|
60
|
+
* `requestedBy` cannot be supplied by a caller that has nobody to name. A
|
|
61
|
+
* caller that finds itself inventing a value for it is a caller that should
|
|
62
|
+
* not be claiming.
|
|
63
|
+
*
|
|
64
|
+
* ## The label is pinned, and a rename does not move it
|
|
65
|
+
*
|
|
66
|
+
* `hosts/{hostId}.sendingLabel` is written ONCE and never rewritten. A site's
|
|
67
|
+
* `subdomain` is mutable — the rename route rewrites that field in place — and
|
|
68
|
+
* a sending domain derived from it would move every time a merchant renamed
|
|
69
|
+
* their site, discarding the accumulated sending reputation that is the whole
|
|
70
|
+
* point of having a domain per tenant, and stranding the return path that mail
|
|
71
|
+
* already in flight will bounce to.
|
|
72
|
+
*
|
|
73
|
+
* The web address follows the slug. The sending identity does not. A merchant
|
|
74
|
+
* who genuinely wants a different sending name gets
|
|
75
|
+
* {@link restartHostSendingDomain}, which says plainly that it starts
|
|
76
|
+
* reputation over — never a silent side effect of renaming.
|
|
77
|
+
*
|
|
78
|
+
* The label is proposed from the site's subdomain AT THE MOMENT OF THE
|
|
79
|
+
* REQUEST, which is the name the merchant is looking at when they ask. It was
|
|
80
|
+
* previously the creation-time slug, because the claim happened at creation
|
|
81
|
+
* and there was no later moment to read; a name derived from what the site is
|
|
82
|
+
* called today is the less surprising of the two, and the pin is what makes it
|
|
83
|
+
* stable from then on either way.
|
|
84
|
+
*/ import { isPlatformSendingDomain, mailLabelCandidate, normalizeSendingDomain, platformSendingApex, platformSendingDomainFor, platformSendingLabel } from "@aglyn/shared-util-email";
|
|
85
|
+
import { holdsDedicatedSendingDomain } from "@aglyn/aglyn/app-utils/dedicated-sending-domain";
|
|
86
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
87
|
+
import { SENDING_DOMAINS_COLLECTION, readSendingDomainRecord } from "./sending-domains.js";
|
|
88
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
89
|
+
/**
|
|
90
|
+
* Whether this org's sites may hold a dedicated domain.
|
|
91
|
+
*
|
|
92
|
+
* One read, and only ever on a CLAIM path — never on a send. A site that does
|
|
93
|
+
* not qualify has a sending identity regardless (the shared pool), so nothing
|
|
94
|
+
* here is on the critical path of a message.
|
|
95
|
+
*
|
|
96
|
+
* The WHOLE org document goes to the gate rather than its `plan` field, and
|
|
97
|
+
* that is what makes a per-org grant real: the entitlement resolves the plan's
|
|
98
|
+
* default and then the org's own overrides, so an account staff have granted
|
|
99
|
+
* one may claim without being moved up a tier — and an account whose
|
|
100
|
+
* subscription has died may not, because `resolveEffectivePlan` reads a dead
|
|
101
|
+
* subscription down to `free`. A single field would have hidden both.
|
|
102
|
+
*
|
|
103
|
+
* Fails closed on a read error, and that is the safe direction: the cost of
|
|
104
|
+
* answering `false` wrongly is a request the merchant makes again, while the
|
|
105
|
+
* cost of answering `true` wrongly is a zone record and a provider slot spent
|
|
106
|
+
* on a site that cannot use them.
|
|
107
|
+
*/ export async function orgHoldsDedicatedSendingDomain(orgId) {
|
|
108
|
+
const id = String(orgId != null ? orgId : '').trim();
|
|
109
|
+
if (!id) return false;
|
|
110
|
+
const snapshot = await firestore().collection('orgs').doc(id).get().catch(()=>null);
|
|
111
|
+
return (snapshot == null ? void 0 : snapshot.exists) ? holdsDedicatedSendingDomain(snapshot.data()) : false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The uniqueness index for pinned mail labels.
|
|
115
|
+
*
|
|
116
|
+
* A root collection with the LABEL as the document id, which is what makes the
|
|
117
|
+
* claim atomic: Firestore's `create` fails when the document exists, so two
|
|
118
|
+
* sites racing for one label produce one winner and one retry rather than two
|
|
119
|
+
* winners. This is the same technique the web-subdomain index uses, kept
|
|
120
|
+
* separate from it on purpose — see `platform-sending-domain.ts` for why the
|
|
121
|
+
* two namespaces do not share a claim.
|
|
122
|
+
*/ export const SENDING_LABELS_COLLECTION = 'sendingLabels';
|
|
123
|
+
/** How many labels to try before giving up on a site's name. */ const MAX_LABEL_ATTEMPTS = 12;
|
|
124
|
+
const labelRef = (label)=>firestore().collection(SENDING_LABELS_COLLECTION).doc(label);
|
|
125
|
+
/**
|
|
126
|
+
* Claim the sending domain a site has ASKED for, or return the one it has.
|
|
127
|
+
*
|
|
128
|
+
* Idempotent in the way that matters for a two-vendor provisioning flow: it is
|
|
129
|
+
* the FIRST of the three writes, and it is the one that decides the name. Once
|
|
130
|
+
* it has returned a domain, every retry — of this function, of the Resend
|
|
131
|
+
* call, of the DNS write — resolves against the same name. A retry can
|
|
132
|
+
* therefore never produce a second Resend domain or a second record set,
|
|
133
|
+
* because there is no second name for them to be created under.
|
|
134
|
+
*
|
|
135
|
+
* The three writes it makes are ordered so that a crash between any two leaves
|
|
136
|
+
* a state the sweep can finish rather than one it must untangle:
|
|
137
|
+
*
|
|
138
|
+
* 1. Claim the label. Fails closed on a race; nothing else has happened yet.
|
|
139
|
+
* 2. Create the domain record at `requested`. This is what the console sweep
|
|
140
|
+
* looks for, so a crash after this point still gets picked up.
|
|
141
|
+
* 3. Point the host at it. A crash before this leaves an unreferenced record
|
|
142
|
+
* that the sweep will provision and the next call will attach.
|
|
143
|
+
*
|
|
144
|
+
* Step 3 is deliberately last and deliberately non-destructive: a host that
|
|
145
|
+
* already carries a `sendingLabel` returns early at the top and never reaches
|
|
146
|
+
* any of this.
|
|
147
|
+
*/ export async function requestHostSendingDomain(options) {
|
|
148
|
+
var _ref, _ref1, _hostSnapshot_get;
|
|
149
|
+
const hostId = String((_ref = options == null ? void 0 : options.hostId) != null ? _ref : '').trim();
|
|
150
|
+
const orgId = String((_ref1 = options == null ? void 0 : options.orgId) != null ? _ref1 : '').trim();
|
|
151
|
+
// Narrowed rather than trusted: `strictNullChecks` is off, so the declared
|
|
152
|
+
// union does not survive a caller that reached this through `as never`, and
|
|
153
|
+
// an unrecognized value stored on the host would make the audit trail read
|
|
154
|
+
// as though somebody had asked when nothing did.
|
|
155
|
+
const requestedBy = (options == null ? void 0 : options.requestedBy) === 'staff' ? 'staff' : 'merchant';
|
|
156
|
+
if (!hostId || !orgId) {
|
|
157
|
+
return {
|
|
158
|
+
domain: null,
|
|
159
|
+
label: null,
|
|
160
|
+
created: false,
|
|
161
|
+
error: 'missing-host'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const hostRef = firestore().collection('hosts').doc(hostId);
|
|
165
|
+
const hostSnapshot = await hostRef.get();
|
|
166
|
+
if (!hostSnapshot.exists) {
|
|
167
|
+
return {
|
|
168
|
+
domain: null,
|
|
169
|
+
label: null,
|
|
170
|
+
created: false,
|
|
171
|
+
error: 'no-host'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/*
|
|
175
|
+
* ALREADY PINNED — the rename guarantee, and the idempotency guarantee, are
|
|
176
|
+
* the same early return.
|
|
177
|
+
*
|
|
178
|
+
* Read from the stored label rather than from the stored domain so the apex
|
|
179
|
+
* remains a configuration value: an operator who moves the mail namespace
|
|
180
|
+
* gets existing sites rebuilt onto the new apex under their existing labels,
|
|
181
|
+
* rather than sites pinned to an apex they have left.
|
|
182
|
+
*/ const pinned = String((_hostSnapshot_get = hostSnapshot.get('sendingLabel')) != null ? _hostSnapshot_get : '').trim();
|
|
183
|
+
if (pinned) {
|
|
184
|
+
const domain = platformSendingDomainFor(pinned);
|
|
185
|
+
return {
|
|
186
|
+
domain: domain || null,
|
|
187
|
+
label: pinned,
|
|
188
|
+
created: false,
|
|
189
|
+
// A pinned label that no longer builds a domain means the apex changed
|
|
190
|
+
// to something the label cannot live under, or the label was written by
|
|
191
|
+
// hand. Naming it is the only useful thing to do — silently re-pinning
|
|
192
|
+
// would move a site's mail, which is the one thing this must not do.
|
|
193
|
+
error: domain ? null : 'pinned-label-unusable'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/*
|
|
197
|
+
* THE PLAN GATE, AT THE ONLY WRITER.
|
|
198
|
+
*
|
|
199
|
+
* Below the pinned-label early return on purpose: a site that already HAS a
|
|
200
|
+
* domain keeps it through a downgrade. Taking one away would move that site's
|
|
201
|
+
* mail onto a cold pooled identity and throw away whatever reputation the
|
|
202
|
+
* name had earned, which is a punishment the merchant did not agree to and
|
|
203
|
+
* which we would be inflicting silently. Reclaiming a downgraded site's
|
|
204
|
+
* domain is a teardown decision with its own surface, not a side effect of
|
|
205
|
+
* the next sweep.
|
|
206
|
+
*
|
|
207
|
+
* Here rather than only at the callers because this is the ONE function that
|
|
208
|
+
* creates a claim — the identity route's request action and
|
|
209
|
+
* `restartHostSendingDomain` both funnel through it. A gate at the call
|
|
210
|
+
* sites is a gate the next one skips, and the next one costs a zone record
|
|
211
|
+
* that nothing reclaims.
|
|
212
|
+
*
|
|
213
|
+
* It is the SECOND of two conditions and not the only one. `requestedBy`
|
|
214
|
+
* says a person asked; this says their plan carries what they asked for.
|
|
215
|
+
* Neither implies the other, and a claim needs both.
|
|
216
|
+
*/ if (!await orgHoldsDedicatedSendingDomain(orgId)) {
|
|
217
|
+
return {
|
|
218
|
+
domain: null,
|
|
219
|
+
label: null,
|
|
220
|
+
created: false,
|
|
221
|
+
error: 'plan-no-dedicated-domain'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const subdomain = String((options == null ? void 0 : options.subdomain) || hostSnapshot.get('subdomain') || '').trim();
|
|
225
|
+
for(let attempt = 1; attempt <= MAX_LABEL_ATTEMPTS; attempt += 1){
|
|
226
|
+
const label = mailLabelCandidate(subdomain, attempt);
|
|
227
|
+
if (!label) continue;
|
|
228
|
+
const domain = platformSendingDomainFor(label);
|
|
229
|
+
if (!domain) continue;
|
|
230
|
+
const claim = {
|
|
231
|
+
label,
|
|
232
|
+
hostId,
|
|
233
|
+
orgId,
|
|
234
|
+
domain,
|
|
235
|
+
claimedAtMs: Date.now()
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
// `create`, not `set`. An existing document is a claim by somebody else
|
|
239
|
+
// — possibly this same host on a previous run, which the early return
|
|
240
|
+
// above has already excluded — and overwriting it would hand two sites
|
|
241
|
+
// one mail domain.
|
|
242
|
+
await labelRef(label).create(claim);
|
|
243
|
+
} catch (unused) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const record = {
|
|
247
|
+
domain,
|
|
248
|
+
status: 'requested',
|
|
249
|
+
// Proposed only. Resend signs on a selector of its own choosing and
|
|
250
|
+
// `recordIssuedSendingDomain` overwrites this with what it returns.
|
|
251
|
+
dkimSelector: 'aglyn',
|
|
252
|
+
dkimPublicKey: null,
|
|
253
|
+
returnPathHost: null,
|
|
254
|
+
createdAtMs: Date.now()
|
|
255
|
+
};
|
|
256
|
+
await firestore().collection('orgs').doc(orgId).collection(SENDING_DOMAINS_COLLECTION).doc(domain).set(record, {
|
|
257
|
+
merge: true
|
|
258
|
+
});
|
|
259
|
+
/*
|
|
260
|
+
* The label, the domain, and WHO ASKED FOR IT.
|
|
261
|
+
*
|
|
262
|
+
* The request stamp is written with the pointer rather than beside it, so
|
|
263
|
+
* a site can never hold a dedicated domain that no request accounts for.
|
|
264
|
+
* It is what lets an operator at the ceiling answer the only question that
|
|
265
|
+
* matters there — which of these slots somebody actually wanted — without
|
|
266
|
+
* reading a year of logs, and it is the audit trail for a support claim
|
|
267
|
+
* made on a merchant's behalf.
|
|
268
|
+
*/ await hostRef.set({
|
|
269
|
+
sendingLabel: label,
|
|
270
|
+
sendingDomain: domain,
|
|
271
|
+
sendingDomainRequestedAtMs: Date.now(),
|
|
272
|
+
sendingDomainRequestedBy: requestedBy
|
|
273
|
+
}, {
|
|
274
|
+
merge: true
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
domain,
|
|
278
|
+
label,
|
|
279
|
+
created: true,
|
|
280
|
+
error: null
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
domain: null,
|
|
285
|
+
label: null,
|
|
286
|
+
created: false,
|
|
287
|
+
error: 'no-label'
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* What a host's deletion has to clean up, read before anything is deleted.
|
|
292
|
+
*
|
|
293
|
+
* Split from the deletion itself so the caller can do the vendor work FIRST
|
|
294
|
+
* and delete our records only once it has succeeded. The other order loses the
|
|
295
|
+
* name of the thing it still has to remove: a zone accumulating records for
|
|
296
|
+
* sites that no longer exist is bad on its own, and a future site reusing the
|
|
297
|
+
* label would inherit a stranger's DKIM key — which is a working signature for
|
|
298
|
+
* a domain somebody else's mail leaves on.
|
|
299
|
+
*/ export async function readHostSendingTeardown(hostId) {
|
|
300
|
+
var _snapshot_get, _snapshot_get1, _ref;
|
|
301
|
+
const id = String(hostId != null ? hostId : '').trim();
|
|
302
|
+
if (!id) return null;
|
|
303
|
+
const snapshot = await firestore().collection('hosts').doc(id).get();
|
|
304
|
+
if (!snapshot.exists) return null;
|
|
305
|
+
const label = String((_snapshot_get = snapshot.get('sendingLabel')) != null ? _snapshot_get : '').trim() || null;
|
|
306
|
+
const domain = normalizeSendingDomain(String((_snapshot_get1 = snapshot.get('sendingDomain')) != null ? _snapshot_get1 : '')) || null;
|
|
307
|
+
if (!label && !domain) return null;
|
|
308
|
+
// The claim carries the org, so a host document that has lost its org
|
|
309
|
+
// pointer can still be torn down.
|
|
310
|
+
const claim = label ? await labelRef(label).get() : null;
|
|
311
|
+
const orgId = String((_ref = claim == null ? void 0 : claim.get('orgId')) != null ? _ref : '').trim() || null;
|
|
312
|
+
let providerDomainId = null;
|
|
313
|
+
let dkimSelector = null;
|
|
314
|
+
let trackingTarget = null;
|
|
315
|
+
if (orgId && domain) {
|
|
316
|
+
var _ref1, _ref2, _ref3;
|
|
317
|
+
const record = readSendingDomainRecord(await firestore().collection('orgs').doc(orgId).collection(SENDING_DOMAINS_COLLECTION).doc(domain).get());
|
|
318
|
+
providerDomainId = (_ref1 = record == null ? void 0 : record.providerDomainId) != null ? _ref1 : null;
|
|
319
|
+
dkimSelector = String((_ref2 = record == null ? void 0 : record.dkimSelector) != null ? _ref2 : '').trim() || null;
|
|
320
|
+
trackingTarget = String((_ref3 = record == null ? void 0 : record.trackingTarget) != null ? _ref3 : '').trim() || null;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
hostId: id,
|
|
324
|
+
orgId,
|
|
325
|
+
label,
|
|
326
|
+
domain,
|
|
327
|
+
providerDomainId,
|
|
328
|
+
dkimSelector,
|
|
329
|
+
trackingTarget
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Drop our own record of a site's sending domain, once the vendors are clean.
|
|
334
|
+
*
|
|
335
|
+
* Releasing the label LAST is deliberate. The label is what makes the domain
|
|
336
|
+
* findable; while it exists, a half-finished teardown is a thing the sweep can
|
|
337
|
+
* find and finish. Released first, a crash mid-teardown would leave a Resend
|
|
338
|
+
* domain and a set of DNS records that nothing points at and nothing will ever
|
|
339
|
+
* look for again.
|
|
340
|
+
*
|
|
341
|
+
* The release is real rather than a tombstone. Once the DKIM record is gone
|
|
342
|
+
* from the zone and the domain object is gone from the account, a future site
|
|
343
|
+
* claiming the label inherits nothing — no key, no records, no reputation. A
|
|
344
|
+
* permanent reservation would only be needed if teardown were unreliable, and
|
|
345
|
+
* the answer to unreliable teardown is to fix it, not to leak a name for every
|
|
346
|
+
* site ever deleted.
|
|
347
|
+
*/ export async function releaseHostSendingDomain(teardown) {
|
|
348
|
+
var _this;
|
|
349
|
+
if (!teardown) return;
|
|
350
|
+
if (teardown.orgId && teardown.domain) {
|
|
351
|
+
await firestore().collection('orgs').doc(teardown.orgId).collection(SENDING_DOMAINS_COLLECTION).doc(teardown.domain).delete().catch(()=>undefined);
|
|
352
|
+
}
|
|
353
|
+
/*
|
|
354
|
+
* Only onto a host that is still there. A merging `set` CREATES the document
|
|
355
|
+
* it is given, so clearing these two fields on a site that has already been
|
|
356
|
+
* erased writes an empty `hosts/{hostId}` back into existence — a fragment
|
|
357
|
+
* of a site the customer asked us to destroy, resurrected by its own
|
|
358
|
+
* cleanup, and one the routing sweep would then have to collect again.
|
|
359
|
+
*/ const hostRef = firestore().collection('hosts').doc(teardown.hostId);
|
|
360
|
+
if ((_this = await hostRef.get().catch(()=>null)) == null ? void 0 : _this.exists) {
|
|
361
|
+
await hostRef.set({
|
|
362
|
+
sendingLabel: firebaseAdmin.firestore.FieldValue.delete(),
|
|
363
|
+
sendingDomain: firebaseAdmin.firestore.FieldValue.delete()
|
|
364
|
+
}, {
|
|
365
|
+
merge: true
|
|
366
|
+
}).catch(()=>undefined);
|
|
367
|
+
}
|
|
368
|
+
if (teardown.label) {
|
|
369
|
+
/*
|
|
370
|
+
* Only if it still names THIS host. A label released by a teardown that
|
|
371
|
+
* raced a re-provision would otherwise delete the claim the new domain
|
|
372
|
+
* depends on, and the next site to ask for that name would be handed one
|
|
373
|
+
* that is already live.
|
|
374
|
+
*/ const ref = labelRef(teardown.label);
|
|
375
|
+
await firestore().runTransaction(async (transaction)=>{
|
|
376
|
+
var _snapshot_get;
|
|
377
|
+
const snapshot = await transaction.get(ref);
|
|
378
|
+
if (!snapshot.exists) return;
|
|
379
|
+
if (String((_snapshot_get = snapshot.get('hostId')) != null ? _snapshot_get : '') !== teardown.hostId) return;
|
|
380
|
+
transaction.delete(ref);
|
|
381
|
+
}).catch(()=>undefined);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Give a site a NEW sending domain, abandoning the one it has.
|
|
386
|
+
*
|
|
387
|
+
* The explicit action behind "I want my mail to come from a different name".
|
|
388
|
+
* It exists so that renaming a site can stay free of side effects: a merchant
|
|
389
|
+
* who wants the sending name to follow the rename asks for it here, and is
|
|
390
|
+
* told what it costs.
|
|
391
|
+
*
|
|
392
|
+
* What it costs is the reputation. A sending domain's value is its age and its
|
|
393
|
+
* history with the receiving networks; a new one starts at zero and is treated
|
|
394
|
+
* with more suspicion than the old one, for weeks. That is why this is not
|
|
395
|
+
* what a rename does, and why the caller is expected to have said so out loud
|
|
396
|
+
* before getting here.
|
|
397
|
+
*
|
|
398
|
+
* Returns the teardown for the OLD domain. The caller must complete the vendor
|
|
399
|
+
* cleanup for it, exactly as for a deleted host — the old Resend domain and
|
|
400
|
+
* its zone records are no more welcome to linger here than there.
|
|
401
|
+
*/ export async function restartHostSendingDomain(options) {
|
|
402
|
+
const teardown = await readHostSendingTeardown(options == null ? void 0 : options.hostId);
|
|
403
|
+
if (teardown) await releaseHostSendingDomain(teardown);
|
|
404
|
+
return {
|
|
405
|
+
teardown,
|
|
406
|
+
provisioned: await requestHostSendingDomain(options)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Platform sending domains that still need vendor work, oldest first.
|
|
411
|
+
*
|
|
412
|
+
* A collection-group query over every org's `sendingDomains`, filtered to the
|
|
413
|
+
* ones inside our own mail apex — a customer's own domain is records they
|
|
414
|
+
* publish, and this sweep must never try to write to a zone we do not own.
|
|
415
|
+
*
|
|
416
|
+
* `orderBy` on the creation time, and not merely `limit`. A `limit()` with no
|
|
417
|
+
* `orderBy` returns documents in ID order, which is a stable arbitrary sample
|
|
418
|
+
* rather than a queue: the same handful of domains would be returned on every
|
|
419
|
+
* run and anything sorting after them would never be provisioned at all.
|
|
420
|
+
*
|
|
421
|
+
* Ordering on a field means a document missing that field is dropped by the
|
|
422
|
+
* query. That is acceptable here and nowhere else in this file, because
|
|
423
|
+
* `requestHostSendingDomain` is the only writer and it always sets
|
|
424
|
+
* `createdAtMs`.
|
|
425
|
+
*/ export async function listPendingSendingDomains(limit = 25) {
|
|
426
|
+
const snapshot = await firestore().collectionGroup(SENDING_DOMAINS_COLLECTION).where('status', '==', 'requested').orderBy('createdAtMs', 'asc').limit(Math.max(1, limit)).get();
|
|
427
|
+
const pending = [];
|
|
428
|
+
for (const doc of snapshot.docs){
|
|
429
|
+
var _doc_ref_parent_parent;
|
|
430
|
+
const record = readSendingDomainRecord(doc);
|
|
431
|
+
if (!record) continue;
|
|
432
|
+
// Ours to provision, or the customer's to publish. Only the first kind.
|
|
433
|
+
if (!isPlatformSendingDomain(record.domain)) continue;
|
|
434
|
+
const orgId = (_doc_ref_parent_parent = doc.ref.parent.parent) == null ? void 0 : _doc_ref_parent_parent.id;
|
|
435
|
+
if (!orgId) continue;
|
|
436
|
+
pending.push({
|
|
437
|
+
orgId,
|
|
438
|
+
record
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return pending;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* The label a platform domain belongs to, for a caller assembling zone record
|
|
445
|
+
* names. Re-derived rather than read off the host, so a cleanup can name the
|
|
446
|
+
* records for a host document that is already gone.
|
|
447
|
+
*/ export function sendingDomainLabel(domain) {
|
|
448
|
+
return platformSendingLabel(domain, platformSendingApex());
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
//# sourceMappingURL=host-sending-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/host-sending-domain.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE PER-SITE SENDING DOMAIN — the claim, and nothing else.\n *\n * Provisioning a site's sending domain is two writes to two vendors and one\n * write of our own. This module owns only the third: the LABEL CLAIM and the\n * record that says a domain has been asked for. It calls no vendor, reads no\n * credential, and is safe for the tenant runtime to import — which matters,\n * because the tenant runtime is where a site first discovers it needs to send.\n *\n * The vendor half lives in the console (`provision-sending-domain.ts`), for\n * the same structural reason `sending-domain-provider.ts` does: the keys that\n * create a Resend domain and write our DNS zone must not be reachable from a\n * process that serves published sites.\n *\n * ## Request here, issue there\n *\n * That split is what makes provisioning ON DEMAND possible at all.\n * {@link requestHostSendingDomain} is a cheap Firestore write with no\n * credential behind it; the console-side sweep picks the claim up and does the\n * vendor work, and the re-check sweep then moves the domain to `verified` on\n * its own.\n *\n * So a site is never blocked waiting on a credential it cannot hold, and the\n * credential is never held by a process that should not have it.\n *\n * ## NOTHING HERE RUNS WITHOUT SOMEBODY ASKING\n *\n * A dedicated subdomain is the one shape in the sending model that draws on a\n * resource we cannot buy our way out of at scale: a slot in the provider's\n * account-wide domain allowance, three records in our own zone, and a\n * permanent place in the re-verification sweep. The shared pool is flat at any\n * number of sites, and a domain the CUSTOMER owns costs our zone nothing at\n * all — so this is the only one that has to be rationed, and the only one a\n * ceiling can bind.\n *\n * It used to be claimed automatically at three moments: site creation, the\n * billing webhook's upgrade transition, and a sweep that walked hosts looking\n * for entitled sites without one. All three made the count grow with paying\n * customers rather than with anybody's decision, and a merchant who never\n * wanted an Aglyn-branded sending name still spent a slot on one.\n *\n * The function is named for a REQUEST rather than for an invariant on purpose.\n * `ensure…` is an invitation to call it defensively — \"just make sure it\n * exists\" — which is exactly how the three automatic callers came to be, and\n * `requestedBy` cannot be supplied by a caller that has nobody to name. A\n * caller that finds itself inventing a value for it is a caller that should\n * not be claiming.\n *\n * ## The label is pinned, and a rename does not move it\n *\n * `hosts/{hostId}.sendingLabel` is written ONCE and never rewritten. A site's\n * `subdomain` is mutable — the rename route rewrites that field in place — and\n * a sending domain derived from it would move every time a merchant renamed\n * their site, discarding the accumulated sending reputation that is the whole\n * point of having a domain per tenant, and stranding the return path that mail\n * already in flight will bounce to.\n *\n * The web address follows the slug. The sending identity does not. A merchant\n * who genuinely wants a different sending name gets\n * {@link restartHostSendingDomain}, which says plainly that it starts\n * reputation over — never a silent side effect of renaming.\n *\n * The label is proposed from the site's subdomain AT THE MOMENT OF THE\n * REQUEST, which is the name the merchant is looking at when they ask. It was\n * previously the creation-time slug, because the claim happened at creation\n * and there was no later moment to read; a name derived from what the site is\n * called today is the less surprising of the two, and the pin is what makes it\n * stable from then on either way.\n */\n\nimport {\n isPlatformSendingDomain,\n mailLabelCandidate,\n normalizeSendingDomain,\n platformSendingApex,\n platformSendingDomainFor,\n platformSendingLabel,\n type SendingDomainRecord,\n} from '@aglyn/shared-util-email'\nimport { holdsDedicatedSendingDomain } from '@aglyn/aglyn/app-utils/dedicated-sending-domain'\nimport firebaseAdmin from './firebase-admin'\nimport { SENDING_DOMAINS_COLLECTION, readSendingDomainRecord } from './sending-domains'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n/**\n * Whether this org's sites may hold a dedicated domain.\n *\n * One read, and only ever on a CLAIM path — never on a send. A site that does\n * not qualify has a sending identity regardless (the shared pool), so nothing\n * here is on the critical path of a message.\n *\n * The WHOLE org document goes to the gate rather than its `plan` field, and\n * that is what makes a per-org grant real: the entitlement resolves the plan's\n * default and then the org's own overrides, so an account staff have granted\n * one may claim without being moved up a tier — and an account whose\n * subscription has died may not, because `resolveEffectivePlan` reads a dead\n * subscription down to `free`. A single field would have hidden both.\n *\n * Fails closed on a read error, and that is the safe direction: the cost of\n * answering `false` wrongly is a request the merchant makes again, while the\n * cost of answering `true` wrongly is a zone record and a provider slot spent\n * on a site that cannot use them.\n */\nexport async function orgHoldsDedicatedSendingDomain(\n orgId: string | null | undefined,\n): Promise<boolean> {\n const id = String(orgId ?? '').trim()\n if (!id) return false\n const snapshot = await firestore()\n .collection('orgs')\n .doc(id)\n .get()\n .catch(() => null)\n return snapshot?.exists\n ? holdsDedicatedSendingDomain(snapshot.data() as never)\n : false\n}\n\n/**\n * The uniqueness index for pinned mail labels.\n *\n * A root collection with the LABEL as the document id, which is what makes the\n * claim atomic: Firestore's `create` fails when the document exists, so two\n * sites racing for one label produce one winner and one retry rather than two\n * winners. This is the same technique the web-subdomain index uses, kept\n * separate from it on purpose — see `platform-sending-domain.ts` for why the\n * two namespaces do not share a claim.\n */\nexport const SENDING_LABELS_COLLECTION = 'sendingLabels'\n\n/** How many labels to try before giving up on a site's name. */\nconst MAX_LABEL_ATTEMPTS = 12\n\nconst labelRef = (label: string) =>\n firestore().collection(SENDING_LABELS_COLLECTION).doc(label)\n\nexport interface SendingLabelClaim {\n label: string\n hostId: string\n orgId: string\n domain: string\n claimedAtMs: number\n}\n\nexport interface HostSendingDomainResult {\n /** The site's sending domain, or null when none could be provisioned. */\n domain: string | null\n label: string | null\n /** True when this call created the claim rather than finding one. */\n created: boolean\n /** Why nothing could be claimed. Null on success. */\n error: string | null\n}\n\n/**\n * Claim the sending domain a site has ASKED for, or return the one it has.\n *\n * Idempotent in the way that matters for a two-vendor provisioning flow: it is\n * the FIRST of the three writes, and it is the one that decides the name. Once\n * it has returned a domain, every retry — of this function, of the Resend\n * call, of the DNS write — resolves against the same name. A retry can\n * therefore never produce a second Resend domain or a second record set,\n * because there is no second name for them to be created under.\n *\n * The three writes it makes are ordered so that a crash between any two leaves\n * a state the sweep can finish rather than one it must untangle:\n *\n * 1. Claim the label. Fails closed on a race; nothing else has happened yet.\n * 2. Create the domain record at `requested`. This is what the console sweep\n * looks for, so a crash after this point still gets picked up.\n * 3. Point the host at it. A crash before this leaves an unreferenced record\n * that the sweep will provision and the next call will attach.\n *\n * Step 3 is deliberately last and deliberately non-destructive: a host that\n * already carries a `sendingLabel` returns early at the top and never reaches\n * any of this.\n */\nexport async function requestHostSendingDomain(options: {\n hostId: string\n orgId: string\n /** The site's CURRENT subdomain. Read once, to propose a label. */\n subdomain: string\n /**\n * WHO ASKED. Required, and there is no default.\n *\n * A dedicated subdomain is a draw on a bounded resource, so the record of\n * who drew it is part of the claim rather than something reconstructed from\n * logs later. `merchant` is somebody with `org.settings` acting on their own\n * workspace; `staff` is a support action taken on their behalf.\n *\n * Its real work is at compile time. A caller that wants to claim\n * defensively — on a plan change, on a sweep, on site creation — has no\n * honest value to put here, and that is the point: the parameter cannot be\n * satisfied by a process, only by a person.\n */\n requestedBy: 'merchant' | 'staff'\n}): Promise<HostSendingDomainResult> {\n const hostId = String(options?.hostId ?? '').trim()\n const orgId = String(options?.orgId ?? '').trim()\n // Narrowed rather than trusted: `strictNullChecks` is off, so the declared\n // union does not survive a caller that reached this through `as never`, and\n // an unrecognized value stored on the host would make the audit trail read\n // as though somebody had asked when nothing did.\n const requestedBy = options?.requestedBy === 'staff' ? 'staff' : 'merchant'\n if (!hostId || !orgId) {\n return { domain: null, label: null, created: false, error: 'missing-host' }\n }\n\n const hostRef = firestore().collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return { domain: null, label: null, created: false, error: 'no-host' }\n }\n\n /*\n * ALREADY PINNED — the rename guarantee, and the idempotency guarantee, are\n * the same early return.\n *\n * Read from the stored label rather than from the stored domain so the apex\n * remains a configuration value: an operator who moves the mail namespace\n * gets existing sites rebuilt onto the new apex under their existing labels,\n * rather than sites pinned to an apex they have left.\n */\n const pinned = String(hostSnapshot.get('sendingLabel') ?? '').trim()\n if (pinned) {\n const domain = platformSendingDomainFor(pinned)\n return {\n domain: domain || null,\n label: pinned,\n created: false,\n // A pinned label that no longer builds a domain means the apex changed\n // to something the label cannot live under, or the label was written by\n // hand. Naming it is the only useful thing to do — silently re-pinning\n // would move a site's mail, which is the one thing this must not do.\n error: domain ? null : 'pinned-label-unusable',\n }\n }\n\n /*\n * THE PLAN GATE, AT THE ONLY WRITER.\n *\n * Below the pinned-label early return on purpose: a site that already HAS a\n * domain keeps it through a downgrade. Taking one away would move that site's\n * mail onto a cold pooled identity and throw away whatever reputation the\n * name had earned, which is a punishment the merchant did not agree to and\n * which we would be inflicting silently. Reclaiming a downgraded site's\n * domain is a teardown decision with its own surface, not a side effect of\n * the next sweep.\n *\n * Here rather than only at the callers because this is the ONE function that\n * creates a claim — the identity route's request action and\n * `restartHostSendingDomain` both funnel through it. A gate at the call\n * sites is a gate the next one skips, and the next one costs a zone record\n * that nothing reclaims.\n *\n * It is the SECOND of two conditions and not the only one. `requestedBy`\n * says a person asked; this says their plan carries what they asked for.\n * Neither implies the other, and a claim needs both.\n */\n if (!(await orgHoldsDedicatedSendingDomain(orgId))) {\n return {\n domain: null,\n label: null,\n created: false,\n error: 'plan-no-dedicated-domain',\n }\n }\n\n const subdomain = String(\n options?.subdomain || hostSnapshot.get('subdomain') || '',\n ).trim()\n\n for (let attempt = 1; attempt <= MAX_LABEL_ATTEMPTS; attempt += 1) {\n const label = mailLabelCandidate(subdomain, attempt)\n if (!label) continue\n const domain = platformSendingDomainFor(label)\n if (!domain) continue\n\n const claim: SendingLabelClaim = {\n label,\n hostId,\n orgId,\n domain,\n claimedAtMs: Date.now(),\n }\n\n try {\n // `create`, not `set`. An existing document is a claim by somebody else\n // — possibly this same host on a previous run, which the early return\n // above has already excluded — and overwriting it would hand two sites\n // one mail domain.\n await labelRef(label).create(claim)\n } catch {\n continue\n }\n\n const record: SendingDomainRecord = {\n domain,\n status: 'requested',\n // Proposed only. Resend signs on a selector of its own choosing and\n // `recordIssuedSendingDomain` overwrites this with what it returns.\n dkimSelector: 'aglyn',\n dkimPublicKey: null,\n returnPathHost: null,\n createdAtMs: Date.now(),\n }\n await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(SENDING_DOMAINS_COLLECTION)\n .doc(domain)\n .set(record, { merge: true })\n\n /*\n * The label, the domain, and WHO ASKED FOR IT.\n *\n * The request stamp is written with the pointer rather than beside it, so\n * a site can never hold a dedicated domain that no request accounts for.\n * It is what lets an operator at the ceiling answer the only question that\n * matters there — which of these slots somebody actually wanted — without\n * reading a year of logs, and it is the audit trail for a support claim\n * made on a merchant's behalf.\n */\n await hostRef.set(\n {\n sendingLabel: label,\n sendingDomain: domain,\n sendingDomainRequestedAtMs: Date.now(),\n sendingDomainRequestedBy: requestedBy,\n },\n { merge: true },\n )\n\n return { domain, label, created: true, error: null }\n }\n\n return { domain: null, label: null, created: false, error: 'no-label' }\n}\n\n/**\n * Everything provisioned for one site, for a caller that must take it apart.\n *\n * Returned as data rather than acted on, because the acting needs vendor\n * credentials this module must not hold. The console's cleanup path takes this\n * and makes the calls.\n */\nexport interface HostSendingDomainTeardown {\n hostId: string\n orgId: string | null\n label: string | null\n domain: string | null\n /** The provider's id for the domain object, when one was recorded. */\n providerDomainId: string | null\n /**\n * The selector the provider signs under, needed to name the DKIM record for\n * deletion. Read from the record rather than assumed: the provider chooses\n * it, and a guessed selector deletes nothing while reporting success — which\n * would leave a live signing key in the zone for a site that no longer\n * exists, ready to be inherited by whoever claims the label next.\n */\n dkimSelector: string | null\n /**\n * The provider's tracking host, when this domain had one.\n *\n * Read because it changes WHEN the domain may be released, not just what is\n * deleted: links in mail this domain has already sent point at that host,\n * and deleting the domain object takes it with them. See the retention\n * window in `teardownSendingDomain`.\n */\n trackingTarget: string | null\n}\n\n/**\n * What a host's deletion has to clean up, read before anything is deleted.\n *\n * Split from the deletion itself so the caller can do the vendor work FIRST\n * and delete our records only once it has succeeded. The other order loses the\n * name of the thing it still has to remove: a zone accumulating records for\n * sites that no longer exist is bad on its own, and a future site reusing the\n * label would inherit a stranger's DKIM key — which is a working signature for\n * a domain somebody else's mail leaves on.\n */\nexport async function readHostSendingTeardown(\n hostId: string,\n): Promise<HostSendingDomainTeardown | null> {\n const id = String(hostId ?? '').trim()\n if (!id) return null\n\n const snapshot = await firestore().collection('hosts').doc(id).get()\n if (!snapshot.exists) return null\n\n const label = String(snapshot.get('sendingLabel') ?? '').trim() || null\n const domain =\n normalizeSendingDomain(String(snapshot.get('sendingDomain') ?? '')) || null\n if (!label && !domain) return null\n\n // The claim carries the org, so a host document that has lost its org\n // pointer can still be torn down.\n const claim = label ? await labelRef(label).get() : null\n const orgId = String(claim?.get('orgId') ?? '').trim() || null\n\n let providerDomainId: string | null = null\n let dkimSelector: string | null = null\n let trackingTarget: string | null = null\n if (orgId && domain) {\n const record = readSendingDomainRecord(\n await firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(SENDING_DOMAINS_COLLECTION)\n .doc(domain)\n .get(),\n )\n providerDomainId = record?.providerDomainId ?? null\n dkimSelector = String(record?.dkimSelector ?? '').trim() || null\n trackingTarget = String(record?.trackingTarget ?? '').trim() || null\n }\n\n return {\n hostId: id,\n orgId,\n label,\n domain,\n providerDomainId,\n dkimSelector,\n trackingTarget,\n }\n}\n\n/**\n * Drop our own record of a site's sending domain, once the vendors are clean.\n *\n * Releasing the label LAST is deliberate. The label is what makes the domain\n * findable; while it exists, a half-finished teardown is a thing the sweep can\n * find and finish. Released first, a crash mid-teardown would leave a Resend\n * domain and a set of DNS records that nothing points at and nothing will ever\n * look for again.\n *\n * The release is real rather than a tombstone. Once the DKIM record is gone\n * from the zone and the domain object is gone from the account, a future site\n * claiming the label inherits nothing — no key, no records, no reputation. A\n * permanent reservation would only be needed if teardown were unreliable, and\n * the answer to unreliable teardown is to fix it, not to leak a name for every\n * site ever deleted.\n */\nexport async function releaseHostSendingDomain(\n teardown: HostSendingDomainTeardown,\n): Promise<void> {\n if (!teardown) return\n\n if (teardown.orgId && teardown.domain) {\n await firestore()\n .collection('orgs')\n .doc(teardown.orgId)\n .collection(SENDING_DOMAINS_COLLECTION)\n .doc(teardown.domain)\n .delete()\n .catch(() => undefined)\n }\n\n /*\n * Only onto a host that is still there. A merging `set` CREATES the document\n * it is given, so clearing these two fields on a site that has already been\n * erased writes an empty `hosts/{hostId}` back into existence — a fragment\n * of a site the customer asked us to destroy, resurrected by its own\n * cleanup, and one the routing sweep would then have to collect again.\n */\n const hostRef = firestore().collection('hosts').doc(teardown.hostId)\n if ((await hostRef.get().catch(() => null))?.exists) {\n await hostRef\n .set(\n {\n sendingLabel: firebaseAdmin.firestore.FieldValue.delete(),\n sendingDomain: firebaseAdmin.firestore.FieldValue.delete(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n\n if (teardown.label) {\n /*\n * Only if it still names THIS host. A label released by a teardown that\n * raced a re-provision would otherwise delete the claim the new domain\n * depends on, and the next site to ask for that name would be handed one\n * that is already live.\n */\n const ref = labelRef(teardown.label)\n await firestore()\n .runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) return\n if (String(snapshot.get('hostId') ?? '') !== teardown.hostId) return\n transaction.delete(ref)\n })\n .catch(() => undefined)\n }\n}\n\n/**\n * Give a site a NEW sending domain, abandoning the one it has.\n *\n * The explicit action behind \"I want my mail to come from a different name\".\n * It exists so that renaming a site can stay free of side effects: a merchant\n * who wants the sending name to follow the rename asks for it here, and is\n * told what it costs.\n *\n * What it costs is the reputation. A sending domain's value is its age and its\n * history with the receiving networks; a new one starts at zero and is treated\n * with more suspicion than the old one, for weeks. That is why this is not\n * what a rename does, and why the caller is expected to have said so out loud\n * before getting here.\n *\n * Returns the teardown for the OLD domain. The caller must complete the vendor\n * cleanup for it, exactly as for a deleted host — the old Resend domain and\n * its zone records are no more welcome to linger here than there.\n */\nexport async function restartHostSendingDomain(options: {\n hostId: string\n orgId: string\n subdomain: string\n /** Carried through to the new claim. See `requestHostSendingDomain`. */\n requestedBy: 'merchant' | 'staff'\n}): Promise<{\n teardown: HostSendingDomainTeardown | null\n provisioned: HostSendingDomainResult\n}> {\n const teardown = await readHostSendingTeardown(options?.hostId)\n if (teardown) await releaseHostSendingDomain(teardown)\n return { teardown, provisioned: await requestHostSendingDomain(options) }\n}\n\n/*==========================================\n What the console sweep picks up\n==========================================*/\n\nexport interface PendingSendingDomain {\n orgId: string\n record: SendingDomainRecord\n}\n\n/**\n * Platform sending domains that still need vendor work, oldest first.\n *\n * A collection-group query over every org's `sendingDomains`, filtered to the\n * ones inside our own mail apex — a customer's own domain is records they\n * publish, and this sweep must never try to write to a zone we do not own.\n *\n * `orderBy` on the creation time, and not merely `limit`. A `limit()` with no\n * `orderBy` returns documents in ID order, which is a stable arbitrary sample\n * rather than a queue: the same handful of domains would be returned on every\n * run and anything sorting after them would never be provisioned at all.\n *\n * Ordering on a field means a document missing that field is dropped by the\n * query. That is acceptable here and nowhere else in this file, because\n * `requestHostSendingDomain` is the only writer and it always sets\n * `createdAtMs`.\n */\nexport async function listPendingSendingDomains(\n limit = 25,\n): Promise<PendingSendingDomain[]> {\n const snapshot = await firestore()\n .collectionGroup(SENDING_DOMAINS_COLLECTION)\n .where('status', '==', 'requested')\n .orderBy('createdAtMs', 'asc')\n .limit(Math.max(1, limit))\n .get()\n\n const pending: PendingSendingDomain[] = []\n for (const doc of snapshot.docs) {\n const record = readSendingDomainRecord(doc)\n if (!record) continue\n // Ours to provision, or the customer's to publish. Only the first kind.\n if (!isPlatformSendingDomain(record.domain)) continue\n const orgId = doc.ref.parent.parent?.id\n if (!orgId) continue\n pending.push({ orgId, record })\n }\n return pending\n}\n\n/**\n * The label a platform domain belongs to, for a caller assembling zone record\n * names. Re-derived rather than read off the host, so a cleanup can name the\n * records for a host document that is already gone.\n */\nexport function sendingDomainLabel(domain: string): string {\n return platformSendingLabel(domain, platformSendingApex())\n}\n\n"],"names":["isPlatformSendingDomain","mailLabelCandidate","normalizeSendingDomain","platformSendingApex","platformSendingDomainFor","platformSendingLabel","holdsDedicatedSendingDomain","firebaseAdmin","SENDING_DOMAINS_COLLECTION","readSendingDomainRecord","firestore","app","orgHoldsDedicatedSendingDomain","orgId","id","String","trim","snapshot","collection","doc","get","catch","exists","data","SENDING_LABELS_COLLECTION","MAX_LABEL_ATTEMPTS","labelRef","label","requestHostSendingDomain","options","hostSnapshot","hostId","requestedBy","domain","created","error","hostRef","pinned","subdomain","attempt","claim","claimedAtMs","Date","now","create","record","status","dkimSelector","dkimPublicKey","returnPathHost","createdAtMs","set","merge","sendingLabel","sendingDomain","sendingDomainRequestedAtMs","sendingDomainRequestedBy","readHostSendingTeardown","providerDomainId","trackingTarget","releaseHostSendingDomain","teardown","delete","undefined","FieldValue","ref","runTransaction","transaction","restartHostSendingDomain","provisioned","listPendingSendingDomains","limit","collectionGroup","where","orderBy","Math","max","pending","docs","parent","push","sendingDomainLabel"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoEC,GAED,SACEA,uBAAuB,EACvBC,kBAAkB,EAClBC,sBAAsB,EACtBC,mBAAmB,EACnBC,wBAAwB,EACxBC,oBAAoB,QAEf,2BAA0B;AACjC,SAASC,2BAA2B,QAAQ,kDAAiD;AAC7F,OAAOC,mBAAmB,sBAAkB;AAC5C,SAASC,0BAA0B,EAAEC,uBAAuB,QAAQ,uBAAmB;AAEvF,MAAMC,YAAY,IAAMH,cAAcI,GAAG,GAAGD,SAAS;AAErD;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,eAAeE,+BACpBC,KAAgC;IAEhC,MAAMC,KAAKC,OAAOF,gBAAAA,QAAS,IAAIG,IAAI;IACnC,IAAI,CAACF,IAAI,OAAO;IAChB,MAAMG,WAAW,MAAMP,YACpBQ,UAAU,CAAC,QACXC,GAAG,CAACL,IACJM,GAAG,GACHC,KAAK,CAAC,IAAM;IACf,OAAOJ,CAAAA,4BAAAA,SAAUK,MAAM,IACnBhB,4BAA4BW,SAASM,IAAI,MACzC;AACN;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMC,4BAA4B,gBAAe;AAExD,8DAA8D,GAC9D,MAAMC,qBAAqB;AAE3B,MAAMC,WAAW,CAACC,QAChBjB,YAAYQ,UAAU,CAACM,2BAA2BL,GAAG,CAACQ;AAoBxD;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,eAAeC,yBAAyBC,OAmB9C;qBA2BuBC;IA1BtB,MAAMC,SAAShB,eAAOc,2BAAAA,QAASE,MAAM,mBAAI,IAAIf,IAAI;IACjD,MAAMH,QAAQE,gBAAOc,2BAAAA,QAAShB,KAAK,oBAAI,IAAIG,IAAI;IAC/C,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,iDAAiD;IACjD,MAAMgB,cAAcH,CAAAA,2BAAAA,QAASG,WAAW,MAAK,UAAU,UAAU;IACjE,IAAI,CAACD,UAAU,CAAClB,OAAO;QACrB,OAAO;YAAEoB,QAAQ;YAAMN,OAAO;YAAMO,SAAS;YAAOC,OAAO;QAAe;IAC5E;IAEA,MAAMC,UAAU1B,YAAYQ,UAAU,CAAC,SAASC,GAAG,CAACY;IACpD,MAAMD,eAAe,MAAMM,QAAQhB,GAAG;IACtC,IAAI,CAACU,aAAaR,MAAM,EAAE;QACxB,OAAO;YAAEW,QAAQ;YAAMN,OAAO;YAAMO,SAAS;YAAOC,OAAO;QAAU;IACvE;IAEA;;;;;;;;GAQC,GACD,MAAME,SAAStB,QAAOe,oBAAAA,aAAaV,GAAG,CAAC,2BAAjBU,oBAAoC,IAAId,IAAI;IAClE,IAAIqB,QAAQ;QACV,MAAMJ,SAAS7B,yBAAyBiC;QACxC,OAAO;YACLJ,QAAQA,UAAU;YAClBN,OAAOU;YACPH,SAAS;YACT,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,qEAAqE;YACrEC,OAAOF,SAAS,OAAO;QACzB;IACF;IAEA;;;;;;;;;;;;;;;;;;;;GAoBC,GACD,IAAI,CAAE,MAAMrB,+BAA+BC,QAAS;QAClD,OAAO;YACLoB,QAAQ;YACRN,OAAO;YACPO,SAAS;YACTC,OAAO;QACT;IACF;IAEA,MAAMG,YAAYvB,OAChBc,CAAAA,2BAAAA,QAASS,SAAS,KAAIR,aAAaV,GAAG,CAAC,gBAAgB,IACvDJ,IAAI;IAEN,IAAK,IAAIuB,UAAU,GAAGA,WAAWd,oBAAoBc,WAAW,EAAG;QACjE,MAAMZ,QAAQ1B,mBAAmBqC,WAAWC;QAC5C,IAAI,CAACZ,OAAO;QACZ,MAAMM,SAAS7B,yBAAyBuB;QACxC,IAAI,CAACM,QAAQ;QAEb,MAAMO,QAA2B;YAC/Bb;YACAI;YACAlB;YACAoB;YACAQ,aAAaC,KAAKC,GAAG;QACvB;QAEA,IAAI;YACF,wEAAwE;YACxE,sEAAsE;YACtE,uEAAuE;YACvE,mBAAmB;YACnB,MAAMjB,SAASC,OAAOiB,MAAM,CAACJ;QAC/B,EAAE,eAAM;YACN;QACF;QAEA,MAAMK,SAA8B;YAClCZ;YACAa,QAAQ;YACR,oEAAoE;YACpE,oEAAoE;YACpEC,cAAc;YACdC,eAAe;YACfC,gBAAgB;YAChBC,aAAaR,KAAKC,GAAG;QACvB;QACA,MAAMjC,YACHQ,UAAU,CAAC,QACXC,GAAG,CAACN,OACJK,UAAU,CAACV,4BACXW,GAAG,CAACc,QACJkB,GAAG,CAACN,QAAQ;YAAEO,OAAO;QAAK;QAE7B;;;;;;;;;KASC,GACD,MAAMhB,QAAQe,GAAG,CACf;YACEE,cAAc1B;YACd2B,eAAerB;YACfsB,4BAA4Bb,KAAKC,GAAG;YACpCa,0BAA0BxB;QAC5B,GACA;YAAEoB,OAAO;QAAK;QAGhB,OAAO;YAAEnB;YAAQN;YAAOO,SAAS;YAAMC,OAAO;QAAK;IACrD;IAEA,OAAO;QAAEF,QAAQ;QAAMN,OAAO;QAAMO,SAAS;QAAOC,OAAO;IAAW;AACxE;AAmCA;;;;;;;;;CASC,GACD,OAAO,eAAesB,wBACpB1B,MAAc;QAQOd,eAEWA;IARhC,MAAMH,KAAKC,OAAOgB,iBAAAA,SAAU,IAAIf,IAAI;IACpC,IAAI,CAACF,IAAI,OAAO;IAEhB,MAAMG,WAAW,MAAMP,YAAYQ,UAAU,CAAC,SAASC,GAAG,CAACL,IAAIM,GAAG;IAClE,IAAI,CAACH,SAASK,MAAM,EAAE,OAAO;IAE7B,MAAMK,QAAQZ,QAAOE,gBAAAA,SAASG,GAAG,CAAC,2BAAbH,gBAAgC,IAAID,IAAI,MAAM;IACnE,MAAMiB,SACJ/B,uBAAuBa,QAAOE,iBAAAA,SAASG,GAAG,CAAC,4BAAbH,iBAAiC,QAAQ;IACzE,IAAI,CAACU,SAAS,CAACM,QAAQ,OAAO;IAE9B,sEAAsE;IACtE,kCAAkC;IAClC,MAAMO,QAAQb,QAAQ,MAAMD,SAASC,OAAOP,GAAG,KAAK;IACpD,MAAMP,QAAQE,eAAOyB,yBAAAA,MAAOpB,GAAG,CAAC,2BAAY,IAAIJ,IAAI,MAAM;IAE1D,IAAI0C,mBAAkC;IACtC,IAAIX,eAA8B;IAClC,IAAIY,iBAAgC;IACpC,IAAI9C,SAASoB,QAAQ;;QACnB,MAAMY,SAASpC,wBACb,MAAMC,YACHQ,UAAU,CAAC,QACXC,GAAG,CAACN,OACJK,UAAU,CAACV,4BACXW,GAAG,CAACc,QACJb,GAAG;QAERsC,4BAAmBb,0BAAAA,OAAQa,gBAAgB,oBAAI;QAC/CX,eAAehC,gBAAO8B,0BAAAA,OAAQE,YAAY,oBAAI,IAAI/B,IAAI,MAAM;QAC5D2C,iBAAiB5C,gBAAO8B,0BAAAA,OAAQc,cAAc,oBAAI,IAAI3C,IAAI,MAAM;IAClE;IAEA,OAAO;QACLe,QAAQjB;QACRD;QACAc;QACAM;QACAyB;QACAX;QACAY;IACF;AACF;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,eAAeC,yBACpBC,QAAmC;QAsB9B;IApBL,IAAI,CAACA,UAAU;IAEf,IAAIA,SAAShD,KAAK,IAAIgD,SAAS5B,MAAM,EAAE;QACrC,MAAMvB,YACHQ,UAAU,CAAC,QACXC,GAAG,CAAC0C,SAAShD,KAAK,EAClBK,UAAU,CAACV,4BACXW,GAAG,CAAC0C,SAAS5B,MAAM,EACnB6B,MAAM,GACNzC,KAAK,CAAC,IAAM0C;IACjB;IAEA;;;;;;GAMC,GACD,MAAM3B,UAAU1B,YAAYQ,UAAU,CAAC,SAASC,GAAG,CAAC0C,SAAS9B,MAAM;IACnE,KAAK,QAAA,MAAMK,QAAQhB,GAAG,GAAGC,KAAK,CAAC,IAAM,0BAAjC,AAAC,MAAwCC,MAAM,EAAE;QACnD,MAAMc,QACHe,GAAG,CACF;YACEE,cAAc9C,cAAcG,SAAS,CAACsD,UAAU,CAACF,MAAM;YACvDR,eAAe/C,cAAcG,SAAS,CAACsD,UAAU,CAACF,MAAM;QAC1D,GACA;YAAEV,OAAO;QAAK,GAEf/B,KAAK,CAAC,IAAM0C;IACjB;IAEA,IAAIF,SAASlC,KAAK,EAAE;QAClB;;;;;KAKC,GACD,MAAMsC,MAAMvC,SAASmC,SAASlC,KAAK;QACnC,MAAMjB,YACHwD,cAAc,CAAC,OAAOC;gBAGVlD;YAFX,MAAMA,WAAW,MAAMkD,YAAY/C,GAAG,CAAC6C;YACvC,IAAI,CAAChD,SAASK,MAAM,EAAE;YACtB,IAAIP,QAAOE,gBAAAA,SAASG,GAAG,CAAC,qBAAbH,gBAA0B,QAAQ4C,SAAS9B,MAAM,EAAE;YAC9DoC,YAAYL,MAAM,CAACG;QACrB,GACC5C,KAAK,CAAC,IAAM0C;IACjB;AACF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,eAAeK,yBAAyBvC,OAM9C;IAIC,MAAMgC,WAAW,MAAMJ,wBAAwB5B,2BAAAA,QAASE,MAAM;IAC9D,IAAI8B,UAAU,MAAMD,yBAAyBC;IAC7C,OAAO;QAAEA;QAAUQ,aAAa,MAAMzC,yBAAyBC;IAAS;AAC1E;AAWA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,eAAeyC,0BACpBC,QAAQ,EAAE;IAEV,MAAMtD,WAAW,MAAMP,YACpB8D,eAAe,CAAChE,4BAChBiE,KAAK,CAAC,UAAU,MAAM,aACtBC,OAAO,CAAC,eAAe,OACvBH,KAAK,CAACI,KAAKC,GAAG,CAAC,GAAGL,QAClBnD,GAAG;IAEN,MAAMyD,UAAkC,EAAE;IAC1C,KAAK,MAAM1D,OAAOF,SAAS6D,IAAI,CAAE;YAKjB3D;QAJd,MAAM0B,SAASpC,wBAAwBU;QACvC,IAAI,CAAC0B,QAAQ;QACb,wEAAwE;QACxE,IAAI,CAAC7C,wBAAwB6C,OAAOZ,MAAM,GAAG;QAC7C,MAAMpB,SAAQM,yBAAAA,IAAI8C,GAAG,CAACc,MAAM,CAACA,MAAM,qBAArB5D,uBAAuBL,EAAE;QACvC,IAAI,CAACD,OAAO;QACZgE,QAAQG,IAAI,CAAC;YAAEnE;YAAOgC;QAAO;IAC/B;IACA,OAAOgC;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBAAmBhD,MAAc;IAC/C,OAAO5B,qBAAqB4B,QAAQ9B;AACtC"}
|