@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,268 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* A SENDING DOMAIN OUTLIVING ITS OWNER — the debt, and who settles it.
|
|
19
|
+
*
|
|
20
|
+
* A site's dedicated sending domain is three resources at three different
|
|
21
|
+
* lifetimes: a domain object at the mail provider (one of a small, plan-capped
|
|
22
|
+
* number of account slots), three records in our own DNS zone, and our
|
|
23
|
+
* Firestore state. Destroying the site destroys none of them. Until this
|
|
24
|
+
* module existed only `/api/hosts/delete` ever released them, so every other
|
|
25
|
+
* way a site could stop existing — an org erasure, a host erasure inside one —
|
|
26
|
+
* spent a provider slot forever and left a live DKIM key in the zone for a
|
|
27
|
+
* name a future site could claim.
|
|
28
|
+
*
|
|
29
|
+
* ## Erasure must not be able to fail because a vendor is down
|
|
30
|
+
*
|
|
31
|
+
* An erasure is a legal obligation with a clock on it. A provider outage, a
|
|
32
|
+
* revoked token or an unconfigured self-host must not stop it, must not leave
|
|
33
|
+
* it half-done, and must not silently drop a slot on the floor either.
|
|
34
|
+
*
|
|
35
|
+
* So the vendor work is ATTEMPTED and the outcome is either settled or
|
|
36
|
+
* RECORDED AS A DEBT. The erasure then proceeds regardless, and a sweep
|
|
37
|
+
* collects the debt later. Nothing here throws.
|
|
38
|
+
*
|
|
39
|
+
* ## The tombstone is the label claim, which is why there is no new collection
|
|
40
|
+
*
|
|
41
|
+
* `sendingLabels/{label}` is a ROOT document created at the moment a site
|
|
42
|
+
* claims its name. Nothing destroys it implicitly: it is outside
|
|
43
|
+
* `recursiveDelete(hosts/{hostId})` and outside `recursiveDelete(orgs/{orgId})`,
|
|
44
|
+
* so it is the ONE record of a site's sending domain that survives both an
|
|
45
|
+
* erased host and an erased workspace. `orgs/{orgId}/sendingDomains/{domain}`,
|
|
46
|
+
* which is where the provider's id and the DKIM selector live, does not — an
|
|
47
|
+
* org erasure destroys it, and with it the only handle on the slot to release.
|
|
48
|
+
*
|
|
49
|
+
* That makes the claim the natural tombstone rather than a designed one, and
|
|
50
|
+
* `releaseHostSendingDomain` already says so in as many words: while the claim
|
|
51
|
+
* exists, a half-finished teardown is a thing a sweep can find and finish. All
|
|
52
|
+
* this adds is the two facts the claim did not carry — the provider's domain
|
|
53
|
+
* id and the selector — copied onto it BEFORE the record holding them goes
|
|
54
|
+
* away, plus a mark saying the owner is gone.
|
|
55
|
+
*
|
|
56
|
+
* A separate `sendingDomainDebts` collection would have needed its own rules,
|
|
57
|
+
* its own index and its own reconciliation with the claim it duplicates, and
|
|
58
|
+
* would have introduced the state this design cannot produce: a debt whose
|
|
59
|
+
* label claim was already released, naming a name a new site can take.
|
|
60
|
+
*
|
|
61
|
+
* ## What the reaper is FORBIDDEN to touch
|
|
62
|
+
*
|
|
63
|
+
* The four shared pool domains (`shared1.mail.aglyn.app` …) are owned by no
|
|
64
|
+
* host by design, so "nothing points at this" describes them exactly. They
|
|
65
|
+
* have no label claim — pool labels are reserved against tenants and
|
|
66
|
+
* `ensureHostSendingDomain` is the only writer of that collection — so a sweep
|
|
67
|
+
* over `sendingLabels` cannot reach one by construction. Every path here still
|
|
68
|
+
* asks {@link sendingDomainTeardownRefusal} before acting, because a guard
|
|
69
|
+
* that rests only on a scan's starting point is a guard the next scan loses.
|
|
70
|
+
*/ import { platformSendingDomainFor, sendingDomainTeardownRefusal } from "@aglyn/shared-util-email";
|
|
71
|
+
import firebaseAdmin from "./firebase-admin.js";
|
|
72
|
+
import { SENDING_LABELS_COLLECTION, releaseHostSendingDomain } from "./host-sending-domain.js";
|
|
73
|
+
import { SENDING_DOMAINS_COLLECTION, readSendingDomainRecord } from "./sending-domains.js";
|
|
74
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
75
|
+
const labelRef = (label)=>firestore().collection(SENDING_LABELS_COLLECTION).doc(label);
|
|
76
|
+
/** One claim document as a debt record. */ export function readSendingDomainDebt(snapshot) {
|
|
77
|
+
var _snapshot_data, _data_hostId, _data_orgId, _data_domain, _data_teardownDetail, _data_providerDomainId, _data_dkimSelector;
|
|
78
|
+
if (!(snapshot == null ? void 0 : snapshot.exists)) return null;
|
|
79
|
+
const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
|
|
80
|
+
return {
|
|
81
|
+
// The document id IS the label — that is what makes the claim atomic — so
|
|
82
|
+
// it is authoritative over the denormalised field beside it, and it is
|
|
83
|
+
// what a paging cursor has to be built from.
|
|
84
|
+
label: String(snapshot.id || data.label || ''),
|
|
85
|
+
hostId: String((_data_hostId = data.hostId) != null ? _data_hostId : '').trim() || null,
|
|
86
|
+
orgId: String((_data_orgId = data.orgId) != null ? _data_orgId : '').trim() || null,
|
|
87
|
+
domain: String((_data_domain = data.domain) != null ? _data_domain : '').trim() || null,
|
|
88
|
+
claimedAtMs: Number(data.claimedAtMs) || null,
|
|
89
|
+
orphanedAtMs: Number(data.orphanedAtMs) || null,
|
|
90
|
+
teardownDetail: String((_data_teardownDetail = data.teardownDetail) != null ? _data_teardownDetail : '').trim() || null,
|
|
91
|
+
teardownAttempts: Number(data.teardownAttempts) || 0,
|
|
92
|
+
providerDomainId: String((_data_providerDomainId = data.providerDomainId) != null ? _data_providerDomainId : '').trim() || null,
|
|
93
|
+
dkimSelector: String((_data_dkimSelector = data.dkimSelector) != null ? _data_dkimSelector : '').trim() || null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Everything needed to release one PINNED label's domain, read by label.
|
|
98
|
+
*
|
|
99
|
+
* Keyed on the label and never on `hosts/{hostId}.sendingDomain`, and the
|
|
100
|
+
* difference is not cosmetic. That field is the site's CURRENT SELECTION and a
|
|
101
|
+
* site may point it at the org's own verified domain through the sending
|
|
102
|
+
* identity surface — a name this deployment did not provision, holds no
|
|
103
|
+
* provider slot for, and whose record belongs to the ORG and is shared with
|
|
104
|
+
* every other site of theirs. A teardown that read the selection would release
|
|
105
|
+
* an agency's verified domain because one of its sites was deleted.
|
|
106
|
+
*
|
|
107
|
+
* The label is the thing this deployment actually created, it is written once
|
|
108
|
+
* and never rewritten, and {@link platformSendingDomainFor} re-derives the
|
|
109
|
+
* domain from it under the current apex and the current reserved set.
|
|
110
|
+
*
|
|
111
|
+
* Works for a host that no longer exists: the claim carries the org, and the
|
|
112
|
+
* provider id and selector are copied onto the claim before an erasure
|
|
113
|
+
* destroys the record they came from.
|
|
114
|
+
*/ export async function readSendingDomainTeardownByLabel(label) {
|
|
115
|
+
var _debt_hostId;
|
|
116
|
+
const name = String(label != null ? label : '').trim();
|
|
117
|
+
if (!name) return null;
|
|
118
|
+
const claim = await labelRef(name).get().catch(()=>null);
|
|
119
|
+
const debt = claim ? readSendingDomainDebt(claim) : null;
|
|
120
|
+
if (!debt) return null;
|
|
121
|
+
const domain = platformSendingDomainFor(name) || debt.domain;
|
|
122
|
+
if (!domain) return null;
|
|
123
|
+
let providerDomainId = debt.providerDomainId;
|
|
124
|
+
let dkimSelector = debt.dkimSelector;
|
|
125
|
+
let trackingTarget = null;
|
|
126
|
+
if (debt.orgId && (!providerDomainId || !dkimSelector)) {
|
|
127
|
+
var _ref, _ref1, _ref2;
|
|
128
|
+
const record = readSendingDomainRecord(await firestore().collection('orgs').doc(debt.orgId).collection(SENDING_DOMAINS_COLLECTION).doc(domain).get().catch(()=>null));
|
|
129
|
+
providerDomainId = (_ref = providerDomainId != null ? providerDomainId : record == null ? void 0 : record.providerDomainId) != null ? _ref : null;
|
|
130
|
+
dkimSelector = dkimSelector != null ? dkimSelector : String((_ref1 = record == null ? void 0 : record.dkimSelector) != null ? _ref1 : '').trim() || null;
|
|
131
|
+
trackingTarget = String((_ref2 = record == null ? void 0 : record.trackingTarget) != null ? _ref2 : '').trim() || null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
hostId: (_debt_hostId = debt.hostId) != null ? _debt_hostId : '',
|
|
135
|
+
orgId: debt.orgId,
|
|
136
|
+
label: name,
|
|
137
|
+
domain,
|
|
138
|
+
providerDomainId,
|
|
139
|
+
dkimSelector,
|
|
140
|
+
trackingTarget
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Copy the vendor handles onto the claim and mark the owner gone.
|
|
145
|
+
*
|
|
146
|
+
* Written with `merge` onto a document that already exists, so it adds the
|
|
147
|
+
* debt to the claim rather than replacing it — the `hostId` the claim carries
|
|
148
|
+
* is what `releaseHostSendingDomain` matches on before deleting it, and losing
|
|
149
|
+
* that would strand the claim permanently.
|
|
150
|
+
*
|
|
151
|
+
* `teardownAttempts` increments so a debt that cannot be settled — a provider
|
|
152
|
+
* that has forgotten the domain, a token that was never restored — is visible
|
|
153
|
+
* as a number climbing rather than as a row that looks the same every day.
|
|
154
|
+
*/ export async function recordSendingDomainDebt(teardown, detail) {
|
|
155
|
+
var _ref;
|
|
156
|
+
const label = String((_ref = teardown == null ? void 0 : teardown.label) != null ? _ref : '').trim();
|
|
157
|
+
if (!label) return;
|
|
158
|
+
const ref = labelRef(label);
|
|
159
|
+
await firestore().runTransaction(async (transaction)=>{
|
|
160
|
+
var _teardown_providerDomainId, _teardown_dkimSelector, _teardown_domain;
|
|
161
|
+
const snapshot = await transaction.get(ref);
|
|
162
|
+
if (!snapshot.exists) return;
|
|
163
|
+
const attempts = Number(snapshot.get('teardownAttempts')) || 0;
|
|
164
|
+
transaction.set(ref, {
|
|
165
|
+
orphanedAtMs: Number(snapshot.get('orphanedAtMs')) || Date.now(),
|
|
166
|
+
teardownDetail: String(detail != null ? detail : 'unknown'),
|
|
167
|
+
teardownAttempts: attempts + 1,
|
|
168
|
+
providerDomainId: (_teardown_providerDomainId = teardown.providerDomainId) != null ? _teardown_providerDomainId : null,
|
|
169
|
+
dkimSelector: (_teardown_dkimSelector = teardown.dkimSelector) != null ? _teardown_dkimSelector : null,
|
|
170
|
+
domain: (_teardown_domain = teardown.domain) != null ? _teardown_domain : null
|
|
171
|
+
}, {
|
|
172
|
+
merge: true
|
|
173
|
+
});
|
|
174
|
+
}).catch(()=>undefined);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Release one site's dedicated sending domain, or record what is still owed.
|
|
178
|
+
*
|
|
179
|
+
* The single entry point for every path that destroys a site. It never throws
|
|
180
|
+
* and it never blocks its caller on a vendor: the worst outcome is a debt on
|
|
181
|
+
* the label claim and a loud line naming the domain.
|
|
182
|
+
*
|
|
183
|
+
* Ordered provider → zone → our records by the driver it calls, for the reason
|
|
184
|
+
* stated there: the provider slot is the scarce resource, and our own record
|
|
185
|
+
* is what keeps a half-finished teardown findable.
|
|
186
|
+
*/ export async function disposeHostSendingDomain(options) {
|
|
187
|
+
var _ref;
|
|
188
|
+
const teardown = (_ref = options == null ? void 0 : options.teardown) != null ? _ref : await readPinnedTeardown(options == null ? void 0 : options.hostId);
|
|
189
|
+
if (!teardown || !teardown.label || !teardown.domain) return 'none';
|
|
190
|
+
/*==========================================
|
|
191
|
+
* ⛔ THE POOL IS NEVER TORN DOWN.
|
|
192
|
+
*
|
|
193
|
+
* A pool member has no label claim, so nothing should ever reach here
|
|
194
|
+
* naming one — which is exactly why the check is here rather than trusted
|
|
195
|
+
* to the caller's starting point. `shared1.mail.aglyn.app` through
|
|
196
|
+
* `shared4` carry the transactional mail of every site with no domain of
|
|
197
|
+
* its own; releasing one stops a quarter of the platform's receipts and
|
|
198
|
+
* password resets, and stops them silently.
|
|
199
|
+
*=========================================*/ const refusal = sendingDomainTeardownRefusal(teardown.domain, teardown.label);
|
|
200
|
+
if (refusal === 'shared-pool') {
|
|
201
|
+
console.error('[sending-domain-debt] REFUSED to tear down the shared pool member', teardown.domain, '— a pool member belongs to the platform, not to a site, and nothing', 'may release it. Something is pointing a host at a reserved label.');
|
|
202
|
+
return 'protected';
|
|
203
|
+
}
|
|
204
|
+
if (refusal === 'not-our-zone') return 'none';
|
|
205
|
+
if (!(options == null ? void 0 : options.tearDown)) {
|
|
206
|
+
// No credential in this process. The debt is the honest state: the
|
|
207
|
+
// domain still exists at both vendors and something else must settle it.
|
|
208
|
+
await recordSendingDomainDebt(teardown, 'no-teardown-driver');
|
|
209
|
+
return 'deferred';
|
|
210
|
+
}
|
|
211
|
+
const result = await options.tearDown(teardown).catch(()=>({
|
|
212
|
+
outcome: 'failed',
|
|
213
|
+
detail: 'threw'
|
|
214
|
+
}));
|
|
215
|
+
/*
|
|
216
|
+
* `skipped` with nothing ever created at the provider is a real release.
|
|
217
|
+
*
|
|
218
|
+
* The driver skips when no credential is configured — a self-host with no
|
|
219
|
+
* mail provider, or a token that has been withdrawn. When the record never
|
|
220
|
+
* carried a provider id there is nothing at either vendor to leak: the id
|
|
221
|
+
* and the DKIM key are stored by the same write, and the zone records are
|
|
222
|
+
* only written once a key exists. Deferring that forever would hold a name
|
|
223
|
+
* out of circulation for a domain that was never created.
|
|
224
|
+
*/ const nothingProvisioned = result.outcome === 'skipped' && !teardown.providerDomainId;
|
|
225
|
+
if (result.outcome === 'removed' || nothingProvisioned) {
|
|
226
|
+
await releaseHostSendingDomain(teardown);
|
|
227
|
+
return 'released';
|
|
228
|
+
}
|
|
229
|
+
await recordSendingDomainDebt(teardown, result.detail || result.outcome);
|
|
230
|
+
console.error('[sending-domain-debt] sending domain not released for', teardown.domain, `(${result.detail || result.outcome}) — the provider slot and the zone`, 'records are still held; the reap-sending-domains sweep will retry.');
|
|
231
|
+
return 'deferred';
|
|
232
|
+
}
|
|
233
|
+
/** The teardown for a host that still exists, taken from its pinned label. */ async function readPinnedTeardown(hostId) {
|
|
234
|
+
var _host_get;
|
|
235
|
+
const id = String(hostId != null ? hostId : '').trim();
|
|
236
|
+
if (!id) return null;
|
|
237
|
+
const host = await firestore().collection('hosts').doc(id).get().catch(()=>null);
|
|
238
|
+
if (!(host == null ? void 0 : host.exists)) return null;
|
|
239
|
+
const teardown = await readSendingDomainTeardownByLabel(String((_host_get = host.get('sendingLabel')) != null ? _host_get : '').trim());
|
|
240
|
+
// The claim may name a host that has since been re-pointed; the site being
|
|
241
|
+
// destroyed is the one the caller named.
|
|
242
|
+
return teardown ? _extends({}, teardown, {
|
|
243
|
+
hostId: id
|
|
244
|
+
}) : null;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Every label claim, oldest first, for the orphan sweep.
|
|
248
|
+
*
|
|
249
|
+
* Ordered by document id rather than by a field, and paged with a cursor. A
|
|
250
|
+
* `limit()` with no ordering returns an arbitrary but STABLE sample — the same
|
|
251
|
+
* handful every run — and ordering on `claimedAtMs` would silently DROP any
|
|
252
|
+
* claim written before that field existed, which for a reaper means an orphan
|
|
253
|
+
* it can never see. `__name__` is present on every document by definition.
|
|
254
|
+
*/ export async function listSendingLabelClaims(options) {
|
|
255
|
+
var _ref, _ref1;
|
|
256
|
+
const base = firestore().collection(SENDING_LABELS_COLLECTION).orderBy('__name__').limit(Math.max(1, (_ref = options == null ? void 0 : options.limit) != null ? _ref : 100));
|
|
257
|
+
const after = String((_ref1 = options == null ? void 0 : options.after) != null ? _ref1 : '').trim();
|
|
258
|
+
const query = after ? base.startAfter(firestore().collection(SENDING_LABELS_COLLECTION).doc(after)) : base;
|
|
259
|
+
const snapshot = await query.get();
|
|
260
|
+
const rows = [];
|
|
261
|
+
for (const doc of snapshot.docs){
|
|
262
|
+
const debt = readSendingDomainDebt(doc);
|
|
263
|
+
if (debt) rows.push(debt);
|
|
264
|
+
}
|
|
265
|
+
return rows;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
//# sourceMappingURL=sending-domain-debt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/sending-domain-debt.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 * A SENDING DOMAIN OUTLIVING ITS OWNER — the debt, and who settles it.\n *\n * A site's dedicated sending domain is three resources at three different\n * lifetimes: a domain object at the mail provider (one of a small, plan-capped\n * number of account slots), three records in our own DNS zone, and our\n * Firestore state. Destroying the site destroys none of them. Until this\n * module existed only `/api/hosts/delete` ever released them, so every other\n * way a site could stop existing — an org erasure, a host erasure inside one —\n * spent a provider slot forever and left a live DKIM key in the zone for a\n * name a future site could claim.\n *\n * ## Erasure must not be able to fail because a vendor is down\n *\n * An erasure is a legal obligation with a clock on it. A provider outage, a\n * revoked token or an unconfigured self-host must not stop it, must not leave\n * it half-done, and must not silently drop a slot on the floor either.\n *\n * So the vendor work is ATTEMPTED and the outcome is either settled or\n * RECORDED AS A DEBT. The erasure then proceeds regardless, and a sweep\n * collects the debt later. Nothing here throws.\n *\n * ## The tombstone is the label claim, which is why there is no new collection\n *\n * `sendingLabels/{label}` is a ROOT document created at the moment a site\n * claims its name. Nothing destroys it implicitly: it is outside\n * `recursiveDelete(hosts/{hostId})` and outside `recursiveDelete(orgs/{orgId})`,\n * so it is the ONE record of a site's sending domain that survives both an\n * erased host and an erased workspace. `orgs/{orgId}/sendingDomains/{domain}`,\n * which is where the provider's id and the DKIM selector live, does not — an\n * org erasure destroys it, and with it the only handle on the slot to release.\n *\n * That makes the claim the natural tombstone rather than a designed one, and\n * `releaseHostSendingDomain` already says so in as many words: while the claim\n * exists, a half-finished teardown is a thing a sweep can find and finish. All\n * this adds is the two facts the claim did not carry — the provider's domain\n * id and the selector — copied onto it BEFORE the record holding them goes\n * away, plus a mark saying the owner is gone.\n *\n * A separate `sendingDomainDebts` collection would have needed its own rules,\n * its own index and its own reconciliation with the claim it duplicates, and\n * would have introduced the state this design cannot produce: a debt whose\n * label claim was already released, naming a name a new site can take.\n *\n * ## What the reaper is FORBIDDEN to touch\n *\n * The four shared pool domains (`shared1.mail.aglyn.app` …) are owned by no\n * host by design, so \"nothing points at this\" describes them exactly. They\n * have no label claim — pool labels are reserved against tenants and\n * `ensureHostSendingDomain` is the only writer of that collection — so a sweep\n * over `sendingLabels` cannot reach one by construction. Every path here still\n * asks {@link sendingDomainTeardownRefusal} before acting, because a guard\n * that rests only on a scan's starting point is a guard the next scan loses.\n */\n\nimport {\n platformSendingDomainFor,\n sendingDomainTeardownRefusal,\n} from '@aglyn/shared-util-email'\nimport firebaseAdmin from './firebase-admin'\nimport {\n SENDING_LABELS_COLLECTION,\n releaseHostSendingDomain,\n type HostSendingDomainTeardown,\n} from './host-sending-domain'\nimport { SENDING_DOMAINS_COLLECTION, readSendingDomainRecord } from './sending-domains'\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\n/**\n * The console-side teardown, injected rather than imported.\n *\n * Releasing the provider's domain object needs a full-access mail credential\n * and removing the zone records needs a DNS token, and neither may be\n * reachable from the tenant runtime — which is what makes them live in\n * `apps/console` and makes this library structurally unable to import them\n * (`sending-domain-credential-isolation.spec.ts` sweeps the tree for it).\n *\n * A caller that HAS those credentials passes the driver in and the domain is\n * released inline. A caller that does not — the tenant runtime, an operator\n * script, a self-host with no provider configured — records the debt and the\n * console's reaper settles it. Both are correct; only the latency differs.\n */\nexport type TeardownSendingDomainDriver = (\n teardown: HostSendingDomainTeardown,\n) => Promise<{ outcome: 'removed' | 'skipped' | 'failed'; detail: string | null }>\n\n/** What became of one site's dedicated sending domain. */\nexport type SendingDomainDisposition =\n /** The site had no dedicated domain of its own. Nothing to release. */\n | 'none'\n /** Provider slot freed, zone records removed, our records dropped. */\n | 'released'\n /** The debt is recorded on the label claim; the reaper owes it a visit. */\n | 'deferred'\n /** A shared pool member. Never torn down, by anything, ever. */\n | 'protected'\n\nexport interface SendingDomainDebtRecord {\n /** The claimed label, and the id of the claim document. */\n label: string\n hostId: string | null\n orgId: string | null\n domain: string | null\n claimedAtMs: number | null\n /** Set when a teardown could not be completed. Null on a healthy claim. */\n orphanedAtMs: number | null\n /** A short code naming what refused, safe to store, log and show. */\n teardownDetail: string | null\n /** How many passes have tried and failed to settle this. */\n teardownAttempts: number\n providerDomainId: string | null\n dkimSelector: string | null\n}\n\nconst labelRef = (label: string) =>\n firestore().collection(SENDING_LABELS_COLLECTION).doc(label)\n\n/** One claim document as a debt record. */\nexport function readSendingDomainDebt(\n snapshot: FirebaseFirestore.DocumentSnapshot,\n): SendingDomainDebtRecord | null {\n if (!snapshot?.exists) return null\n const data = snapshot.data() ?? {}\n return {\n // The document id IS the label — that is what makes the claim atomic — so\n // it is authoritative over the denormalised field beside it, and it is\n // what a paging cursor has to be built from.\n label: String(snapshot.id || data.label || ''),\n hostId: String(data.hostId ?? '').trim() || null,\n orgId: String(data.orgId ?? '').trim() || null,\n domain: String(data.domain ?? '').trim() || null,\n claimedAtMs: Number(data.claimedAtMs) || null,\n orphanedAtMs: Number(data.orphanedAtMs) || null,\n teardownDetail: String(data.teardownDetail ?? '').trim() || null,\n teardownAttempts: Number(data.teardownAttempts) || 0,\n providerDomainId: String(data.providerDomainId ?? '').trim() || null,\n dkimSelector: String(data.dkimSelector ?? '').trim() || null,\n }\n}\n\n/**\n * Everything needed to release one PINNED label's domain, read by label.\n *\n * Keyed on the label and never on `hosts/{hostId}.sendingDomain`, and the\n * difference is not cosmetic. That field is the site's CURRENT SELECTION and a\n * site may point it at the org's own verified domain through the sending\n * identity surface — a name this deployment did not provision, holds no\n * provider slot for, and whose record belongs to the ORG and is shared with\n * every other site of theirs. A teardown that read the selection would release\n * an agency's verified domain because one of its sites was deleted.\n *\n * The label is the thing this deployment actually created, it is written once\n * and never rewritten, and {@link platformSendingDomainFor} re-derives the\n * domain from it under the current apex and the current reserved set.\n *\n * Works for a host that no longer exists: the claim carries the org, and the\n * provider id and selector are copied onto the claim before an erasure\n * destroys the record they came from.\n */\nexport async function readSendingDomainTeardownByLabel(\n label: string | null | undefined,\n): Promise<HostSendingDomainTeardown | null> {\n const name = String(label ?? '').trim()\n if (!name) return null\n\n const claim = await labelRef(name)\n .get()\n .catch(() => null)\n const debt = claim ? readSendingDomainDebt(claim) : null\n if (!debt) return null\n\n const domain = platformSendingDomainFor(name) || debt.domain\n if (!domain) return null\n\n let providerDomainId = debt.providerDomainId\n let dkimSelector = debt.dkimSelector\n let trackingTarget: string | null = null\n if (debt.orgId && (!providerDomainId || !dkimSelector)) {\n const record = readSendingDomainRecord(\n await firestore()\n .collection('orgs')\n .doc(debt.orgId)\n .collection(SENDING_DOMAINS_COLLECTION)\n .doc(domain)\n .get()\n .catch(() => null as never),\n )\n providerDomainId = providerDomainId ?? record?.providerDomainId ?? null\n dkimSelector = dkimSelector ?? (String(record?.dkimSelector ?? '').trim() || null)\n trackingTarget = String(record?.trackingTarget ?? '').trim() || null\n }\n\n return {\n hostId: debt.hostId ?? '',\n orgId: debt.orgId,\n label: name,\n domain,\n providerDomainId,\n dkimSelector,\n trackingTarget,\n }\n}\n\n/**\n * Copy the vendor handles onto the claim and mark the owner gone.\n *\n * Written with `merge` onto a document that already exists, so it adds the\n * debt to the claim rather than replacing it — the `hostId` the claim carries\n * is what `releaseHostSendingDomain` matches on before deleting it, and losing\n * that would strand the claim permanently.\n *\n * `teardownAttempts` increments so a debt that cannot be settled — a provider\n * that has forgotten the domain, a token that was never restored — is visible\n * as a number climbing rather than as a row that looks the same every day.\n */\nexport async function recordSendingDomainDebt(\n teardown: HostSendingDomainTeardown,\n detail: string | null,\n): Promise<void> {\n const label = String(teardown?.label ?? '').trim()\n if (!label) return\n const ref = labelRef(label)\n await firestore()\n .runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) return\n const attempts = Number(snapshot.get('teardownAttempts')) || 0\n transaction.set(\n ref,\n {\n orphanedAtMs: Number(snapshot.get('orphanedAtMs')) || Date.now(),\n teardownDetail: String(detail ?? 'unknown'),\n teardownAttempts: attempts + 1,\n providerDomainId: teardown.providerDomainId ?? null,\n dkimSelector: teardown.dkimSelector ?? null,\n domain: teardown.domain ?? null,\n },\n { merge: true },\n )\n })\n .catch(() => undefined)\n}\n\n/**\n * Release one site's dedicated sending domain, or record what is still owed.\n *\n * The single entry point for every path that destroys a site. It never throws\n * and it never blocks its caller on a vendor: the worst outcome is a debt on\n * the label claim and a loud line naming the domain.\n *\n * Ordered provider → zone → our records by the driver it calls, for the reason\n * stated there: the provider slot is the scarce resource, and our own record\n * is what keeps a half-finished teardown findable.\n */\nexport async function disposeHostSendingDomain(options: {\n /** The site being destroyed. Its `sendingLabel` is read before it goes. */\n hostId?: string | null\n /** Pre-read teardown, for a caller that has already lost the host document. */\n teardown?: HostSendingDomainTeardown | null\n tearDown?: TeardownSendingDomainDriver | null\n}): Promise<SendingDomainDisposition> {\n const teardown = options?.teardown ?? (await readPinnedTeardown(options?.hostId))\n if (!teardown || !teardown.label || !teardown.domain) return 'none'\n\n /*==========================================\n * ⛔ THE POOL IS NEVER TORN DOWN.\n *\n * A pool member has no label claim, so nothing should ever reach here\n * naming one — which is exactly why the check is here rather than trusted\n * to the caller's starting point. `shared1.mail.aglyn.app` through\n * `shared4` carry the transactional mail of every site with no domain of\n * its own; releasing one stops a quarter of the platform's receipts and\n * password resets, and stops them silently.\n *=========================================*/\n const refusal = sendingDomainTeardownRefusal(teardown.domain, teardown.label)\n if (refusal === 'shared-pool') {\n console.error(\n '[sending-domain-debt] REFUSED to tear down the shared pool member',\n teardown.domain,\n '— a pool member belongs to the platform, not to a site, and nothing',\n 'may release it. Something is pointing a host at a reserved label.',\n )\n return 'protected'\n }\n if (refusal === 'not-our-zone') return 'none'\n\n if (!options?.tearDown) {\n // No credential in this process. The debt is the honest state: the\n // domain still exists at both vendors and something else must settle it.\n await recordSendingDomainDebt(teardown, 'no-teardown-driver')\n return 'deferred'\n }\n\n const result = await options\n .tearDown(teardown)\n .catch(() => ({ outcome: 'failed' as const, detail: 'threw' }))\n\n /*\n * `skipped` with nothing ever created at the provider is a real release.\n *\n * The driver skips when no credential is configured — a self-host with no\n * mail provider, or a token that has been withdrawn. When the record never\n * carried a provider id there is nothing at either vendor to leak: the id\n * and the DKIM key are stored by the same write, and the zone records are\n * only written once a key exists. Deferring that forever would hold a name\n * out of circulation for a domain that was never created.\n */\n const nothingProvisioned =\n result.outcome === 'skipped' && !teardown.providerDomainId\n if (result.outcome === 'removed' || nothingProvisioned) {\n await releaseHostSendingDomain(teardown)\n return 'released'\n }\n\n await recordSendingDomainDebt(teardown, result.detail || result.outcome)\n console.error(\n '[sending-domain-debt] sending domain not released for',\n teardown.domain,\n `(${result.detail || result.outcome}) — the provider slot and the zone`,\n 'records are still held; the reap-sending-domains sweep will retry.',\n )\n return 'deferred'\n}\n\n/** The teardown for a host that still exists, taken from its pinned label. */\nasync function readPinnedTeardown(\n hostId: string | null | undefined,\n): Promise<HostSendingDomainTeardown | null> {\n const id = String(hostId ?? '').trim()\n if (!id) return null\n const host = await firestore()\n .collection('hosts')\n .doc(id)\n .get()\n .catch(() => null)\n if (!host?.exists) return null\n const teardown = await readSendingDomainTeardownByLabel(\n String(host.get('sendingLabel') ?? '').trim(),\n )\n // The claim may name a host that has since been re-pointed; the site being\n // destroyed is the one the caller named.\n return teardown ? { ...teardown, hostId: id } : null\n}\n\n/**\n * Every label claim, oldest first, for the orphan sweep.\n *\n * Ordered by document id rather than by a field, and paged with a cursor. A\n * `limit()` with no ordering returns an arbitrary but STABLE sample — the same\n * handful every run — and ordering on `claimedAtMs` would silently DROP any\n * claim written before that field existed, which for a reaper means an orphan\n * it can never see. `__name__` is present on every document by definition.\n */\nexport async function listSendingLabelClaims(options: {\n limit: number\n after?: string | null\n}): Promise<SendingDomainDebtRecord[]> {\n const base = firestore()\n .collection(SENDING_LABELS_COLLECTION)\n .orderBy('__name__')\n .limit(Math.max(1, options?.limit ?? 100))\n const after = String(options?.after ?? '').trim()\n const query = after\n ? base.startAfter(\n firestore().collection(SENDING_LABELS_COLLECTION).doc(after),\n )\n : base\n const snapshot = await query.get()\n const rows: SendingDomainDebtRecord[] = []\n for (const doc of snapshot.docs) {\n const debt = readSendingDomainDebt(doc)\n if (debt) rows.push(debt)\n }\n return rows\n}\n"],"names":["platformSendingDomainFor","sendingDomainTeardownRefusal","firebaseAdmin","SENDING_LABELS_COLLECTION","releaseHostSendingDomain","SENDING_DOMAINS_COLLECTION","readSendingDomainRecord","firestore","app","labelRef","label","collection","doc","readSendingDomainDebt","snapshot","data","exists","String","id","hostId","trim","orgId","domain","claimedAtMs","Number","orphanedAtMs","teardownDetail","teardownAttempts","providerDomainId","dkimSelector","readSendingDomainTeardownByLabel","debt","name","claim","get","catch","trackingTarget","record","recordSendingDomainDebt","teardown","detail","ref","runTransaction","transaction","attempts","set","Date","now","merge","undefined","disposeHostSendingDomain","options","readPinnedTeardown","refusal","console","error","tearDown","result","outcome","nothingProvisioned","host","listSendingLabelClaims","base","orderBy","limit","Math","max","after","query","startAfter","rows","docs","push"],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* RE-CHECKING A VERIFIED SENDING DOMAIN.
|
|
19
|
+
*
|
|
20
|
+
* `verifySendingDomain` runs when an admin presses Verify and never again, so
|
|
21
|
+
* a domain that proved itself once was trusted for as long as the record
|
|
22
|
+
* existed. DNS does not hold still: a customer migrates their zone, prunes an
|
|
23
|
+
* unrecognized TXT record, or lets the domain lapse, and the platform keeps
|
|
24
|
+
* signing mail as a name whose owner has withdrawn permission to.
|
|
25
|
+
*
|
|
26
|
+
* ## The failure this must not become
|
|
27
|
+
*
|
|
28
|
+
* The obvious sweep — re-run the check nightly, write what it says — is worse
|
|
29
|
+
* than no sweep. A DNS lookup cannot tell "the record is gone" from "nobody
|
|
30
|
+
* answered", so the obvious sweep un-verifies every customer at once during a
|
|
31
|
+
* resolver outage and every one of them silently stops being able to mail.
|
|
32
|
+
* That is a platform-wide, self-inflicted outage in exchange for closing a
|
|
33
|
+
* slow leak.
|
|
34
|
+
*
|
|
35
|
+
* Two independent things stop it:
|
|
36
|
+
*
|
|
37
|
+
* 1. **The probe's third outcome.** `assessSendingRecords` answers
|
|
38
|
+
* `inconclusive` unless all three lookups ANSWERED, and `dns-probe.ts` asks
|
|
39
|
+
* public resolvers before the runtime's own precisely so a stale local
|
|
40
|
+
* resolver cannot manufacture a conclusive miss. An inconclusive pass here
|
|
41
|
+
* writes nothing but the check time.
|
|
42
|
+
* 2. **The drift discipline.** Even a conclusive miss is counted rather than
|
|
43
|
+
* acted on, through the same {@link assessDomainDrift} the SSO domain sweep
|
|
44
|
+
* uses. An unattended sweep is not an admin watching a button: nobody sees
|
|
45
|
+
* its verdict, so it needs more evidence than one answer before it costs a
|
|
46
|
+
* customer their sending.
|
|
47
|
+
*
|
|
48
|
+
* ## Where this diverges from the SSO sweep, and why
|
|
49
|
+
*
|
|
50
|
+
* `assessDomainDrift` has no `revoke` — its strongest verdict is `report`,
|
|
51
|
+
* because revoking an SSO domain locks people out of their own account and
|
|
52
|
+
* that is a human's call. Here the same verdict DOES change the status, and
|
|
53
|
+
* the asymmetry is in what the change costs: an un-verified sending domain
|
|
54
|
+
* makes the site's sends REFUSE, and a refusal is recoverable by publishing
|
|
55
|
+
* the record and pressing Verify. It never quietly moves the tenant's mail
|
|
56
|
+
* back onto the shared platform domain, which is the outcome the whole feature
|
|
57
|
+
* exists to prevent and which no amount of drift would justify.
|
|
58
|
+
*
|
|
59
|
+
* ## What it does not do
|
|
60
|
+
*
|
|
61
|
+
* It re-checks `verified` domains only. Walking `records-issued` domains to
|
|
62
|
+
* see whether a customer has published yet is a different job with a different
|
|
63
|
+
* cadence and a different failure mode (an onboarding poller, not a trust
|
|
64
|
+
* expiry), and folding the two together would mean one interval serving
|
|
65
|
+
* neither.
|
|
66
|
+
*/
|
|
67
|
+
import { type DomainProbeStatus } from './sso-drift-logic';
|
|
68
|
+
/**
|
|
69
|
+
* How stale a verified domain's last check must be before it is re-read.
|
|
70
|
+
*
|
|
71
|
+
* Daily. The thing being detected — a customer editing their zone — happens on
|
|
72
|
+
* a human timescale, and a tighter cadence buys hours of detection for a
|
|
73
|
+
* multiple of the DNS traffic and the write volume. It also sets the floor on
|
|
74
|
+
* how long a genuinely removed record keeps sending: three conclusive misses
|
|
75
|
+
* at a day apart, which the age floor below holds to at least three days.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SENDING_DOMAIN_RECHECK_AFTER_MS: number;
|
|
78
|
+
/**
|
|
79
|
+
* Conclusive failures in a row before a verified domain is un-verified.
|
|
80
|
+
*
|
|
81
|
+
* Three. The evidence for each one is already strong — all three lookups
|
|
82
|
+
* answered, across pinned public resolvers — so this is not compensating for a
|
|
83
|
+
* weak probe. It is compensating for the one failure the probe cannot see: the
|
|
84
|
+
* resolvers agreeing on a wrong answer, which `dns-probe.ts` documents having
|
|
85
|
+
* happened once already with a stale zone.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SENDING_DOMAIN_FAILURES_BEFORE_REVOKE = 3;
|
|
88
|
+
/**
|
|
89
|
+
* And the wall-clock floor the same run must clear, independent of the count.
|
|
90
|
+
*
|
|
91
|
+
* Both, for the reason the SSO threshold gives: a count alone can be run up in
|
|
92
|
+
* minutes by a beat firing more often than anyone intended, or by somebody
|
|
93
|
+
* re-running the sweep by hand, and "we checked three times" then reads as
|
|
94
|
+
* diligence while meaning nothing.
|
|
95
|
+
*
|
|
96
|
+
* Three days is what three daily checks ought to take. It is not a long time
|
|
97
|
+
* to keep signing for a domain whose records are gone: once the DKIM record is
|
|
98
|
+
* removed our signature stops validating anyway, so the mail is already
|
|
99
|
+
* failing — un-verifying is how the product stops PRETENDING otherwise, not
|
|
100
|
+
* what stops the bad mail.
|
|
101
|
+
*/
|
|
102
|
+
export declare const SENDING_DOMAIN_DRIFT_MIN_AGE_MS: number;
|
|
103
|
+
/** Domains one beat will re-check. The rest are picked up on the next one. */
|
|
104
|
+
export declare const SENDING_DOMAIN_RECHECK_BATCH = 25;
|
|
105
|
+
/** What one pass settled. */
|
|
106
|
+
export interface SendingDomainRecheckSummary {
|
|
107
|
+
/** Domains probed. */
|
|
108
|
+
checked: number;
|
|
109
|
+
/** Probes that established nothing, and changed nothing. */
|
|
110
|
+
held: number;
|
|
111
|
+
/** Domains still publishing their records; any failure run ended. */
|
|
112
|
+
cleared: number;
|
|
113
|
+
/** Conclusive misses recorded, without acting on them yet. */
|
|
114
|
+
counted: number;
|
|
115
|
+
/** Domains moved out of `verified`. */
|
|
116
|
+
revoked: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A probe verdict as the drift assessor's vocabulary.
|
|
120
|
+
*
|
|
121
|
+
* The mapping is the whole safety property of this module written in three
|
|
122
|
+
* lines, which is why it is a named function rather than a ternary inside the
|
|
123
|
+
* loop: `inconclusive` becomes `unreachable`, which `assessDomainDrift` holds
|
|
124
|
+
* on — it neither counts the failure nor clears a run already gathered. An
|
|
125
|
+
* outage must not manufacture evidence, and must not launder away evidence
|
|
126
|
+
* either.
|
|
127
|
+
*/
|
|
128
|
+
export declare function driftProbeStatus(verdict: 'verified' | 'failed' | 'inconclusive'): DomainProbeStatus;
|
|
129
|
+
export interface SendingDomainRecheckOptions {
|
|
130
|
+
nowMs?: number;
|
|
131
|
+
/** Injectable for tests; defaults to the Admin SDK's Firestore. */
|
|
132
|
+
firestore?: any;
|
|
133
|
+
batch?: number;
|
|
134
|
+
recheckAfterMs?: number;
|
|
135
|
+
failuresBeforeRevoke?: number;
|
|
136
|
+
minAgeMs?: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Re-check the verified sending domains whose last check has gone stale.
|
|
140
|
+
*
|
|
141
|
+
* Bounded and idempotent, as every handler on the job beat must be: each pass
|
|
142
|
+
* stamps `lastCheckedAtMs`, which moves the domain to the back of the queue,
|
|
143
|
+
* so the sweep resumes without carrying a cursor and an overlapping beat
|
|
144
|
+
* re-checks at worst a domain that was just checked.
|
|
145
|
+
*
|
|
146
|
+
* Never throws for one bad zone. A single unresolvable domain must not stop
|
|
147
|
+
* the platform's other customers from being checked, and the runner's error
|
|
148
|
+
* isolation is a coarser tool than that — it would lose the rest of the batch.
|
|
149
|
+
*/
|
|
150
|
+
export declare function recheckSendingDomains(options?: SendingDomainRecheckOptions): Promise<SendingDomainRecheckSummary>;
|
|
151
|
+
export default recheckSendingDomains;
|