@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,397 @@
|
|
|
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
|
+
* ONE-OFF USAGE INVOICES (AGL-3011).
|
|
19
|
+
*
|
|
20
|
+
* A workspace's accrued usage, charged to the card on file while the billing
|
|
21
|
+
* period is still open, rather than waiting for the renewal invoice. The
|
|
22
|
+
* caller decides WHEN and HOW MUCH; this module only knows how to turn that
|
|
23
|
+
* decision into a Stripe invoice and report what happened.
|
|
24
|
+
*
|
|
25
|
+
* ## Why it is here and not in the plugin that calls it
|
|
26
|
+
*
|
|
27
|
+
* Stripe credentials, the pinned API version and the idempotency discipline
|
|
28
|
+
* are platform concerns, and a plugin that minted its own invoices would be
|
|
29
|
+
* a second Stripe integration to keep in step with this one. Nothing in this
|
|
30
|
+
* module names a plugin, a meter or a product: the caller hands over a
|
|
31
|
+
* customer, an amount, a product and its own metadata, and every figure on
|
|
32
|
+
* the invoice came from the caller.
|
|
33
|
+
*
|
|
34
|
+
* ## Why an invoice and not a PaymentIntent
|
|
35
|
+
*
|
|
36
|
+
* An invoice carries invoice-level automatic tax, produces a document the
|
|
37
|
+
* customer can see in Billing and pay from it, and reaches the platform's
|
|
38
|
+
* existing `invoice.paid` tax-ledger and revenue path. A raw PaymentIntent
|
|
39
|
+
* has none of that, and a failure on one would simply be lost.
|
|
40
|
+
*
|
|
41
|
+
* ## ⚠️ NOTHING RETRIES A FAILED CHARGE HERE (AGL-3023)
|
|
42
|
+
*
|
|
43
|
+
* These invoices are created `auto_advance: false`, so Stripe runs no
|
|
44
|
+
* automatic collection on them: no retry schedule, no dunning emails, no
|
|
45
|
+
* further attempt of any kind. Measured on a test clock — the invoice sat at
|
|
46
|
+
* `attempt_count: 1, next_payment_attempt: null` through four clock
|
|
47
|
+
* advances over a week.
|
|
48
|
+
*
|
|
49
|
+
* `auto_advance: false` is deliberate, because it is what lets this module
|
|
50
|
+
* finalize and pay as its own steps and RETURN the outcome rather than a
|
|
51
|
+
* promise of one. The cost is that re-collection is ours: a failed charge
|
|
52
|
+
* leaves an open invoice that the customer can pay from Billing, and nothing
|
|
53
|
+
* chases it until they do.
|
|
54
|
+
*
|
|
55
|
+
* Do not restore a claim that Smart Retries cover these. An earlier version
|
|
56
|
+
* of this comment said so; it was never true of an invoice created this way,
|
|
57
|
+
* and a comment that is wrong about money is worse than no comment.
|
|
58
|
+
*
|
|
59
|
+
* ## Why a failure here never threatens the subscription
|
|
60
|
+
*
|
|
61
|
+
* This invoice is not a subscription invoice. It has its own lifecycle, so a
|
|
62
|
+
* card that declines fails THIS invoice and leaves the plan alone; Stripe's
|
|
63
|
+
* subscription dunning — which ends by canceling the subscription — is not
|
|
64
|
+
* reachable from here.
|
|
65
|
+
*/ /**
|
|
66
|
+
* The API version every call below is pinned to.
|
|
67
|
+
*
|
|
68
|
+
* Pinned rather than taking the account default, because the parameters on
|
|
69
|
+
* an invoice item have moved between versions — `2025-03-31.basil` replaced
|
|
70
|
+
* the top-level `price` with `pricing[price]` — and a charge that silently
|
|
71
|
+
* changed shape under a Dashboard upgrade is the failure this constant
|
|
72
|
+
* exists to prevent. The same string the checkout route pins.
|
|
73
|
+
*/ export const USAGE_INVOICE_API_VERSION = '2024-06-20';
|
|
74
|
+
/**
|
|
75
|
+
* The smallest invoice this module will raise, in cents (AGL-3023).
|
|
76
|
+
*
|
|
77
|
+
* Stripe's minimum chargeable amount in USD. Below it there is nothing to
|
|
78
|
+
* charge, and an invoice raised anyway is finalized as PAID having collected
|
|
79
|
+
* nothing — the same "paid without money" shape that a caller crediting its
|
|
80
|
+
* own claim would read as a successful charge. Measured on a test clock: a
|
|
81
|
+
* $0.40 invoice came back `status: paid` and every later call answered
|
|
82
|
+
* "Invoice is already paid".
|
|
83
|
+
*
|
|
84
|
+
* Enforced HERE rather than left to each caller's own floor, because this is
|
|
85
|
+
* the module that knows about Stripe. A caller is free to hold a higher
|
|
86
|
+
* floor of its own; none may go under this one.
|
|
87
|
+
*/ export const USAGE_INVOICE_MIN_CHARGE_CENTS = 50;
|
|
88
|
+
async function stripe(fetchImpl, secretKey, path, params, idempotencyKey) {
|
|
89
|
+
const response = await fetchImpl(`https://api.stripe.com/v1/${path}`, {
|
|
90
|
+
method: params ? 'POST' : 'GET',
|
|
91
|
+
headers: _extends({
|
|
92
|
+
Authorization: `Bearer ${secretKey}`,
|
|
93
|
+
'Stripe-Version': USAGE_INVOICE_API_VERSION
|
|
94
|
+
}, params ? {
|
|
95
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
96
|
+
} : {}, params && idempotencyKey ? {
|
|
97
|
+
'Idempotency-Key': idempotencyKey
|
|
98
|
+
} : {}),
|
|
99
|
+
body: params ? new URLSearchParams(params).toString() : undefined
|
|
100
|
+
});
|
|
101
|
+
const body = await response.json().catch(()=>({}));
|
|
102
|
+
return {
|
|
103
|
+
ok: response.ok,
|
|
104
|
+
status: response.status,
|
|
105
|
+
body
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function stripeError(answer, fallback) {
|
|
109
|
+
var _answer_body;
|
|
110
|
+
const error = (_answer_body = answer.body) == null ? void 0 : _answer_body['error'];
|
|
111
|
+
const message = error == null ? void 0 : error.message;
|
|
112
|
+
return typeof message === 'string' && message ? message : fallback;
|
|
113
|
+
}
|
|
114
|
+
/** `metadata[key]=value` pairs, with every value coerced to a string. */ function metadataParams(metadata) {
|
|
115
|
+
const params = {};
|
|
116
|
+
for (const [key, value] of Object.entries(metadata)){
|
|
117
|
+
if (value === undefined || value === null) continue;
|
|
118
|
+
params[`metadata[${key}]`] = String(value);
|
|
119
|
+
}
|
|
120
|
+
return params;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Charges one workspace's accrued usage to the card on file.
|
|
124
|
+
*
|
|
125
|
+
* Four Stripe writes, each with its own idempotency key derived from the
|
|
126
|
+
* caller's: the invoice item, the invoice, the finalization and the pay. A
|
|
127
|
+
* retry of the whole attempt with the same key re-reads the objects the
|
|
128
|
+
* first attempt made rather than making a second set.
|
|
129
|
+
*
|
|
130
|
+
* `auto_advance` is left OFF and the invoice is finalized and paid here, so
|
|
131
|
+
* the answer this returns is the outcome rather than a promise of one. The
|
|
132
|
+
* webhook remains the authority — this only gets there sooner.
|
|
133
|
+
*
|
|
134
|
+
* The amount is NOT validated against anything: the caller is the only one
|
|
135
|
+
* that knows what was accrued, and a helper second-guessing it would be a
|
|
136
|
+
* second figure to drift from the first. It is checked for being a positive
|
|
137
|
+
* whole number of cents, which is a shape check, not a judgement.
|
|
138
|
+
*/ export async function chargeOrgUsageInvoice(request, options = {}) {
|
|
139
|
+
var _ref, _options_secretKey, _options_fetchImpl, _ref1, _ref2;
|
|
140
|
+
var _created_body, _finalized_body;
|
|
141
|
+
const secretKey = (_ref = (_options_secretKey = options.secretKey) != null ? _options_secretKey : process.env.STRIPE_SECRET_KEY) != null ? _ref : '';
|
|
142
|
+
const fetchImpl = (_options_fetchImpl = options.fetchImpl) != null ? _options_fetchImpl : fetch;
|
|
143
|
+
const empty = {
|
|
144
|
+
ok: false,
|
|
145
|
+
invoiceId: null,
|
|
146
|
+
status: null,
|
|
147
|
+
totalCents: 0,
|
|
148
|
+
amountPaidCents: 0,
|
|
149
|
+
requiresAction: false,
|
|
150
|
+
error: null
|
|
151
|
+
};
|
|
152
|
+
if (!secretKey) return _extends({}, empty, {
|
|
153
|
+
error: 'Stripe is not configured'
|
|
154
|
+
});
|
|
155
|
+
const amountCents = Math.floor(Number(request.amountCents));
|
|
156
|
+
if (!Number.isFinite(amountCents) || amountCents <= 0) {
|
|
157
|
+
return _extends({}, empty, {
|
|
158
|
+
error: 'A usage invoice needs a positive amount'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (amountCents < USAGE_INVOICE_MIN_CHARGE_CENTS) {
|
|
162
|
+
// Refused rather than attempted: Stripe finalizes a sub-minimum invoice
|
|
163
|
+
// as paid without collecting, so attempting one produces a document that
|
|
164
|
+
// claims to be settled and is not. Carrying the amount to the next
|
|
165
|
+
// charge is the caller's business and the honest answer here is "no".
|
|
166
|
+
return _extends({}, empty, {
|
|
167
|
+
error: `A usage invoice must be at least ${USAGE_INVOICE_MIN_CHARGE_CENTS} ` + `cents; ${amountCents} would finalize as paid having collected nothing`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!request.stripeCustomerId || !request.productId) {
|
|
171
|
+
return _extends({}, empty, {
|
|
172
|
+
error: 'A usage invoice needs a customer and a product'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const currency = String(request.currency || 'usd').toLowerCase();
|
|
176
|
+
const metadata = metadataParams(request.metadata);
|
|
177
|
+
const key = (object)=>`${request.idempotencyKey}:${object}`;
|
|
178
|
+
// THE INVOICE FIRST, THEN THE LINE ATTACHED TO IT BY ID (AGL-3023).
|
|
179
|
+
//
|
|
180
|
+
// The order is the fix, and the reason is a default. `POST /v1/invoices`
|
|
181
|
+
// documents `pending_invoice_items_behavior` as "Defaults to `exclude` if
|
|
182
|
+
// the parameter is omitted" — so an invoice created after a pending item,
|
|
183
|
+
// without that parameter, sweeps up NOTHING. Every invoice comes out
|
|
184
|
+
// empty, finalizes at zero, and a zero-total invoice is already paid the
|
|
185
|
+
// moment it finalizes. That is what the first test-clock drill measured.
|
|
186
|
+
//
|
|
187
|
+
// Passing `pending_invoice_items_behavior: 'include'` would also fix the
|
|
188
|
+
// zero, and it is the wrong fix: "include" means every pending item on
|
|
189
|
+
// that customer, not ours. A proration, another plugin's usage line, an
|
|
190
|
+
// item some other part of the platform staged — all of it would land on an
|
|
191
|
+
// invoice this module then reports as an AI overage charge for an amount
|
|
192
|
+
// it made up. Attaching our line to our invoice BY ID binds exactly one
|
|
193
|
+
// line and cannot pick up a second.
|
|
194
|
+
const created = await stripe(fetchImpl, secretKey, 'invoices', _extends({
|
|
195
|
+
customer: request.stripeCustomerId,
|
|
196
|
+
collection_method: 'charge_automatically',
|
|
197
|
+
auto_advance: 'false',
|
|
198
|
+
'automatic_tax[enabled]': 'true',
|
|
199
|
+
// Stated rather than left to the default, so this call says what it
|
|
200
|
+
// means: this invoice carries the line added below and nothing else.
|
|
201
|
+
pending_invoice_items_behavior: 'exclude'
|
|
202
|
+
}, metadata), key('invoice'));
|
|
203
|
+
if (!created.ok) {
|
|
204
|
+
return _extends({}, empty, {
|
|
205
|
+
error: stripeError(created, 'The usage invoice was refused')
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const invoiceId = String((_ref1 = (_created_body = created.body) == null ? void 0 : _created_body['id']) != null ? _ref1 : '');
|
|
209
|
+
if (!invoiceId) {
|
|
210
|
+
return _extends({}, empty, {
|
|
211
|
+
error: 'Stripe returned an invoice with no id'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// The line, addressed to the draft above. `price_data` rather than a bare
|
|
215
|
+
// amount, because the PRODUCT is what carries the tax code the account's
|
|
216
|
+
// automatic tax computes from, and an amount with no product behind it is
|
|
217
|
+
// an untaxed line on a taxed invoice. `tax_behavior` is stated rather than
|
|
218
|
+
// left to the account default: US sales tax is added on top of the
|
|
219
|
+
// platform's prices, and an unspecified behavior is refused outright when
|
|
220
|
+
// automatic tax is on.
|
|
221
|
+
const item = await stripe(fetchImpl, secretKey, 'invoiceitems', _extends({
|
|
222
|
+
customer: request.stripeCustomerId,
|
|
223
|
+
invoice: invoiceId,
|
|
224
|
+
'price_data[product]': request.productId,
|
|
225
|
+
'price_data[currency]': currency,
|
|
226
|
+
'price_data[unit_amount]': String(amountCents),
|
|
227
|
+
'price_data[tax_behavior]': 'exclusive',
|
|
228
|
+
description: request.description
|
|
229
|
+
}, metadata), key('invoiceitem'));
|
|
230
|
+
if (!item.ok) {
|
|
231
|
+
return _extends({}, empty, {
|
|
232
|
+
invoiceId,
|
|
233
|
+
status: 'draft',
|
|
234
|
+
error: stripeError(item, 'The usage line was refused')
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const finalized = await stripe(fetchImpl, secretKey, `invoices/${invoiceId}/finalize`, {
|
|
238
|
+
auto_advance: 'false'
|
|
239
|
+
}, key('finalize'));
|
|
240
|
+
if (!finalized.ok) {
|
|
241
|
+
var _ref3;
|
|
242
|
+
var _finalized_body1;
|
|
243
|
+
return _extends({}, empty, {
|
|
244
|
+
invoiceId,
|
|
245
|
+
status: String((_ref3 = (_finalized_body1 = finalized.body) == null ? void 0 : _finalized_body1['status']) != null ? _ref3 : 'draft'),
|
|
246
|
+
error: stripeError(finalized, 'The usage invoice could not be finalized')
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// WHAT THE INVOICE ACTUALLY CAME TO (AGL-3023).
|
|
250
|
+
//
|
|
251
|
+
// Checked before paying, because a zero-total invoice is not a cheap
|
|
252
|
+
// charge — it is a charge that did not happen, and Stripe marks it paid on
|
|
253
|
+
// finalization. Calling `pay` on one answers "Invoice is already paid",
|
|
254
|
+
// which reads like a failure of the card and is nothing of the sort.
|
|
255
|
+
//
|
|
256
|
+
// The comparison is `< amountCents` rather than `!== 0`: automatic tax
|
|
257
|
+
// ADDS to the total, so a correct invoice is at least what we asked for
|
|
258
|
+
// and usually more. Anything less means the line did not land.
|
|
259
|
+
const totalCents = Math.max(0, Math.floor(Number((_ref2 = (_finalized_body = finalized.body) == null ? void 0 : _finalized_body['total']) != null ? _ref2 : 0)));
|
|
260
|
+
if (totalCents < amountCents) {
|
|
261
|
+
var _ref4;
|
|
262
|
+
var _finalized_body2;
|
|
263
|
+
return _extends({}, empty, {
|
|
264
|
+
invoiceId,
|
|
265
|
+
status: String((_ref4 = (_finalized_body2 = finalized.body) == null ? void 0 : _finalized_body2['status']) != null ? _ref4 : null) || null,
|
|
266
|
+
totalCents,
|
|
267
|
+
error: `The usage invoice finalized at ${totalCents} cents for a ` + `${amountCents}-cent line, so the line did not reach it`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const paid = await stripe(fetchImpl, secretKey, `invoices/${invoiceId}/pay`, {}, key('pay'));
|
|
271
|
+
return readPayOutcome(invoiceId, totalCents, paid);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The charge's outcome, told apart from the transport.
|
|
275
|
+
*
|
|
276
|
+
* A declined card is a 402 from Stripe with a real invoice behind it, not a
|
|
277
|
+
* fault of ours — so the invoice id and the status survive the failure, and
|
|
278
|
+
* the caller can record which invoice to wait on. `requires_action` is
|
|
279
|
+
* singled out because it reads like "not yet" and is in fact "no": nobody is
|
|
280
|
+
* present to complete a 3-D Secure challenge on an off-session charge.
|
|
281
|
+
*
|
|
282
|
+
* ## `paid` is not enough; money is (AGL-3023)
|
|
283
|
+
*
|
|
284
|
+
* Success requires BOTH a `paid` status and an `amount_paid` that covers the
|
|
285
|
+
* line. An invoice can read `paid` having collected nothing — a zero total
|
|
286
|
+
* is marked paid at finalization — and a caller that credited a workspace
|
|
287
|
+
* from our own requested figure would record a payment that never happened.
|
|
288
|
+
* On a usage bound that is the worst possible failure: the balance the gate
|
|
289
|
+
* refuses on would be cleared by bookkeeping rather than by money, and the
|
|
290
|
+
* workspace could spend without limit. So what is reported here is what
|
|
291
|
+
* Stripe says it took.
|
|
292
|
+
*/ function readPayOutcome(invoiceId, totalCents, paid) {
|
|
293
|
+
var _status, _amount_paid, _ref;
|
|
294
|
+
var _paid_body, _error_payment_intent;
|
|
295
|
+
const invoice = paid.ok ? paid.body : null;
|
|
296
|
+
const status = String((_status = (invoice != null ? invoice : {})['status']) != null ? _status : '') || null;
|
|
297
|
+
const amountPaidCents = Math.max(0, Math.floor(Number((_amount_paid = (invoice != null ? invoice : {})['amount_paid']) != null ? _amount_paid : 0)));
|
|
298
|
+
if (paid.ok && status === 'paid' && amountPaidCents >= totalCents && totalCents > 0) {
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
invoiceId,
|
|
302
|
+
status,
|
|
303
|
+
totalCents,
|
|
304
|
+
amountPaidCents,
|
|
305
|
+
requiresAction: false,
|
|
306
|
+
error: null
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (paid.ok && status === 'paid') {
|
|
310
|
+
// Stripe answered 200 and `paid`, and took less than the invoice came
|
|
311
|
+
// to. Reported as a failure with the figures, because the alternative is
|
|
312
|
+
// crediting a workspace for money nobody received.
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
invoiceId,
|
|
316
|
+
status,
|
|
317
|
+
totalCents,
|
|
318
|
+
amountPaidCents,
|
|
319
|
+
requiresAction: false,
|
|
320
|
+
error: `The usage invoice reports paid having collected ${amountPaidCents} ` + `cents of ${totalCents}`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const error = (_paid_body = paid.body) == null ? void 0 : _paid_body['error'];
|
|
324
|
+
const intentStatus = String((_ref = error == null ? void 0 : (_error_payment_intent = error.payment_intent) == null ? void 0 : _error_payment_intent.status) != null ? _ref : '');
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
invoiceId,
|
|
328
|
+
status: status != null ? status : 'open',
|
|
329
|
+
totalCents,
|
|
330
|
+
amountPaidCents,
|
|
331
|
+
requiresAction: intentStatus === 'requires_action',
|
|
332
|
+
error: stripeError(paid, 'The card on file did not complete the charge')
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Finds a usage invoice by the caller's own claim id.
|
|
337
|
+
*
|
|
338
|
+
* The reconciliation read for a caller that claimed a charge and then lost
|
|
339
|
+
* the process before it learned the outcome — a crash, a killed background
|
|
340
|
+
* task. Searching by the caller's metadata answers "did this claim ever
|
|
341
|
+
* become an invoice" without the caller having to persist a Stripe id it
|
|
342
|
+
* never received.
|
|
343
|
+
*
|
|
344
|
+
* Stripe's search index is eventually consistent, so an empty answer means
|
|
345
|
+
* "not found YET" and never "not created". Callers reconcile on a schedule
|
|
346
|
+
* with that in mind rather than treating the first miss as proof.
|
|
347
|
+
*/ export async function findOrgUsageInvoice(query, options = {}) {
|
|
348
|
+
var _ref, _options_secretKey, _options_fetchImpl, _ref1, _found_id, _found_status, _found_amount_paid;
|
|
349
|
+
var _answer_body;
|
|
350
|
+
const secretKey = (_ref = (_options_secretKey = options.secretKey) != null ? _options_secretKey : process.env.STRIPE_SECRET_KEY) != null ? _ref : '';
|
|
351
|
+
const fetchImpl = (_options_fetchImpl = options.fetchImpl) != null ? _options_fetchImpl : fetch;
|
|
352
|
+
if (!secretKey) {
|
|
353
|
+
return {
|
|
354
|
+
invoiceId: null,
|
|
355
|
+
status: null,
|
|
356
|
+
amountPaidCents: 0,
|
|
357
|
+
error: 'Stripe is not configured'
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// Single quotes around the value are Stripe's own search syntax; a value
|
|
361
|
+
// carrying one would break the query, so it is refused rather than escaped
|
|
362
|
+
// — every id this is called with is generated by us from `[A-Za-z0-9-]`.
|
|
363
|
+
if (/['\\]/.test(query.metadataValue)) {
|
|
364
|
+
return {
|
|
365
|
+
invoiceId: null,
|
|
366
|
+
status: null,
|
|
367
|
+
amountPaidCents: 0,
|
|
368
|
+
error: 'Unsearchable claim id'
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const search = `metadata['${query.metadataKey}']:'${query.metadataValue}'`;
|
|
372
|
+
const answer = await stripe(fetchImpl, secretKey, `invoices/search?query=${encodeURIComponent(search)}&limit=1`, null, null);
|
|
373
|
+
if (!answer.ok) {
|
|
374
|
+
return {
|
|
375
|
+
invoiceId: null,
|
|
376
|
+
status: null,
|
|
377
|
+
amountPaidCents: 0,
|
|
378
|
+
error: stripeError(answer, 'Invoice search failed')
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const data = (_ref1 = (_answer_body = answer.body) == null ? void 0 : _answer_body['data']) != null ? _ref1 : [];
|
|
382
|
+
const found = data[0];
|
|
383
|
+
if (!found) return {
|
|
384
|
+
invoiceId: null,
|
|
385
|
+
status: null,
|
|
386
|
+
amountPaidCents: 0,
|
|
387
|
+
error: null
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
invoiceId: String((_found_id = found['id']) != null ? _found_id : '') || null,
|
|
391
|
+
status: String((_found_status = found['status']) != null ? _found_status : '') || null,
|
|
392
|
+
amountPaidCents: Math.max(0, Math.floor(Number((_found_amount_paid = found['amount_paid']) != null ? _found_amount_paid : 0))),
|
|
393
|
+
error: null
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//# sourceMappingURL=usage-invoice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/usage-invoice.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 * ONE-OFF USAGE INVOICES (AGL-3011).\n *\n * A workspace's accrued usage, charged to the card on file while the billing\n * period is still open, rather than waiting for the renewal invoice. The\n * caller decides WHEN and HOW MUCH; this module only knows how to turn that\n * decision into a Stripe invoice and report what happened.\n *\n * ## Why it is here and not in the plugin that calls it\n *\n * Stripe credentials, the pinned API version and the idempotency discipline\n * are platform concerns, and a plugin that minted its own invoices would be\n * a second Stripe integration to keep in step with this one. Nothing in this\n * module names a plugin, a meter or a product: the caller hands over a\n * customer, an amount, a product and its own metadata, and every figure on\n * the invoice came from the caller.\n *\n * ## Why an invoice and not a PaymentIntent\n *\n * An invoice carries invoice-level automatic tax, produces a document the\n * customer can see in Billing and pay from it, and reaches the platform's\n * existing `invoice.paid` tax-ledger and revenue path. A raw PaymentIntent\n * has none of that, and a failure on one would simply be lost.\n *\n * ## ⚠️ NOTHING RETRIES A FAILED CHARGE HERE (AGL-3023)\n *\n * These invoices are created `auto_advance: false`, so Stripe runs no\n * automatic collection on them: no retry schedule, no dunning emails, no\n * further attempt of any kind. Measured on a test clock — the invoice sat at\n * `attempt_count: 1, next_payment_attempt: null` through four clock\n * advances over a week.\n *\n * `auto_advance: false` is deliberate, because it is what lets this module\n * finalize and pay as its own steps and RETURN the outcome rather than a\n * promise of one. The cost is that re-collection is ours: a failed charge\n * leaves an open invoice that the customer can pay from Billing, and nothing\n * chases it until they do.\n *\n * Do not restore a claim that Smart Retries cover these. An earlier version\n * of this comment said so; it was never true of an invoice created this way,\n * and a comment that is wrong about money is worse than no comment.\n *\n * ## Why a failure here never threatens the subscription\n *\n * This invoice is not a subscription invoice. It has its own lifecycle, so a\n * card that declines fails THIS invoice and leaves the plan alone; Stripe's\n * subscription dunning — which ends by canceling the subscription — is not\n * reachable from here.\n */\n\n/**\n * The API version every call below is pinned to.\n *\n * Pinned rather than taking the account default, because the parameters on\n * an invoice item have moved between versions — `2025-03-31.basil` replaced\n * the top-level `price` with `pricing[price]` — and a charge that silently\n * changed shape under a Dashboard upgrade is the failure this constant\n * exists to prevent. The same string the checkout route pins.\n */\nexport const USAGE_INVOICE_API_VERSION = '2024-06-20'\n\n/**\n * The smallest invoice this module will raise, in cents (AGL-3023).\n *\n * Stripe's minimum chargeable amount in USD. Below it there is nothing to\n * charge, and an invoice raised anyway is finalized as PAID having collected\n * nothing — the same \"paid without money\" shape that a caller crediting its\n * own claim would read as a successful charge. Measured on a test clock: a\n * $0.40 invoice came back `status: paid` and every later call answered\n * \"Invoice is already paid\".\n *\n * Enforced HERE rather than left to each caller's own floor, because this is\n * the module that knows about Stripe. A caller is free to hold a higher\n * floor of its own; none may go under this one.\n */\nexport const USAGE_INVOICE_MIN_CHARGE_CENTS = 50\n\n/** What a caller asks to be charged. */\nexport interface OrgUsageInvoiceRequest {\n /** The workspace the charge belongs to, for the invoice's metadata. */\n orgId: string\n /** The Stripe customer to bill — resolved by the caller from org billing. */\n stripeCustomerId: string\n /** The Stripe product the line is billed against; it carries the tax code. */\n productId: string\n /** The amount in whole cents. Must be positive. */\n amountCents: number\n /** ISO 4217, lower case. */\n currency: string\n /** The line's description, as the customer reads it on the invoice. */\n description: string\n /**\n * Metadata stamped on BOTH the invoice item and the invoice.\n *\n * The caller's own claim ticket travels here — which plugin asked, which\n * accrual this settles — so the webhook can route the outcome back without\n * core knowing what any of it means.\n */\n metadata: Readonly<Record<string, string>>\n /**\n * One string that identifies this attempt.\n *\n * Every write below derives its own key from it, because Stripe's\n * idempotency layer is account-scoped and compares parameters: sending one\n * key to `/v1/invoiceitems` and then to `/v1/invoices` would make the\n * second call fail outright. Same discipline as the enterprise\n * provisioning route.\n */\n idempotencyKey: string\n}\n\n/** What became of the attempt. */\nexport interface OrgUsageInvoiceResult {\n ok: boolean\n /** The invoice, once it exists — present even when the charge failed. */\n invoiceId: string | null\n /**\n * Stripe's own invoice status: `paid`, `open`, `void`, `uncollectible`, or\n * `draft` when finalization itself failed.\n */\n status: string | null\n /**\n * What the finalized invoice actually came to, and what Stripe actually\n * collected, in cents (AGL-3023).\n *\n * Both are reported because the caller must never credit a workspace from\n * the amount it ASKED to charge. An invoice that finalized at zero, or one\n * that reports `paid` having collected nothing, is not a payment — and a\n * bound enforced by our own bookkeeping rather than by money is not a\n * bound at all.\n */\n totalCents: number\n amountPaidCents: number\n /**\n * Whether Stripe asked for more from the cardholder (3-D Secure). It is\n * not a payment, and a caller extending credit must treat it as a failure\n * rather than wait: nobody is at the keyboard on an off-session charge.\n */\n requiresAction: boolean\n /** Stripe's message when something went wrong, for logs and staff. */\n error: string | null\n}\n\ntype StripeFetch = typeof fetch\n\ninterface StripeAnswer {\n ok: boolean\n status: number\n body: Record<string, unknown>\n}\n\nasync function stripe(\n fetchImpl: StripeFetch,\n secretKey: string,\n path: string,\n params: Record<string, string> | null,\n idempotencyKey: string | null,\n): Promise<StripeAnswer> {\n const response = await fetchImpl(`https://api.stripe.com/v1/${path}`, {\n method: params ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${secretKey}`,\n 'Stripe-Version': USAGE_INVOICE_API_VERSION,\n ...(params ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),\n // Stripe documents idempotency keys as having no effect on GET, so\n // only the writes carry one.\n ...(params && idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),\n },\n body: params ? new URLSearchParams(params).toString() : undefined,\n })\n const body = (await response.json().catch(() => ({}))) as Record<string, unknown>\n return { ok: response.ok, status: response.status, body }\n}\n\nfunction stripeError(answer: StripeAnswer, fallback: string): string {\n const error = answer.body?.['error'] as { message?: unknown } | undefined\n const message = error?.message\n return typeof message === 'string' && message ? message : fallback\n}\n\n/** `metadata[key]=value` pairs, with every value coerced to a string. */\nfunction metadataParams(\n metadata: Readonly<Record<string, string>>,\n): Record<string, string> {\n const params: Record<string, string> = {}\n for (const [key, value] of Object.entries(metadata)) {\n if (value === undefined || value === null) continue\n params[`metadata[${key}]`] = String(value)\n }\n return params\n}\n\n/**\n * Charges one workspace's accrued usage to the card on file.\n *\n * Four Stripe writes, each with its own idempotency key derived from the\n * caller's: the invoice item, the invoice, the finalization and the pay. A\n * retry of the whole attempt with the same key re-reads the objects the\n * first attempt made rather than making a second set.\n *\n * `auto_advance` is left OFF and the invoice is finalized and paid here, so\n * the answer this returns is the outcome rather than a promise of one. The\n * webhook remains the authority — this only gets there sooner.\n *\n * The amount is NOT validated against anything: the caller is the only one\n * that knows what was accrued, and a helper second-guessing it would be a\n * second figure to drift from the first. It is checked for being a positive\n * whole number of cents, which is a shape check, not a judgement.\n */\nexport async function chargeOrgUsageInvoice(\n request: OrgUsageInvoiceRequest,\n options: { secretKey?: string; fetchImpl?: StripeFetch } = {},\n): Promise<OrgUsageInvoiceResult> {\n const secretKey = options.secretKey ?? process.env.STRIPE_SECRET_KEY ?? ''\n const fetchImpl = options.fetchImpl ?? fetch\n const empty: OrgUsageInvoiceResult = {\n ok: false,\n invoiceId: null,\n status: null,\n totalCents: 0,\n amountPaidCents: 0,\n requiresAction: false,\n error: null,\n }\n if (!secretKey) return { ...empty, error: 'Stripe is not configured' }\n const amountCents = Math.floor(Number(request.amountCents))\n if (!Number.isFinite(amountCents) || amountCents <= 0) {\n return { ...empty, error: 'A usage invoice needs a positive amount' }\n }\n if (amountCents < USAGE_INVOICE_MIN_CHARGE_CENTS) {\n // Refused rather than attempted: Stripe finalizes a sub-minimum invoice\n // as paid without collecting, so attempting one produces a document that\n // claims to be settled and is not. Carrying the amount to the next\n // charge is the caller's business and the honest answer here is \"no\".\n return {\n ...empty,\n error:\n `A usage invoice must be at least ${USAGE_INVOICE_MIN_CHARGE_CENTS} ` +\n `cents; ${amountCents} would finalize as paid having collected nothing`,\n }\n }\n if (!request.stripeCustomerId || !request.productId) {\n return { ...empty, error: 'A usage invoice needs a customer and a product' }\n }\n const currency = String(request.currency || 'usd').toLowerCase()\n const metadata = metadataParams(request.metadata)\n const key = (object: string) => `${request.idempotencyKey}:${object}`\n\n // THE INVOICE FIRST, THEN THE LINE ATTACHED TO IT BY ID (AGL-3023).\n //\n // The order is the fix, and the reason is a default. `POST /v1/invoices`\n // documents `pending_invoice_items_behavior` as \"Defaults to `exclude` if\n // the parameter is omitted\" — so an invoice created after a pending item,\n // without that parameter, sweeps up NOTHING. Every invoice comes out\n // empty, finalizes at zero, and a zero-total invoice is already paid the\n // moment it finalizes. That is what the first test-clock drill measured.\n //\n // Passing `pending_invoice_items_behavior: 'include'` would also fix the\n // zero, and it is the wrong fix: \"include\" means every pending item on\n // that customer, not ours. A proration, another plugin's usage line, an\n // item some other part of the platform staged — all of it would land on an\n // invoice this module then reports as an AI overage charge for an amount\n // it made up. Attaching our line to our invoice BY ID binds exactly one\n // line and cannot pick up a second.\n const created = await stripe(\n fetchImpl,\n secretKey,\n 'invoices',\n {\n customer: request.stripeCustomerId,\n collection_method: 'charge_automatically',\n auto_advance: 'false',\n 'automatic_tax[enabled]': 'true',\n // Stated rather than left to the default, so this call says what it\n // means: this invoice carries the line added below and nothing else.\n pending_invoice_items_behavior: 'exclude',\n ...metadata,\n },\n key('invoice'),\n )\n if (!created.ok) {\n return { ...empty, error: stripeError(created, 'The usage invoice was refused') }\n }\n const invoiceId = String(created.body?.['id'] ?? '')\n if (!invoiceId) {\n return { ...empty, error: 'Stripe returned an invoice with no id' }\n }\n\n // The line, addressed to the draft above. `price_data` rather than a bare\n // amount, because the PRODUCT is what carries the tax code the account's\n // automatic tax computes from, and an amount with no product behind it is\n // an untaxed line on a taxed invoice. `tax_behavior` is stated rather than\n // left to the account default: US sales tax is added on top of the\n // platform's prices, and an unspecified behavior is refused outright when\n // automatic tax is on.\n const item = await stripe(\n fetchImpl,\n secretKey,\n 'invoiceitems',\n {\n customer: request.stripeCustomerId,\n invoice: invoiceId,\n 'price_data[product]': request.productId,\n 'price_data[currency]': currency,\n 'price_data[unit_amount]': String(amountCents),\n 'price_data[tax_behavior]': 'exclusive',\n description: request.description,\n ...metadata,\n },\n key('invoiceitem'),\n )\n if (!item.ok) {\n return {\n ...empty,\n invoiceId,\n status: 'draft',\n error: stripeError(item, 'The usage line was refused'),\n }\n }\n\n const finalized = await stripe(\n fetchImpl,\n secretKey,\n `invoices/${invoiceId}/finalize`,\n { auto_advance: 'false' },\n key('finalize'),\n )\n if (!finalized.ok) {\n return {\n ...empty,\n invoiceId,\n status: String(finalized.body?.['status'] ?? 'draft'),\n error: stripeError(finalized, 'The usage invoice could not be finalized'),\n }\n }\n\n // WHAT THE INVOICE ACTUALLY CAME TO (AGL-3023).\n //\n // Checked before paying, because a zero-total invoice is not a cheap\n // charge — it is a charge that did not happen, and Stripe marks it paid on\n // finalization. Calling `pay` on one answers \"Invoice is already paid\",\n // which reads like a failure of the card and is nothing of the sort.\n //\n // The comparison is `< amountCents` rather than `!== 0`: automatic tax\n // ADDS to the total, so a correct invoice is at least what we asked for\n // and usually more. Anything less means the line did not land.\n const totalCents = Math.max(0, Math.floor(Number(finalized.body?.['total'] ?? 0)))\n if (totalCents < amountCents) {\n return {\n ...empty,\n invoiceId,\n status: String(finalized.body?.['status'] ?? null) || null,\n totalCents,\n error:\n `The usage invoice finalized at ${totalCents} cents for a ` +\n `${amountCents}-cent line, so the line did not reach it`,\n }\n }\n\n const paid = await stripe(\n fetchImpl,\n secretKey,\n `invoices/${invoiceId}/pay`,\n {},\n key('pay'),\n )\n return readPayOutcome(invoiceId, totalCents, paid)\n}\n\n/**\n * The charge's outcome, told apart from the transport.\n *\n * A declined card is a 402 from Stripe with a real invoice behind it, not a\n * fault of ours — so the invoice id and the status survive the failure, and\n * the caller can record which invoice to wait on. `requires_action` is\n * singled out because it reads like \"not yet\" and is in fact \"no\": nobody is\n * present to complete a 3-D Secure challenge on an off-session charge.\n *\n * ## `paid` is not enough; money is (AGL-3023)\n *\n * Success requires BOTH a `paid` status and an `amount_paid` that covers the\n * line. An invoice can read `paid` having collected nothing — a zero total\n * is marked paid at finalization — and a caller that credited a workspace\n * from our own requested figure would record a payment that never happened.\n * On a usage bound that is the worst possible failure: the balance the gate\n * refuses on would be cleared by bookkeeping rather than by money, and the\n * workspace could spend without limit. So what is reported here is what\n * Stripe says it took.\n */\nfunction readPayOutcome(\n invoiceId: string,\n totalCents: number,\n paid: StripeAnswer,\n): OrgUsageInvoiceResult {\n const invoice = paid.ok ? paid.body : null\n const status = String((invoice ?? {})['status'] ?? '') || null\n const amountPaidCents = Math.max(\n 0,\n Math.floor(Number((invoice ?? {})['amount_paid'] ?? 0)),\n )\n if (paid.ok && status === 'paid' && amountPaidCents >= totalCents && totalCents > 0) {\n return {\n ok: true,\n invoiceId,\n status,\n totalCents,\n amountPaidCents,\n requiresAction: false,\n error: null,\n }\n }\n if (paid.ok && status === 'paid') {\n // Stripe answered 200 and `paid`, and took less than the invoice came\n // to. Reported as a failure with the figures, because the alternative is\n // crediting a workspace for money nobody received.\n return {\n ok: false,\n invoiceId,\n status,\n totalCents,\n amountPaidCents,\n requiresAction: false,\n error:\n `The usage invoice reports paid having collected ${amountPaidCents} ` +\n `cents of ${totalCents}`,\n }\n }\n const error = paid.body?.['error'] as\n | { message?: unknown; payment_intent?: { status?: unknown } }\n | undefined\n const intentStatus = String(error?.payment_intent?.status ?? '')\n return {\n ok: false,\n invoiceId,\n status: status ?? 'open',\n totalCents,\n amountPaidCents,\n requiresAction: intentStatus === 'requires_action',\n error: stripeError(paid, 'The card on file did not complete the charge'),\n }\n}\n\n/**\n * Finds a usage invoice by the caller's own claim id.\n *\n * The reconciliation read for a caller that claimed a charge and then lost\n * the process before it learned the outcome — a crash, a killed background\n * task. Searching by the caller's metadata answers \"did this claim ever\n * become an invoice\" without the caller having to persist a Stripe id it\n * never received.\n *\n * Stripe's search index is eventually consistent, so an empty answer means\n * \"not found YET\" and never \"not created\". Callers reconcile on a schedule\n * with that in mind rather than treating the first miss as proof.\n */\nexport async function findOrgUsageInvoice(\n query: { metadataKey: string; metadataValue: string },\n options: { secretKey?: string; fetchImpl?: StripeFetch } = {},\n): Promise<{\n invoiceId: string | null\n status: string | null\n /** What that invoice collected, in cents — 0 when none was found. */\n amountPaidCents: number\n error: string | null\n}> {\n const secretKey = options.secretKey ?? process.env.STRIPE_SECRET_KEY ?? ''\n const fetchImpl = options.fetchImpl ?? fetch\n if (!secretKey) {\n return {\n invoiceId: null,\n status: null,\n amountPaidCents: 0,\n error: 'Stripe is not configured',\n }\n }\n // Single quotes around the value are Stripe's own search syntax; a value\n // carrying one would break the query, so it is refused rather than escaped\n // — every id this is called with is generated by us from `[A-Za-z0-9-]`.\n if (/['\\\\]/.test(query.metadataValue)) {\n return { invoiceId: null, status: null, amountPaidCents: 0, error: 'Unsearchable claim id' }\n }\n const search = `metadata['${query.metadataKey}']:'${query.metadataValue}'`\n const answer = await stripe(\n fetchImpl,\n secretKey,\n `invoices/search?query=${encodeURIComponent(search)}&limit=1`,\n null,\n null,\n )\n if (!answer.ok) {\n return {\n invoiceId: null,\n status: null,\n amountPaidCents: 0,\n error: stripeError(answer, 'Invoice search failed'),\n }\n }\n const data = (answer.body?.['data'] ?? []) as Array<Record<string, unknown>>\n const found = data[0]\n if (!found) return { invoiceId: null, status: null, amountPaidCents: 0, error: null }\n return {\n invoiceId: String(found['id'] ?? '') || null,\n status: String(found['status'] ?? '') || null,\n amountPaidCents: Math.max(0, Math.floor(Number(found['amount_paid'] ?? 0))),\n error: null,\n }\n}\n"],"names":["USAGE_INVOICE_API_VERSION","USAGE_INVOICE_MIN_CHARGE_CENTS","stripe","fetchImpl","secretKey","path","params","idempotencyKey","response","method","headers","Authorization","body","URLSearchParams","toString","undefined","json","catch","ok","status","stripeError","answer","fallback","error","message","metadataParams","metadata","key","value","Object","entries","String","chargeOrgUsageInvoice","request","options","created","finalized","process","env","STRIPE_SECRET_KEY","fetch","empty","invoiceId","totalCents","amountPaidCents","requiresAction","amountCents","Math","floor","Number","isFinite","stripeCustomerId","productId","currency","toLowerCase","object","customer","collection_method","auto_advance","pending_invoice_items_behavior","item","invoice","description","max","paid","readPayOutcome","intentStatus","payment_intent","findOrgUsageInvoice","query","found","test","metadataValue","search","metadataKey","encodeURIComponent","data"],"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
|
+
import { type AglynPostalAddress } from '@aglyn/aglyn/server';
|
|
18
|
+
export interface SeedUserProfileInput {
|
|
19
|
+
/** The provider's single name string, when the assertion carries one. */
|
|
20
|
+
displayName?: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Avatar and phone from the same assertion (AGL-1131). Callers pass these
|
|
23
|
+
* through `resolveIdpPhotoUrl` / `resolveIdpPhone`, which is where the
|
|
24
|
+
* https-only check on the photo lives — this function does not re-validate.
|
|
25
|
+
*/
|
|
26
|
+
photoUrl?: string | null;
|
|
27
|
+
phoneNumber?: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Postal address from the same assertion (AGL-1963). Callers pass this
|
|
30
|
+
* through `resolveIdpAddress`, whose loose `IdpAddressParts` is structurally
|
|
31
|
+
* an `AglynPostalAddress` — it is the seed below, not the caller, that
|
|
32
|
+
* decides whether the parts amount to an address at all. Stored only when
|
|
33
|
+
* the assertion carries a STREET LINE (AGL-1566): a directory that releases
|
|
34
|
+
* a home city and postcode and no street has given us personal data with no
|
|
35
|
+
* use, not an address.
|
|
36
|
+
*/
|
|
37
|
+
address?: AglynPostalAddress | null;
|
|
38
|
+
/** Injectable for tests; defaults to the admin app's Firestore. */
|
|
39
|
+
firestore?: any;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create `users/{uid}` if it is missing and prefill the name fields from the
|
|
43
|
+
* identity provider, without ever overwriting what the user has typed.
|
|
44
|
+
*
|
|
45
|
+
* Only ABSENT fields are written, so re-running on every sign-in — which is
|
|
46
|
+
* what the SSO route does — cannot undo an edit: rename yourself in Basic
|
|
47
|
+
* info and the IdP's copy stays out of it.
|
|
48
|
+
*
|
|
49
|
+
* Best-effort by contract. Provisioning access must not fail because a
|
|
50
|
+
* cosmetic prefill did, so the caller is expected to let a rejection through
|
|
51
|
+
* rather than surface it; the profile self-heals on the next sign-in.
|
|
52
|
+
*
|
|
53
|
+
* @returns what the seed actually wrote, for logging and tests.
|
|
54
|
+
*/
|
|
55
|
+
export declare function seedUserProfile(uid: string, input?: SeedUserProfileInput): Promise<{
|
|
56
|
+
created: boolean;
|
|
57
|
+
fields: string[];
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* "Delete the phone number you hold for me" — the second half of Privacy
|
|
61
|
+
* Policy v4 §11 (AGL-1592). Deliberately adjacent to `seedUserProfile`,
|
|
62
|
+
* because the two are halves of one invariant: this sets the marker that one
|
|
63
|
+
* reads, and changing either without the other silently restores the defect.
|
|
64
|
+
*
|
|
65
|
+
* WHAT IT DOES, AND WHY IT IS NOT JUST A DELETE
|
|
66
|
+
*
|
|
67
|
+
* 1. Removes `users/{uid}.phoneNumber` outright (`FieldValue.delete()`, not
|
|
68
|
+
* `null` — the request was to stop holding it, and a nulled field still
|
|
69
|
+
* reads as "we have a phone slot for this person").
|
|
70
|
+
* 2. Stamps `phoneNumberErasedAt`, which is what stops the IdP re-asserting
|
|
71
|
+
* it on the next sign-in.
|
|
72
|
+
* 3. Writes a suppression record for the number, with `erasePhoneOnFile`.
|
|
73
|
+
*
|
|
74
|
+
* Step 3 is the one that looks contradictory and is not. Deleting every copy
|
|
75
|
+
* of a number does NOT protect the person from being called — it destroys the
|
|
76
|
+
* only artefact that could recognise them, so the same number arriving later
|
|
77
|
+
* from a customer's CRM, a support ticket, or their own re-typed profile is a
|
|
78
|
+
* number we have never heard of and will happily dial. The minimal retained
|
|
79
|
+
* record is what makes the deletion request mean what the person meant by it.
|
|
80
|
+
* The full reasoning, and the statutory carve-outs it rests on, are in
|
|
81
|
+
* contact-suppression.ts.
|
|
82
|
+
*
|
|
83
|
+
* §11 does not currently say this. It should — see the wording proposed on
|
|
84
|
+
* AGL-1592. Do not resolve the mismatch by making the code forget the number.
|
|
85
|
+
*
|
|
86
|
+
* WHY THE MARKER LIVES ON A CLIENT-WRITABLE DOCUMENT. `users/{uid}` is
|
|
87
|
+
* writable by its owner (and by staff) under the Firestore rules, so this
|
|
88
|
+
* marker is not tamper-proof. It does not need to be: the only party who can
|
|
89
|
+
* clear it is the person it protects, and a person clearing their own erasure
|
|
90
|
+
* marker is a person changing their mind, not an attacker. The adversary here
|
|
91
|
+
* is our own seeding code path, which is server-side and honours it. Moving
|
|
92
|
+
* the marker to a server-only document would buy nothing and cost a second
|
|
93
|
+
* read on every sign-in.
|
|
94
|
+
*
|
|
95
|
+
* A stale marker is harmless: it only ever suppresses an IdP prefill, and a
|
|
96
|
+
* user who types a number back in has a non-blank field, which the seed skips
|
|
97
|
+
* anyway.
|
|
98
|
+
*
|
|
99
|
+
* @param phoneNumber the number to suppress. Defaults to whatever the profile
|
|
100
|
+
* currently holds — pass it explicitly when the request named a number
|
|
101
|
+
* we do not have on file.
|
|
102
|
+
* @returns `suppressed` is the E.164 that went onto the do-not-contact list,
|
|
103
|
+
* or null when there was no recognizable number to record.
|
|
104
|
+
*/
|
|
105
|
+
export declare function forgetUserPhoneNumber(input: {
|
|
106
|
+
uid: string;
|
|
107
|
+
phoneNumber?: string | null;
|
|
108
|
+
/** Staff member who took the request, for the suppression record. */
|
|
109
|
+
recordedByUid?: string | null;
|
|
110
|
+
note?: string | null;
|
|
111
|
+
firestore?: any;
|
|
112
|
+
}): Promise<{
|
|
113
|
+
cleared: boolean;
|
|
114
|
+
suppressed: string | null;
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* "Delete the address you hold for me" (AGL-1963) — the address half of the
|
|
118
|
+
* same §11 invariant `forgetUserPhoneNumber` implements for the phone, and
|
|
119
|
+
* deliberately adjacent to both for the same reason: this sets the marker
|
|
120
|
+
* that `seedUserProfile` reads, and changing either without the other
|
|
121
|
+
* silently restores the defect.
|
|
122
|
+
*
|
|
123
|
+
* A guard whose marker nothing ever writes is decorative, so this exists
|
|
124
|
+
* before anything needs it. Two callers do:
|
|
125
|
+
*
|
|
126
|
+
* - Manage Account, when someone clears their own address. That is the
|
|
127
|
+
* common case by a wide margin — nobody files a support ticket to remove
|
|
128
|
+
* a street address, they empty the field — and without a marker their
|
|
129
|
+
* next SSO sign-in puts it straight back.
|
|
130
|
+
* - Staff, handling an erasure request against an account the person can no
|
|
131
|
+
* longer sign in to.
|
|
132
|
+
*
|
|
133
|
+
* `FieldValue.delete()`, not `null`, on the same reasoning as the phone: the
|
|
134
|
+
* request was to stop holding it, and a nulled field still reads as "we have
|
|
135
|
+
* an address slot for this person".
|
|
136
|
+
*
|
|
137
|
+
* REVERSIBLE BY THE PERSON IT PROTECTS, and only by them. `users/{uid}` is
|
|
138
|
+
* writable by its owner (and by staff) under the Firestore rules, so this
|
|
139
|
+
* marker is not tamper-proof and does not need to be — the adversary it
|
|
140
|
+
* guards against is our own seeding path, which is server-side and honours
|
|
141
|
+
* it. Someone who types an address back in has a non-blank field, which the
|
|
142
|
+
* seed skips anyway — and Manage Account drops the marker on that same save,
|
|
143
|
+
* so opting back into IdP prefill is just filling the field in again.
|
|
144
|
+
*/
|
|
145
|
+
export declare function forgetUserAddress(input: {
|
|
146
|
+
uid: string;
|
|
147
|
+
firestore?: any;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
cleared: boolean;
|
|
150
|
+
}>;
|
|
151
|
+
export default seedUserProfile;
|