@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,119 @@
|
|
|
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
|
+
*/ import { createHmac, timingSafeEqual } from "crypto";
|
|
17
|
+
import { tokenSigningSecret } from "./media-signing.js";
|
|
18
|
+
/**
|
|
19
|
+
* Editor-presence HINT tokens for the tenant admin bar on `*.aglyn.app`
|
|
20
|
+
* (AGL-1842, the cross-site half AGL-1829 could not reach).
|
|
21
|
+
*
|
|
22
|
+
* The console session lives on `app.aglyn.com`; `*.aglyn.app` is a different
|
|
23
|
+
* registrable domain, so neither the AGL-1829 hint cookie nor the silent
|
|
24
|
+
* iframe probe can cross over — storage partitioning keeps the two worlds
|
|
25
|
+
* apart, correctly. What CAN cross is a top-level navigation: at console
|
|
26
|
+
* sign-in the browser is bounced once through
|
|
27
|
+
* `https://console.aglyn.app/api/edit-hint/set?sig=<bounce token>` and back,
|
|
28
|
+
* and in that first-party moment the tenant app plants a signed hint cookie
|
|
29
|
+
* on `Domain=.aglyn.app` that every tenant subdomain can present to its own
|
|
30
|
+
* server afterwards.
|
|
31
|
+
*
|
|
32
|
+
* Two kinds, deliberately non-interchangeable:
|
|
33
|
+
*
|
|
34
|
+
* - `bounce` — rides the redirect URL once. URLs are logged and shareable,
|
|
35
|
+
* so its TTL is seconds ({@link EDIT_HINT_BOUNCE_TTL_MS}).
|
|
36
|
+
* - `cookie` — the planted `aglyn_edit_hint` value, HttpOnly, living the
|
|
37
|
+
* {@link EDIT_HINT_COOKIE_TTL_MS} window between console visits.
|
|
38
|
+
*
|
|
39
|
+
* The kind is INSIDE the signed context string, so a leaked 60-second bounce
|
|
40
|
+
* URL can never be replayed as a week-long cookie, and a stolen cookie value
|
|
41
|
+
* can never be planted elsewhere through the bounce endpoint. The `edit-hint:`
|
|
42
|
+
* prefix domain-separates both from the `edit-bar:`/`media:`/commerce
|
|
43
|
+
* signatures sharing `TOKEN_SIGNING_SECRET`.
|
|
44
|
+
*
|
|
45
|
+
* A hint is NOT edit access: it names a uid and nothing else. Everything
|
|
46
|
+
* real happens server-side at the tenant's `/api/edit-access/exchange`,
|
|
47
|
+
* which re-authorizes that uid against the specific host being viewed with
|
|
48
|
+
* the same membership gate the console's token mint applies. A forged,
|
|
49
|
+
* expired, or stale hint costs one refused POST.
|
|
50
|
+
*/ /** Seconds, not minutes: the bounce URL is single-use inside one redirect. */ export const EDIT_HINT_BOUNCE_TTL_MS = 60 * 1000;
|
|
51
|
+
/** 7 days — matches the AGL-1829 marker cookie's refresh cadence. */ export const EDIT_HINT_COOKIE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
52
|
+
/**
|
|
53
|
+
* The HttpOnly signed hint cookie on `Domain=.aglyn.app`. Sibling of the
|
|
54
|
+
* JS-visible `aglyn_editor=1` marker (`EDITOR_HINT_COOKIE`), which the
|
|
55
|
+
* admin-bar stub reads to arm; this one only ever travels back to the
|
|
56
|
+
* tenant's own exchange route, where it is verified server-side.
|
|
57
|
+
*/ export const EDIT_HINT_COOKIE = 'aglyn_edit_hint';
|
|
58
|
+
/** Version tag, mirroring the edit-access token's debuggability rationale. */ const TOKEN_PREFIX = 'aglyn-edit-hint-v1';
|
|
59
|
+
function signPayload(kind, payloadB64) {
|
|
60
|
+
return createHmac('sha256', tokenSigningSecret()).update(`edit-hint:${kind}:${payloadB64}`).digest('base64url');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Mints a hint of the given kind for a uid the caller has ALREADY verified
|
|
64
|
+
* (the console route verifies a Firebase ID token; the bounce endpoint
|
|
65
|
+
* verifies a `bounce` hint). The mint itself checks nothing.
|
|
66
|
+
*/ export function mintEditHintToken(kind, uid, nowMs = Date.now()) {
|
|
67
|
+
if (!uid) throw new Error('uid is required');
|
|
68
|
+
const exp = nowMs + (kind === 'bounce' ? EDIT_HINT_BOUNCE_TTL_MS : EDIT_HINT_COOKIE_TTL_MS);
|
|
69
|
+
const claims = {
|
|
70
|
+
uid,
|
|
71
|
+
exp
|
|
72
|
+
};
|
|
73
|
+
const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url');
|
|
74
|
+
return {
|
|
75
|
+
token: `${TOKEN_PREFIX}.${payload}.${signPayload(kind, payload)}`,
|
|
76
|
+
expiresAtMs: exp
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Verifies a presented hint AS the stated kind and returns its claims, or
|
|
81
|
+
* `null` for anything else — expired, tampered, malformed, wrong version,
|
|
82
|
+
* wrong KIND (the replay wall), or a deploy with no signing secret (fail
|
|
83
|
+
* closed, like every signature in this family).
|
|
84
|
+
*/ export function verifyEditHintToken(kind, token, nowMs = Date.now()) {
|
|
85
|
+
if (typeof token !== 'string' || token.length === 0 || token.length > 4096) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const parts = token.split('.');
|
|
89
|
+
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX) return null;
|
|
90
|
+
const [, payload, sig] = parts;
|
|
91
|
+
let expected;
|
|
92
|
+
try {
|
|
93
|
+
expected = signPayload(kind, payload);
|
|
94
|
+
} catch (unused) {
|
|
95
|
+
// Secret missing — refuse rather than trusting anything.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// Length-checked before timingSafeEqual, which THROWS on attacker-length
|
|
99
|
+
// input — a 500 where a refusal belongs (same note as edit-access-token).
|
|
100
|
+
if (sig.length !== expected.length) return null;
|
|
101
|
+
if (!timingSafeEqual(new Uint8Array(Buffer.from(sig)), new Uint8Array(Buffer.from(expected)))) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
let claims;
|
|
105
|
+
try {
|
|
106
|
+
claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
107
|
+
} catch (unused) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const exp = Number(claims == null ? void 0 : claims.exp);
|
|
111
|
+
if (!Number.isFinite(exp) || exp <= nowMs) return null;
|
|
112
|
+
if (typeof claims.uid !== 'string' || !claims.uid) return null;
|
|
113
|
+
return {
|
|
114
|
+
uid: claims.uid,
|
|
115
|
+
exp
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=edit-hint-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/edit-hint-token.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\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport { tokenSigningSecret } from './media-signing'\n\n/**\n * Editor-presence HINT tokens for the tenant admin bar on `*.aglyn.app`\n * (AGL-1842, the cross-site half AGL-1829 could not reach).\n *\n * The console session lives on `app.aglyn.com`; `*.aglyn.app` is a different\n * registrable domain, so neither the AGL-1829 hint cookie nor the silent\n * iframe probe can cross over — storage partitioning keeps the two worlds\n * apart, correctly. What CAN cross is a top-level navigation: at console\n * sign-in the browser is bounced once through\n * `https://console.aglyn.app/api/edit-hint/set?sig=<bounce token>` and back,\n * and in that first-party moment the tenant app plants a signed hint cookie\n * on `Domain=.aglyn.app` that every tenant subdomain can present to its own\n * server afterwards.\n *\n * Two kinds, deliberately non-interchangeable:\n *\n * - `bounce` — rides the redirect URL once. URLs are logged and shareable,\n * so its TTL is seconds ({@link EDIT_HINT_BOUNCE_TTL_MS}).\n * - `cookie` — the planted `aglyn_edit_hint` value, HttpOnly, living the\n * {@link EDIT_HINT_COOKIE_TTL_MS} window between console visits.\n *\n * The kind is INSIDE the signed context string, so a leaked 60-second bounce\n * URL can never be replayed as a week-long cookie, and a stolen cookie value\n * can never be planted elsewhere through the bounce endpoint. The `edit-hint:`\n * prefix domain-separates both from the `edit-bar:`/`media:`/commerce\n * signatures sharing `TOKEN_SIGNING_SECRET`.\n *\n * A hint is NOT edit access: it names a uid and nothing else. Everything\n * real happens server-side at the tenant's `/api/edit-access/exchange`,\n * which re-authorizes that uid against the specific host being viewed with\n * the same membership gate the console's token mint applies. A forged,\n * expired, or stale hint costs one refused POST.\n */\n\n/** Seconds, not minutes: the bounce URL is single-use inside one redirect. */\nexport const EDIT_HINT_BOUNCE_TTL_MS = 60 * 1000\n\n/** 7 days — matches the AGL-1829 marker cookie's refresh cadence. */\nexport const EDIT_HINT_COOKIE_TTL_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * The HttpOnly signed hint cookie on `Domain=.aglyn.app`. Sibling of the\n * JS-visible `aglyn_editor=1` marker (`EDITOR_HINT_COOKIE`), which the\n * admin-bar stub reads to arm; this one only ever travels back to the\n * tenant's own exchange route, where it is verified server-side.\n */\nexport const EDIT_HINT_COOKIE = 'aglyn_edit_hint'\n\n/** Version tag, mirroring the edit-access token's debuggability rationale. */\nconst TOKEN_PREFIX = 'aglyn-edit-hint-v1'\n\nexport type EditHintKind = 'bounce' | 'cookie'\n\nexport interface EditHintClaims {\n uid: string\n /** Expiry, epoch ms. */\n exp: number\n}\n\nfunction signPayload(kind: EditHintKind, payloadB64: string): string {\n return createHmac('sha256', tokenSigningSecret())\n .update(`edit-hint:${kind}:${payloadB64}`)\n .digest('base64url')\n}\n\nexport interface MintedEditHintToken {\n token: string\n expiresAtMs: number\n}\n\n/**\n * Mints a hint of the given kind for a uid the caller has ALREADY verified\n * (the console route verifies a Firebase ID token; the bounce endpoint\n * verifies a `bounce` hint). The mint itself checks nothing.\n */\nexport function mintEditHintToken(\n kind: EditHintKind,\n uid: string,\n nowMs: number = Date.now(),\n): MintedEditHintToken {\n if (!uid) throw new Error('uid is required')\n const exp =\n nowMs +\n (kind === 'bounce' ? EDIT_HINT_BOUNCE_TTL_MS : EDIT_HINT_COOKIE_TTL_MS)\n const claims: EditHintClaims = { uid, exp }\n const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString(\n 'base64url',\n )\n return {\n token: `${TOKEN_PREFIX}.${payload}.${signPayload(kind, payload)}`,\n expiresAtMs: exp,\n }\n}\n\n/**\n * Verifies a presented hint AS the stated kind and returns its claims, or\n * `null` for anything else — expired, tampered, malformed, wrong version,\n * wrong KIND (the replay wall), or a deploy with no signing secret (fail\n * closed, like every signature in this family).\n */\nexport function verifyEditHintToken(\n kind: EditHintKind,\n token: unknown,\n nowMs: number = Date.now(),\n): EditHintClaims | null {\n if (typeof token !== 'string' || token.length === 0 || token.length > 4096) {\n return null\n }\n const parts = token.split('.')\n if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX) return null\n const [, payload, sig] = parts\n let expected: string\n try {\n expected = signPayload(kind, payload)\n } catch {\n // Secret missing — refuse rather than trusting anything.\n return null\n }\n // Length-checked before timingSafeEqual, which THROWS on attacker-length\n // input — a 500 where a refusal belongs (same note as edit-access-token).\n if (sig.length !== expected.length) return null\n if (\n !timingSafeEqual(\n new Uint8Array(Buffer.from(sig)),\n new Uint8Array(Buffer.from(expected)),\n )\n ) {\n return null\n }\n let claims: EditHintClaims\n try {\n claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n const exp = Number(claims?.exp)\n if (!Number.isFinite(exp) || exp <= nowMs) return null\n if (typeof claims.uid !== 'string' || !claims.uid) return null\n return { uid: claims.uid, exp }\n}\n"],"names":["createHmac","timingSafeEqual","tokenSigningSecret","EDIT_HINT_BOUNCE_TTL_MS","EDIT_HINT_COOKIE_TTL_MS","EDIT_HINT_COOKIE","TOKEN_PREFIX","signPayload","kind","payloadB64","update","digest","mintEditHintToken","uid","nowMs","Date","now","Error","exp","claims","payload","Buffer","from","JSON","stringify","toString","token","expiresAtMs","verifyEditHintToken","length","parts","split","sig","expected","Uint8Array","parse","Number","isFinite"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,eAAe,QAAQ,SAAQ;AACpD,SAASC,kBAAkB,QAAQ,qBAAiB;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GAED,4EAA4E,GAC5E,OAAO,MAAMC,0BAA0B,KAAK,KAAI;AAEhD,mEAAmE,GACnE,OAAO,MAAMC,0BAA0B,IAAI,KAAK,KAAK,KAAK,KAAI;AAE9D;;;;;CAKC,GACD,OAAO,MAAMC,mBAAmB,kBAAiB;AAEjD,4EAA4E,GAC5E,MAAMC,eAAe;AAUrB,SAASC,YAAYC,IAAkB,EAAEC,UAAkB;IACzD,OAAOT,WAAW,UAAUE,sBACzBQ,MAAM,CAAC,CAAC,UAAU,EAAEF,KAAK,CAAC,EAAEC,YAAY,EACxCE,MAAM,CAAC;AACZ;AAOA;;;;CAIC,GACD,OAAO,SAASC,kBACdJ,IAAkB,EAClBK,GAAW,EACXC,QAAgBC,KAAKC,GAAG,EAAE;IAE1B,IAAI,CAACH,KAAK,MAAM,IAAII,MAAM;IAC1B,MAAMC,MACJJ,QACCN,CAAAA,SAAS,WAAWL,0BAA0BC,uBAAsB;IACvE,MAAMe,SAAyB;QAAEN;QAAKK;IAAI;IAC1C,MAAME,UAAUC,OAAOC,IAAI,CAACC,KAAKC,SAAS,CAACL,SAAS,QAAQM,QAAQ,CAClE;IAEF,OAAO;QACLC,OAAO,GAAGpB,aAAa,CAAC,EAAEc,QAAQ,CAAC,EAAEb,YAAYC,MAAMY,UAAU;QACjEO,aAAaT;IACf;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASU,oBACdpB,IAAkB,EAClBkB,KAAc,EACdZ,QAAgBC,KAAKC,GAAG,EAAE;IAE1B,IAAI,OAAOU,UAAU,YAAYA,MAAMG,MAAM,KAAK,KAAKH,MAAMG,MAAM,GAAG,MAAM;QAC1E,OAAO;IACT;IACA,MAAMC,QAAQJ,MAAMK,KAAK,CAAC;IAC1B,IAAID,MAAMD,MAAM,KAAK,KAAKC,KAAK,CAAC,EAAE,KAAKxB,cAAc,OAAO;IAC5D,MAAM,GAAGc,SAASY,IAAI,GAAGF;IACzB,IAAIG;IACJ,IAAI;QACFA,WAAW1B,YAAYC,MAAMY;IAC/B,EAAE,eAAM;QACN,yDAAyD;QACzD,OAAO;IACT;IACA,yEAAyE;IACzE,0EAA0E;IAC1E,IAAIY,IAAIH,MAAM,KAAKI,SAASJ,MAAM,EAAE,OAAO;IAC3C,IACE,CAAC5B,gBACC,IAAIiC,WAAWb,OAAOC,IAAI,CAACU,OAC3B,IAAIE,WAAWb,OAAOC,IAAI,CAACW,aAE7B;QACA,OAAO;IACT;IACA,IAAId;IACJ,IAAI;QACFA,SAASI,KAAKY,KAAK,CAACd,OAAOC,IAAI,CAACF,SAAS,aAAaK,QAAQ,CAAC;IACjE,EAAE,eAAM;QACN,OAAO;IACT;IACA,MAAMP,MAAMkB,OAAOjB,0BAAAA,OAAQD,GAAG;IAC9B,IAAI,CAACkB,OAAOC,QAAQ,CAACnB,QAAQA,OAAOJ,OAAO,OAAO;IAClD,IAAI,OAAOK,OAAON,GAAG,KAAK,YAAY,CAACM,OAAON,GAAG,EAAE,OAAO;IAC1D,OAAO;QAAEA,KAAKM,OAAON,GAAG;QAAEK;IAAI;AAChC"}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type EmailDeliveryEvent, type EmailDeliveryEventType, type EmailDeliveryHistorySource, type EmailDeliverySnapshot } from '@aglyn/shared-util-email';
|
|
18
|
+
export declare const EMAIL_DELIVERIES_COLLECTION = "emailDeliveries";
|
|
19
|
+
export declare const EMAIL_DELIVERY_MESSAGES_COLLECTION = "messages";
|
|
20
|
+
/** The most messages one staff read will return. */
|
|
21
|
+
export declare const EMAIL_DELIVERY_READ_LIMIT = 50;
|
|
22
|
+
/**
|
|
23
|
+
* The most distinct links one message records.
|
|
24
|
+
*
|
|
25
|
+
* A newsletter with forty links clicked by one reader must not grow the
|
|
26
|
+
* document without bound; the first few tell a staffer what they need.
|
|
27
|
+
*/
|
|
28
|
+
export declare const EMAIL_DELIVERY_MAX_LINKS = 10;
|
|
29
|
+
/** One message as the staff view reads it. */
|
|
30
|
+
export interface EmailDeliveryRecord {
|
|
31
|
+
/** The provider's message id — also the document id. */
|
|
32
|
+
messageId: string;
|
|
33
|
+
provider: string;
|
|
34
|
+
to: string;
|
|
35
|
+
subject: string | null;
|
|
36
|
+
/** The sender label, e.g. `'invite'`. Null for a send that carried none. */
|
|
37
|
+
context: string | null;
|
|
38
|
+
/** Furthest-along (worst) lifecycle state seen. */
|
|
39
|
+
status: EmailDeliveryEventType;
|
|
40
|
+
/** Epoch ms per state, absent for states that never happened. */
|
|
41
|
+
timestamps: Partial<Record<EmailDeliveryEventType, number>>;
|
|
42
|
+
/** First event we saw for this message. Always present — the sort key. */
|
|
43
|
+
firstSeenAtMs: number;
|
|
44
|
+
openCount: number;
|
|
45
|
+
clickCount: number;
|
|
46
|
+
/** Distinct destinations followed, capped. */
|
|
47
|
+
clickedLinks: string[];
|
|
48
|
+
bounceType: string | null;
|
|
49
|
+
detail: string | null;
|
|
50
|
+
hostId: string | null;
|
|
51
|
+
campaignId: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What one {@link recordEmailDeliveryEvent} call did.
|
|
55
|
+
*
|
|
56
|
+
* `firstOfType` exists so a CAMPAIGN counter can be incremented once per
|
|
57
|
+
* recipient without buying a read of its own. This transaction already holds
|
|
58
|
+
* the message's prior state, and "has this message ever been opened before"
|
|
59
|
+
* is the fact a distinct-openers count needs — deriving it here costs
|
|
60
|
+
* nothing, and deriving it anywhere else costs a document read per event.
|
|
61
|
+
*
|
|
62
|
+
* It is also what makes those counters idempotent, on the same reasoning the
|
|
63
|
+
* webhook's replay guard rests on: a redelivered or replayed event finds the
|
|
64
|
+
* state already recorded and reports `false`, so the counter cannot be
|
|
65
|
+
* incremented twice for one message's first open.
|
|
66
|
+
*/
|
|
67
|
+
export interface EmailDeliveryEventOutcome {
|
|
68
|
+
/**
|
|
69
|
+
* No event of this TYPE had been recorded against this message before.
|
|
70
|
+
*
|
|
71
|
+
* Read off `timestamps`, which is written for every event type, rather than
|
|
72
|
+
* off `openCount`/`clickCount`, which exist for two of them.
|
|
73
|
+
*/
|
|
74
|
+
firstOfType: boolean;
|
|
75
|
+
/** The message this event was recorded against. */
|
|
76
|
+
providerMessageId: string;
|
|
77
|
+
/** The recipient, lowercased — the person the event is about. */
|
|
78
|
+
to: string;
|
|
79
|
+
/** Which event this was. */
|
|
80
|
+
type: EmailDeliveryEventType;
|
|
81
|
+
/** When it happened, epoch ms. */
|
|
82
|
+
at: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Records one normalized event against its message.
|
|
86
|
+
*
|
|
87
|
+
* A transaction rather than a merge-set, for one property that matters to the
|
|
88
|
+
* reader: `firstSeenAtMs` must be written exactly once and must never be
|
|
89
|
+
* absent. Events arrive out of order — an `opened` can beat its own `sent`
|
|
90
|
+
* through the queue — so "create with the first event's time, then leave it
|
|
91
|
+
* alone" needs a read in the same atomic step as the write. A document missing
|
|
92
|
+
* that field would be dropped from the `orderBy` read entirely and the message
|
|
93
|
+
* would simply not appear, which is the failure mode a delivery log can least
|
|
94
|
+
* afford.
|
|
95
|
+
*
|
|
96
|
+
* @returns the outcome, or `null` when nothing was written. `null` is the
|
|
97
|
+
* ordinary answer for an address that is not an address; it is never
|
|
98
|
+
* an error.
|
|
99
|
+
*/
|
|
100
|
+
export declare function recordEmailDeliveryEvent(event: EmailDeliveryEvent, firestore?: any): Promise<EmailDeliveryEventOutcome | null>;
|
|
101
|
+
/**
|
|
102
|
+
* Records one message the PROVIDER already knows about — the history import.
|
|
103
|
+
*
|
|
104
|
+
* ## Why this is not just `recordEmailDeliveryEvent` with a made-up event
|
|
105
|
+
*
|
|
106
|
+
* A snapshot is weaker evidence than an event, in two specific ways, and
|
|
107
|
+
* writing it as an event would silently promote it:
|
|
108
|
+
*
|
|
109
|
+
* - **It carries no counts.** A provider's list reports one `last_event` per
|
|
110
|
+
* message and no engagement detail, so `opened` means "at least once" and
|
|
111
|
+
* can never mean "three times". Incrementing `openCount` from a snapshot
|
|
112
|
+
* would invent a number, and re-running the import would invent it again.
|
|
113
|
+
* - **It can be STALER than what we already hold.** The event feed is live;
|
|
114
|
+
* an import is a page of results fetched some time ago. So the status is
|
|
115
|
+
* merged with {@link worstDeliveryStatus} rather than assigned, and a row
|
|
116
|
+
* the webhook has already advanced is never walked backwards.
|
|
117
|
+
*
|
|
118
|
+
* Everything else it fills is a gap-fill only: `subject` and `sentAt` are
|
|
119
|
+
* written when absent and left alone when present. The net effect is that
|
|
120
|
+
* importing history is idempotent and can be run as often as you like, and a
|
|
121
|
+
* message the event feed has covered is untouched by it.
|
|
122
|
+
*
|
|
123
|
+
* `context` is deliberately NOT recoverable here. It comes from a send tag,
|
|
124
|
+
* and the list endpoint does not return tags — so an imported row shows the
|
|
125
|
+
* subject and the status but cannot say which of our senders produced it. The
|
|
126
|
+
* card renders that absence rather than guessing.
|
|
127
|
+
*
|
|
128
|
+
* @returns whether a row was written or updated.
|
|
129
|
+
*/
|
|
130
|
+
export declare function recordEmailDeliverySnapshot(snapshot: EmailDeliverySnapshot, firestore?: any): Promise<boolean>;
|
|
131
|
+
/**
|
|
132
|
+
* Records a batch, independently — one bad event must not lose the others.
|
|
133
|
+
*
|
|
134
|
+
* @returns one outcome per event that was WRITTEN; events that wrote nothing
|
|
135
|
+
* are absent, so the length is still the count the old return value
|
|
136
|
+
* reported.
|
|
137
|
+
*/
|
|
138
|
+
export declare function recordEmailDeliveryEvents(events: EmailDeliveryEvent[], firestore?: any): Promise<EmailDeliveryEventOutcome[]>;
|
|
139
|
+
/** What one person's mail says about whether they are still listening. */
|
|
140
|
+
export interface EmailPersonEngagement {
|
|
141
|
+
/** The later of {@link lastOpenedAtMs} and {@link lastClickedAtMs}. */
|
|
142
|
+
lastEngagedAtMs: number | null;
|
|
143
|
+
lastOpenedAtMs: number | null;
|
|
144
|
+
/**
|
|
145
|
+
* Clicks are the metric to lean on. Apple's Mail Privacy Protection
|
|
146
|
+
* prefetches images, so an open is partly a statement about the recipient's
|
|
147
|
+
* mail client; a click is a statement about the recipient.
|
|
148
|
+
*/
|
|
149
|
+
lastClickedAtMs: number | null;
|
|
150
|
+
}
|
|
151
|
+
/** The empty answer, so a caller never has to invent one. */
|
|
152
|
+
export declare const NO_PERSON_ENGAGEMENT: EmailPersonEngagement;
|
|
153
|
+
/**
|
|
154
|
+
* Advances the engagement stamps for the people these outcomes are about.
|
|
155
|
+
*
|
|
156
|
+
* A transaction, and it buys exactly one property: the stamps only ever move
|
|
157
|
+
* FORWARD. Provider events are not ordered, and a replay of an event whose
|
|
158
|
+
* first delivery never landed can carry an instant from months ago — a blind
|
|
159
|
+
* merge-set would let that overwrite a fresh stamp and quietly make an active
|
|
160
|
+
* subscriber look cold to a control whose whole job is refusing to mail cold
|
|
161
|
+
* people. Reading before writing is a cheaper unit than the write beside it,
|
|
162
|
+
* and it happens at most once per message per event type.
|
|
163
|
+
*
|
|
164
|
+
* Never throws, for the same reason nothing else in this file does: a rollup
|
|
165
|
+
* that failed loses a stamp, and a rollup that threw would lose the webhook's
|
|
166
|
+
* acknowledgement and teach the provider to retry the whole event.
|
|
167
|
+
*
|
|
168
|
+
* @returns how many person documents were written.
|
|
169
|
+
*/
|
|
170
|
+
export declare function recordPersonEngagement(outcomes: readonly EmailDeliveryEventOutcome[], firestore?: any): Promise<number>;
|
|
171
|
+
/**
|
|
172
|
+
* One person's engagement, by address. Never throws.
|
|
173
|
+
*
|
|
174
|
+
* Returns {@link NO_PERSON_ENGAGEMENT} for an address we hold nothing about,
|
|
175
|
+
* AND for a read that failed. The two are deliberately the same answer here:
|
|
176
|
+
* every caller uses this to decide whether to REFUSE something, and both
|
|
177
|
+
* readings must resolve to "we have no evidence this person is cold", which
|
|
178
|
+
* is the only safe direction for a control that stops mail.
|
|
179
|
+
*/
|
|
180
|
+
export declare function readPersonEngagement(email: string | null | undefined, firestore?: any): Promise<EmailPersonEngagement>;
|
|
181
|
+
/**
|
|
182
|
+
* Engagement for many people at once, keyed by their person key.
|
|
183
|
+
*
|
|
184
|
+
* A `getAll` rather than a query: these are keyed document reads, so this
|
|
185
|
+
* needs no index, cannot be truncated by a `limit`, and cannot drop somebody
|
|
186
|
+
* for missing a field the way an `orderBy` would. The audience materializer
|
|
187
|
+
* calls it a page at a time and counts every read against its scan budget.
|
|
188
|
+
*
|
|
189
|
+
* A key with no document is present in the result with
|
|
190
|
+
* {@link NO_PERSON_ENGAGEMENT}, so a caller never has to tell "absent" from
|
|
191
|
+
* "not read" — and a failure returns every requested key that way for the
|
|
192
|
+
* same reason {@link readPersonEngagement} does.
|
|
193
|
+
*/
|
|
194
|
+
export declare function readPersonEngagementByKeys(keys: readonly string[], firestore?: any): Promise<Map<string, EmailPersonEngagement>>;
|
|
195
|
+
/** The field on `emailDeliveries/{key}` holding the per-host touches. */
|
|
196
|
+
export declare const EMAIL_TOUCH_FIELD = "campaignTouches";
|
|
197
|
+
/**
|
|
198
|
+
* How many sites' touches one person's document keeps.
|
|
199
|
+
*
|
|
200
|
+
* A cap, not a page size: the map lives in a document with a 1 MiB ceiling
|
|
201
|
+
* and nothing else bounds how many sites may mail one address.
|
|
202
|
+
*/
|
|
203
|
+
export declare const EMAIL_TOUCH_MAX_HOSTS = 10;
|
|
204
|
+
/** The last campaign one person clicked on one site. */
|
|
205
|
+
export interface EmailCampaignTouch {
|
|
206
|
+
hostId: string;
|
|
207
|
+
campaignId: string;
|
|
208
|
+
/** When the click happened, epoch ms — the provider's instant. */
|
|
209
|
+
clickedAtMs: number;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Records that this person clicked this campaign's mail. Never throws.
|
|
213
|
+
*
|
|
214
|
+
* Forward-only, in a transaction, for the reason {@link recordPersonEngagement}
|
|
215
|
+
* is: provider delivery is at-least-once and unordered, so a replayed click
|
|
216
|
+
* from last month must not displace this week's. That same property is what
|
|
217
|
+
* makes this idempotent — a redelivered event finds its own instant already
|
|
218
|
+
* stored and writes nothing.
|
|
219
|
+
*
|
|
220
|
+
* @returns whether the touch moved forward.
|
|
221
|
+
*/
|
|
222
|
+
export declare function recordEmailCampaignTouch(touch: {
|
|
223
|
+
email: string | null | undefined;
|
|
224
|
+
hostId: string;
|
|
225
|
+
campaignId: string;
|
|
226
|
+
atMs: number;
|
|
227
|
+
}, firestore?: any): Promise<boolean>;
|
|
228
|
+
/**
|
|
229
|
+
* The last campaign this person clicked on this site, or `null`.
|
|
230
|
+
*
|
|
231
|
+
* One keyed document read — no query, no index, and nothing that can be
|
|
232
|
+
* truncated. `null` for an address we hold no touch for AND for a read that
|
|
233
|
+
* failed, which are the same answer on purpose: both mean "we cannot say
|
|
234
|
+
* which campaign preceded this order", and the only safe thing to do with
|
|
235
|
+
* that is credit nobody.
|
|
236
|
+
*/
|
|
237
|
+
export declare function readEmailCampaignTouch(email: string | null | undefined, hostId: string, firestore?: any): Promise<EmailCampaignTouch | null>;
|
|
238
|
+
/** What one {@link importEmailDeliveryHistory} run did. */
|
|
239
|
+
export interface EmailDeliveryImportResult {
|
|
240
|
+
/** Provider messages read. */
|
|
241
|
+
scanned: number;
|
|
242
|
+
/** Per-recipient rows written or refreshed. */
|
|
243
|
+
recorded: number;
|
|
244
|
+
pages: number;
|
|
245
|
+
/** Cursor to resume from, or null when the history was exhausted. */
|
|
246
|
+
nextCursor: string | null;
|
|
247
|
+
/** True when the page budget ran out before the history did. */
|
|
248
|
+
truncated: boolean;
|
|
249
|
+
}
|
|
250
|
+
/** Default page budget for one import run. 100 messages per page. */
|
|
251
|
+
export declare const EMAIL_DELIVERY_IMPORT_MAX_PAGES = 20;
|
|
252
|
+
/**
|
|
253
|
+
* Imports already-sent mail from a provider into the log.
|
|
254
|
+
*
|
|
255
|
+
* Bounded by PAGES rather than run to completion: this is called from a
|
|
256
|
+
* request handler, and an account with a large history would otherwise hold
|
|
257
|
+
* one open until it timed out — losing every page it had already written,
|
|
258
|
+
* because a partial import that reports nothing is indistinguishable from one
|
|
259
|
+
* that did nothing. Instead it stops at the budget, returns `nextCursor`, and
|
|
260
|
+
* the caller resumes. Every page is written before the next is fetched, so an
|
|
261
|
+
* interrupted run keeps its work.
|
|
262
|
+
*
|
|
263
|
+
* Idempotent by construction — see {@link recordEmailDeliverySnapshot}: a
|
|
264
|
+
* message the event feed already covered is not walked backwards, and
|
|
265
|
+
* re-running invents no counts.
|
|
266
|
+
*
|
|
267
|
+
* The `source` is injected rather than constructed here. This module may not
|
|
268
|
+
* know which provider is in use, and a test must be able to run the whole
|
|
269
|
+
* loop — pagination, cursor handling, the stop condition — without a network.
|
|
270
|
+
*/
|
|
271
|
+
export declare function importEmailDeliveryHistory(options: {
|
|
272
|
+
source: EmailDeliveryHistorySource;
|
|
273
|
+
cursor?: string | null;
|
|
274
|
+
maxPages?: number;
|
|
275
|
+
firestore?: any;
|
|
276
|
+
}): Promise<EmailDeliveryImportResult>;
|
|
277
|
+
/**
|
|
278
|
+
* The messages sent to one address, newest first.
|
|
279
|
+
*
|
|
280
|
+
* Ordered on `firstSeenAtMs`, which the writer guarantees on creation, rather
|
|
281
|
+
* than on a per-state timestamp that only some rows carry: `orderBy` drops
|
|
282
|
+
* every document missing the field, so ordering on `timestamps.sent` would
|
|
283
|
+
* silently hide any message whose `sent` webhook never arrived — exactly the
|
|
284
|
+
* message a staffer is looking for.
|
|
285
|
+
*
|
|
286
|
+
* @returns the rows, or an empty array. The caller distinguishes "none" from
|
|
287
|
+
* "could not read" through {@link readEmailDeliveryHistory}.
|
|
288
|
+
*/
|
|
289
|
+
export declare function readEmailDeliveries(email: string | null | undefined, options?: {
|
|
290
|
+
limit?: number;
|
|
291
|
+
firestore?: any;
|
|
292
|
+
}): Promise<EmailDeliveryRecord[]>;
|
|
293
|
+
/**
|
|
294
|
+
* {@link readEmailDeliveries} with the read failure kept separate from an
|
|
295
|
+
* empty result.
|
|
296
|
+
*
|
|
297
|
+
* The same shape `devices` uses on the staff detail route, for the same
|
|
298
|
+
* reason: "we have no record of any email to this person" and "we could not
|
|
299
|
+
* reach the log" lead a staffer to opposite next actions, and a card that
|
|
300
|
+
* renders both as an empty table sends them down the wrong one.
|
|
301
|
+
*/
|
|
302
|
+
export declare function readEmailDeliveryHistory(email: string | null | undefined, options?: {
|
|
303
|
+
limit?: number;
|
|
304
|
+
firestore?: any;
|
|
305
|
+
}): Promise<{
|
|
306
|
+
lookupFailed: boolean;
|
|
307
|
+
rows: EmailDeliveryRecord[];
|
|
308
|
+
}>;
|
|
309
|
+
/** The most recipient rows one campaign-engagement read returns. */
|
|
310
|
+
export declare const EMAIL_CAMPAIGN_ENGAGEMENT_PAGE_SIZE = 25;
|
|
311
|
+
/**
|
|
312
|
+
* How many campaigns one engagement read can span.
|
|
313
|
+
*
|
|
314
|
+
* Firestore's `in` operator takes at most 30 values, and the query below runs
|
|
315
|
+
* as a merge of one sub-query per value — so this is a hard limit of the
|
|
316
|
+
* store rather than a number worth tuning. A design used by more campaigns
|
|
317
|
+
* than this reads its most recent 30, and the caller is told so.
|
|
318
|
+
*/
|
|
319
|
+
export declare const EMAIL_CAMPAIGN_ENGAGEMENT_MAX_CAMPAIGNS = 30;
|
|
320
|
+
/** Which recipients a campaign-engagement read returns. */
|
|
321
|
+
export type EmailEngagementFilter = 'all' | 'opened' | 'clicked';
|
|
322
|
+
/** One page of recipient rows. */
|
|
323
|
+
export interface EmailCampaignEngagementPage {
|
|
324
|
+
rows: EmailDeliveryRecord[];
|
|
325
|
+
/**
|
|
326
|
+
* Cursor for the next page, or null at the end.
|
|
327
|
+
*
|
|
328
|
+
* The full document PATH of the last row, which is
|
|
329
|
+
* `emailDeliveries/{sha256(address)}/messages/{messageId}`. It is re-read
|
|
330
|
+
* as a snapshot to resume the query, rather than resuming from the ordered
|
|
331
|
+
* VALUE: a value cursor positions after every document sharing it, so two
|
|
332
|
+
* messages recorded in the same millisecond would lose one of them between
|
|
333
|
+
* pages — silently, and only under load.
|
|
334
|
+
*/
|
|
335
|
+
cursor: string | null;
|
|
336
|
+
/** The read failed, as distinct from finding nothing. */
|
|
337
|
+
lookupFailed: boolean;
|
|
338
|
+
/** Campaigns past {@link EMAIL_CAMPAIGN_ENGAGEMENT_MAX_CAMPAIGNS}. */
|
|
339
|
+
campaignsOmitted: number;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The recipients of one site's campaigns, newest message first.
|
|
343
|
+
*
|
|
344
|
+
* ## What each filter orders on, and why it is not one query with a flag
|
|
345
|
+
*
|
|
346
|
+
* `all` orders on `firstSeenAtMs`, which {@link recordEmailDeliveryEvent}
|
|
347
|
+
* guarantees on creation. `opened` and `clicked` carry an inequality —
|
|
348
|
+
* `openCount > 0` — and Firestore requires the first ordering to be on the
|
|
349
|
+
* inequality's own field, so those two order on the count and then on the
|
|
350
|
+
* time. That is not a workaround: a message never opened has no `openCount`
|
|
351
|
+
* field at all, so the inequality is also what excludes it, and the ordering
|
|
352
|
+
* puts the most engaged recipient first, which is the order a merchant reads
|
|
353
|
+
* such a table in.
|
|
354
|
+
*
|
|
355
|
+
* ## Never throws
|
|
356
|
+
*
|
|
357
|
+
* Same contract as the rest of this file: `lookupFailed` distinguishes a read
|
|
358
|
+
* that could not run — a missing index is the likely one — from a campaign
|
|
359
|
+
* nobody opened. Rendering those two the same way is how a merchant concludes
|
|
360
|
+
* their campaign reached nobody.
|
|
361
|
+
*/
|
|
362
|
+
export declare function readCampaignEngagement(options: {
|
|
363
|
+
/** The site whose mail this is. The caller must already have proven it. */
|
|
364
|
+
hostId: string;
|
|
365
|
+
/** Campaign ids to read, most recent first. */
|
|
366
|
+
campaignIds: readonly string[];
|
|
367
|
+
filter?: EmailEngagementFilter;
|
|
368
|
+
limit?: number;
|
|
369
|
+
/** A `cursor` from a previous page. */
|
|
370
|
+
cursor?: string | null;
|
|
371
|
+
firestore?: any;
|
|
372
|
+
}): Promise<EmailCampaignEngagementPage>;
|
|
373
|
+
/**
|
|
374
|
+
* A record that delivery data WAS held for an address and has been erased.
|
|
375
|
+
*
|
|
376
|
+
* Written into the parent `emailDeliveries/{emailKey}` document, which the
|
|
377
|
+
* messages subcollection otherwise leaves empty.
|
|
378
|
+
*
|
|
379
|
+
* ⚠️ It carries no address, no subject, no message id and no uid — nothing
|
|
380
|
+
* the erasure was performed to destroy. `count` is a magnitude, which is what
|
|
381
|
+
* makes the row honest without reconstituting anything: it says data existed
|
|
382
|
+
* and is gone, and nothing about what it was.
|
|
383
|
+
*/
|
|
384
|
+
export interface EmailDeliveryErasure {
|
|
385
|
+
/** Epoch ms. */
|
|
386
|
+
at: number;
|
|
387
|
+
/** How many messages were removed. */
|
|
388
|
+
count: number;
|
|
389
|
+
}
|
|
390
|
+
/** One account's mail, gathered from every address it holds. */
|
|
391
|
+
export interface EmailDeliveryHistory {
|
|
392
|
+
lookupFailed: boolean;
|
|
393
|
+
rows: EmailDeliveryRecord[];
|
|
394
|
+
/**
|
|
395
|
+
* The addresses actually read, in the order they were given.
|
|
396
|
+
*
|
|
397
|
+
* The card names them. A staffer looking at mail sent to an address that is
|
|
398
|
+
* no longer this account's primary has to be able to see that that is what
|
|
399
|
+
* they are looking at.
|
|
400
|
+
*/
|
|
401
|
+
addressesRead: string[];
|
|
402
|
+
/**
|
|
403
|
+
* Erasure tombstones found, keyed by address.
|
|
404
|
+
*
|
|
405
|
+
* An address whose records were erased under somebody's request reads as an
|
|
406
|
+
* empty table otherwise — which is the precise failure this card's copy
|
|
407
|
+
* warns about, recreated by the fix for it.
|
|
408
|
+
*/
|
|
409
|
+
erasures: Record<string, EmailDeliveryErasure>;
|
|
410
|
+
}
|
|
411
|
+
/** The tombstone on one address, or null. Never throws. */
|
|
412
|
+
export declare function readEmailDeliveryErasure(email: string | null | undefined, firestore?: any): Promise<EmailDeliveryErasure | null>;
|
|
413
|
+
/**
|
|
414
|
+
* Every message sent to any address this account holds, newest first.
|
|
415
|
+
*
|
|
416
|
+
* Merged and re-sorted rather than concatenated: the rows are one person's
|
|
417
|
+
* mail and a staffer reads them as a timeline, so grouping them by which
|
|
418
|
+
* address happened to receive them would put the answer in two places and
|
|
419
|
+
* make "what was the last thing we sent them" a question about two tables.
|
|
420
|
+
* Each row keeps its own `to`, so the card can still say which address.
|
|
421
|
+
*
|
|
422
|
+
* `lookupFailed` is true when ANY address failed. A partial read of a
|
|
423
|
+
* delivery log is the same hazard as an empty one — it under-reports mail we
|
|
424
|
+
* sent — and reporting it as a clean result is how a staffer comes to tell a
|
|
425
|
+
* customer something untrue.
|
|
426
|
+
*/
|
|
427
|
+
export declare function readEmailDeliveryHistoryForAddresses(addresses: readonly string[], options?: {
|
|
428
|
+
limit?: number;
|
|
429
|
+
firestore?: any;
|
|
430
|
+
}): Promise<EmailDeliveryHistory>;
|
|
431
|
+
/** What one multi-address erasure did. */
|
|
432
|
+
export interface EmailDeliveryErasureResult {
|
|
433
|
+
/** Messages removed, across every address that was erased. */
|
|
434
|
+
removed: number;
|
|
435
|
+
/** The addresses actually erased. Tombstoned, one document each. */
|
|
436
|
+
addresses: string[];
|
|
437
|
+
/**
|
|
438
|
+
* Addresses left INTACT because another account is also known to hold them.
|
|
439
|
+
*
|
|
440
|
+
* Never empty and ignorable: a caller erasing an account has to treat a
|
|
441
|
+
* non-empty list as an erasure it did not finish. See
|
|
442
|
+
* {@link eraseEmailDeliveriesForAddresses}.
|
|
443
|
+
*/
|
|
444
|
+
contestedAddresses: string[];
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Erase the delivery log for every address an account holds, except the ones
|
|
448
|
+
* a second account also holds.
|
|
449
|
+
*
|
|
450
|
+
* ## The shared-address decision
|
|
451
|
+
*
|
|
452
|
+
* The log describes an ADDRESS, not an account. Where one account holds an
|
|
453
|
+
* address, erasing it is simply erasing the subject's mail, and this sweeps
|
|
454
|
+
* it.
|
|
455
|
+
*
|
|
456
|
+
* Where TWO accounts hold one address, the same rows are two people's answer
|
|
457
|
+
* to "what did you send me", and the two readings are incompatible:
|
|
458
|
+
*
|
|
459
|
+
* - **One human, two accounts** — the ordinary live shape, an account whose
|
|
460
|
+
* federated provider address is another account's primary. Erasing is
|
|
461
|
+
* right; the mail is the requester's.
|
|
462
|
+
* - **A genuinely shared mailbox** — `billing@`, `support@`, a role account
|
|
463
|
+
* two different people hold. Erasing destroys the second person's delivery
|
|
464
|
+
* history for an address they legitimately hold, and they asked for
|
|
465
|
+
* nothing.
|
|
466
|
+
*
|
|
467
|
+
* ⛔ **Nothing here can tell those apart.** The difference is a fact about the
|
|
468
|
+
* humans, and the data holds no fact about the humans — only that two account
|
|
469
|
+
* records name one address. So this function does not choose. It erases what
|
|
470
|
+
* it can decide about and reports the rest as CONTESTED, and `eraseUser`
|
|
471
|
+
* refuses the whole erasure rather than half-perform one: destroying the
|
|
472
|
+
* second party's mail has no remedy, and quietly leaving it while reporting
|
|
473
|
+
* the erasure complete is the gap this area exists to close. Refusing is the
|
|
474
|
+
* only outcome that is neither, and it is reversible — a human decides which
|
|
475
|
+
* reading applies, detaches the address or confirms the account, and the
|
|
476
|
+
* erasure runs.
|
|
477
|
+
*
|
|
478
|
+
* ⚠️ A contested address is not tombstoned. The tombstone means "the records
|
|
479
|
+
* here were removed under an erasure request", and writing one over rows that
|
|
480
|
+
* are still present would tell the second holder their mail is gone while it
|
|
481
|
+
* sits underneath — a worse misreading than the blank table, because it is
|
|
482
|
+
* confidently wrong rather than merely empty. Nothing was removed, so their
|
|
483
|
+
* card renders their mail exactly as before.
|
|
484
|
+
*
|
|
485
|
+
* ⚠️ `shared` is one-directional evidence. True proves a second holder; false
|
|
486
|
+
* only means none was found, because there is no lookup for an account
|
|
487
|
+
* holding an address through a federated provider (see
|
|
488
|
+
* `account-addresses.ts`). So the tombstone is still written for EVERY
|
|
489
|
+
* address that IS erased, not only ones believed unshared — it costs one
|
|
490
|
+
* small document and closes the case where a second holder exists behind the
|
|
491
|
+
* gap in the probe and would otherwise meet a blank table.
|
|
492
|
+
*
|
|
493
|
+
* ⛔ Only addresses the account HOLDS, resolved through the one resolver. An
|
|
494
|
+
* address arriving here that the account does not hold erases a stranger's
|
|
495
|
+
* mail, which no erasure request authorises.
|
|
496
|
+
*/
|
|
497
|
+
export declare function eraseEmailDeliveriesForAddresses(addresses: readonly {
|
|
498
|
+
address: string;
|
|
499
|
+
shared?: boolean;
|
|
500
|
+
}[], firestore?: any): Promise<EmailDeliveryErasureResult>;
|
|
501
|
+
/**
|
|
502
|
+
* Deletes everything recorded for one address.
|
|
503
|
+
*
|
|
504
|
+
* The log holds an address, the subjects sent to it and when they were opened
|
|
505
|
+
* — personal data by any reading — so the erasure path has to be able to reach
|
|
506
|
+
* it. Batched because a long-lived account can hold hundreds of rows and a
|
|
507
|
+
* single `delete()` per document would be one round trip each.
|
|
508
|
+
*/
|
|
509
|
+
export declare function eraseEmailDeliveries(email: string | null | undefined, firestore?: any): Promise<number>;
|