@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 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/sso-provisioning.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 * Self-serve enterprise SSO provisioning (AGL-1210).\n *\n * Before this, `tenantManager()` appeared six times in the repo and every one\n * was `authForTenant(...)` — authenticating against a pool somebody had made by\n * hand in the Firebase console. There was no `createTenant` and no\n * `createProviderConfig` anywhere, so onboarding an SSO org meant a person\n * clicking through GCIP and then writing `tenantId`/`providerId` onto the org\n * doc. This module is the write half that removes that step.\n *\n * ## Domain verification is the security gate, not a formality\n *\n * `ssoDomains/{domain}` routes every sign-in for a domain to whichever org\n * claims it. Letting an org self-assert that claim would be an\n * account-takeover vector: claim `competitor.com`, and their employees' sign-in\n * attempts arrive at your IdP. `domainVerified` used to be staff-attested,\n * which is exactly the human step self-serve removes — so the attestation has\n * to be replaced by proof, not deleted.\n *\n * The proof is a DNS TXT record at `_aglyn-challenge.<domain>` carrying a token\n * we generated for THAT org and THAT domain. Only someone who controls the\n * domain's zone can publish it, and a token issued to one org is worthless to\n * another. Nothing reaches `sso.domains` — the array sign-in actually consults\n * — until a lookup has seen the record.\n *\n * DNS resolution mirrors `/api/domains/verify` (AGL-734): pinned public\n * resolvers rather than the runtime's default, because a stale zone once made\n * Vercel's own resolver return NXDOMAIN for records every public resolver could\n * see. The fallback is deliberately narrow — a genuine \"no such record\" answer\n * must stay a failure, and only an unreachable resolver falls through.\n */\n\nimport { promises as dns, Resolver as CallbackResolver } from 'dns'\nimport { randomBytes } from 'crypto'\nimport type { BaseAuth } from 'firebase-admin/auth'\nimport firebaseAdmin from './firebase-admin'\nimport type { DomainProbe } from './sso-drift-logic'\n\nconst auth = () => firebaseAdmin.app().auth()\nconst firestore = () => firebaseAdmin.app().firestore()\n\n/** Subdomain the challenge record is published at. */\nexport const SSO_CHALLENGE_PREFIX = '_aglyn-challenge'\n\n/** Prefix on the TXT value, so an unrelated record cannot pass by accident. */\nexport const SSO_TXT_PREFIX = 'aglyn-domain-verification='\n\nconst DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\\.[a-z0-9-]{1,63})+$/\n\n/** Same pinned resolvers as the custom-domain check — see the header note. */\nconst PUBLIC_RESOLVERS = ['1.1.1.1', '8.8.8.8']\n\n/**\n * Domains nobody may claim for SSO, however well they prove ownership.\n *\n * A consumer mailbox provider is shared by millions of unrelated people, so\n * \"proves control of the zone\" and \"is entitled to govern every account on it\"\n * come apart completely. Verifying `gmail.com` is possible for exactly one\n * party, and routing every Gmail user's sign-in to their IdP is not a thing\n * that party should be able to do either.\n */\nconst PUBLIC_EMAIL_DOMAINS = new Set([\n 'gmail.com',\n 'googlemail.com',\n 'outlook.com',\n 'hotmail.com',\n 'live.com',\n 'msn.com',\n 'yahoo.com',\n 'ymail.com',\n 'aol.com',\n 'icloud.com',\n 'me.com',\n 'mac.com',\n 'proton.me',\n 'protonmail.com',\n 'pm.me',\n 'gmx.com',\n 'gmx.net',\n 'mail.com',\n 'zoho.com',\n 'yandex.com',\n 'fastmail.com',\n 'hey.com',\n 'qq.com',\n '163.com',\n '126.com',\n])\n\nexport function normalizeSsoDomain(input: string): string {\n const raw = String(input ?? '')\n .trim()\n .toLowerCase()\n const at = raw.lastIndexOf('@')\n return (at >= 0 ? raw.slice(at + 1) : raw).replace(/^@+/, '').replace(/\\.$/, '')\n}\n\n/**\n * `{ value, error }` rather than a discriminated union on purpose: an\n * `{ ok: true } | { ok: false }` union does not narrow reliably once it crosses\n * a library boundary in this repo (`strictNullChecks` is off), and the caller\n * ends up unable to reach either arm's fields. Both keys always present, one of\n * them always null.\n */\nexport interface DomainCheck {\n domain: string | null\n error: string | null\n}\n\nexport function validateSsoDomain(input: string): DomainCheck {\n const domain = normalizeSsoDomain(input)\n if (!domain || !DOMAIN_PATTERN.test(domain)) {\n return { domain: null, error: 'Enter a valid domain, for example acme.com' }\n }\n if (PUBLIC_EMAIL_DOMAINS.has(domain)) {\n return {\n domain: null,\n error:\n 'Public email domains cannot be used for SSO — they are shared by ' +\n 'people outside your organization. Use a domain your organization owns.',\n }\n }\n return { domain, error: null }\n}\n\n/**\n * TXT records at the challenge host, lowercased and unwrapped.\n *\n * A TXT answer arrives as an array of string CHUNKS per record (DNS splits\n * strings over 255 bytes), so each record is joined before comparison —\n * otherwise a long token would never match.\n */\nexport async function resolveChallengeTxt(domain: string): Promise<string[]> {\n const host = `${SSO_CHALLENGE_PREFIX}.${domain}`\n const flatten = (records: string[][]) =>\n records.map((chunks) => chunks.join('').trim())\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_RESOLVERS)\n const records = await new Promise<string[][]>((resolve, reject) => {\n resolver.resolveTxt(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses),\n )\n })\n return flatten(records)\n } catch (error) {\n // ENOTFOUND/ENODATA are real answers — the name has no TXT record. Only an\n // unreachable resolver falls through, so a genuine miss stays a failure.\n const code = (error as NodeJS.ErrnoException)?.code\n if (code === 'ENOTFOUND' || code === 'ENODATA' || code === 'NXDOMAIN') {\n return []\n }\n try {\n return flatten(await dns.resolveTxt(host))\n } catch {\n return []\n }\n }\n}\n\n/** The exact TXT value an org must publish for a domain. */\nexport function challengeValue(token: string): string {\n return `${SSO_TXT_PREFIX}${token}`\n}\n\n/**\n * Does this set of TXT records prove control of the zone for THIS token?\n *\n * Extracted so the comparison that domain verification rests on is reachable\n * from a test without standing up Firestore. It is deliberately an exact match\n * on a whole record: a substring test would accept\n * `aglyn-domain-verification=<token>-and-more`, and a prefix test would accept\n * any record that merely starts the same way. Neither proves anything.\n */\nexport function recordsProveOwnership(\n records: readonly string[],\n token: string,\n): boolean {\n if (!token) return false\n return records.includes(challengeValue(token))\n}\n\n/*==========================================\n * PERIODIC RE-VERIFICATION (AGL-1210, the residual the 2026-08-23 audit named).\n *\n * A domain is proven ONCE, when an admin presses Verify, and then routes that\n * domain's sign-ins forever. A company that lets its domain lapse — or sells\n * it — keeps the routing until a human revokes it. The audit called that\n * \"the account-takeover shape in slow motion\", and it is the last residual on\n * an otherwise strong proof.\n *\n * ## Why this REPORTS and never revokes\n *\n * The obvious fix — re-run the lookup, drop the domain when it fails — is a\n * worse bug than the one it closes. `resolveChallengeTxt` cannot tell \"the\n * record is gone\" from \"the resolver did not answer\", because BOTH return an\n * empty array. So a resolver blip during the sweep looks exactly like every\n * customer simultaneously deleting their TXT record, and an automated revoke\n * would log out every enterprise on the platform at once. That is the\n * `reverify-plugin-versions` argument — a lint that can stop a plugin in every\n * workspace is a kill switch with no human in it — and the lockdown argument,\n * where an unreachable Firestore is an outage rather than a verdict.\n *\n * So this half adds two things and deliberately nothing else:\n *\n * 1. {@link probeChallengeTxt} — a THIRD state. `unreachable` is not\n * evidence, and is never counted as a failure.\n * 2. {@link assessDomainDrift} — consecutive CONCLUSIVE failures over a\n * minimum wall-clock age before a domain is even reported as drifted.\n *\n * Revocation stays exactly where it was: {@link revokeDomain}, an explicit act\n * by a person. Detection with a human decision beats an automated action that\n * can log out a paying customer.\n *\n * ## Why the risk tolerates a slow answer\n *\n * The threat needs a domain to lapse, clear ~30 days of registrar grace and\n * ~30 days of redemption, drop, and be re-bought. That is months, not days.\n * And the first harm is not silent credential theft: routing still points at\n * the ORIGINAL org's IdP, where the new owner's staff have no account. The\n * concrete harm is that the new legitimate owner cannot claim their own domain\n * (`issueDomainClaim` refuses a domain another org already routes) and their\n * users are misdirected. Weeks of grace cost that risk almost nothing, and buy\n * immunity from the resolver blip that would otherwise be self-inflicted.\n *=========================================*/\n\n/**\n * The pure half of re-verification — the probe STATES and the decision made\n * from them — lives in `sso-drift-logic.ts` and is re-exported here so the\n * public surface is one module. It has no imports at all, which is what lets\n * the console route's spec run the REAL decision against faked I/O.\n */\nexport {\n assessDomainDrift,\n SSO_DRIFT_FAILURES_BEFORE_REPORT,\n SSO_DRIFT_MIN_AGE_MS,\n type DomainDriftAction,\n type DomainDriftState,\n type DomainDriftVerdict,\n type DomainProbe,\n type DomainProbeStatus,\n} from './sso-drift-logic'\n\n/**\n * Re-check a claim's TXT record, distinguishing silence from absence.\n *\n * Same pinned public resolvers and same fallback as {@link resolveChallengeTxt}\n * — this is not a second opinion about DNS, it is the same lookup reporting one\n * more bit. NXDOMAIN/ENOTFOUND/ENODATA are ANSWERS (\"that name has no TXT\n * record\") and stay conclusive; anything else from both the pinned resolvers\n * and the runtime fallback is `unreachable`.\n *\n * Never throws: a sweep across every org must not stop at the first bad zone.\n */\nexport async function probeChallengeTxt(\n domain: string,\n token: string,\n): Promise<DomainProbe> {\n const host = `${SSO_CHALLENGE_PREFIX}.${domain}`\n const flatten = (records: string[][]) =>\n records.map((chunks) => chunks.join('').trim())\n const settle = (records: string[]): DomainProbe => ({\n status: recordsProveOwnership(records, token) ? 'proven' : 'missing',\n records: records.slice(0, 5),\n })\n /** Codes that are a real answer rather than a failure to get one. */\n const conclusive = (code: string) =>\n code === 'ENOTFOUND' || code === 'ENODATA' || code === 'NXDOMAIN'\n\n try {\n const resolver = new CallbackResolver()\n resolver.setServers(PUBLIC_RESOLVERS)\n const records = await new Promise<string[][]>((resolve, reject) => {\n resolver.resolveTxt(host, (error, addresses) =>\n error ? reject(error) : resolve(addresses),\n )\n })\n return settle(flatten(records))\n } catch (error) {\n const code = (error as NodeJS.ErrnoException)?.code\n if (conclusive(code)) return settle([])\n try {\n return settle(flatten(await dns.resolveTxt(host)))\n } catch (fallbackError) {\n const fallbackCode = (fallbackError as NodeJS.ErrnoException)?.code\n // The pinned resolvers gave us nothing usable. Only the fallback's own\n // answer can still make this conclusive; otherwise we genuinely do not\n // know, and saying so is the entire feature.\n if (conclusive(fallbackCode)) return settle([])\n return { status: 'unreachable', records: [] }\n }\n }\n}\n\nexport interface SsoDomainClaimView {\n domain: string\n token: string\n verified: boolean\n /** Fully-qualified host the record goes on. */\n recordHost: string\n /** Exact value the record must carry. */\n recordValue: string\n lastRecords?: string[]\n}\n\nconst claimRef = (orgId: string, domain: string) =>\n firestore().collection('orgs').doc(orgId).collection('ssoDomains').doc(domain)\n\n/**\n * Start (or re-read) a claim on a domain. Idempotent: an existing claim keeps\n * its token, because reissuing would invalidate a record the customer may have\n * already published and turn a working setup into a mysterious failure.\n */\nexport async function issueDomainClaim(\n orgId: string,\n rawDomain: string,\n): Promise<{ claim: SsoDomainClaimView | null; error: string | null }> {\n const { domain, error } = validateSsoDomain(rawDomain)\n if (!domain) return { claim: null, error: error ?? 'Invalid domain' }\n\n // One domain, one org. Without this a second org could hold a live claim on\n // a domain already governed elsewhere and race it at verification time.\n const existing = await firestore()\n .collection('ssoDomains')\n .doc(domain)\n .get()\n if (existing.exists && existing.get('orgId') !== orgId) {\n return {\n claim: null,\n error: 'That domain is already verified by another organization.',\n }\n }\n\n const ref = claimRef(orgId, domain)\n const snapshot = await ref.get()\n const stored = snapshot.exists\n ? (snapshot.get('token') as string | undefined)\n : undefined\n const token = stored || randomBytes(24).toString('base64url')\n if (!snapshot.exists) {\n await ref.set({\n domain,\n token,\n verified: false,\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n } else if (!stored) {\n // A claim document that EXISTS but carries NO token is a shape AGL-1887\n // introduced: {@link attestSsoDomain} writes the attestation marker and\n // nothing else, because a staff attestation has no DNS challenge behind\n // it. Before self-serve, every claim document was created here and so\n // always had a token — `exists` and `has a token` were the same fact.\n //\n // They are not any more, and conflating them broke the upgrade path for\n // exactly the orgs AGL-1887 exists to serve. `exists` alone decided\n // whether to persist, so \"Set up DNS proof\" on an attested domain minted\n // a token, showed the admin a TXT record built from it, and stored it\n // NOWHERE — a different value on every click. `verifyDomainClaim` re-reads\n // `snapshot.get('token')`, finds `undefined`, and could never match the\n // record they had just published. An attested domain could never become a\n // verified one.\n //\n // MERGE, and `verified` is deliberately NOT written: the attestation has\n // to survive being upgraded, and a claim already midway through DNS is not\n // this branch's to demote.\n await ref.set({ domain, token }, { merge: true })\n }\n return {\n error: null,\n claim: {\n domain,\n token,\n verified: Boolean(snapshot.get('verified')),\n recordHost: `${SSO_CHALLENGE_PREFIX}.${domain}`,\n recordValue: challengeValue(token),\n lastRecords: snapshot.get('lastRecords') as string[] | undefined,\n },\n }\n}\n\nexport interface DomainVerifyResult {\n domain: string\n verified: boolean\n /** What the lookup actually saw — the difference between \"missing\" and \"wrong\". */\n records: string[]\n expected: string\n}\n\n/**\n * Check a claim against live DNS and, on success, promote the domain into\n * `sso.domains` — the array that sign-in consults.\n *\n * Failure is recorded but never destructive: a transient DNS outage must not\n * silently un-govern a domain mid-flight. Removal is `revokeDomain`, which is\n * an explicit act.\n */\nexport async function verifyDomainClaim(\n orgId: string,\n rawDomain: string,\n): Promise<{ result: DomainVerifyResult | null; error: string | null }> {\n const { domain, error } = validateSsoDomain(rawDomain)\n if (!domain) return { result: null, error: error ?? 'Invalid domain' }\n\n const ref = claimRef(orgId, domain)\n const snapshot = await ref.get()\n if (!snapshot.exists) {\n return { result: null, error: 'No claim on that domain — add it first.' }\n }\n const token = snapshot.get('token') as string\n const expected = challengeValue(token)\n const records = await resolveChallengeTxt(domain)\n const verified = recordsProveOwnership(records, token)\n\n await ref.set(\n {\n verified,\n lastCheckedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n ...(verified\n ? {\n verifiedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n lastRecords: firebaseAdmin.firestore.FieldValue.delete(),\n }\n : { lastRecords: records.slice(0, 5) }),\n },\n { merge: true },\n )\n\n if (verified) {\n await firestore()\n .collection('orgs')\n .doc(orgId)\n .set(\n {\n sso: {\n domains: firebaseAdmin.firestore.FieldValue.arrayUnion(domain),\n domainVerified: true,\n },\n },\n { merge: true },\n )\n }\n\n return { error: null, result: { domain, verified, records, expected } }\n}\n\n/**\n * Drop a domain: out of `sso.domains`, and its public routing doc deactivated.\n *\n * Order matters. The public doc goes first, because it is the one an attacker\n * would benefit from: while it says `active`, sign-ins for the domain still\n * route to this org's IdP regardless of what the org doc says.\n */\nexport async function revokeDomain(\n orgId: string,\n rawDomain: string,\n): Promise<void> {\n const domain = normalizeSsoDomain(rawDomain)\n if (!domain) return\n const publicRef = firestore().collection('ssoDomains').doc(domain)\n const publicSnapshot = await publicRef.get()\n if (publicSnapshot.exists && publicSnapshot.get('orgId') === orgId) {\n await publicRef.set({ active: false }, { merge: true })\n }\n await firestore()\n .collection('orgs')\n .doc(orgId)\n .set(\n {\n sso: {\n domains: firebaseAdmin.firestore.FieldValue.arrayRemove(domain),\n },\n },\n { merge: true },\n )\n await claimRef(orgId, domain).delete().catch(() => undefined)\n}\n\n/**\n * What the customer needs to type into their IdP. Derived, never stored —\n * these are properties of OUR deployment, and a stale stored copy would send\n * somebody to configure an ACS URL we no longer serve.\n *\n * The callback is Firebase Auth's own handler on the project `authDomain`;\n * `rpEntityId` is the relying-party entity id their IdP will see.\n */\nexport interface SsoServiceMetadata {\n acsUrl: string\n entityId: string\n authDomain: string\n}\n\n/**\n * Thrown by `ssoServiceMetadata()` when this deployment has no auth origin at\n * all (AGL-2020).\n *\n * Deliberately names the variables and NOT a value: handing the operator\n * a `*.firebaseapp.com` example here is how they end up pasting somebody\n * else's project id into their IdP. `sso-provisioning.spec.ts` asserts this\n * string mentions no Aglyn host.\n */\nexport const SSO_AUTH_ORIGIN_UNCONFIGURED_MESSAGE =\n 'Single sign-on is not configured for this deployment: no auth origin ' +\n 'could be resolved. Set NEXT_PUBLIC_FIREBASE_PROJECT_ID — or one of ' +\n 'NEXT_PUBLIC_FIREBASE_AUTH_HANDLER_HOST, NEXT_PUBLIC_WORKSPACE_DOMAIN or ' +\n 'NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN — and rebuild.'\n\n/**\n * A HOST, never a URL.\n *\n * These strings are compared byte for byte — by GCIP against the assertion's\n * Audience, and by the IdP against the Reply URL it posts to. An operator who\n * pastes `https://auth.aglyn.com/` into the env var would otherwise produce\n * `https://https://auth.aglyn.com//__/auth/handler` and reject every\n * assertion, which presents as \"SSO stopped working\" with nothing in the\n * config that looks wrong.\n */\nfunction normalizeAuthHost(value: string): string {\n return String(value ?? '')\n .trim()\n .replace(/^https?:\\/\\//i, '')\n .replace(/\\/.*$/, '')\n .toLowerCase()\n}\n\n/**\n * The auth origin this deployment actually serves (AGL-1381).\n *\n * Mirrors `resolveFirebaseAuthDomain()` in the client's `firebase-config`: on a\n * workspace deployment the whole OAuth/SAML handshake is funnelled through one\n * dedicated same-site origin, `auth.<workspaceDomain>`, which reverse-proxies\n * Firebase's `/__/auth/*` helpers (AGL-462). The IdP has to post the assertion\n * to THAT origin — the one holding the pending-redirect state — or browser\n * storage partitioning severs the two halves of the flow.\n *\n * The order is load-bearing:\n *\n * 1. `NEXT_PUBLIC_FIREBASE_AUTH_HANDLER_HOST` — an explicit override wins.\n * 2. `auth.<NEXT_PUBLIC_WORKSPACE_DOMAIN>` — the deployed workspace origin.\n * Derived only when the variable is actually set, never defaulted: falling\n * back to `auth.aglyn.com` would point a self-hosted install at OUR auth\n * origin, and it would also make the `firebaseapp.com` arm unreachable.\n * It sits ABOVE `AUTH_DOMAIN` because production sets both, and the\n * branded host is the one production serves.\n * 3–4. the project's `*.firebaseapp.com` domain — localhost, previews that\n * are not on the workspace domain, the emulator, and self-host installs\n * that have not stood up an auth subdomain.\n *\n * When all four are absent this THROWS (AGL-2020). It used to default the\n * project id to `aglyn-main` — ours — which is the anti-pattern AGL-1919\n * exists to prevent, and it was silent in both directions that matter:\n * `provisionSsoPool()` wrote our origin into the operator's OWN GCIP provider\n * as `rpEntityId`/`callbackURL`, and the console printed it as the Reply URL\n * for their customer to paste into their IdP. A third party's SAML assertions\n * would then be posted at infrastructure the operator does not run.\n *\n * Throwing rather than degrading is deliberate, and this is the one place in\n * the self-host audit where \"run with the feature off\" is not available: there\n * is no empty value to fall back to. `authDomain: ''` emits\n * `https:///__/auth/handler`, which GCIP accepts on write and which then\n * rejects every assertion afterwards with nothing in the config that looks\n * wrong — the same \"looks configured, works never\" shape as an App Check\n * provider built on `undefined`. SSO is opt-in and deliberately configured, so\n * an operator reaching this has asked for it; a named error telling them which\n * variable to set is strictly better than either wrong origin.\n *\n * The READ path degrades instead of 500ing: `/api/orgs/sso?action=status`\n * catches this and returns `metadata: null` with the message, so the settings\n * page renders the reason rather than a wrong instruction. The WRITE path\n * (`provisionSsoPool`) lets it fly.\n *\n * `entityId` carries the scheme. It is not cosmetic: live GCIP for the first\n * SSO org holds `https://auth.aglyn.com`, and Google Workspace is already\n * sending that exact Audience. Emitting the bare host here would be written\n * over the working config by the next `provisionSsoPool()` save and lock out\n * every SSO-only user in the org. `sso-provisioning.spec.ts` pins both strings\n * character for character for that reason.\n */\nexport function ssoServiceMetadata(): SsoServiceMetadata {\n const workspaceDomain = normalizeAuthHost(\n process.env['NEXT_PUBLIC_WORKSPACE_DOMAIN'],\n )\n const projectId = String(\n process.env['NEXT_PUBLIC_FIREBASE_PROJECT_ID'] ?? '',\n ).trim()\n const authDomain =\n normalizeAuthHost(process.env['NEXT_PUBLIC_FIREBASE_AUTH_HANDLER_HOST']) ||\n (workspaceDomain ? `auth.${workspaceDomain}` : '') ||\n normalizeAuthHost(process.env['NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN']) ||\n (projectId ? `${projectId}.firebaseapp.com` : '')\n if (!authDomain) throw new Error(SSO_AUTH_ORIGIN_UNCONFIGURED_MESSAGE)\n return {\n authDomain,\n acsUrl: `https://${authDomain}/__/auth/handler`,\n entityId: `https://${authDomain}`,\n }\n}\n\n/**\n * GCIP display names are constrained (4–20 chars, must start with a letter,\n * letters/digits/hyphens only). An org slug can violate every one of those, so\n * derive rather than pass through — a rejected display name would surface as an\n * opaque provisioning failure at the worst moment.\n */\nexport function poolDisplayName(orgSlug: string, orgId: string): string {\n const base = `${orgSlug || orgId}`\n .toLowerCase()\n .replace(/[^a-z0-9-]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '')\n .replace(/^[^a-z]+/, '')\n const padded = (base || 'org').slice(0, 20)\n return padded.length >= 4 ? padded : `${padded}-org`.slice(0, 20)\n}\n\nexport interface SamlIdpMetadata {\n entityId: string\n ssoUrl: string\n certificates: string[]\n}\n\nexport interface ProvisionResult {\n tenantId: string\n providerId: string\n /** True when this call created the pool rather than reusing one. */\n createdPool: boolean\n /** True when this call created the provider rather than updating one. */\n createdProvider: boolean\n}\n\n/**\n * Create (or update) the org's GCIP pool and its SAML provider.\n *\n * Idempotent by construction: an org that already has a `tenantId` reuses it,\n * and a provider that already exists is UPDATED rather than recreated. Creating\n * a second pool would strand every user in the first one — they are separate\n * user namespaces, so \"just make a new one\" silently locks out everybody who\n * has already signed in.\n */\nexport async function provisionSsoPool(options: {\n orgId: string\n orgSlug: string\n existingTenantId?: string\n existingProviderId?: string\n displayName: string\n idp: SamlIdpMetadata\n}): Promise<ProvisionResult> {\n const { orgId, orgSlug, existingTenantId, existingProviderId, idp } = options\n const manager = auth().tenantManager()\n const metadata = ssoServiceMetadata()\n\n let tenantId = existingTenantId\n let createdPool = false\n if (!tenantId) {\n const tenant = await manager.createTenant({\n displayName: poolDisplayName(orgSlug, orgId),\n emailSignInConfig: { enabled: false, passwordRequired: false },\n })\n tenantId = tenant.tenantId\n createdPool = true\n }\n\n const tenantAuth: BaseAuth = manager.authForTenant(tenantId)\n const providerId = existingProviderId || `saml.${poolDisplayName(orgSlug, orgId)}`\n const config = {\n displayName: options.displayName || 'Single sign-on',\n enabled: true,\n idpEntityId: idp.entityId,\n ssoURL: idp.ssoUrl,\n x509Certificates: idp.certificates,\n rpEntityId: metadata.entityId,\n callbackURL: metadata.acsUrl,\n }\n\n let createdProvider = false\n try {\n await tenantAuth.getProviderConfig(providerId)\n await tenantAuth.updateProviderConfig(providerId, config)\n } catch {\n await tenantAuth.createProviderConfig({ providerId, ...config })\n createdProvider = true\n }\n\n return { tenantId, providerId, createdPool, createdProvider }\n}\n\n/**\n * Is this claim a STAFF ATTESTATION that the org owns the domain? (AGL-1887)\n *\n * The second of exactly two ways a domain can be proven, and the one that\n * exists for orgs onboarded before self-serve. Their domain was written onto\n * `sso.domains` by a person who checked, with no claim document behind it — so\n * `unpublishSsoDomains` deactivated their routing happily and\n * `publishSsoDomains` then refused to bring it back. Off worked, on did not,\n * and for `aglyn-org` the owner's only credential lives inside the pool that\n * stops answering (AGL-1888).\n *\n * A POSITIVE MARKER, never an absence. The rejected alternative was to treat\n * \"a routing doc already exists naming this org\" as permission to re-publish,\n * which makes `unpublish` non-final and would let a domain whose claim was\n * revoked come back. What is trusted here is a field a staff member wrote on\n * purpose, and it says WHO, so the trust is attributable.\n *\n * WHY THIS IS NOT A CLIENT-REACHABLE HOLE, which is the only question that\n * matters about it:\n *\n * - `orgs/{orgId}/ssoDomains/{domain}` is `allow read, write: if false` in\n * `cloud/firebase-firestore.rules` — deny-all for every client, reads\n * included. Only the Admin SDK, which bypasses rules, can write here.\n * - No customer-reachable route writes this field. `issueDomainClaim` is the\n * only claim writer on the org's own path and it writes `domain`, `token`,\n * `verified: false` and `createdAt` — never `attestedBy`.\n * - {@link attestSsoDomain} is the only writer, and it is staff-only by\n * construction: it lives here, server-side, and takes the attesting uid.\n *\n * That is why AGL-1887's proposed rules change is NOT part of this: the\n * subcollection is already deny-all, and adding a rule permitting a narrower\n * write than \"none\" would loosen it, not tighten it.\n *\n * A non-empty STRING, checked strictly. `true`, `1` or `{}` are not an\n * attestation by anybody, and a field that can be satisfied by any truthy\n * value is one a future careless write can satisfy by accident.\n */\nexport function isStaffAttestedClaim(attestedBy: unknown): boolean {\n return typeof attestedBy === 'string' && attestedBy.trim().length > 0\n}\n\n/**\n * Record that staff have verified, out of band, that an org owns a domain\n * (AGL-1887).\n *\n * This is the write half of the attestation {@link isStaffAttestedClaim}\n * reads, and it exists so there is exactly ONE way the marker can be created\n * and it is a named function with an actor on it — rather than a field somebody\n * sets by hand in the Firebase console with no record of who or why.\n *\n * `verified` is left FALSE and untouched. The two facts are different: DNS\n * verification is a proof we re-ran ourselves, an attestation is a person\n * vouching. Collapsing them would make an attested domain indistinguishable\n * from a DNS-proven one in the data, and the `verified` flag is what\n * `verifyDomainClaim` owns.\n *\n * Not exposed through `/api/orgs/sso`, deliberately. An org admin proving\n * their own domain has DNS for that; an attestation is us saying we checked,\n * and a customer-reachable path that wrote it would be a customer-reachable\n * path to publishing any domain.\n */\nexport async function attestSsoDomain(options: {\n orgId: string\n domain: string\n /** The staff member vouching. Recorded, and required. */\n attestedByUid: string\n note?: string | null\n}): Promise<{ domain: string | null; error: string | null }> {\n const { domain, error } = validateSsoDomain(options.domain)\n if (!domain) return { domain: null, error: error ?? 'Invalid domain' }\n if (!isStaffAttestedClaim(options.attestedByUid)) {\n return { domain: null, error: 'An attestation needs the staff uid making it.' }\n }\n\n // Same uniqueness rule `issueDomainClaim` enforces. An attestation must not\n // be a way around \"one domain, one org\" — if another org already holds live\n // routing for this domain, that conflict is resolved by a human before\n // anything is written, not by whoever attests last.\n const existing = await firestore().collection('ssoDomains').doc(domain).get()\n if (existing.exists && existing.get('orgId') !== options.orgId) {\n return {\n domain: null,\n error: 'That domain is already verified by another organization.',\n }\n }\n\n await claimRef(options.orgId, domain).set(\n {\n domain,\n attestedBy: options.attestedByUid.trim(),\n attestedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n attestationNote: options.note?.trim() || null,\n },\n // MERGE, so an existing claim keeps its token and its `verified` state. A\n // fresh document is created for the pre-self-serve orgs that have none,\n // which is the whole point, but an attestation must never reset a claim\n // the org is midway through proving by DNS.\n { merge: true },\n )\n return { domain, error: null }\n}\n\n/**\n * Publish the public routing docs — the last step, and the one that actually\n * turns SSO on for a domain.\n *\n * Only PROVEN domains are written — DNS-verified, or staff-attested per\n * {@link isStaffAttestedClaim}. This function is the single writer of\n * `ssoDomains`, so the invariant \"a live routing doc implies proven ownership\"\n * is enforceable by reading one place.\n */\nexport async function publishSsoDomains(options: {\n orgId: string\n tenantId: string\n providerId: string\n protocol: 'saml' | 'oidc'\n displayName?: string\n domains: string[]\n}): Promise<string[]> {\n const { orgId, tenantId, providerId, protocol, displayName } = options\n const published: string[] = []\n const batch = firestore().batch()\n for (const raw of options.domains) {\n const domain = normalizeSsoDomain(raw)\n if (!domain) continue\n // Re-read the claim rather than trusting the caller's list: this is the\n // boundary where an unverified domain would become live, so the check\n // belongs here and not only at the call site.\n //\n // TWO ways to be proven, and no third (AGL-1887). `verified === true` is\n // DNS, re-checked by us; `attestedBy` is a named staff member vouching for\n // an org onboarded before self-serve existed. Both are POSITIVE markers on\n // the org's own claim document, which no client can write.\n //\n // What this must never become is an unconditional accept, or an inference\n // from the routing doc already existing. That check is what stops an org\n // publishing routing for a domain it does not own and intercepting another\n // company's sign-ins — see `sso-publish-gate.emulator.spec.ts`, which\n // exercises every refusal branch, and `sso-attested-restore.spec.ts`,\n // which pins the boundary this loosening must not cross.\n const claim = await claimRef(orgId, domain).get()\n if (!claim.exists) continue\n const proven =\n claim.get('verified') === true ||\n isStaffAttestedClaim(claim.get('attestedBy'))\n if (!proven) continue\n batch.set(\n firestore().collection('ssoDomains').doc(domain),\n {\n orgId,\n tenantId,\n providerId,\n protocol,\n displayName: displayName ?? null,\n active: true,\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n published.push(domain)\n }\n await batch.commit()\n return published\n}\n\n/** Deactivate every routing doc this org owns (disable, not delete). */\nexport async function unpublishSsoDomains(orgId: string): Promise<void> {\n const owned = await firestore()\n .collection('ssoDomains')\n .where('orgId', '==', orgId)\n .get()\n if (owned.empty) return\n const batch = firestore().batch()\n owned.docs.forEach((doc) => batch.set(doc.ref, { active: false }, { merge: true }))\n await batch.commit()\n}\n"],"names":["promises","dns","Resolver","CallbackResolver","randomBytes","firebaseAdmin","auth","app","firestore","SSO_CHALLENGE_PREFIX","SSO_TXT_PREFIX","DOMAIN_PATTERN","PUBLIC_RESOLVERS","PUBLIC_EMAIL_DOMAINS","Set","normalizeSsoDomain","input","raw","String","trim","toLowerCase","at","lastIndexOf","slice","replace","validateSsoDomain","domain","test","error","has","resolveChallengeTxt","host","flatten","records","map","chunks","join","resolver","setServers","Promise","resolve","reject","resolveTxt","addresses","code","challengeValue","token","recordsProveOwnership","includes","assessDomainDrift","SSO_DRIFT_FAILURES_BEFORE_REPORT","SSO_DRIFT_MIN_AGE_MS","probeChallengeTxt","settle","status","conclusive","fallbackError","fallbackCode","claimRef","orgId","collection","doc","issueDomainClaim","rawDomain","claim","existing","get","exists","ref","snapshot","stored","undefined","toString","set","verified","createdAt","FieldValue","serverTimestamp","merge","Boolean","recordHost","recordValue","lastRecords","verifyDomainClaim","result","expected","lastCheckedAt","verifiedAt","delete","sso","domains","arrayUnion","domainVerified","revokeDomain","publicRef","publicSnapshot","active","arrayRemove","catch","SSO_AUTH_ORIGIN_UNCONFIGURED_MESSAGE","normalizeAuthHost","value","ssoServiceMetadata","process","workspaceDomain","env","projectId","authDomain","Error","acsUrl","entityId","poolDisplayName","orgSlug","base","padded","length","provisionSsoPool","options","existingTenantId","existingProviderId","idp","manager","tenantManager","metadata","tenantId","createdPool","tenant","createTenant","displayName","emailSignInConfig","enabled","passwordRequired","tenantAuth","authForTenant","providerId","config","idpEntityId","ssoURL","ssoUrl","x509Certificates","certificates","rpEntityId","callbackURL","createdProvider","getProviderConfig","updateProviderConfig","createProviderConfig","isStaffAttestedClaim","attestedBy","attestSsoDomain","attestedByUid","attestedAt","attestationNote","note","publishSsoDomains","protocol","published","batch","proven","updatedAt","push","commit","unpublishSsoDomains","owned","where","empty","docs","forEach"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 SendEmailResult } from '@aglyn/shared-util-email';
|
|
18
|
+
/**
|
|
19
|
+
* A staff alert by email, from LIBRARY code (AGL-2925).
|
|
20
|
+
*
|
|
21
|
+
* `STAFF_ALERT_EMAIL` is the one inbox every subsystem's alert lands in —
|
|
22
|
+
* the console's `emailStaffAlert` in `apps/console/app/api/_lib/
|
|
23
|
+
* usage-alert-email.ts` sends the cron's alerts there. A library cannot
|
|
24
|
+
* call that, because a library cannot import from an app. So a library's
|
|
25
|
+
* own alert — the AI meter's platform-wide free-spend ceiling is one —
|
|
26
|
+
* sends through this, to the same address, with the same posture: unset is
|
|
27
|
+
* the ordinary answer outside production and reports `unconfigured`, and
|
|
28
|
+
* nothing here throws, because the mail is a courtesy and the refusal it
|
|
29
|
+
* announces is the control.
|
|
30
|
+
*
|
|
31
|
+
* Platform email metering is loaded lazily and only after a send, so that
|
|
32
|
+
* importing this module — which every unit test of the meter does — never
|
|
33
|
+
* pulls the Firestore-backed meter, and with it the admin app, into a test
|
|
34
|
+
* that only wanted to count credits.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sendStaffAlertEmail(input: {
|
|
37
|
+
subject: string;
|
|
38
|
+
text: string;
|
|
39
|
+
/** Resend tag / log label. */
|
|
40
|
+
context: string;
|
|
41
|
+
}): Promise<SendEmailResult>;
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { sendEmail } from "@aglyn/shared-util-email";
|
|
17
|
+
/**
|
|
18
|
+
* A staff alert by email, from LIBRARY code (AGL-2925).
|
|
19
|
+
*
|
|
20
|
+
* `STAFF_ALERT_EMAIL` is the one inbox every subsystem's alert lands in —
|
|
21
|
+
* the console's `emailStaffAlert` in `apps/console/app/api/_lib/
|
|
22
|
+
* usage-alert-email.ts` sends the cron's alerts there. A library cannot
|
|
23
|
+
* call that, because a library cannot import from an app. So a library's
|
|
24
|
+
* own alert — the AI meter's platform-wide free-spend ceiling is one —
|
|
25
|
+
* sends through this, to the same address, with the same posture: unset is
|
|
26
|
+
* the ordinary answer outside production and reports `unconfigured`, and
|
|
27
|
+
* nothing here throws, because the mail is a courtesy and the refusal it
|
|
28
|
+
* announces is the control.
|
|
29
|
+
*
|
|
30
|
+
* Platform email metering is loaded lazily and only after a send, so that
|
|
31
|
+
* importing this module — which every unit test of the meter does — never
|
|
32
|
+
* pulls the Firestore-backed meter, and with it the admin app, into a test
|
|
33
|
+
* that only wanted to count credits.
|
|
34
|
+
*/ export async function sendStaffAlertEmail(input) {
|
|
35
|
+
var _process_env_STAFF_ALERT_EMAIL;
|
|
36
|
+
const to = String((_process_env_STAFF_ALERT_EMAIL = process.env.STAFF_ALERT_EMAIL) != null ? _process_env_STAFF_ALERT_EMAIL : '').trim();
|
|
37
|
+
if (!to.includes('@')) return {
|
|
38
|
+
sent: false,
|
|
39
|
+
reason: 'unconfigured'
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
const result = await sendEmail({
|
|
43
|
+
to,
|
|
44
|
+
subject: input.subject,
|
|
45
|
+
text: input.text,
|
|
46
|
+
context: input.context
|
|
47
|
+
});
|
|
48
|
+
if (result.sent) {
|
|
49
|
+
// Platform-scoped (AGL-1438): our own alert is our own cost.
|
|
50
|
+
const { meterPlatformEmail } = await import("./email-metering.js");
|
|
51
|
+
await meterPlatformEmail().catch(()=>undefined);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('[staff-alert-email] send failed', error);
|
|
56
|
+
return {
|
|
57
|
+
sent: false,
|
|
58
|
+
reason: 'network'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=staff-alert-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/staff-alert-email.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 { sendEmail, type SendEmailResult } from '@aglyn/shared-util-email'\n\n/**\n * A staff alert by email, from LIBRARY code (AGL-2925).\n *\n * `STAFF_ALERT_EMAIL` is the one inbox every subsystem's alert lands in —\n * the console's `emailStaffAlert` in `apps/console/app/api/_lib/\n * usage-alert-email.ts` sends the cron's alerts there. A library cannot\n * call that, because a library cannot import from an app. So a library's\n * own alert — the AI meter's platform-wide free-spend ceiling is one —\n * sends through this, to the same address, with the same posture: unset is\n * the ordinary answer outside production and reports `unconfigured`, and\n * nothing here throws, because the mail is a courtesy and the refusal it\n * announces is the control.\n *\n * Platform email metering is loaded lazily and only after a send, so that\n * importing this module — which every unit test of the meter does — never\n * pulls the Firestore-backed meter, and with it the admin app, into a test\n * that only wanted to count credits.\n */\nexport async function sendStaffAlertEmail(input: {\n subject: string\n text: string\n /** Resend tag / log label. */\n context: string\n}): Promise<SendEmailResult> {\n const to = String(process.env.STAFF_ALERT_EMAIL ?? '').trim()\n if (!to.includes('@')) return { sent: false, reason: 'unconfigured' }\n try {\n const result = await sendEmail({\n to,\n subject: input.subject,\n text: input.text,\n context: input.context,\n })\n if (result.sent) {\n // Platform-scoped (AGL-1438): our own alert is our own cost.\n const { meterPlatformEmail } = await import('./email-metering')\n await meterPlatformEmail().catch(() => undefined)\n }\n return result\n } catch (error) {\n console.error('[staff-alert-email] send failed', error)\n return { sent: false, reason: 'network' }\n }\n}\n"],"names":["sendEmail","sendStaffAlertEmail","input","process","to","String","env","STAFF_ALERT_EMAIL","trim","includes","sent","reason","result","subject","text","context","meterPlatformEmail","catch","undefined","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,SAAS,QAA8B,2BAA0B;AAE1E;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,eAAeC,oBAAoBC,KAKzC;QACmBC;IAAlB,MAAMC,KAAKC,QAAOF,iCAAAA,QAAQG,GAAG,CAACC,iBAAiB,YAA7BJ,iCAAiC,IAAIK,IAAI;IAC3D,IAAI,CAACJ,GAAGK,QAAQ,CAAC,MAAM,OAAO;QAAEC,MAAM;QAAOC,QAAQ;IAAe;IACpE,IAAI;QACF,MAAMC,SAAS,MAAMZ,UAAU;YAC7BI;YACAS,SAASX,MAAMW,OAAO;YACtBC,MAAMZ,MAAMY,IAAI;YAChBC,SAASb,MAAMa,OAAO;QACxB;QACA,IAAIH,OAAOF,IAAI,EAAE;YACf,6DAA6D;YAC7D,MAAM,EAAEM,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC;YAC5C,MAAMA,qBAAqBC,KAAK,CAAC,IAAMC;QACzC;QACA,OAAON;IACT,EAAE,OAAOO,OAAO;QACdC,QAAQD,KAAK,CAAC,mCAAmCA;QACjD,OAAO;YAAET,MAAM;YAAOC,QAAQ;QAAU;IAC1C;AACF"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Whether a stored Connect linkage can actually take money HERE (AGL-2471).
|
|
19
|
+
*
|
|
20
|
+
* THE DEFECT. Production Firestore held three Connect linkages and all three
|
|
21
|
+
* named TEST-mode accounts. One — `profiles/7AVEMtDa…`, `stripeAccountId:
|
|
22
|
+
* acct_1TulDeRbL3B9Ioqz` — also carried `stripeChargesEnabled: true`, which
|
|
23
|
+
* was the entire payments-readiness test every money door made:
|
|
24
|
+
*
|
|
25
|
+
* if (!accountId || !ownerProfile.get('stripeChargesEnabled')) …
|
|
26
|
+
*
|
|
27
|
+
* So three storefronts presented as payments-ready, minted a LIVE Checkout
|
|
28
|
+
* session with a TEST-mode `payment_intent_data[transfer_data][destination]`,
|
|
29
|
+
* and were refused by Stripe as a generic 502 at the shopper's checkout. A
|
|
30
|
+
* merchant told they are ready to take money, who silently cannot.
|
|
31
|
+
*
|
|
32
|
+
* WHY THE ACCOUNT ID IS NOT THE EVIDENCE. `acct_1TulDeRbL3B9Ioqz` is a real
|
|
33
|
+
* production value naming a TEST account. Stripe's account ids carry no mode
|
|
34
|
+
* marker — live and test ids are the same shape — so no amount of reading the
|
|
35
|
+
* string can tell them apart. What DOES tell them apart is Stripe itself:
|
|
36
|
+
* retrieving that id with the live key answers
|
|
37
|
+
*
|
|
38
|
+
* 400 The account acct_1TulDeRbL3B9Ioqz was a test account created with a
|
|
39
|
+
* testmode key, and therefore can only be used with testmode keys.
|
|
40
|
+
*
|
|
41
|
+
* The Account object, notably, has NO `livemode` field of its own (verified
|
|
42
|
+
* against the live API: the payload carries `charges_enabled`,
|
|
43
|
+
* `payouts_enabled`, `capabilities` and no `livemode`). So the mode is
|
|
44
|
+
* recorded at the two moments Stripe does state it:
|
|
45
|
+
*
|
|
46
|
+
* 1. `account.updated` — the EVENT carries `livemode`, and it is Stripe's
|
|
47
|
+
* own statement about the account the event is describing;
|
|
48
|
+
* 2. onboarding — the account was just created or retrieved successfully
|
|
49
|
+
* with the platform key, and Stripe hard-refuses cross-mode retrieval
|
|
50
|
+
* (above), so the account's mode IS the key's mode; the key's mode is
|
|
51
|
+
* then confirmed against the API via `resolvePlatformStripeMode`.
|
|
52
|
+
*
|
|
53
|
+
* WHY THE RULE IS ASYMMETRIC, AND NOT "REFUSE ANYTHING UNPROVEN". An absent
|
|
54
|
+
* `stripeAccountLivemode` refuses only on a LIVE deployment. That is where
|
|
55
|
+
* real money moves and where the defect was found, and a live deployment can
|
|
56
|
+
* always re-establish the field: the merchant reconnects, or Stripe sends one
|
|
57
|
+
* `account.updated`. Everywhere else — a test-key deployment, a developer
|
|
58
|
+
* machine, a self-hosted install still in sandbox, the whole test suite — an
|
|
59
|
+
* unverified linkage keeps the behaviour it had before, because no real money
|
|
60
|
+
* can move there and Stripe enforces the mode boundary itself. Refusing
|
|
61
|
+
* everywhere would have bought production nothing extra and broken every
|
|
62
|
+
* non-production deployment.
|
|
63
|
+
*
|
|
64
|
+
* A PROVEN mismatch (`stripeAccountLivemode` recorded and disagreeing) is
|
|
65
|
+
* refused in BOTH directions, because that costs nothing to detect and is
|
|
66
|
+
* never right.
|
|
67
|
+
*
|
|
68
|
+
* WHAT IS SNIFFED, AND WHY THAT ONE IS FAIR. The SECRET KEY states its own
|
|
69
|
+
* mode — `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` — and that is a
|
|
70
|
+
* documented, stable Stripe invariant, unlike the account id. It is used only
|
|
71
|
+
* to answer "which mode is this deployment", never "which mode is this
|
|
72
|
+
* account", and even there `resolvePlatformStripeMode` prefers the API.
|
|
73
|
+
*
|
|
74
|
+
* HOW IT GOT THERE. A developer machine runs against PRODUCTION Firestore
|
|
75
|
+
* (`FIREBASE_PROJECT_ID=aglyn-main` in every dev env file) while
|
|
76
|
+
* `apps/console/.env.development.local` sets `STRIPE_SECRET_KEY` to the
|
|
77
|
+
* `sk_test_` key by deliberate policy (AGL-1137: localhost must never touch
|
|
78
|
+
* live Stripe). The connect route then creates a test-mode Express account
|
|
79
|
+
* and writes it into the production database. Nothing about that is going to
|
|
80
|
+
* change — the split key is the right call — so the gate has to be the thing
|
|
81
|
+
* that notices.
|
|
82
|
+
*/
|
|
83
|
+
/** Which Stripe world a key or an account belongs to. */
|
|
84
|
+
export type StripeMode = 'live' | 'test';
|
|
85
|
+
/**
|
|
86
|
+
* Why a stored linkage may not be charged against. Every value other than
|
|
87
|
+
* `'ready'` is a refusal, and the mode ones are refusals the old two-field
|
|
88
|
+
* test could not make.
|
|
89
|
+
*/
|
|
90
|
+
export type ConnectReadiness =
|
|
91
|
+
/** Charge away. */
|
|
92
|
+
'ready'
|
|
93
|
+
/** No Connect account is stored at all — the onboarding call to action. */
|
|
94
|
+
| 'not-connected'
|
|
95
|
+
/** Stripe will not let this account take charges. */
|
|
96
|
+
| 'charges-disabled'
|
|
97
|
+
/**
|
|
98
|
+
* The linkage predates AGL-2471 (or was written by something that did not
|
|
99
|
+
* record mode), so its mode was never established — AND this deployment is
|
|
100
|
+
* LIVE. This is the shape all three poisoned production records have.
|
|
101
|
+
* The connect route and the `account.updated` sync both write the field, so
|
|
102
|
+
* a genuine merchant self-heals on the next of either.
|
|
103
|
+
*/
|
|
104
|
+
| 'mode-unverified'
|
|
105
|
+
/** The account belongs to the OTHER Stripe world. The AGL-2471 defect. */
|
|
106
|
+
| 'mode-mismatch';
|
|
107
|
+
export interface ConnectReadinessInput {
|
|
108
|
+
/** `stripeAccountId` off the profile, whatever shape the document has. */
|
|
109
|
+
accountId?: unknown;
|
|
110
|
+
/** `stripeChargesEnabled` off the profile. */
|
|
111
|
+
chargesEnabled?: unknown;
|
|
112
|
+
/** `stripeAccountLivemode` off the profile — absent before AGL-2471. */
|
|
113
|
+
accountLivemode?: unknown;
|
|
114
|
+
/** Defaults to the mode of `STRIPE_SECRET_KEY`. */
|
|
115
|
+
platformMode?: StripeMode | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The mode a Stripe secret key states about itself, or `undefined` when the
|
|
119
|
+
* string is not a Stripe secret key.
|
|
120
|
+
*
|
|
121
|
+
* Deliberately narrow: it matches `sk_`/`rk_` followed by `live`/`test`, and
|
|
122
|
+
* refuses everything else rather than guessing. Handing it an `acct_…` id
|
|
123
|
+
* returns `undefined` — the whole point is that account ids say nothing.
|
|
124
|
+
*/
|
|
125
|
+
export declare function platformStripeMode(key?: string | undefined): StripeMode | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* The platform's mode, asked of Stripe and only then of the key.
|
|
128
|
+
*
|
|
129
|
+
* `GET /v1/balance` is a singleton that carries `livemode`, so it states the
|
|
130
|
+
* mode of whatever key made the call — no object has to be created to find
|
|
131
|
+
* out. The key prefix is the fallback for a restricted key that cannot read
|
|
132
|
+
* balance; without it, a self-hosted install with a scoped key could not
|
|
133
|
+
* finish onboarding at all.
|
|
134
|
+
*/
|
|
135
|
+
export declare function resolvePlatformStripeMode(key?: string | undefined, fetchImpl?: typeof fetch): Promise<StripeMode | undefined>;
|
|
136
|
+
/**
|
|
137
|
+
* Decides whether a stored Connect linkage may be charged against.
|
|
138
|
+
*
|
|
139
|
+
* Order matters: the two refusals that existed before AGL-2471 are asked
|
|
140
|
+
* first, so an unconnected or restricted merchant still gets the answer they
|
|
141
|
+
* always got, and only a linkage that WOULD have passed reaches the mode
|
|
142
|
+
* question.
|
|
143
|
+
*/
|
|
144
|
+
export declare function connectReadiness(input: ConnectReadinessInput): ConnectReadiness;
|
|
145
|
+
/**
|
|
146
|
+
* The form every money door calls: `true` only when the linkage may be
|
|
147
|
+
* charged, with the two mode refusals reported to the server log.
|
|
148
|
+
*
|
|
149
|
+
* The log line is the part the shopper's generic 409 cannot carry. Before
|
|
150
|
+
* this, a mode mismatch surfaced as a 502 from Stripe with nothing anywhere
|
|
151
|
+
* naming the cause; the sale is still refused the same way, but now somebody
|
|
152
|
+
* can find out why.
|
|
153
|
+
*/
|
|
154
|
+
export declare function connectLinkageIsReady(input: ConnectReadinessInput, context?: {
|
|
155
|
+
subject?: string;
|
|
156
|
+
}): boolean;
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
* Whether a stored Connect linkage can actually take money HERE (AGL-2471).
|
|
18
|
+
*
|
|
19
|
+
* THE DEFECT. Production Firestore held three Connect linkages and all three
|
|
20
|
+
* named TEST-mode accounts. One — `profiles/7AVEMtDa…`, `stripeAccountId:
|
|
21
|
+
* acct_1TulDeRbL3B9Ioqz` — also carried `stripeChargesEnabled: true`, which
|
|
22
|
+
* was the entire payments-readiness test every money door made:
|
|
23
|
+
*
|
|
24
|
+
* if (!accountId || !ownerProfile.get('stripeChargesEnabled')) …
|
|
25
|
+
*
|
|
26
|
+
* So three storefronts presented as payments-ready, minted a LIVE Checkout
|
|
27
|
+
* session with a TEST-mode `payment_intent_data[transfer_data][destination]`,
|
|
28
|
+
* and were refused by Stripe as a generic 502 at the shopper's checkout. A
|
|
29
|
+
* merchant told they are ready to take money, who silently cannot.
|
|
30
|
+
*
|
|
31
|
+
* WHY THE ACCOUNT ID IS NOT THE EVIDENCE. `acct_1TulDeRbL3B9Ioqz` is a real
|
|
32
|
+
* production value naming a TEST account. Stripe's account ids carry no mode
|
|
33
|
+
* marker — live and test ids are the same shape — so no amount of reading the
|
|
34
|
+
* string can tell them apart. What DOES tell them apart is Stripe itself:
|
|
35
|
+
* retrieving that id with the live key answers
|
|
36
|
+
*
|
|
37
|
+
* 400 The account acct_1TulDeRbL3B9Ioqz was a test account created with a
|
|
38
|
+
* testmode key, and therefore can only be used with testmode keys.
|
|
39
|
+
*
|
|
40
|
+
* The Account object, notably, has NO `livemode` field of its own (verified
|
|
41
|
+
* against the live API: the payload carries `charges_enabled`,
|
|
42
|
+
* `payouts_enabled`, `capabilities` and no `livemode`). So the mode is
|
|
43
|
+
* recorded at the two moments Stripe does state it:
|
|
44
|
+
*
|
|
45
|
+
* 1. `account.updated` — the EVENT carries `livemode`, and it is Stripe's
|
|
46
|
+
* own statement about the account the event is describing;
|
|
47
|
+
* 2. onboarding — the account was just created or retrieved successfully
|
|
48
|
+
* with the platform key, and Stripe hard-refuses cross-mode retrieval
|
|
49
|
+
* (above), so the account's mode IS the key's mode; the key's mode is
|
|
50
|
+
* then confirmed against the API via `resolvePlatformStripeMode`.
|
|
51
|
+
*
|
|
52
|
+
* WHY THE RULE IS ASYMMETRIC, AND NOT "REFUSE ANYTHING UNPROVEN". An absent
|
|
53
|
+
* `stripeAccountLivemode` refuses only on a LIVE deployment. That is where
|
|
54
|
+
* real money moves and where the defect was found, and a live deployment can
|
|
55
|
+
* always re-establish the field: the merchant reconnects, or Stripe sends one
|
|
56
|
+
* `account.updated`. Everywhere else — a test-key deployment, a developer
|
|
57
|
+
* machine, a self-hosted install still in sandbox, the whole test suite — an
|
|
58
|
+
* unverified linkage keeps the behaviour it had before, because no real money
|
|
59
|
+
* can move there and Stripe enforces the mode boundary itself. Refusing
|
|
60
|
+
* everywhere would have bought production nothing extra and broken every
|
|
61
|
+
* non-production deployment.
|
|
62
|
+
*
|
|
63
|
+
* A PROVEN mismatch (`stripeAccountLivemode` recorded and disagreeing) is
|
|
64
|
+
* refused in BOTH directions, because that costs nothing to detect and is
|
|
65
|
+
* never right.
|
|
66
|
+
*
|
|
67
|
+
* WHAT IS SNIFFED, AND WHY THAT ONE IS FAIR. The SECRET KEY states its own
|
|
68
|
+
* mode — `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` — and that is a
|
|
69
|
+
* documented, stable Stripe invariant, unlike the account id. It is used only
|
|
70
|
+
* to answer "which mode is this deployment", never "which mode is this
|
|
71
|
+
* account", and even there `resolvePlatformStripeMode` prefers the API.
|
|
72
|
+
*
|
|
73
|
+
* HOW IT GOT THERE. A developer machine runs against PRODUCTION Firestore
|
|
74
|
+
* (`FIREBASE_PROJECT_ID=aglyn-main` in every dev env file) while
|
|
75
|
+
* `apps/console/.env.development.local` sets `STRIPE_SECRET_KEY` to the
|
|
76
|
+
* `sk_test_` key by deliberate policy (AGL-1137: localhost must never touch
|
|
77
|
+
* live Stripe). The connect route then creates a test-mode Express account
|
|
78
|
+
* and writes it into the production database. Nothing about that is going to
|
|
79
|
+
* change — the split key is the right call — so the gate has to be the thing
|
|
80
|
+
* that notices.
|
|
81
|
+
*/ /** Which Stripe world a key or an account belongs to. */ /**
|
|
82
|
+
* The mode a Stripe secret key states about itself, or `undefined` when the
|
|
83
|
+
* string is not a Stripe secret key.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately narrow: it matches `sk_`/`rk_` followed by `live`/`test`, and
|
|
86
|
+
* refuses everything else rather than guessing. Handing it an `acct_…` id
|
|
87
|
+
* returns `undefined` — the whole point is that account ids say nothing.
|
|
88
|
+
*/ export function platformStripeMode(key = process.env.STRIPE_SECRET_KEY) {
|
|
89
|
+
const match = /^[sr]k_(live|test)_/.exec(String(key != null ? key : '').trim());
|
|
90
|
+
return match ? match[1] : undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The platform's mode, asked of Stripe and only then of the key.
|
|
94
|
+
*
|
|
95
|
+
* `GET /v1/balance` is a singleton that carries `livemode`, so it states the
|
|
96
|
+
* mode of whatever key made the call — no object has to be created to find
|
|
97
|
+
* out. The key prefix is the fallback for a restricted key that cannot read
|
|
98
|
+
* balance; without it, a self-hosted install with a scoped key could not
|
|
99
|
+
* finish onboarding at all.
|
|
100
|
+
*/ export async function resolvePlatformStripeMode(key = process.env.STRIPE_SECRET_KEY, fetchImpl = fetch) {
|
|
101
|
+
const fromKey = platformStripeMode(key);
|
|
102
|
+
if (!key) return fromKey;
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetchImpl('https://api.stripe.com/v1/balance', {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
headers: {
|
|
107
|
+
Authorization: `Bearer ${key}`
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
const payload = await response.json();
|
|
112
|
+
if (typeof (payload == null ? void 0 : payload.livemode) === 'boolean') {
|
|
113
|
+
return payload.livemode ? 'live' : 'test';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (unused) {
|
|
117
|
+
// Network or transport failure — fall through to the key, which is the
|
|
118
|
+
// same answer we would have recorded before this existed.
|
|
119
|
+
}
|
|
120
|
+
return fromKey;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Decides whether a stored Connect linkage may be charged against.
|
|
124
|
+
*
|
|
125
|
+
* Order matters: the two refusals that existed before AGL-2471 are asked
|
|
126
|
+
* first, so an unconnected or restricted merchant still gets the answer they
|
|
127
|
+
* always got, and only a linkage that WOULD have passed reaches the mode
|
|
128
|
+
* question.
|
|
129
|
+
*/ export function connectReadiness(input) {
|
|
130
|
+
const accountId = typeof input.accountId === 'string' ? input.accountId.trim() : '';
|
|
131
|
+
if (!accountId) return 'not-connected';
|
|
132
|
+
if (input.chargesEnabled !== true) return 'charges-disabled';
|
|
133
|
+
const platformMode = 'platformMode' in input ? input.platformMode : platformStripeMode();
|
|
134
|
+
// Three-valued on purpose, exactly as AGL-1997 reads `payoutsEnabled`: only
|
|
135
|
+
// a literal boolean is a recorded answer. A string `'true'` or a `1` is a
|
|
136
|
+
// field somebody else wrote, and inventing `true` from it would re-open the
|
|
137
|
+
// hole this closes.
|
|
138
|
+
if (typeof input.accountLivemode === 'boolean') {
|
|
139
|
+
// A PROVEN mismatch is refused in either direction. It costs nothing to
|
|
140
|
+
// detect and it is always wrong.
|
|
141
|
+
if (!platformMode) return 'ready';
|
|
142
|
+
return input.accountLivemode === (platformMode === 'live') ? 'ready' : 'mode-mismatch';
|
|
143
|
+
}
|
|
144
|
+
return platformMode === 'live' ? 'mode-unverified' : 'ready';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The form every money door calls: `true` only when the linkage may be
|
|
148
|
+
* charged, with the two mode refusals reported to the server log.
|
|
149
|
+
*
|
|
150
|
+
* The log line is the part the shopper's generic 409 cannot carry. Before
|
|
151
|
+
* this, a mode mismatch surfaced as a 502 from Stripe with nothing anywhere
|
|
152
|
+
* naming the cause; the sale is still refused the same way, but now somebody
|
|
153
|
+
* can find out why.
|
|
154
|
+
*/ export function connectLinkageIsReady(input, context) {
|
|
155
|
+
const readiness = connectReadiness(input);
|
|
156
|
+
if (readiness === 'mode-mismatch' || readiness === 'mode-unverified') {
|
|
157
|
+
console.error(`[AGL-2471] Refusing a charge against Connect account ` + `${String(input.accountId)}${(context == null ? void 0 : context.subject) ? ` (${context.subject})` : ''}: ${readiness}. The stored linkage is not verified for this ` + `deployment's Stripe mode; the merchant must re-onboard.`);
|
|
158
|
+
}
|
|
159
|
+
return readiness === 'ready';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=stripe-account-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/stripe-account-mode.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 * Whether a stored Connect linkage can actually take money HERE (AGL-2471).\n *\n * THE DEFECT. Production Firestore held three Connect linkages and all three\n * named TEST-mode accounts. One — `profiles/7AVEMtDa…`, `stripeAccountId:\n * acct_1TulDeRbL3B9Ioqz` — also carried `stripeChargesEnabled: true`, which\n * was the entire payments-readiness test every money door made:\n *\n * if (!accountId || !ownerProfile.get('stripeChargesEnabled')) …\n *\n * So three storefronts presented as payments-ready, minted a LIVE Checkout\n * session with a TEST-mode `payment_intent_data[transfer_data][destination]`,\n * and were refused by Stripe as a generic 502 at the shopper's checkout. A\n * merchant told they are ready to take money, who silently cannot.\n *\n * WHY THE ACCOUNT ID IS NOT THE EVIDENCE. `acct_1TulDeRbL3B9Ioqz` is a real\n * production value naming a TEST account. Stripe's account ids carry no mode\n * marker — live and test ids are the same shape — so no amount of reading the\n * string can tell them apart. What DOES tell them apart is Stripe itself:\n * retrieving that id with the live key answers\n *\n * 400 The account acct_1TulDeRbL3B9Ioqz was a test account created with a\n * testmode key, and therefore can only be used with testmode keys.\n *\n * The Account object, notably, has NO `livemode` field of its own (verified\n * against the live API: the payload carries `charges_enabled`,\n * `payouts_enabled`, `capabilities` and no `livemode`). So the mode is\n * recorded at the two moments Stripe does state it:\n *\n * 1. `account.updated` — the EVENT carries `livemode`, and it is Stripe's\n * own statement about the account the event is describing;\n * 2. onboarding — the account was just created or retrieved successfully\n * with the platform key, and Stripe hard-refuses cross-mode retrieval\n * (above), so the account's mode IS the key's mode; the key's mode is\n * then confirmed against the API via `resolvePlatformStripeMode`.\n *\n * WHY THE RULE IS ASYMMETRIC, AND NOT \"REFUSE ANYTHING UNPROVEN\". An absent\n * `stripeAccountLivemode` refuses only on a LIVE deployment. That is where\n * real money moves and where the defect was found, and a live deployment can\n * always re-establish the field: the merchant reconnects, or Stripe sends one\n * `account.updated`. Everywhere else — a test-key deployment, a developer\n * machine, a self-hosted install still in sandbox, the whole test suite — an\n * unverified linkage keeps the behaviour it had before, because no real money\n * can move there and Stripe enforces the mode boundary itself. Refusing\n * everywhere would have bought production nothing extra and broken every\n * non-production deployment.\n *\n * A PROVEN mismatch (`stripeAccountLivemode` recorded and disagreeing) is\n * refused in BOTH directions, because that costs nothing to detect and is\n * never right.\n *\n * WHAT IS SNIFFED, AND WHY THAT ONE IS FAIR. The SECRET KEY states its own\n * mode — `sk_live_`, `sk_test_`, `rk_live_`, `rk_test_` — and that is a\n * documented, stable Stripe invariant, unlike the account id. It is used only\n * to answer \"which mode is this deployment\", never \"which mode is this\n * account\", and even there `resolvePlatformStripeMode` prefers the API.\n *\n * HOW IT GOT THERE. A developer machine runs against PRODUCTION Firestore\n * (`FIREBASE_PROJECT_ID=aglyn-main` in every dev env file) while\n * `apps/console/.env.development.local` sets `STRIPE_SECRET_KEY` to the\n * `sk_test_` key by deliberate policy (AGL-1137: localhost must never touch\n * live Stripe). The connect route then creates a test-mode Express account\n * and writes it into the production database. Nothing about that is going to\n * change — the split key is the right call — so the gate has to be the thing\n * that notices.\n */\n\n/** Which Stripe world a key or an account belongs to. */\nexport type StripeMode = 'live' | 'test'\n\n/**\n * Why a stored linkage may not be charged against. Every value other than\n * `'ready'` is a refusal, and the mode ones are refusals the old two-field\n * test could not make.\n */\nexport type ConnectReadiness =\n /** Charge away. */\n | 'ready'\n /** No Connect account is stored at all — the onboarding call to action. */\n | 'not-connected'\n /** Stripe will not let this account take charges. */\n | 'charges-disabled'\n /**\n * The linkage predates AGL-2471 (or was written by something that did not\n * record mode), so its mode was never established — AND this deployment is\n * LIVE. This is the shape all three poisoned production records have.\n * The connect route and the `account.updated` sync both write the field, so\n * a genuine merchant self-heals on the next of either.\n */\n | 'mode-unverified'\n /** The account belongs to the OTHER Stripe world. The AGL-2471 defect. */\n | 'mode-mismatch'\n\nexport interface ConnectReadinessInput {\n /** `stripeAccountId` off the profile, whatever shape the document has. */\n accountId?: unknown\n /** `stripeChargesEnabled` off the profile. */\n chargesEnabled?: unknown\n /** `stripeAccountLivemode` off the profile — absent before AGL-2471. */\n accountLivemode?: unknown\n /** Defaults to the mode of `STRIPE_SECRET_KEY`. */\n platformMode?: StripeMode | undefined\n}\n\n/**\n * The mode a Stripe secret key states about itself, or `undefined` when the\n * string is not a Stripe secret key.\n *\n * Deliberately narrow: it matches `sk_`/`rk_` followed by `live`/`test`, and\n * refuses everything else rather than guessing. Handing it an `acct_…` id\n * returns `undefined` — the whole point is that account ids say nothing.\n */\nexport function platformStripeMode(\n key: string | undefined = process.env.STRIPE_SECRET_KEY,\n): StripeMode | undefined {\n const match = /^[sr]k_(live|test)_/.exec(String(key ?? '').trim())\n return match ? (match[1] as StripeMode) : undefined\n}\n\n/**\n * The platform's mode, asked of Stripe and only then of the key.\n *\n * `GET /v1/balance` is a singleton that carries `livemode`, so it states the\n * mode of whatever key made the call — no object has to be created to find\n * out. The key prefix is the fallback for a restricted key that cannot read\n * balance; without it, a self-hosted install with a scoped key could not\n * finish onboarding at all.\n */\nexport async function resolvePlatformStripeMode(\n key: string | undefined = process.env.STRIPE_SECRET_KEY,\n fetchImpl: typeof fetch = fetch,\n): Promise<StripeMode | undefined> {\n const fromKey = platformStripeMode(key)\n if (!key) return fromKey\n try {\n const response = await fetchImpl('https://api.stripe.com/v1/balance', {\n method: 'GET',\n headers: { Authorization: `Bearer ${key}` },\n })\n if (response.ok) {\n const payload = await response.json()\n if (typeof payload?.livemode === 'boolean') {\n return payload.livemode ? 'live' : 'test'\n }\n }\n } catch {\n // Network or transport failure — fall through to the key, which is the\n // same answer we would have recorded before this existed.\n }\n return fromKey\n}\n\n/**\n * Decides whether a stored Connect linkage may be charged against.\n *\n * Order matters: the two refusals that existed before AGL-2471 are asked\n * first, so an unconnected or restricted merchant still gets the answer they\n * always got, and only a linkage that WOULD have passed reaches the mode\n * question.\n */\nexport function connectReadiness(\n input: ConnectReadinessInput,\n): ConnectReadiness {\n const accountId =\n typeof input.accountId === 'string' ? input.accountId.trim() : ''\n if (!accountId) return 'not-connected'\n if (input.chargesEnabled !== true) return 'charges-disabled'\n const platformMode =\n 'platformMode' in input ? input.platformMode : platformStripeMode()\n // Three-valued on purpose, exactly as AGL-1997 reads `payoutsEnabled`: only\n // a literal boolean is a recorded answer. A string `'true'` or a `1` is a\n // field somebody else wrote, and inventing `true` from it would re-open the\n // hole this closes.\n if (typeof input.accountLivemode === 'boolean') {\n // A PROVEN mismatch is refused in either direction. It costs nothing to\n // detect and it is always wrong.\n if (!platformMode) return 'ready'\n return input.accountLivemode === (platformMode === 'live')\n ? 'ready'\n : 'mode-mismatch'\n }\n return platformMode === 'live' ? 'mode-unverified' : 'ready'\n}\n\n/**\n * The form every money door calls: `true` only when the linkage may be\n * charged, with the two mode refusals reported to the server log.\n *\n * The log line is the part the shopper's generic 409 cannot carry. Before\n * this, a mode mismatch surfaced as a 502 from Stripe with nothing anywhere\n * naming the cause; the sale is still refused the same way, but now somebody\n * can find out why.\n */\nexport function connectLinkageIsReady(\n input: ConnectReadinessInput,\n context?: { subject?: string },\n): boolean {\n const readiness = connectReadiness(input)\n if (readiness === 'mode-mismatch' || readiness === 'mode-unverified') {\n console.error(\n `[AGL-2471] Refusing a charge against Connect account ` +\n `${String(input.accountId)}${\n context?.subject ? ` (${context.subject})` : ''\n }: ${readiness}. The stored linkage is not verified for this ` +\n `deployment's Stripe mode; the merchant must re-onboard.`,\n )\n }\n return readiness === 'ready'\n}\n"],"names":["platformStripeMode","key","process","env","STRIPE_SECRET_KEY","match","exec","String","trim","undefined","resolvePlatformStripeMode","fetchImpl","fetch","fromKey","response","method","headers","Authorization","ok","payload","json","livemode","connectReadiness","input","accountId","chargesEnabled","platformMode","accountLivemode","connectLinkageIsReady","context","readiness","console","error","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEC,GAED,uDAAuD,GAqCvD;;;;;;;CAOC,GACD,OAAO,SAASA,mBACdC,MAA0BC,QAAQC,GAAG,CAACC,iBAAiB;IAEvD,MAAMC,QAAQ,sBAAsBC,IAAI,CAACC,OAAON,cAAAA,MAAO,IAAIO,IAAI;IAC/D,OAAOH,QAASA,KAAK,CAAC,EAAE,GAAkBI;AAC5C;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,0BACpBT,MAA0BC,QAAQC,GAAG,CAACC,iBAAiB,EACvDO,YAA0BC,KAAK;IAE/B,MAAMC,UAAUb,mBAAmBC;IACnC,IAAI,CAACA,KAAK,OAAOY;IACjB,IAAI;QACF,MAAMC,WAAW,MAAMH,UAAU,qCAAqC;YACpEI,QAAQ;YACRC,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEhB,KAAK;YAAC;QAC5C;QACA,IAAIa,SAASI,EAAE,EAAE;YACf,MAAMC,UAAU,MAAML,SAASM,IAAI;YACnC,IAAI,QAAOD,2BAAAA,QAASE,QAAQ,MAAK,WAAW;gBAC1C,OAAOF,QAAQE,QAAQ,GAAG,SAAS;YACrC;QACF;IACF,EAAE,eAAM;IACN,uEAAuE;IACvE,0DAA0D;IAC5D;IACA,OAAOR;AACT;AAEA;;;;;;;CAOC,GACD,OAAO,SAASS,iBACdC,KAA4B;IAE5B,MAAMC,YACJ,OAAOD,MAAMC,SAAS,KAAK,WAAWD,MAAMC,SAAS,CAAChB,IAAI,KAAK;IACjE,IAAI,CAACgB,WAAW,OAAO;IACvB,IAAID,MAAME,cAAc,KAAK,MAAM,OAAO;IAC1C,MAAMC,eACJ,kBAAkBH,QAAQA,MAAMG,YAAY,GAAG1B;IACjD,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IAC5E,oBAAoB;IACpB,IAAI,OAAOuB,MAAMI,eAAe,KAAK,WAAW;QAC9C,wEAAwE;QACxE,iCAAiC;QACjC,IAAI,CAACD,cAAc,OAAO;QAC1B,OAAOH,MAAMI,eAAe,KAAMD,CAAAA,iBAAiB,MAAK,IACpD,UACA;IACN;IACA,OAAOA,iBAAiB,SAAS,oBAAoB;AACvD;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,sBACdL,KAA4B,EAC5BM,OAA8B;IAE9B,MAAMC,YAAYR,iBAAiBC;IACnC,IAAIO,cAAc,mBAAmBA,cAAc,mBAAmB;QACpEC,QAAQC,KAAK,CACX,CAAC,qDAAqD,CAAC,GACrD,GAAGzB,OAAOgB,MAAMC,SAAS,IACvBK,CAAAA,2BAAAA,QAASI,OAAO,IAAG,CAAC,EAAE,EAAEJ,QAAQI,OAAO,CAAC,CAAC,CAAC,GAAG,GAC9C,EAAE,EAAEH,UAAU,8CAA8C,CAAC,GAC9D,CAAC,uDAAuD,CAAC;IAE/D;IACA,OAAOA,cAAc;AACvB"}
|